From 7b9df674d5aae413aafd6283546f840f800a99f6 Mon Sep 17 00:00:00 2001 From: Bernd Haberstumpf Date: Mon, 3 Feb 2025 22:28:19 +0100 Subject: [PATCH 01/20] Initial version of UxReporter part of openfasttrace WIP, not working. --- .vscode/launch.json | 1 + oft-self-trace.sh | 1 + parent/pom.xml | 6 + pom.xml | 1 + product/pom.xml | 4 + .../TestAllServicesAvailable.java | 2 +- .../TestInitializingServiceLoader.java | 4 +- reporter/ux/pom.xml | 32 +++ reporter/ux/src/main/java/module-info.java | 13 ++ .../openfasttrace/report/ux/Collector.java | 196 ++++++++++++++++++ .../openfasttrace/report/ux/UxReporter.java | 39 ++++ .../report/ux/UxReporterFactory.java | 38 ++++ .../report/ux/model/Coverage.java | 19 ++ .../report/ux/model/UxModel.java | 138 ++++++++++++ .../report/ux/model/UxSpecItem.java | 186 +++++++++++++++++ ...e.openfasttrace.api.report.ReporterFactory | 1 + .../report/ux/CollectorTest.java | 127 ++++++++++++ .../report/ux/UxReporterFactoryTest.java | 14 ++ 18 files changed, 820 insertions(+), 2 deletions(-) create mode 100644 reporter/ux/pom.xml create mode 100644 reporter/ux/src/main/java/module-info.java create mode 100644 reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java create mode 100644 reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporter.java create mode 100644 reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporterFactory.java create mode 100644 reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/Coverage.java create mode 100644 reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxModel.java create mode 100644 reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxSpecItem.java create mode 100644 reporter/ux/src/main/resources/META-INF/services/org.itsallcode.openfasttrace.api.report.ReporterFactory create mode 100644 reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/CollectorTest.java create mode 100644 reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/UxReporterFactoryTest.java diff --git a/.vscode/launch.json b/.vscode/launch.json index 94799b60..e77f8d67 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -23,6 +23,7 @@ "${workspaceFolder}/reporter/plaintext/src", "${workspaceFolder}/reporter/html/src", "${workspaceFolder}/reporter/aspec/src", + "${workspaceFolder}/reporter/ux/src", "${workspaceFolder}/product/src/test/java", "${workspaceFolder}/api/src", "${workspaceFolder}/exporter/specobject/src", diff --git a/oft-self-trace.sh b/oft-self-trace.sh index 47b612c0..983d830a 100755 --- a/oft-self-trace.sh +++ b/oft-self-trace.sh @@ -27,6 +27,7 @@ if $oft_script trace \ "$base_dir/reporter/plaintext/src" \ "$base_dir/reporter/html/src" \ "$base_dir/reporter/aspec/src" \ + "$base_dir/reporter/ux/src" \ "$base_dir/product/src/test/java" \ "$base_dir/api/src" \ "$base_dir/exporter/specobject/src" \ diff --git a/parent/pom.xml b/parent/pom.xml index f1b712d0..14492b26 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -186,6 +186,12 @@ ${revision} compile + + org.itsallcode.openfasttrace + openfasttrace-reporter-ux + ${revision} + compile + org.itsallcode.openfasttrace openfasttrace-testutil diff --git a/pom.xml b/pom.xml index 1d9da986..d7c43adc 100644 --- a/pom.xml +++ b/pom.xml @@ -33,6 +33,7 @@ reporter/plaintext reporter/html reporter/aspec + reporter/ux testutil diff --git a/product/pom.xml b/product/pom.xml index ced14315..6cc121cf 100644 --- a/product/pom.xml +++ b/product/pom.xml @@ -57,6 +57,10 @@ org.itsallcode.openfasttrace openfasttrace-reporter-aspec + + org.itsallcode.openfasttrace + openfasttrace-reporter-ux + org.itsallcode.openfasttrace openfasttrace-testutil diff --git a/product/src/test/java/org/itsallcode/openfasttrace/TestAllServicesAvailable.java b/product/src/test/java/org/itsallcode/openfasttrace/TestAllServicesAvailable.java index 33368f05..c238dc79 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/TestAllServicesAvailable.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/TestAllServicesAvailable.java @@ -100,7 +100,7 @@ void exporterAvailable(final String format) @ParameterizedTest @CsvSource( - { "aspec", "html", "plain" }) + { "aspec", "html", "plain", "ux" }) void reporterAvailable(final String format) { if (!reporterLoader.isFormatSupported(format)) diff --git a/product/src/test/java/org/itsallcode/openfasttrace/core/serviceloader/TestInitializingServiceLoader.java b/product/src/test/java/org/itsallcode/openfasttrace/core/serviceloader/TestInitializingServiceLoader.java index 63f89839..8f91bffc 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/core/serviceloader/TestInitializingServiceLoader.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/core/serviceloader/TestInitializingServiceLoader.java @@ -21,6 +21,7 @@ import org.itsallcode.openfasttrace.report.aspec.ASpecReporterFactory; import org.itsallcode.openfasttrace.report.html.HtmlReporterFactory; import org.itsallcode.openfasttrace.report.plaintext.PlaintextReporterFactory; +import org.itsallcode.openfasttrace.report.ux.UxReporterFactory; import org.junit.jupiter.api.Test; /** @@ -83,9 +84,10 @@ void testReporterFactoriesRegistered() final ReporterContext context = new ReporterContext(null); final List services = getRegisteredServices(ReporterFactory.class, context); - assertThat(services, hasSize(3)); + assertThat(services, hasSize(4)); assertThat(services, containsInAnyOrder(instanceOf(PlaintextReporterFactory.class), instanceOf(ASpecReporterFactory.class), + instanceOf(UxReporterFactory.class), instanceOf(HtmlReporterFactory.class))); for (final ReporterFactory factory : services) { diff --git a/reporter/ux/pom.xml b/reporter/ux/pom.xml new file mode 100644 index 00000000..ae01cf9f --- /dev/null +++ b/reporter/ux/pom.xml @@ -0,0 +1,32 @@ + + 4.0.0 + openfasttrace-reporter-ux + OpenFastTrace UX Reporter + + ../../parent/pom.xml + org.itsallcode.openfasttrace + openfasttrace-parent + ${revision} + + + ${reproducible.build.timestamp} + + + + org.itsallcode.openfasttrace + openfasttrace-api + + + org.itsallcode.openfasttrace + openfasttrace-testutil + test + + + org.itsallcode.openfasttrace + openfasttrace-core + test + + + \ No newline at end of file diff --git a/reporter/ux/src/main/java/module-info.java b/reporter/ux/src/main/java/module-info.java new file mode 100644 index 00000000..319cf608 --- /dev/null +++ b/reporter/ux/src/main/java/module-info.java @@ -0,0 +1,13 @@ +/** + * This provides an interactive HTML requirement browser. + * + * @provides org.itsallcode.openfasttrace.api.report.ReporterFactory + */ +module org.itsallcode.openfasttrace.report.ux +{ + requires transitive org.itsallcode.openfasttrace.api; + requires java.logging; + + provides org.itsallcode.openfasttrace.api.report.ReporterFactory + with org.itsallcode.openfasttrace.report.ux.UxReporterFactory; +} diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java new file mode 100644 index 00000000..155fba16 --- /dev/null +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java @@ -0,0 +1,196 @@ +package org.itsallcode.openfasttrace.report.ux; + +import org.itsallcode.openfasttrace.api.core.LinkStatus; +import org.itsallcode.openfasttrace.api.core.LinkedSpecificationItem; +import org.itsallcode.openfasttrace.report.ux.model.Coverage; +import org.itsallcode.openfasttrace.report.ux.model.UxSpecItem; + +import java.util.*; +import java.util.stream.Collectors; + +public class Collector { + + private final List items = new ArrayList<>(); + private final List uxItems = new ArrayList<>(); + + private final List allTypes = new ArrayList<>(); + private final List orderedTypes = new ArrayList<>(); + + private final Map> itemCoverages = new HashMap<>(); + + public Collector() { + } + + public Collector collect(List specItems) { + this.items.clear(); + this.items.addAll(specItems); + + initializeIndexes(); + collectItemCoverages(); + + return this; + } + + public List getAllTypes() { + return allTypes; + } + + public List getOrderedTypes() { + return orderedTypes; + } + + public Map> getItemCoverages() { + return itemCoverages; + } + + public List getUxItems() { + return uxItems; + } + + // + // private members + + // Types and indexes + + /** + * Fill alTypes and orderedTypes. + */ + private void initializeIndexes() { + allTypes.clear(); + allTypes.addAll(collectAllTypes(items)); + orderedTypes.clear(); + orderedTypes.addAll(createOrderedTypes(items, allTypes)); + } + + /** + * @return Get all types of all specItems. + */ + static Set collectAllTypes(List items) { + return items.stream().map(LinkedSpecificationItem::getArtifactType).collect(Collectors.toSet()); + } + + /** + * Provide a list of artifact types sorted by needs dependencies extracted form items. + * + * @param items + * Items to process + * @param allTypes + * all existing types + * @return order types + */ + static List createOrderedTypes(final List items, + final List allTypes) { + final List orderedTypes = new ArrayList<>(); + final Map dependenciesByType = collectDependentTypes(items); + + // Kahn's BFS algorithm + while( !dependenciesByType.isEmpty() && orderedTypes.size() < allTypes.size() ) { + for( final Map.Entry neededTypeEntry : dependenciesByType.entrySet().stream() + .toList() ) { + final String type = neededTypeEntry.getKey(); + final TypeDependencies dependencies = neededTypeEntry.getValue(); + if( dependencies.needs.isEmpty() ) { + orderedTypes.add(0, type); + dependencies.provides.forEach( + (providerType) -> dependenciesByType.get(providerType).needs.remove(type)); + dependenciesByType.remove(type); + } + } + } + + return orderedTypes; + } + + static class TypeDependencies { + public final Set provides = new HashSet<>(); + public final Set needs = new HashSet<>(); + + @Override public String toString() { + return String.format("{provides{%s}, needs[%s]}", String.join(",", provides), String.join(",", needs)); + } + } // TypeDependencies + + /** + * @return superset of all types needed by a type for all types of all items + */ + static Map collectDependentTypes(final List items) { + final Map dependenciesByType = new HashMap<>(); + for( final LinkedSpecificationItem item : items ) { + final String itemType = item.getArtifactType(); + final TypeDependencies dependencies = dependenciesByType.getOrDefault(itemType, new TypeDependencies()); + + // Add needed to processed item + dependencies.needs.addAll(item.getNeedsArtifactTypes()); + dependenciesByType.put(itemType, dependencies); + + // Add item type to provides of all needed types + for( final String need : dependencies.needs ) { + final TypeDependencies providerDependencies = dependenciesByType.getOrDefault(need, + new TypeDependencies()); + providerDependencies.provides.add(itemType); + dependenciesByType.put(need, providerDependencies); + } + } + return dependenciesByType; + } + + // Covered Status + + void collectItemCoverages() { + itemCoverages.clear(); + final Map coverages = initializedCoverages(orderedTypes); + for( final LinkedSpecificationItem item : items ) { + collectItemCoverage(item, coverages); + } + } + + void collectItemCoverage(final LinkedSpecificationItem item, final Map coverages) { + // Item Coverage already collected + if( mergeCoverages(itemCoverages.get(item), coverages) ) return; + + // End of the tree + if( item.getNeedsArtifactTypes().isEmpty() ) { + updateItemCoverage(item, true, coverages); + return; + } + + // Traverse down + for( final LinkedSpecificationItem coveringItem : item.getLinksByStatus(LinkStatus.COVERED_SHALLOW) ) { + collectItemCoverage(coveringItem, coverages); + } + + // Refresh this coverage + updateItemCoverage(item, item.isCoveredShallowWithApprovedItems(), coverages); + } + + void updateItemCoverage(final LinkedSpecificationItem item, + final boolean covered, + final Map coverages) { + final Map targetCoverages = itemCoverages.getOrDefault(item, new HashMap<>()); + mergeCoverages(coverages, targetCoverages); + targetCoverages.put(item.getArtifactType(), covered ? Coverage.COVERED : Coverage.UNCOVERED); + itemCoverages.put(item, targetCoverages); + } + + static boolean mergeCoverages(final Map fromCoverages, + final Map toCoverages) { + if( fromCoverages == null ) return false; + for( final Map.Entry fromCoverage : fromCoverages.entrySet() ) { + final Coverage fromCoverageValue = fromCoverage.getValue(); + final Coverage toCoverageVales = toCoverages.get(fromCoverage.getKey()); + toCoverages.put(fromCoverage.getKey(), mergeCoverType(fromCoverageValue, toCoverageVales)); + } + return true; + } + + static Coverage mergeCoverType(Coverage type1, Coverage type2) { + return type1 == Coverage.UNCOVERED || type2 == Coverage.UNCOVERED ? Coverage.UNCOVERED + : type1 == Coverage.COVERED || type2 == Coverage.COVERED ? Coverage.COVERED + : Coverage.NONE; + } + + static Map initializedCoverages(final List allTypes) { + return allTypes.stream().collect(Collectors.toMap(type -> type, (any) -> Coverage.NONE)); + } + +} // Collector diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporter.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporter.java new file mode 100644 index 00000000..7832540f --- /dev/null +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporter.java @@ -0,0 +1,39 @@ +package org.itsallcode.openfasttrace.report.ux; + +import org.itsallcode.openfasttrace.api.core.Trace; +import org.itsallcode.openfasttrace.api.report.Reportable; +import org.itsallcode.openfasttrace.api.report.ReporterContext; + +import java.io.OutputStream; +import java.util.logging.Logger; + +/** + * + */ +public class UxReporter implements Reportable +{ + + private static final Logger LOG = Logger.getLogger(UxReporter.class.getName()); + + public UxReporter() { + } + + /** + * + * @param trace the traced data + * @param context settings + */ + public UxReporter(final Trace trace, final ReporterContext context) + { + LOG.info(String.format("constructor(context=%s",context.toString())); + } + + /** + * Generate output + * @param outputStream The file to write output to + */ + @Override public void renderToStream(OutputStream outputStream) + { + LOG.info("renderToStream"); + } +} diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporterFactory.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporterFactory.java new file mode 100644 index 00000000..0a85f187 --- /dev/null +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporterFactory.java @@ -0,0 +1,38 @@ +package org.itsallcode.openfasttrace.report.ux; + +import org.itsallcode.openfasttrace.api.core.Trace; +import org.itsallcode.openfasttrace.api.report.Reportable; +import org.itsallcode.openfasttrace.api.report.ReporterFactory; + +import java.util.logging.Logger; + +/** + * Creates the UX exporter + */ +public class UxReporterFactory extends ReporterFactory { + + private static final String UX_REPORT_FORMAT = "ux"; + + public UxReporterFactory() { + } + + /** + * + * @param format to check + * @return if equal to 'ux' + */ + @Override public boolean supportsFormat(String format) + { + return UX_REPORT_FORMAT.equalsIgnoreCase(format); + } + + /** + * Creates the exporter. + * @param trace the traces to process + * @return the report + */ + @Override public Reportable createImporter(Trace trace) + { + return new UxReporter(trace,this.getContext()); + } +} diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/Coverage.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/Coverage.java new file mode 100644 index 00000000..41e20b62 --- /dev/null +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/Coverage.java @@ -0,0 +1,19 @@ +package org.itsallcode.openfasttrace.report.ux.model; + +public enum Coverage +{ + COVERED(0), + UNCOVERED(1), + NONE(2); + + Coverage(int index) { + this.index = index; + } + + private final int index; + + public int getIndex() + { + return index; + } +} diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxModel.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxModel.java new file mode 100644 index 00000000..449fb7d3 --- /dev/null +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxModel.java @@ -0,0 +1,138 @@ +package org.itsallcode.openfasttrace.report.ux.model; + +import org.itsallcode.openfasttrace.api.core.SpecificationItem; + +import java.util.List; + +/** + * Surrounding model that is used to generate the specitem_data model for OpenFastTrace-UX. + */ +public class UxModel +{ + private final String name; + private final List artifactTypes; + private final int numberOfSpecItems; + private final int uncoveredSpecItems; + + private UxModel(Builder builder) + { + name = builder.name; + artifactTypes = builder.artifactTypes; + numberOfSpecItems = builder.numberOfSpecItems; + uncoveredSpecItems = builder.uncoveredSpecItems; + } + + /** + * @return Name of the project + */ + public String getName() + { + return name; + } + + /** + * @return types of {@link SpecificationItem}s trace + */ + public List getArtifactTypes() + { + return artifactTypes; + } + + /** + * @return Total number of {@link SpecificationItem}s traced + */ + public int getNumberOfSpecItems() + { + return numberOfSpecItems; + } + + /** + * @return Number of traced {@link SpecificationItem}s that have deep uncoverered or a staled coverage. + */ + public int getUncoveredSpecItems() + { + return uncoveredSpecItems; + } + + /** + * {@code UxModel} builder static inner class. + */ + public static final class Builder + { + private String name; + private List artifactTypes; + private int numberOfSpecItems; + private int uncoveredSpecItems; + + private Builder() + { + } + + public static Builder builder() + { + return new Builder(); + } + + /** + * Sets the {@code name} and returns a reference to this Builder enabling method chaining. + * + * @param name + * the {@code name} to set + * @return a reference to this Builder + */ + public Builder withName(String name) + { + this.name = name; + return this; + } + + /** + * Sets the {@code artifactTypes} and returns a reference to this Builder enabling method chaining. + * + * @param artifactTypes + * the {@code artifactTypes} to set + * @return a reference to this Builder + */ + public Builder withArtifactTypes(List artifactTypes) + { + this.artifactTypes = artifactTypes; + return this; + } + + /** + * Sets the {@code numberOfSpecItems} and returns a reference to this Builder enabling method chaining. + * + * @param numberOfSpecItems + * the {@code numberOfSpecItems} to set + * @return a reference to this Builder + */ + public Builder withNumberOfSpecItems(int numberOfSpecItems) + { + this.numberOfSpecItems = numberOfSpecItems; + return this; + } + + /** + * Sets the {@code uncoveredSpecItems} and returns a reference to this Builder enabling method chaining. + * + * @param uncoveredSpecItems + * the {@code uncoveredSpecItems} to set + * @return a reference to this Builder + */ + public Builder withUncoveredSpecItems(int uncoveredSpecItems) + { + this.uncoveredSpecItems = uncoveredSpecItems; + return this; + } + + /** + * Returns a {@code UxModel} built from the parameters previously set. + * + * @return a {@code UxModel} built with parameters of this {@code UxModel.Builder} + */ + public UxModel build() + { + return new UxModel(this); + } + } +} diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxSpecItem.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxSpecItem.java new file mode 100644 index 00000000..8dcb8b2d --- /dev/null +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxSpecItem.java @@ -0,0 +1,186 @@ +package org.itsallcode.openfasttrace.report.ux.model; + +import org.itsallcode.openfasttrace.api.core.LinkedSpecificationItem; + +import java.util.List; + +public class UxSpecItem +{ + private final int index; + private final int typeIndex; + private final List neededTypeIndexes; + private final int statusId; + private final List coveredBy; + private final List covers; + private final LinkedSpecificationItem item; + + private UxSpecItem(Builder builder) + { + index = builder.index; + typeIndex = builder.typeIndex; + neededTypeIndexes = builder.neededTypeIndexes; + statusId = builder.statusId; + coveredBy = builder.coveredBy; + covers = builder.covers; + item = builder.item; + } + + public int getIndex() + { + return index; + } + + public int getTypeIndex() + { + return typeIndex; + } + + public List getNeededTypeIndexes() + { + return neededTypeIndexes; + } + + public int getStatusId() + { + return statusId; + } + + public List getCoveredBy() + { + return coveredBy; + } + + public List getCovers() + { + return covers; + } + + public LinkedSpecificationItem getItem() + { + return item; + } + + /** + * {@code UxSpecItem} builder static inner class. + */ + public static final class Builder + { + private int index; + private int typeIndex; + private List neededTypeIndexes; + private int statusId; + private List coveredBy; + private List covers; + private LinkedSpecificationItem item; + + private Builder() + { + } + + public static Builder builder() + { + return new Builder(); + } + + /** + * Sets the {@code index} and returns a reference to this Builder enabling method chaining. + * + * @param index + * the {@code index} to set + * @return a reference to this Builder + */ + public Builder withIndex(int index) + { + this.index = index; + return this; + } + + /** + * Sets the {@code typeIndex} and returns a reference to this Builder enabling method chaining. + * + * @param typeIndex + * the {@code typeIndex} to set + * @return a reference to this Builder + */ + public Builder withTypeIndex(int typeIndex) + { + this.typeIndex = typeIndex; + return this; + } + + /** + * Sets the {@code neededTypeIndexes} and returns a reference to this Builder enabling method chaining. + * + * @param neededTypeIndexes + * the {@code neededTypeIndexes} to set + * @return a reference to this Builder + */ + public Builder withNeededTypeIndexes(List neededTypeIndexes) + { + this.neededTypeIndexes = neededTypeIndexes; + return this; + } + + /** + * Sets the {@code statusId} and returns a reference to this Builder enabling method chaining. + * + * @param statusId + * the {@code statusId} to set + * @return a reference to this Builder + */ + public Builder withStatusId(int statusId) + { + this.statusId = statusId; + return this; + } + + /** + * Sets the {@code coveredBy} and returns a reference to this Builder enabling method chaining. + * + * @param coveredBy + * the {@code coveredBy} to set + * @return a reference to this Builder + */ + public Builder withCoveredBy(List coveredBy) + { + this.coveredBy = coveredBy; + return this; + } + + /** + * Sets the {@code covers} and returns a reference to this Builder enabling method chaining. + * + * @param covers + * the {@code covers} to set + * @return a reference to this Builder + */ + public Builder withCovers(List covers) + { + this.covers = covers; + return this; + } + + /** + * Sets the {@code item} and returns a reference to this Builder enabling method chaining. + * + * @param item + * the {@code item} to set + * @return a reference to this Builder + */ + public Builder withItem(LinkedSpecificationItem item) + { + this.item = item; + return this; + } + + /** + * Returns a {@code UxSpecItem} built from the parameters previously set. + * + * @return a {@code UxSpecItem} built with parameters of this {@code UxSpecItem.Builder} + */ + public UxSpecItem build() + { + return new UxSpecItem(this); + } + } +} diff --git a/reporter/ux/src/main/resources/META-INF/services/org.itsallcode.openfasttrace.api.report.ReporterFactory b/reporter/ux/src/main/resources/META-INF/services/org.itsallcode.openfasttrace.api.report.ReporterFactory new file mode 100644 index 00000000..a71f95f1 --- /dev/null +++ b/reporter/ux/src/main/resources/META-INF/services/org.itsallcode.openfasttrace.api.report.ReporterFactory @@ -0,0 +1 @@ +org.itsallcode.openfasttrace.report.ux.UxReporterFactory diff --git a/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/CollectorTest.java b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/CollectorTest.java new file mode 100644 index 00000000..2acb560e --- /dev/null +++ b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/CollectorTest.java @@ -0,0 +1,127 @@ +package org.itsallcode.openfasttrace.report.ux; + +import org.itsallcode.openfasttrace.api.core.ItemStatus; +import org.itsallcode.openfasttrace.api.core.LinkedSpecificationItem; +import org.itsallcode.openfasttrace.api.core.SpecificationItem; +import org.itsallcode.openfasttrace.api.core.SpecificationItemId; +import org.itsallcode.openfasttrace.core.Linker; +import org.itsallcode.openfasttrace.report.ux.model.Coverage; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.*; +import java.util.stream.Collectors; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.itsallcode.matcher.auto.AutoMatcher.containsInAnyOrder; + +class CollectorTest { + + public static final List ORDERED_SAMPLE_TYPES = List.of("fea", "req", "arch", "utest"); + + public static final List SAMPLE_ITEMS = List.of( + item("fea~fea1", ItemStatus.APPROVED, Set.of("req")), + item("req~req1", ItemStatus.APPROVED, Set.of("arch"), Set.of("fea~fea1")), + item("req~req2", ItemStatus.APPROVED, Set.of("arch"), Set.of("fea~fea1")), + item("arch~arch1", ItemStatus.APPROVED, Set.of("utest"), Set.of("req~req1")), + item("arch~arch2", ItemStatus.APPROVED, Set.of("utest"), Set.of("req~req1")), + item("arch~arch3", ItemStatus.APPROVED, Set.of("utest"), Set.of("req~req2")), + item("utest~utest1", ItemStatus.APPROVED, Set.of(), Set.of("arch~arch1")), + item("utest~utest2", ItemStatus.APPROVED, Set.of(), Set.of("arch~arch2")) + ); + + private static final List LINKED_SAMPLE_ITEMS = new Linker(SAMPLE_ITEMS).link(); + + private Collector collector = null; + + @BeforeEach + void setUp() { + collector = new Collector().collect(LINKED_SAMPLE_ITEMS); + } + + @AfterEach + void tearDown() { + } + + @Test + void testCollectAllTypes() { + final Set types = Collector.collectAllTypes(LINKED_SAMPLE_ITEMS); + assertThat(types, containsInAnyOrder(ORDERED_SAMPLE_TYPES)); + } + + @Test + void testCollectDependentTypes() { + final Map dependencies = Collector.collectDependentTypes( + LINKED_SAMPLE_ITEMS); + System.out.println(dependencies); + } + + @Test + void testCreateOrderedTypes() { + final Set allTypes = Collector.collectAllTypes(LINKED_SAMPLE_ITEMS); + + final List items = LINKED_SAMPLE_ITEMS.stream().unordered().toList(); + final List orderedTypes1 = Collector.createOrderedTypes(items, allTypes.stream().toList()); + System.out.println(String.join(",", orderedTypes1)); + assertThat(orderedTypes1, contains(ORDERED_SAMPLE_TYPES)); + + final List reverseItems = new ArrayList<>(LINKED_SAMPLE_ITEMS); + Collections.reverse(reverseItems); + final List orderedTypes2 = Collector.createOrderedTypes(items, allTypes.stream().toList()); + System.out.println(String.join(",", orderedTypes2)); + assertThat(orderedTypes2, contains(ORDERED_SAMPLE_TYPES)); + } + + @Test + void testItemCoverages() { + final Map> coverages = collector.getItemCoverages(); + System.out.println(coverages.entrySet().stream().map(entry -> + String.format("%s%s", entry.getKey().getId(), entry.getValue()) + ).collect(Collectors.joining(",\n"))); + } + + @Test + void testInitializedCoverages() { + final Map coverages = Collector.initializedCoverages(ORDERED_SAMPLE_TYPES); + System.out.println(coverages); + } + + // + // Helpers + + private static SpecificationItem item(final String id, ItemStatus status, Set needs) { + final SpecificationItemId specId = id(id); + SpecificationItem.Builder builder = SpecificationItem.builder() + .id(specId) + .title("Title " + id) + .description("Descriptive text for " + id) + .status(status); + needs.forEach(builder::addNeedsArtifactType); + + return builder.build(); + } + + private static SpecificationItem item(final String id, + ItemStatus status, + Set needs, + Set coverages) { + final SpecificationItemId specId = id(id); + SpecificationItem.Builder builder = SpecificationItem.builder() + .id(specId) + .title("Title " + id) + .description("Descriptive text for " + id) + .status(status); + needs.forEach(builder::addNeedsArtifactType); + coverages.forEach(coverage -> builder.addCoveredId(id(coverage))); + + return builder.build(); + } + + private static SpecificationItemId id(final String id) { + return id.matches("/~.*~") ? + new SpecificationItemId.Builder(id).build() : + new SpecificationItemId.Builder(id + "~1").build(); + } +} \ No newline at end of file diff --git a/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/UxReporterFactoryTest.java b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/UxReporterFactoryTest.java new file mode 100644 index 00000000..bd9fc59c --- /dev/null +++ b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/UxReporterFactoryTest.java @@ -0,0 +1,14 @@ +package org.itsallcode.openfasttrace.report.ux; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class UxReporterFactoryTest +{ + @Test + public void test() + { + assertTrue(true); + } +} \ No newline at end of file From 9639ada7ac27407743185c58d05e0f1475532b86 Mon Sep 17 00:00:00 2001 From: Bernd Haberstumpf Date: Fri, 7 Feb 2025 20:04:54 +0100 Subject: [PATCH 02/20] Collector implementation and test of the coverages of SpecItems --- .../openfasttrace/report/ux/Collector.java | 174 +++++++++--- .../report/ux/CollectorTest.java | 247 ++++++++++++++++-- 2 files changed, 368 insertions(+), 53 deletions(-) diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java index 155fba16..670c5d02 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java @@ -1,26 +1,36 @@ package org.itsallcode.openfasttrace.report.ux; -import org.itsallcode.openfasttrace.api.core.LinkStatus; -import org.itsallcode.openfasttrace.api.core.LinkedSpecificationItem; +import org.itsallcode.openfasttrace.api.core.*; import org.itsallcode.openfasttrace.report.ux.model.Coverage; +import org.itsallcode.openfasttrace.report.ux.model.UxModel; import org.itsallcode.openfasttrace.report.ux.model.UxSpecItem; import java.util.*; import java.util.stream.Collectors; +/** + * Collector traverses a {@link LinkedSpecificationItem} tree and provides a {@link UxSpecItem} and + * a {@link UxModel} based on the parsed items. + */ public class Collector { private final List items = new ArrayList<>(); + private final List ids = new ArrayList<>(); private final List uxItems = new ArrayList<>(); private final List allTypes = new ArrayList<>(); private final List orderedTypes = new ArrayList<>(); - private final Map> itemCoverages = new HashMap<>(); + final List> itemCoverages = new ArrayList<>(); public Collector() { } + /** + * Fill in the caches of the Collector based on the given items. + * + * @param specItems {@link LinkedSpecificationItem} model. + */ public Collector collect(List specItems) { this.items.clear(); this.items.addAll(specItems); @@ -31,17 +41,30 @@ public Collector collect(List specItems) { return this; } + /** + * @return unordered list of {@link SpecificationItem} types. + */ public List getAllTypes() { return allTypes; } + /** + * @return {@link SpecificationItem} types ordered base on the downward linkage of items. + */ public List getOrderedTypes() { return orderedTypes; } - public Map> getItemCoverages() { - return itemCoverages; - } + /** + * ItemCoverages provide a shallow coverages for each type of {@link SpecificationItem} type based on the linkage + * of a SpecItem. + * The linkage tree is flattended, means the shallows coverages of all types merged. Merged means that the type is + * not part of the tree {@link Coverage#NONE} is returned, {@link Coverage#UNCOVERED} is returned when at least one + * item of the type is uncovered. {@link Coverage#COVERED} is returned when all items of a type are covered. + * + * @return list of coverages indexes by {@link LinkedSpecificationItem} handed in to {@link #collect(List)}. + */ + public List> getItemCoverages() {return itemCoverages;} public List getUxItems() { return uxItems; @@ -59,7 +82,10 @@ private void initializeIndexes() { allTypes.clear(); allTypes.addAll(collectAllTypes(items)); orderedTypes.clear(); - orderedTypes.addAll(createOrderedTypes(items, allTypes)); + orderedTypes.addAll(createOrderedTypes(items)); + + ids.clear(); + ids.addAll(items.stream().map(LinkedSpecificationItem::getId).toList()); } /** @@ -74,19 +100,17 @@ static Set collectAllTypes(List items) { * * @param items * Items to process - * @param allTypes - * all existing types * @return order types */ - static List createOrderedTypes(final List items, - final List allTypes) { + static List createOrderedTypes(final List items) { final List orderedTypes = new ArrayList<>(); final Map dependenciesByType = collectDependentTypes(items); // Kahn's BFS algorithm - while( !dependenciesByType.isEmpty() && orderedTypes.size() < allTypes.size() ) { - for( final Map.Entry neededTypeEntry : dependenciesByType.entrySet().stream() - .toList() ) { + while( !dependenciesByType.isEmpty() ) { + final Map previousDependenciesByType = new HashMap<>(dependenciesByType); + + for( final Map.Entry neededTypeEntry : previousDependenciesByType.entrySet() ) { final String type = neededTypeEntry.getKey(); final TypeDependencies dependencies = neededTypeEntry.getValue(); if( dependencies.needs.isEmpty() ) { @@ -96,6 +120,12 @@ static List createOrderedTypes(final List items dependenciesByType.remove(type); } } + + // Break circles + if( dependenciesByType.size() == previousDependenciesByType.size() ) { + orderedTypes.addAll(0, dependenciesByType.keySet()); + dependenciesByType.clear(); + } } return orderedTypes; @@ -136,44 +166,94 @@ static Map collectDependentTypes(final List coverages = initializedCoverages(orderedTypes); - for( final LinkedSpecificationItem item : items ) { - collectItemCoverage(item, coverages); + for( int i = 0; i < items.size(); i++ ) { + itemCoverages.add(null); + } + + // Fill coverages + for( int i = 0; i < items.size(); i++ ) { + collectItemCoverage(i); } } - void collectItemCoverage(final LinkedSpecificationItem item, final Map coverages) { - // Item Coverage already collected - if( mergeCoverages(itemCoverages.get(item), coverages) ) return; + /** + * Calculate the coverages for a given {@link LinkedSpecificationItem}. + * The method traverses the tree recursively merging the coverage of all items with the same type + * with {@link #mergeCoverages(Map, Map)} + * + * @param index The index within the {@link LinkedSpecificationItem} list. + * @return coverages of the item + */ + Map collectItemCoverage(final int index) { + // Coverage already collected + final Map targetCoverage = itemCoverages.get(index); + if( targetCoverage != null ) { + System.out.println("<<< already covered index " + index); + return targetCoverage; + } + + final Map coverages = initializedCoverages(orderedTypes); // End of the tree + final LinkedSpecificationItem item = items.get(index); if( item.getNeedsArtifactTypes().isEmpty() ) { - updateItemCoverage(item, true, coverages); - return; + System.out.println("<<< final " + item.getId()); + return updateItemCoverage(index, + item.getArtifactType(), + item.getStatus() == ItemStatus.APPROVED, + coverages); } // Traverse down for( final LinkedSpecificationItem coveringItem : item.getLinksByStatus(LinkStatus.COVERED_SHALLOW) ) { - collectItemCoverage(coveringItem, coverages); + int coveringIndex = ids.indexOf(coveringItem.getId()); + System.out.println(">>> coveringItem (" +coveringIndex + ")" + coveringItem.getId()); + final Map collectedCoverages = collectItemCoverage(coveringIndex); + mergeCoverages(collectedCoverages, coverages); } // Refresh this coverage - updateItemCoverage(item, item.isCoveredShallowWithApprovedItems(), coverages); + updateItemCoverage(index, item.getArtifactType(), item.isCoveredShallowWithApprovedItems(), coverages); + + System.out.println("<<< intermediate " + item.getId()); + return coverages; } - void updateItemCoverage(final LinkedSpecificationItem item, - final boolean covered, - final Map coverages) { - final Map targetCoverages = itemCoverages.getOrDefault(item, new HashMap<>()); - mergeCoverages(coverages, targetCoverages); - targetCoverages.put(item.getArtifactType(), covered ? Coverage.COVERED : Coverage.UNCOVERED); - itemCoverages.put(item, targetCoverages); + /** + * Updates the given coverages by setting the coverage of the goven type and updates the {@link #itemCoverages}. + * + * @param index The index of the item + * @param artifactType The type of the coverage + * @param covered true if the type is covered + * @param coverages the coverages to update + * @return the coverages + */ + Map updateItemCoverage(final int index, + final String artifactType, + final boolean covered, + final Map coverages) { + coverages.put(artifactType, covered ? Coverage.COVERED : Coverage.UNCOVERED); + itemCoverages.set(index, coverages); + return coverages; } + /** + * Merges to SpecItemType coverages resulting in a superset with Coverage types merged by mergeCoverType. + * + * @param fromCoverages + * types to be merged into toCoverage, may be null + * @param toCoverages + * the target types + * @return true = merged + */ static boolean mergeCoverages(final Map fromCoverages, - final Map toCoverages) { + final Map toCoverages) { if( fromCoverages == null ) return false; for( final Map.Entry fromCoverage : fromCoverages.entrySet() ) { final Coverage fromCoverageValue = fromCoverage.getValue(); @@ -183,14 +263,36 @@ static boolean mergeCoverages(final Map fromCoverages, return true; } + /** + * Merges two coverage types. + * + * At least one coverage type is uncovered, result is uncovered, no type on with returns NONE, both covered + * returns covered. + * + * @param type1 + * First input coverage + * @param type2 + * Second input coverage + * @return merge input coverage + */ static Coverage mergeCoverType(Coverage type1, Coverage type2) { - return type1 == Coverage.UNCOVERED || type2 == Coverage.UNCOVERED ? Coverage.UNCOVERED - : type1 == Coverage.COVERED || type2 == Coverage.COVERED ? Coverage.COVERED - : Coverage.NONE; + return type1 == org.itsallcode.openfasttrace.report.ux.model.Coverage.UNCOVERED || type2 == org.itsallcode.openfasttrace.report.ux.model.Coverage.UNCOVERED ? + org.itsallcode.openfasttrace.report.ux.model.Coverage.UNCOVERED + : + type1 == org.itsallcode.openfasttrace.report.ux.model.Coverage.COVERED || type2 == org.itsallcode.openfasttrace.report.ux.model.Coverage.COVERED ? + org.itsallcode.openfasttrace.report.ux.model.Coverage.COVERED + : + org.itsallcode.openfasttrace.report.ux.model.Coverage.NONE; } + /** + * @param allTypes + * all known SpecItem types + * @return Map with all SpecItemTypes as name and Coverage.NONE + */ static Map initializedCoverages(final List allTypes) { - return allTypes.stream().collect(Collectors.toMap(type -> type, (any) -> Coverage.NONE)); + return allTypes.stream().collect( + Collectors.toMap(type -> type, (any) -> org.itsallcode.openfasttrace.report.ux.model.Coverage.NONE)); } } // Collector diff --git a/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/CollectorTest.java b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/CollectorTest.java index 2acb560e..261afce1 100644 --- a/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/CollectorTest.java +++ b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/CollectorTest.java @@ -1,5 +1,6 @@ package org.itsallcode.openfasttrace.report.ux; +import org.hamcrest.Matcher; import org.itsallcode.openfasttrace.api.core.ItemStatus; import org.itsallcode.openfasttrace.api.core.LinkedSpecificationItem; import org.itsallcode.openfasttrace.api.core.SpecificationItem; @@ -9,18 +10,28 @@ import org.junit.jupiter.api.AfterEach; 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; +import org.junit.jupiter.params.provider.MethodSource; import java.util.*; import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.*; import static org.itsallcode.matcher.auto.AutoMatcher.containsInAnyOrder; class CollectorTest { + /** + * Coverage types in ordered from based on SAMPLE_ITEM linkage + */ public static final List ORDERED_SAMPLE_TYPES = List.of("fea", "req", "arch", "utest"); + /** + * Sample for items on all level fea,req,arch,utest with upwards linkes + */ public static final List SAMPLE_ITEMS = List.of( item("fea~fea1", ItemStatus.APPROVED, Set.of("req")), item("req~req1", ItemStatus.APPROVED, Set.of("arch"), Set.of("fea~fea1")), @@ -32,7 +43,24 @@ class CollectorTest { item("utest~utest2", ItemStatus.APPROVED, Set.of(), Set.of("arch~arch2")) ); + /** + * Sample for items on all level fea,req,arch,utest with a circular link + */ + public static final List SAMPLE_ITEM_CYCLE = List.of( + item("fea~fea1", ItemStatus.APPROVED, Set.of("req")), + item("req~req1", ItemStatus.APPROVED, Set.of("arch"), Set.of("fea~fea1")), + item("req~req2", ItemStatus.APPROVED, Set.of("arch"), Set.of("fea~fea1")), + item("arch~arch1", ItemStatus.APPROVED, Set.of("utest"), Set.of("req~req1")), + item("arch~arch2", ItemStatus.APPROVED, Set.of("utest"), Set.of("req~req1")), + item("arch~arch3", ItemStatus.APPROVED, Set.of("utest"), Set.of("req~req2")), + item("arch~cycle", ItemStatus.APPROVED, Set.of("utest"), Set.of("utest~cycle")), + item("utest~utest1", ItemStatus.APPROVED, Set.of(), Set.of("arch~arch1")), + item("utest~utest2", ItemStatus.APPROVED, Set.of(), Set.of("arch~arch2")), + item("utest~cycle", ItemStatus.APPROVED, Set.of(), Set.of("arch~cycle")) + ); + private static final List LINKED_SAMPLE_ITEMS = new Linker(SAMPLE_ITEMS).link(); + private static final List LINKED_SAMPLE_ITEMS_CYCLE = new Linker(SAMPLE_ITEM_CYCLE).link(); private Collector collector = null; @@ -45,10 +73,16 @@ void setUp() { void tearDown() { } + // Collect SpecItems Types + + /** + * Test extract SpecItems types as unordered set via Collector.collectAllTypes. + */ @Test void testCollectAllTypes() { final Set types = Collector.collectAllTypes(LINKED_SAMPLE_ITEMS); - assertThat(types, containsInAnyOrder(ORDERED_SAMPLE_TYPES)); + System.out.println("testCollectAllTypes " + types); + assertThat(types, containsInAnyOrder(ORDERED_SAMPLE_TYPES.toArray())); } @Test @@ -58,34 +92,213 @@ void testCollectDependentTypes() { System.out.println(dependencies); } + /** + * Test creates an ordered list of SpecItems types with Collector.createOrderedTypes injecting SpecItems with a + * cycle. + */ @Test void testCreateOrderedTypes() { - final Set allTypes = Collector.collectAllTypes(LINKED_SAMPLE_ITEMS); - - final List items = LINKED_SAMPLE_ITEMS.stream().unordered().toList(); - final List orderedTypes1 = Collector.createOrderedTypes(items, allTypes.stream().toList()); + // Order sample items with a cycle + final List orderedTypes1 = Collector.createOrderedTypes(LINKED_SAMPLE_ITEMS_CYCLE); System.out.println(String.join(",", orderedTypes1)); - assertThat(orderedTypes1, contains(ORDERED_SAMPLE_TYPES)); + assertThat(orderedTypes1, contains(ORDERED_SAMPLE_TYPES.toArray())); - final List reverseItems = new ArrayList<>(LINKED_SAMPLE_ITEMS); + // Order sample items with a cycle in reverse order + final List reverseItems = new ArrayList<>(LINKED_SAMPLE_ITEMS_CYCLE); Collections.reverse(reverseItems); - final List orderedTypes2 = Collector.createOrderedTypes(items, allTypes.stream().toList()); + final List orderedTypes2 = Collector.createOrderedTypes(reverseItems); System.out.println(String.join(",", orderedTypes2)); - assertThat(orderedTypes2, contains(ORDERED_SAMPLE_TYPES)); + assertThat(orderedTypes2, contains(ORDERED_SAMPLE_TYPES.toArray())); + } + + // Collect coverages + + /** + * Tests thatCollector.initializedCoverages return a Map with all SpecItem types set Coverage.NONE. + */ + @Test + void testInitializedCoverages() { + final Map coverages = Collector.initializedCoverages(ORDERED_SAMPLE_TYPES); + System.out.println(coverages); + assertThat(coverages, allOf( + hasEntry("utest", Coverage.NONE), + hasEntry("fea", Coverage.NONE), + hasEntry("arch", Coverage.NONE), + hasEntry("req", Coverage.NONE) + )); + } + + /** + * Helper to produce tuples of all permutations of coverage types. + */ + private static Stream provideCoveragePermutations() { + return Arrays.stream(Coverage.values()).flatMap(firstCoverage -> + Arrays.stream(Coverage.values()).map(secondCoverage -> + Arguments.of(firstCoverage, secondCoverage) + )); + } + + /** + * Tests that Collector.mergeCoverType returns the fitting coverage for all permutations of coverage types. + */ + @ParameterizedTest + @MethodSource( "provideCoveragePermutations" ) + void testMergeCoverageType(final Coverage firstCoverage, final Coverage secondCoverage) { + if( ( firstCoverage == Coverage.UNCOVERED || secondCoverage == Coverage.UNCOVERED ) ) { + assertThat(Collector.mergeCoverType(firstCoverage, secondCoverage), equalTo(Coverage.UNCOVERED)); + } + else if( firstCoverage == Coverage.COVERED || secondCoverage == Coverage.COVERED ) { + assertThat(Collector.mergeCoverType(firstCoverage, secondCoverage), equalTo(Coverage.COVERED)); + } + else { + assertThat(Collector.mergeCoverType(firstCoverage, secondCoverage), equalTo(Coverage.NONE)); + } + } + + private static final Map fromCoverages = Map.of( + "fea", Coverage.COVERED, + "arch", Coverage.UNCOVERED, + "req", Coverage.NONE + ); + private static final Map toCoverages = Map.of( + "fea", Coverage.COVERED, + "arch", Coverage.COVERED, + "utest", Coverage.NONE + ); + + /** + * Test that Collector.mergeCoverages with empty from return false and does not change to toCoverage. + */ + @Test + void testMergeCoverages() { + final Map expectedToCoverages = Map.of( + "fea", Coverage.COVERED, + "arch", Coverage.UNCOVERED, + "req", Coverage.NONE, + "utest", Coverage.NONE + ); + + final Map toCoverageT1 = new HashMap<>(toCoverages); + final boolean result = Collector.mergeCoverages(fromCoverages, toCoverageT1); + assertThat(result, is(true)); + assertThat(toCoverageT1, equalTo(expectedToCoverages)); + } + + /** + * Test that Collector.mergeCoverages with empty from return false and does not change to toCoverage. + */ + @Test + void testMergeCoveragesWithEmptyFrom() { + final Map toCoverageT1 = new HashMap<>(toCoverages); + assertThat(Collector.mergeCoverages(null, toCoverageT1), is(false)); + assertThat(toCoverageT1, equalTo(toCoverages)); + } + + /** + * Test updating (merging) an entry into Collector.itemCoverages with testUpdateItemCoverage. + */ + @Test + void testUpdateItemCoverageAddingFirstEntry() { + final LinkedSpecificationItem sampleItem = new Linker(List.of( + item("req~req1", ItemStatus.APPROVED, Set.of("arch")) + )).link().get(0); + final Map sampleCoverages = new HashMap<>(Map.of( + "fea", Coverage.NONE, + "req", Coverage.NONE, + "arch", Coverage.UNCOVERED, + "utest", Coverage.COVERED + )); + + final Collector collector = new Collector().collect(List.of()); + collector.itemCoverages.add(0, sampleCoverages); + collector.updateItemCoverage(0, sampleItem.getArtifactType(), true, sampleCoverages); + + final List> result = collector.getItemCoverages(); + assertThat(result.size(), is(1)); + assertThat(result.get(0), equalTo(Map.of( + "fea", Coverage.NONE, + "req", Coverage.COVERED, + "arch", Coverage.UNCOVERED, + "utest", Coverage.COVERED + ))); } + /** + * Test collected item coverage with {@link #SAMPLE_ITEMS}. + */ @Test void testItemCoverages() { - final Map> coverages = collector.getItemCoverages(); - System.out.println(coverages.entrySet().stream().map(entry -> - String.format("%s%s", entry.getKey().getId(), entry.getValue()) - ).collect(Collectors.joining(",\n"))); + final List> coverages = collector.getItemCoverages(); + System.out.println("0:" + coverages.get(0)); + assertThat(coverages.get(0), + allOf(coverages(Coverage.COVERED, Coverage.COVERED, Coverage.UNCOVERED, Coverage.COVERED))); + System.out.println("1:" + coverages.get(1)); + assertThat(coverages.get(1), + allOf(coverages(Coverage.NONE, Coverage.COVERED, Coverage.COVERED, Coverage.COVERED))); + System.out.println("2:" + coverages.get(2)); + assertThat(coverages.get(2), + allOf(coverages(Coverage.NONE, Coverage.COVERED, Coverage.UNCOVERED, Coverage.NONE))); + System.out.println("3:" + coverages.get(3)); + assertThat(coverages.get(3), + allOf(coverages(Coverage.NONE, Coverage.NONE, Coverage.COVERED, Coverage.COVERED))); + System.out.println("4:" + coverages.get(4)); + assertThat(coverages.get(4), + allOf(coverages(Coverage.NONE, Coverage.NONE, Coverage.COVERED, Coverage.COVERED))); + System.out.println("5:" + coverages.get(5)); + assertThat(coverages.get(5), + allOf(coverages(Coverage.NONE, Coverage.NONE, Coverage.UNCOVERED))); + System.out.println("6:" + coverages.get(6)); + assertThat(coverages.get(6), + allOf(coverages(Coverage.NONE, Coverage.NONE, Coverage.NONE, Coverage.COVERED))); + System.out.println("7:" + coverages.get(7)); + assertThat(coverages.get(7), + allOf(coverages(Coverage.NONE,Coverage.NONE, Coverage.NONE, Coverage.COVERED))); + } + + private static List>> coverages(Coverage... coverage) { + final List stack = new ArrayList<>(Arrays.stream(coverage).toList()); + return ORDERED_SAMPLE_TYPES.stream().map(type -> + hasEntry(type, !stack.isEmpty() ? stack.remove(0) : Coverage.NONE) + ).collect(Collectors.toList()); } + /** + * Test collected item coverage with {@link #LINKED_SAMPLE_ITEMS_CYCLE}. + */ @Test - void testInitializedCoverages() { - final Map coverages = Collector.initializedCoverages(ORDERED_SAMPLE_TYPES); - System.out.println(coverages); + void testItemCoveragesWithCycle() { + final List> coverages = collector.collect(LINKED_SAMPLE_ITEMS_CYCLE).getItemCoverages(); + System.out.println(coverages.stream().map(Object::toString).collect(Collectors.joining(",\n"))); + System.out.println("0:" + coverages.get(0)); + assertThat(coverages.get(0), + allOf(coverages(Coverage.COVERED, Coverage.COVERED, Coverage.UNCOVERED, Coverage.COVERED))); + System.out.println("1:" + coverages.get(1)); + assertThat(coverages.get(1), + allOf(coverages(Coverage.NONE, Coverage.COVERED, Coverage.COVERED, Coverage.COVERED))); + System.out.println("2:" + coverages.get(2)); + assertThat(coverages.get(2), + allOf(coverages(Coverage.NONE, Coverage.COVERED, Coverage.UNCOVERED, Coverage.NONE))); + System.out.println("3:" + coverages.get(3)); + assertThat(coverages.get(3), + allOf(coverages(Coverage.NONE, Coverage.NONE, Coverage.COVERED, Coverage.COVERED))); + System.out.println("4:" + coverages.get(4)); + assertThat(coverages.get(4), + allOf(coverages(Coverage.NONE, Coverage.NONE, Coverage.COVERED, Coverage.COVERED))); + System.out.println("5:" + coverages.get(5)); + assertThat(coverages.get(5), + allOf(coverages(Coverage.NONE, Coverage.NONE, Coverage.UNCOVERED))); + System.out.println("6:" + coverages.get(6)); + assertThat(coverages.get(6), + allOf(coverages(Coverage.NONE, Coverage.NONE, Coverage.COVERED, Coverage.COVERED))); + System.out.println("7:" + coverages.get(7)); + assertThat(coverages.get(7), + allOf(coverages(Coverage.NONE,Coverage.NONE, Coverage.NONE, Coverage.COVERED))); + System.out.println("8:" + coverages.get(8)); + assertThat(coverages.get(8), + allOf(coverages(Coverage.NONE,Coverage.NONE, Coverage.NONE, Coverage.COVERED))); + System.out.println("9:" + coverages.get(8)); + assertThat(coverages.get(9), + allOf(coverages(Coverage.NONE,Coverage.NONE, Coverage.NONE, Coverage.COVERED))); } // From cf796a37eae643d12657788f93ace93a965bfa8d Mon Sep 17 00:00:00 2001 From: Bernd Haberstumpf Date: Sun, 9 Feb 2025 19:45:18 +0100 Subject: [PATCH 03/20] First running version of the UX generators that outputs a JavaScript model model. --- .../openfasttrace/api/ReportSettings.java | 12 +- .../openfasttrace/report/ux/Collector.java | 163 ++++++++++- .../openfasttrace/report/ux/UxReporter.java | 8 + .../report/ux/generator/IGenerator.java | 12 + .../report/ux/generator/JsGenerator.java | 155 ++++++++++ .../report/ux/model/UxModel.java | 100 +++++-- .../report/ux/model/UxSpecItem.java | 268 +++++++++++++----- .../report/ux/CollectorTest.java | 176 +++--------- .../openfasttrace/report/ux/SampleData.java | 117 ++++++++ .../report/ux/generator/JsGeneratorTest.java | 30 ++ 10 files changed, 787 insertions(+), 254 deletions(-) create mode 100644 reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/IGenerator.java create mode 100644 reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/JsGenerator.java create mode 100644 reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/SampleData.java create mode 100644 reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/generator/JsGeneratorTest.java diff --git a/api/src/main/java/org/itsallcode/openfasttrace/api/ReportSettings.java b/api/src/main/java/org/itsallcode/openfasttrace/api/ReportSettings.java index b8af61b7..494dd19c 100644 --- a/api/src/main/java/org/itsallcode/openfasttrace/api/ReportSettings.java +++ b/api/src/main/java/org/itsallcode/openfasttrace/api/ReportSettings.java @@ -19,7 +19,12 @@ public class ReportSettings private final ColorScheme colorScheme; private final DetailsSectionDisplay detailsSectionDisplay; - private ReportSettings(final Builder builder) + /** + * Settings for a reporter. + * + * @param builder builder for a reporter + */ + protected ReportSettings(final Builder builder) { this.verbosity = builder.verbosity; this.showOrigin = builder.showOrigin; @@ -121,7 +126,10 @@ public static class Builder private ReportVerbosity verbosity = ReportVerbosity.FAILURE_DETAILS; private ColorScheme colorScheme = ColorScheme.BLACK_AND_WHITE; - private Builder() + /** + * Create the builder + */ + protected Builder() { // empty by intention } diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java index 670c5d02..2d2aadfe 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java @@ -5,6 +5,8 @@ import org.itsallcode.openfasttrace.report.ux.model.UxModel; import org.itsallcode.openfasttrace.report.ux.model.UxSpecItem; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.*; import java.util.stream.Collectors; @@ -16,20 +18,28 @@ public class Collector { private final List items = new ArrayList<>(); private final List ids = new ArrayList<>(); - private final List uxItems = new ArrayList<>(); private final List allTypes = new ArrayList<>(); private final List orderedTypes = new ArrayList<>(); + private final List tags = new ArrayList<>(); + private final Set uniqueTags = new HashSet<>(); + final List> itemCoverages = new ArrayList<>(); + private final List isCovered = new ArrayList<>(); + + private final List uxItems = new ArrayList<>(); + private UxModel uxModel = null; + public Collector() { } /** * Fill in the caches of the Collector based on the given items. * - * @param specItems {@link LinkedSpecificationItem} model. + * @param specItems + * {@link LinkedSpecificationItem} model. */ public Collector collect(List specItems) { this.items.clear(); @@ -37,6 +47,8 @@ public Collector collect(List specItems) { initializeIndexes(); collectItemCoverages(); + collectUxItems(); + collectUxModel(); return this; } @@ -55,6 +67,13 @@ public List getOrderedTypes() { return orderedTypes; } + /** + * @return all tags of all items. + */ + public List getTags() { + return tags; + } + /** * ItemCoverages provide a shallow coverages for each type of {@link SpecificationItem} type based on the linkage * of a SpecItem. @@ -64,8 +83,20 @@ public List getOrderedTypes() { * * @return list of coverages indexes by {@link LinkedSpecificationItem} handed in to {@link #collect(List)}. */ - public List> getItemCoverages() {return itemCoverages;} + public List> getItemCoverages() { + return itemCoverages; + } + + /** + * @return the meta model of the collected items. + */ + public UxModel getUxModel() { + return uxModel; + } + /** + * @return All {@link UxSpecItem} matching all items given to {@link #collect(List)} + */ public List getUxItems() { return uxItems; } @@ -73,6 +104,85 @@ public List getUxItems() { // // private members + // UxModel + + private void collectUxModel() { + uxModel = UxModel.Builder.builder() + .withProjectName(generateProjectName("")) + .withArtifactTypes(orderedTypes) + .withNumberOfSpecItems(items.size()) + .withUncoveredSpecItems((int)isCovered.stream().filter(covered -> covered).count()) + .withTags(tags) + .withItems(uxItems) + .build(); + } + + private String generateProjectName(final String name) { + final StringBuilder projectName = new StringBuilder(); + projectName.append(LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME).replaceAll(":", ".")); + if( name != null && !name.isEmpty() ) projectName.append(name).append("-"); + + return projectName.toString(); + } + + private void collectUxItems() { + uxItems.clear(); + for( int i = 0; i < items.size(); i++ ) { + uxItems.add(createUxSpecItem(i)); + } + } + + UxSpecItem createUxSpecItem(final int index) { + final LinkedSpecificationItem item = items.get(index); + return UxSpecItem.Builder.builder() + .withIndex(index) + .withTypeIndex(orderedTypes.indexOf(item.getArtifactType())) + .withName(toName(item)) + .withFullName(item.getId().toString()) + .withTagIndex(toTagIndex(item)) + .withNeededTypeIndex(typeToIndex(orderedTypes)) + .withCoveredIndex(toCoveragesIds(index)) + .withCoveringIndex(toItemIndex(item.getLinksByStatus(LinkStatus.COVERS))) + .withCoveredByIndex(toItemIndex(item.getLinksByStatus(LinkStatus.COVERED_SHALLOW))) + .withDependsIndex(toIdIndex(item.getItem().getDependOnIds())) + .withStatusId(item.getItem().getStatus().ordinal()) + //.withPath() + .withItem(item) + .build(); + } + + private List typeToIndex(final List types) { + return types.stream().map(orderedTypes::indexOf).toList(); + } + + private String toName(final LinkedSpecificationItem item) { + final String title = item.getTitle(); + if( title != null && !title.isEmpty() ) return title; + final String name = item.getId().getName(); + final int version = item.getId().getRevision(); + return version > 1 ? name + ":" + version : name; + } + + private List toCoveragesIds(final int index) { + final Map coverages = itemCoverages.get(index); + return orderedTypes.stream().map(type -> { + final Coverage coverage = coverages.get(type); + return coverage != null ? coverage.ordinal() : Coverage.NONE.ordinal(); + }).toList(); + } + + private List toItemIndex(final List items) { + return items.stream().map(item -> ids.indexOf(item.getId())).toList(); + } + + private List toIdIndex(final List ids) { + return ids.stream().map(this.ids::indexOf).filter(id -> id >= 0).toList(); + } + + private List toTagIndex(final LinkedSpecificationItem item) { + return item.getTags().stream().map(tags::indexOf).toList(); + } + // Types and indexes /** @@ -86,15 +196,28 @@ private void initializeIndexes() { ids.clear(); ids.addAll(items.stream().map(LinkedSpecificationItem::getId).toList()); + + tags.clear(); + uniqueTags.clear(); + tags.addAll(collectTags(items, uniqueTags)); } /** * @return Get all types of all specItems. */ - static Set collectAllTypes(List items) { + static Set collectAllTypes(final List items) { return items.stream().map(LinkedSpecificationItem::getArtifactType).collect(Collectors.toSet()); } + static List collectTags(final List items, Set uniqueTags) { + final List tags = new ArrayList<>(); + for( final LinkedSpecificationItem item : items ) { + item.getTags().stream().filter(tag -> !uniqueTags.contains(tag)).forEach(tags::add); + } + + return tags; + } + /** * Provide a list of artifact types sorted by needs dependencies extracted form items. * @@ -178,16 +301,27 @@ void collectItemCoverages() { // Fill coverages for( int i = 0; i < items.size(); i++ ) { - collectItemCoverage(i); + final Map itemCoverage = collectItemCoverage(i); + isCovered.add(collectIsCovered(itemCoverage)); } } + /** + * @param itemCoverage + * collected coverages for an item + * @return true if item is fully covered + */ + private boolean collectIsCovered(final Map itemCoverage) { + return itemCoverage.values().stream().noneMatch(coverage -> coverage == Coverage.UNCOVERED); + } + /** * Calculate the coverages for a given {@link LinkedSpecificationItem}. * The method traverses the tree recursively merging the coverage of all items with the same type * with {@link #mergeCoverages(Map, Map)} - * - * @param index The index within the {@link LinkedSpecificationItem} list. + * + * @param index + * The index within the {@link LinkedSpecificationItem} list. * @return coverages of the item */ Map collectItemCoverage(final int index) { @@ -213,7 +347,7 @@ Map collectItemCoverage(final int index) { // Traverse down for( final LinkedSpecificationItem coveringItem : item.getLinksByStatus(LinkStatus.COVERED_SHALLOW) ) { int coveringIndex = ids.indexOf(coveringItem.getId()); - System.out.println(">>> coveringItem (" +coveringIndex + ")" + coveringItem.getId()); + System.out.println(">>> coveringItem (" + coveringIndex + ")" + coveringItem.getId()); final Map collectedCoverages = collectItemCoverage(coveringIndex); mergeCoverages(collectedCoverages, coverages); } @@ -228,10 +362,14 @@ Map collectItemCoverage(final int index) { /** * Updates the given coverages by setting the coverage of the goven type and updates the {@link #itemCoverages}. * - * @param index The index of the item - * @param artifactType The type of the coverage - * @param covered true if the type is covered - * @param coverages the coverages to update + * @param index + * The index of the item + * @param artifactType + * The type of the coverage + * @param covered + * true if the type is covered + * @param coverages + * the coverages to update * @return the coverages */ Map updateItemCoverage(final int index, @@ -265,7 +403,6 @@ static boolean mergeCoverages(final Map fromCoverages, /** * Merges two coverage types. - * * At least one coverage type is uncovered, result is uncovered, no type on with returns NONE, both covered * returns covered. * diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporter.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporter.java index 7832540f..9c7912e6 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporter.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporter.java @@ -3,6 +3,8 @@ import org.itsallcode.openfasttrace.api.core.Trace; import org.itsallcode.openfasttrace.api.report.Reportable; import org.itsallcode.openfasttrace.api.report.ReporterContext; +import org.itsallcode.openfasttrace.report.ux.generator.IGenerator; +import org.itsallcode.openfasttrace.report.ux.generator.JsGenerator; import java.io.OutputStream; import java.util.logging.Logger; @@ -15,7 +17,10 @@ public class UxReporter implements Reportable private static final Logger LOG = Logger.getLogger(UxReporter.class.getName()); + private final Collector collector; + public UxReporter() { + collector = new Collector(); } /** @@ -26,6 +31,7 @@ public UxReporter() { public UxReporter(final Trace trace, final ReporterContext context) { LOG.info(String.format("constructor(context=%s",context.toString())); + collector = new Collector().collect(trace.getItems()); } /** @@ -35,5 +41,7 @@ public UxReporter(final Trace trace, final ReporterContext context) @Override public void renderToStream(OutputStream outputStream) { LOG.info("renderToStream"); + final IGenerator generator = new JsGenerator(); + generator.generate(outputStream, collector.getUxModel()); } } diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/IGenerator.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/IGenerator.java new file mode 100644 index 00000000..52292524 --- /dev/null +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/IGenerator.java @@ -0,0 +1,12 @@ +package org.itsallcode.openfasttrace.report.ux.generator; + +import org.itsallcode.openfasttrace.report.ux.model.UxModel; + +import java.io.OutputStream; + +public interface IGenerator { + + String type(); + + public void generate(final OutputStream out, final UxModel model); +} diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/JsGenerator.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/JsGenerator.java new file mode 100644 index 00000000..6b580231 --- /dev/null +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/JsGenerator.java @@ -0,0 +1,155 @@ +package org.itsallcode.openfasttrace.report.ux.generator; + +import org.itsallcode.openfasttrace.api.core.Location; +import org.itsallcode.openfasttrace.report.ux.model.UxModel; +import org.itsallcode.openfasttrace.report.ux.model.UxSpecItem; + +import java.io.OutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.stream.Collectors; + +public class JsGenerator implements IGenerator { + + public static final String TYPE = "js"; + + /** + * Indentation spaces + */ + private static final int INDENT = 2; + private static final int LINE_LENGTH = 120; + + private int indent = 0; + private PrintStream out = null; + + public JsGenerator() { + } + + @Override public String type() { + return TYPE; + } + + @Override public void generate(final OutputStream outputStream, final UxModel model) { + out = new PrintStream(outputStream, false, StandardCharsets.UTF_8); + generateHeader(model); + generateMetaData(model); + generateSpecItemsOpen(model); + model.getItems().forEach(this::generateSpecItem); + generateSpecItemsClose(model); + generateFooter(model); + } + + private void generateHeader(final UxModel model) { + printOpen("(function (window,undefined) {"); + printOpen("window.specitem = {"); + } + + private void generateMetaData(final UxModel model) { + printOpen("meta: {"); + println("projectName", model.getProjectName()); + println("types", model.getArtifactTypes()); + println("tags", model.getTags()); + println("item_count", model.getItems().size()); + println("item_covered", model.getItems().size() - model.getUncoveredSpecItems()); + println("item_uncovered", model.getUncoveredSpecItems()); + printClose("},"); + } + + private void generateSpecItemsOpen(final UxModel model) { + printOpen("specitems: ["); + } + + private void generateSpecItem(final UxSpecItem item) { + printOpen("{"); + println("index", item.getIndex()); + println("type", item.getTypeIndex()); + println("name", item.getName()); + println("fullName", item.getFullName()); + println("tags", item.getTagIndex()); + println("version", item.getItem().getRevision()); + println("content", item.getItem().getItem().getDescription()); + println("provides", item.getProvidesIndex()); + println("covered", item.getCoveredIndex()); + println("uncovered", item.getUncoveredIndex()); + println("covering", item.getCoveringIndex()); + println("coveredBy", item.getCoveredByIndex()); + println("depends", item.getDependsIndex()); + println("status", item.getStatusId()); + println("path", item.getPath()); + final Location location = item.getItem().getItem().getLocation(); + println("sourceFile", location != null ? location.getPath() : ""); + println("sourceLine", location != null ? location.getLine() : 0); + println("comments", item.getItem().getItem().getComment()); + printClose("},"); + } + + private void generateSpecItemsClose(final UxModel model) { + printClose("]"); + } + + private void generateFooter(final UxModel model) { + printClose("}"); + printClose("})(window);"); + } + + private void printf(final String format, Object... args) { + out.print(" ".repeat(indent)); + out.printf(format, args); + out.println(); + } + + private void println(final String name, final int value) { + printf("%s: %d,", name, value); + } + + private void println(final String name, final String value) { + printf("%s: %s", name, wrap(value, name.length())); + } + + private void println(final String name, final List values) { + printf("%s: [%s],", + name, + values.stream().map(value -> + ( value instanceof String ) ? "\"" + value + "\"" : value.toString() + ).collect(Collectors.joining(", "))); + } + + private void printOpen(String text) { + out.println(" ".repeat(indent) + text); + indentBegin(); + } + + private void printClose(String text) { + indentEnd(); + out.println(" ".repeat(indent) + text); + } + + private void indentBegin() { + indent += INDENT; + } + + private void indentEnd() { + indent -= INDENT; + } + + private String wrap(final String text, final int offset) { + if( text.length() < ( LINE_LENGTH - offset - INDENT - 2 ) ) return "\"" + text + "\","; + + final StringBuilder b = new StringBuilder(); + b.append(System.lineSeparator()); + + indentBegin(); + final int fragmentLength = LINE_LENGTH - offset - INDENT - 3; + for( int i = 0; i < text.length(); i += fragmentLength ) { + b.append(" ".repeat(indent)); + b.append(i == 0 ? "\"" : "+ \""); + b.append(text, i, Math.min(i + fragmentLength, text.length())); + b.append(( i + fragmentLength ) < text.length() ? "\"" + System.lineSeparator() : "\","); + } + indentEnd(); + + return b.toString(); + + } +} diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxModel.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxModel.java index 449fb7d3..c91291e1 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxModel.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxModel.java @@ -3,31 +3,37 @@ import org.itsallcode.openfasttrace.api.core.SpecificationItem; import java.util.List; +import java.util.Map; /** * Surrounding model that is used to generate the specitem_data model for OpenFastTrace-UX. */ public class UxModel { - private final String name; + private final String projectName; private final List artifactTypes; + private final List tags; + private final int numberOfSpecItems; private final int uncoveredSpecItems; - private UxModel(Builder builder) - { - name = builder.name; + private final List items; + + private UxModel(Builder builder) { + projectName = builder.projectName; artifactTypes = builder.artifactTypes; + tags = builder.tags; numberOfSpecItems = builder.numberOfSpecItems; uncoveredSpecItems = builder.uncoveredSpecItems; + items = builder.items; } /** * @return Name of the project */ - public String getName() + public String getProjectName() { - return name; + return projectName; } /** @@ -54,48 +60,59 @@ public int getUncoveredSpecItems() return uncoveredSpecItems; } + /** + * @return all tags of all items in index order used by {@link UxSpecItem}. + */ + public List getTags() { + return tags; + } + + /** + * @return items within the model + */ + public List getItems() { + return items; + } + /** * {@code UxModel} builder static inner class. */ - public static final class Builder - { - private String name; + public static final class Builder { private List artifactTypes; + private List tags; private int numberOfSpecItems; private int uncoveredSpecItems; + private List items; + private String projectName; - private Builder() - { + private Builder() { } - public static Builder builder() - { + public static Builder builder() { return new Builder(); } /** - * Sets the {@code name} and returns a reference to this Builder enabling method chaining. + * Sets the {@code artifactTypes} and returns a reference to this Builder enabling method chaining. * - * @param name - * the {@code name} to set + * @param artifactTypes + * the {@code artifactTypes} to set * @return a reference to this Builder */ - public Builder withName(String name) - { - this.name = name; + public Builder withArtifactTypes(List artifactTypes) { + this.artifactTypes = artifactTypes; return this; } /** - * Sets the {@code artifactTypes} and returns a reference to this Builder enabling method chaining. + * Sets the {@code tags} and returns a reference to this Builder enabling method chaining. * - * @param artifactTypes - * the {@code artifactTypes} to set + * @param tags + * the {@code tags} to set * @return a reference to this Builder */ - public Builder withArtifactTypes(List artifactTypes) - { - this.artifactTypes = artifactTypes; + public Builder withTags(List tags) { + this.tags = tags; return this; } @@ -106,8 +123,7 @@ public Builder withArtifactTypes(List artifactTypes) * the {@code numberOfSpecItems} to set * @return a reference to this Builder */ - public Builder withNumberOfSpecItems(int numberOfSpecItems) - { + public Builder withNumberOfSpecItems(int numberOfSpecItems) { this.numberOfSpecItems = numberOfSpecItems; return this; } @@ -119,20 +135,42 @@ public Builder withNumberOfSpecItems(int numberOfSpecItems) * the {@code uncoveredSpecItems} to set * @return a reference to this Builder */ - public Builder withUncoveredSpecItems(int uncoveredSpecItems) - { + public Builder withUncoveredSpecItems(int uncoveredSpecItems) { this.uncoveredSpecItems = uncoveredSpecItems; return this; } + /** + * Sets the {@code items} and returns a reference to this Builder enabling method chaining. + * + * @param items + * the {@code items} to set + * @return a reference to this Builder + */ + public Builder withItems(List items) { + this.items = items; + return this; + } + /** * Returns a {@code UxModel} built from the parameters previously set. * * @return a {@code UxModel} built with parameters of this {@code UxModel.Builder} */ - public UxModel build() - { + public UxModel build() { return new UxModel(this); } + + /** + * Sets the {@code projectName} and returns a reference to this Builder enabling method chaining. + * + * @param projectName + * the {@code projectName} to set + * @return a reference to this Builder + */ + public Builder withProjectName(String projectName) { + this.projectName = projectName; + return this; + } } } diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxSpecItem.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxSpecItem.java index 8dcb8b2d..f9a41735 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxSpecItem.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxSpecItem.java @@ -2,83 +2,129 @@ import org.itsallcode.openfasttrace.api.core.LinkedSpecificationItem; +import java.util.ArrayList; import java.util.List; public class UxSpecItem { private final int index; private final int typeIndex; - private final List neededTypeIndexes; + private final String name; + private final String fullName; + private final List tagIndex; + private final List providesIndex; + private final List neededTypeIndex; + private final List coveredIndex; + private final List uncoveredIndex; + private final List coveringIndex; + private final List coveredByIndex; + private final List dependsIndex; private final int statusId; - private final List coveredBy; - private final List covers; + private final List path; private final LinkedSpecificationItem item; - private UxSpecItem(Builder builder) - { + private UxSpecItem(Builder builder) { index = builder.index; typeIndex = builder.typeIndex; - neededTypeIndexes = builder.neededTypeIndexes; + name = builder.name; + fullName = builder.fullName; + tagIndex = builder.tagIndex; + providesIndex = builder.providesIndex; + neededTypeIndex = builder.neededTypeIndex; + coveredIndex = builder.coveredIndex; + uncoveredIndex = builder.uncoveredIndex; + coveringIndex = builder.coveringIndex; + coveredByIndex = builder.coveredByIndex; + dependsIndex = builder.dependsIndex; statusId = builder.statusId; - coveredBy = builder.coveredBy; - covers = builder.covers; + path = builder.path; item = builder.item; } - public int getIndex() - { + public int getIndex() { return index; } - public int getTypeIndex() - { + public int getTypeIndex() { return typeIndex; } - public List getNeededTypeIndexes() - { - return neededTypeIndexes; + public String getName() { + return name; } - public int getStatusId() - { - return statusId; + public String getFullName() { + return fullName; + } + + public List getTagIndex() { + return tagIndex; + } + + public List getProvidesIndex() { + return providesIndex; + } + + public List getNeededTypeIndex() { + return neededTypeIndex; + } + + public List getCoveredIndex() { + return coveredIndex; + } + + public List getUncoveredIndex() { + return uncoveredIndex; } - public List getCoveredBy() - { - return coveredBy; + public List getCoveringIndex() { + return coveringIndex; } - public List getCovers() - { - return covers; + public List getCoveredByIndex() { + return coveredByIndex; } - public LinkedSpecificationItem getItem() - { + public List getDependsIndex() { + return dependsIndex; + } + + public int getStatusId() { + return statusId; + } + + public List getPath() { + return path; + } + + public LinkedSpecificationItem getItem() { return item; } /** * {@code UxSpecItem} builder static inner class. */ - public static final class Builder - { + public static final class Builder { private int index; private int typeIndex; - private List neededTypeIndexes; + private String name; + private String fullName; + private List tagIndex = new ArrayList<>(); + private List providesIndex = new ArrayList<>(); + private List neededTypeIndex; + private List coveredIndex; + private List uncoveredIndex = new ArrayList<>(); + private List coveringIndex; + private List coveredByIndex; + private List dependsIndex = new ArrayList<>(); private int statusId; - private List coveredBy; - private List covers; + private List path = new ArrayList<>(); private LinkedSpecificationItem item; - private Builder() - { + private Builder() { } - public static Builder builder() - { + public static Builder builder() { return new Builder(); } @@ -89,8 +135,7 @@ public static Builder builder() * the {@code index} to set * @return a reference to this Builder */ - public Builder withIndex(int index) - { + public Builder withIndex(int index) { this.index = index; return this; } @@ -102,61 +147,152 @@ public Builder withIndex(int index) * the {@code typeIndex} to set * @return a reference to this Builder */ - public Builder withTypeIndex(int typeIndex) - { + public Builder withTypeIndex(int typeIndex) { this.typeIndex = typeIndex; return this; } /** - * Sets the {@code neededTypeIndexes} and returns a reference to this Builder enabling method chaining. + * Sets the {@code name} and returns a reference to this Builder enabling method chaining. * - * @param neededTypeIndexes - * the {@code neededTypeIndexes} to set + * @param name + * the {@code name} to set * @return a reference to this Builder */ - public Builder withNeededTypeIndexes(List neededTypeIndexes) - { - this.neededTypeIndexes = neededTypeIndexes; + public Builder withName(String name) { + this.name = name; return this; } /** - * Sets the {@code statusId} and returns a reference to this Builder enabling method chaining. + * Sets the {@code fullName} and returns a reference to this Builder enabling method chaining. * - * @param statusId - * the {@code statusId} to set + * @param fullName + * the {@code fullName} to set * @return a reference to this Builder */ - public Builder withStatusId(int statusId) - { - this.statusId = statusId; + public Builder withFullName(String fullName) { + this.fullName = fullName; + return this; + } + + /** + * Sets the {@code tagIndex} and returns a reference to this Builder enabling method chaining. + * + * @param tagIndex + * the {@code tagIndex} to set + * @return a reference to this Builder + */ + public Builder withTagIndex(List tagIndex) { + this.tagIndex = tagIndex; + return this; + } + + /** + * Sets the {@code providesIndex} and returns a reference to this Builder enabling method chaining. + * + * @param providesIndex + * the {@code providesIndex} to set + * @return a reference to this Builder + */ + public Builder withProvidesIndex(List providesIndex) { + this.providesIndex = providesIndex; + return this; + } + + /** + * Sets the {@code neededTypeIndex} and returns a reference to this Builder enabling method chaining. + * + * @param neededTypeIndex + * the {@code neededTypeIndex} to set + * @return a reference to this Builder + */ + public Builder withNeededTypeIndex(List neededTypeIndex) { + this.neededTypeIndex = neededTypeIndex; return this; } /** - * Sets the {@code coveredBy} and returns a reference to this Builder enabling method chaining. + * Sets the {@code coveredIndex} and returns a reference to this Builder enabling method chaining. * - * @param coveredBy - * the {@code coveredBy} to set + * @param coveredIndex + * the {@code coveredIndex} to set * @return a reference to this Builder */ - public Builder withCoveredBy(List coveredBy) - { - this.coveredBy = coveredBy; + public Builder withCoveredIndex(List coveredIndex) { + this.coveredIndex = coveredIndex; + return this; + } + + /** + * Sets the {@code uncoveredIndex} and returns a reference to this Builder enabling method chaining. + * + * @param uncoveredIndex + * the {@code uncoveredIndex} to set + * @return a reference to this Builder + */ + public Builder withUncoveredIndex(List uncoveredIndex) { + this.uncoveredIndex = uncoveredIndex; + return this; + } + + /** + * Sets the {@code coveringIndex} and returns a reference to this Builder enabling method chaining. + * + * @param coveringIndex + * the {@code coveringIndex} to set + * @return a reference to this Builder + */ + public Builder withCoveringIndex(List coveringIndex) { + this.coveringIndex = coveringIndex; + return this; + } + + /** + * Sets the {@code coveredByIndex} and returns a reference to this Builder enabling method chaining. + * + * @param coveredByIndex + * the {@code coveredByIndex} to set + * @return a reference to this Builder + */ + public Builder withCoveredByIndex(List coveredByIndex) { + this.coveredByIndex = coveredByIndex; + return this; + } + + /** + * Sets the {@code dependsIndex} and returns a reference to this Builder enabling method chaining. + * + * @param dependsIndex + * the {@code dependsIndex} to set + * @return a reference to this Builder + */ + public Builder withDependsIndex(List dependsIndex) { + this.dependsIndex = dependsIndex; + return this; + } + + /** + * Sets the {@code statusId} and returns a reference to this Builder enabling method chaining. + * + * @param statusId + * the {@code statusId} to set + * @return a reference to this Builder + */ + public Builder withStatusId(int statusId) { + this.statusId = statusId; return this; } /** - * Sets the {@code covers} and returns a reference to this Builder enabling method chaining. + * Sets the {@code path} and returns a reference to this Builder enabling method chaining. * - * @param covers - * the {@code covers} to set + * @param path + * the {@code path} to set * @return a reference to this Builder */ - public Builder withCovers(List covers) - { - this.covers = covers; + public Builder withPath(List path) { + this.path = path; return this; } @@ -167,8 +303,7 @@ public Builder withCovers(List covers) * the {@code item} to set * @return a reference to this Builder */ - public Builder withItem(LinkedSpecificationItem item) - { + public Builder withItem(LinkedSpecificationItem item) { this.item = item; return this; } @@ -178,8 +313,7 @@ public Builder withItem(LinkedSpecificationItem item) * * @return a {@code UxSpecItem} built with parameters of this {@code UxSpecItem.Builder} */ - public UxSpecItem build() - { + public UxSpecItem build() { return new UxSpecItem(this); } } diff --git a/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/CollectorTest.java b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/CollectorTest.java index 261afce1..6fbb46ee 100644 --- a/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/CollectorTest.java +++ b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/CollectorTest.java @@ -1,22 +1,17 @@ package org.itsallcode.openfasttrace.report.ux; -import org.hamcrest.Matcher; import org.itsallcode.openfasttrace.api.core.ItemStatus; import org.itsallcode.openfasttrace.api.core.LinkedSpecificationItem; -import org.itsallcode.openfasttrace.api.core.SpecificationItem; -import org.itsallcode.openfasttrace.api.core.SpecificationItemId; import org.itsallcode.openfasttrace.core.Linker; import org.itsallcode.openfasttrace.report.ux.model.Coverage; import org.junit.jupiter.api.AfterEach; 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; import org.junit.jupiter.params.provider.MethodSource; import java.util.*; import java.util.stream.Collectors; -import java.util.stream.Stream; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; @@ -24,49 +19,11 @@ class CollectorTest { - /** - * Coverage types in ordered from based on SAMPLE_ITEM linkage - */ - public static final List ORDERED_SAMPLE_TYPES = List.of("fea", "req", "arch", "utest"); - - /** - * Sample for items on all level fea,req,arch,utest with upwards linkes - */ - public static final List SAMPLE_ITEMS = List.of( - item("fea~fea1", ItemStatus.APPROVED, Set.of("req")), - item("req~req1", ItemStatus.APPROVED, Set.of("arch"), Set.of("fea~fea1")), - item("req~req2", ItemStatus.APPROVED, Set.of("arch"), Set.of("fea~fea1")), - item("arch~arch1", ItemStatus.APPROVED, Set.of("utest"), Set.of("req~req1")), - item("arch~arch2", ItemStatus.APPROVED, Set.of("utest"), Set.of("req~req1")), - item("arch~arch3", ItemStatus.APPROVED, Set.of("utest"), Set.of("req~req2")), - item("utest~utest1", ItemStatus.APPROVED, Set.of(), Set.of("arch~arch1")), - item("utest~utest2", ItemStatus.APPROVED, Set.of(), Set.of("arch~arch2")) - ); - - /** - * Sample for items on all level fea,req,arch,utest with a circular link - */ - public static final List SAMPLE_ITEM_CYCLE = List.of( - item("fea~fea1", ItemStatus.APPROVED, Set.of("req")), - item("req~req1", ItemStatus.APPROVED, Set.of("arch"), Set.of("fea~fea1")), - item("req~req2", ItemStatus.APPROVED, Set.of("arch"), Set.of("fea~fea1")), - item("arch~arch1", ItemStatus.APPROVED, Set.of("utest"), Set.of("req~req1")), - item("arch~arch2", ItemStatus.APPROVED, Set.of("utest"), Set.of("req~req1")), - item("arch~arch3", ItemStatus.APPROVED, Set.of("utest"), Set.of("req~req2")), - item("arch~cycle", ItemStatus.APPROVED, Set.of("utest"), Set.of("utest~cycle")), - item("utest~utest1", ItemStatus.APPROVED, Set.of(), Set.of("arch~arch1")), - item("utest~utest2", ItemStatus.APPROVED, Set.of(), Set.of("arch~arch2")), - item("utest~cycle", ItemStatus.APPROVED, Set.of(), Set.of("arch~cycle")) - ); - - private static final List LINKED_SAMPLE_ITEMS = new Linker(SAMPLE_ITEMS).link(); - private static final List LINKED_SAMPLE_ITEMS_CYCLE = new Linker(SAMPLE_ITEM_CYCLE).link(); - private Collector collector = null; @BeforeEach void setUp() { - collector = new Collector().collect(LINKED_SAMPLE_ITEMS); + collector = new Collector().collect(SampleData.LINKED_SAMPLE_ITEMS); } @AfterEach @@ -80,15 +37,15 @@ void tearDown() { */ @Test void testCollectAllTypes() { - final Set types = Collector.collectAllTypes(LINKED_SAMPLE_ITEMS); + final Set types = Collector.collectAllTypes(SampleData.LINKED_SAMPLE_ITEMS); System.out.println("testCollectAllTypes " + types); - assertThat(types, containsInAnyOrder(ORDERED_SAMPLE_TYPES.toArray())); + assertThat(types, containsInAnyOrder(SampleData.ORDERED_SAMPLE_TYPES.toArray())); } @Test void testCollectDependentTypes() { final Map dependencies = Collector.collectDependentTypes( - LINKED_SAMPLE_ITEMS); + SampleData.LINKED_SAMPLE_ITEMS); System.out.println(dependencies); } @@ -99,16 +56,16 @@ void testCollectDependentTypes() { @Test void testCreateOrderedTypes() { // Order sample items with a cycle - final List orderedTypes1 = Collector.createOrderedTypes(LINKED_SAMPLE_ITEMS_CYCLE); + final List orderedTypes1 = Collector.createOrderedTypes(SampleData.LINKED_SAMPLE_ITEMS_CYCLE); System.out.println(String.join(",", orderedTypes1)); - assertThat(orderedTypes1, contains(ORDERED_SAMPLE_TYPES.toArray())); + assertThat(orderedTypes1, contains(SampleData.ORDERED_SAMPLE_TYPES.toArray())); // Order sample items with a cycle in reverse order - final List reverseItems = new ArrayList<>(LINKED_SAMPLE_ITEMS_CYCLE); + final List reverseItems = new ArrayList<>(SampleData.LINKED_SAMPLE_ITEMS_CYCLE); Collections.reverse(reverseItems); final List orderedTypes2 = Collector.createOrderedTypes(reverseItems); System.out.println(String.join(",", orderedTypes2)); - assertThat(orderedTypes2, contains(ORDERED_SAMPLE_TYPES.toArray())); + assertThat(orderedTypes2, contains(SampleData.ORDERED_SAMPLE_TYPES.toArray())); } // Collect coverages @@ -118,7 +75,7 @@ void testCreateOrderedTypes() { */ @Test void testInitializedCoverages() { - final Map coverages = Collector.initializedCoverages(ORDERED_SAMPLE_TYPES); + final Map coverages = Collector.initializedCoverages(SampleData.ORDERED_SAMPLE_TYPES); System.out.println(coverages); assertThat(coverages, allOf( hasEntry("utest", Coverage.NONE), @@ -128,16 +85,6 @@ void testInitializedCoverages() { )); } - /** - * Helper to produce tuples of all permutations of coverage types. - */ - private static Stream provideCoveragePermutations() { - return Arrays.stream(Coverage.values()).flatMap(firstCoverage -> - Arrays.stream(Coverage.values()).map(secondCoverage -> - Arguments.of(firstCoverage, secondCoverage) - )); - } - /** * Tests that Collector.mergeCoverType returns the fitting coverage for all permutations of coverage types. */ @@ -155,17 +102,6 @@ else if( firstCoverage == Coverage.COVERED || secondCoverage == Coverage.COVERED } } - private static final Map fromCoverages = Map.of( - "fea", Coverage.COVERED, - "arch", Coverage.UNCOVERED, - "req", Coverage.NONE - ); - private static final Map toCoverages = Map.of( - "fea", Coverage.COVERED, - "arch", Coverage.COVERED, - "utest", Coverage.NONE - ); - /** * Test that Collector.mergeCoverages with empty from return false and does not change to toCoverage. */ @@ -178,8 +114,8 @@ void testMergeCoverages() { "utest", Coverage.NONE ); - final Map toCoverageT1 = new HashMap<>(toCoverages); - final boolean result = Collector.mergeCoverages(fromCoverages, toCoverageT1); + final Map toCoverageT1 = new HashMap<>(SampleData.toCoverages); + final boolean result = Collector.mergeCoverages(SampleData.fromCoverages, toCoverageT1); assertThat(result, is(true)); assertThat(toCoverageT1, equalTo(expectedToCoverages)); } @@ -189,9 +125,9 @@ void testMergeCoverages() { */ @Test void testMergeCoveragesWithEmptyFrom() { - final Map toCoverageT1 = new HashMap<>(toCoverages); + final Map toCoverageT1 = new HashMap<>(SampleData.toCoverages); assertThat(Collector.mergeCoverages(null, toCoverageT1), is(false)); - assertThat(toCoverageT1, equalTo(toCoverages)); + assertThat(toCoverageT1, equalTo(SampleData.toCoverages)); } /** @@ -200,7 +136,7 @@ void testMergeCoveragesWithEmptyFrom() { @Test void testUpdateItemCoverageAddingFirstEntry() { final LinkedSpecificationItem sampleItem = new Linker(List.of( - item("req~req1", ItemStatus.APPROVED, Set.of("arch")) + SampleData.item("req~req1", ItemStatus.APPROVED, Set.of("arch")) )).link().get(0); final Map sampleCoverages = new HashMap<>(Map.of( "fea", Coverage.NONE, @@ -224,117 +160,75 @@ void testUpdateItemCoverageAddingFirstEntry() { } /** - * Test collected item coverage with {@link #SAMPLE_ITEMS}. + * Test collected item coverage with {@link SampleData#SAMPLE_ITEMS}. */ @Test void testItemCoverages() { final List> coverages = collector.getItemCoverages(); System.out.println("0:" + coverages.get(0)); assertThat(coverages.get(0), - allOf(coverages(Coverage.COVERED, Coverage.COVERED, Coverage.UNCOVERED, Coverage.COVERED))); + allOf(SampleData.coverages(Coverage.COVERED, Coverage.COVERED, Coverage.UNCOVERED, Coverage.COVERED))); System.out.println("1:" + coverages.get(1)); assertThat(coverages.get(1), - allOf(coverages(Coverage.NONE, Coverage.COVERED, Coverage.COVERED, Coverage.COVERED))); + allOf(SampleData.coverages(Coverage.NONE, Coverage.COVERED, Coverage.COVERED, Coverage.COVERED))); System.out.println("2:" + coverages.get(2)); assertThat(coverages.get(2), - allOf(coverages(Coverage.NONE, Coverage.COVERED, Coverage.UNCOVERED, Coverage.NONE))); + allOf(SampleData.coverages(Coverage.NONE, Coverage.COVERED, Coverage.UNCOVERED, Coverage.NONE))); System.out.println("3:" + coverages.get(3)); assertThat(coverages.get(3), - allOf(coverages(Coverage.NONE, Coverage.NONE, Coverage.COVERED, Coverage.COVERED))); + allOf(SampleData.coverages(Coverage.NONE, Coverage.NONE, Coverage.COVERED, Coverage.COVERED))); System.out.println("4:" + coverages.get(4)); assertThat(coverages.get(4), - allOf(coverages(Coverage.NONE, Coverage.NONE, Coverage.COVERED, Coverage.COVERED))); + allOf(SampleData.coverages(Coverage.NONE, Coverage.NONE, Coverage.COVERED, Coverage.COVERED))); System.out.println("5:" + coverages.get(5)); assertThat(coverages.get(5), - allOf(coverages(Coverage.NONE, Coverage.NONE, Coverage.UNCOVERED))); + allOf(SampleData.coverages(Coverage.NONE, Coverage.NONE, Coverage.UNCOVERED))); System.out.println("6:" + coverages.get(6)); assertThat(coverages.get(6), - allOf(coverages(Coverage.NONE, Coverage.NONE, Coverage.NONE, Coverage.COVERED))); + allOf(SampleData.coverages(Coverage.NONE, Coverage.NONE, Coverage.NONE, Coverage.COVERED))); System.out.println("7:" + coverages.get(7)); assertThat(coverages.get(7), - allOf(coverages(Coverage.NONE,Coverage.NONE, Coverage.NONE, Coverage.COVERED))); - } - - private static List>> coverages(Coverage... coverage) { - final List stack = new ArrayList<>(Arrays.stream(coverage).toList()); - return ORDERED_SAMPLE_TYPES.stream().map(type -> - hasEntry(type, !stack.isEmpty() ? stack.remove(0) : Coverage.NONE) - ).collect(Collectors.toList()); + allOf(SampleData.coverages(Coverage.NONE,Coverage.NONE, Coverage.NONE, Coverage.COVERED))); } /** - * Test collected item coverage with {@link #LINKED_SAMPLE_ITEMS_CYCLE}. + * Test collected item coverage with {@link SampleData#LINKED_SAMPLE_ITEMS_CYCLE}. */ @Test void testItemCoveragesWithCycle() { - final List> coverages = collector.collect(LINKED_SAMPLE_ITEMS_CYCLE).getItemCoverages(); + final List> coverages = collector.collect(SampleData.LINKED_SAMPLE_ITEMS_CYCLE).getItemCoverages(); System.out.println(coverages.stream().map(Object::toString).collect(Collectors.joining(",\n"))); System.out.println("0:" + coverages.get(0)); assertThat(coverages.get(0), - allOf(coverages(Coverage.COVERED, Coverage.COVERED, Coverage.UNCOVERED, Coverage.COVERED))); + allOf(SampleData.coverages(Coverage.COVERED, Coverage.COVERED, Coverage.UNCOVERED, Coverage.COVERED))); System.out.println("1:" + coverages.get(1)); assertThat(coverages.get(1), - allOf(coverages(Coverage.NONE, Coverage.COVERED, Coverage.COVERED, Coverage.COVERED))); + allOf(SampleData.coverages(Coverage.NONE, Coverage.COVERED, Coverage.COVERED, Coverage.COVERED))); System.out.println("2:" + coverages.get(2)); assertThat(coverages.get(2), - allOf(coverages(Coverage.NONE, Coverage.COVERED, Coverage.UNCOVERED, Coverage.NONE))); + allOf(SampleData.coverages(Coverage.NONE, Coverage.COVERED, Coverage.UNCOVERED, Coverage.NONE))); System.out.println("3:" + coverages.get(3)); assertThat(coverages.get(3), - allOf(coverages(Coverage.NONE, Coverage.NONE, Coverage.COVERED, Coverage.COVERED))); + allOf(SampleData.coverages(Coverage.NONE, Coverage.NONE, Coverage.COVERED, Coverage.COVERED))); System.out.println("4:" + coverages.get(4)); assertThat(coverages.get(4), - allOf(coverages(Coverage.NONE, Coverage.NONE, Coverage.COVERED, Coverage.COVERED))); + allOf(SampleData.coverages(Coverage.NONE, Coverage.NONE, Coverage.COVERED, Coverage.COVERED))); System.out.println("5:" + coverages.get(5)); assertThat(coverages.get(5), - allOf(coverages(Coverage.NONE, Coverage.NONE, Coverage.UNCOVERED))); + allOf(SampleData.coverages(Coverage.NONE, Coverage.NONE, Coverage.UNCOVERED))); System.out.println("6:" + coverages.get(6)); assertThat(coverages.get(6), - allOf(coverages(Coverage.NONE, Coverage.NONE, Coverage.COVERED, Coverage.COVERED))); + allOf(SampleData.coverages(Coverage.NONE, Coverage.NONE, Coverage.COVERED, Coverage.COVERED))); System.out.println("7:" + coverages.get(7)); assertThat(coverages.get(7), - allOf(coverages(Coverage.NONE,Coverage.NONE, Coverage.NONE, Coverage.COVERED))); + allOf(SampleData.coverages(Coverage.NONE,Coverage.NONE, Coverage.NONE, Coverage.COVERED))); System.out.println("8:" + coverages.get(8)); assertThat(coverages.get(8), - allOf(coverages(Coverage.NONE,Coverage.NONE, Coverage.NONE, Coverage.COVERED))); + allOf(SampleData.coverages(Coverage.NONE,Coverage.NONE, Coverage.NONE, Coverage.COVERED))); System.out.println("9:" + coverages.get(8)); assertThat(coverages.get(9), - allOf(coverages(Coverage.NONE,Coverage.NONE, Coverage.NONE, Coverage.COVERED))); - } - - // - // Helpers - - private static SpecificationItem item(final String id, ItemStatus status, Set needs) { - final SpecificationItemId specId = id(id); - SpecificationItem.Builder builder = SpecificationItem.builder() - .id(specId) - .title("Title " + id) - .description("Descriptive text for " + id) - .status(status); - needs.forEach(builder::addNeedsArtifactType); - - return builder.build(); + allOf(SampleData.coverages(Coverage.NONE,Coverage.NONE, Coverage.NONE, Coverage.COVERED))); } - private static SpecificationItem item(final String id, - ItemStatus status, - Set needs, - Set coverages) { - final SpecificationItemId specId = id(id); - SpecificationItem.Builder builder = SpecificationItem.builder() - .id(specId) - .title("Title " + id) - .description("Descriptive text for " + id) - .status(status); - needs.forEach(builder::addNeedsArtifactType); - coverages.forEach(coverage -> builder.addCoveredId(id(coverage))); - return builder.build(); - } - - private static SpecificationItemId id(final String id) { - return id.matches("/~.*~") ? - new SpecificationItemId.Builder(id).build() : - new SpecificationItemId.Builder(id + "~1").build(); - } } \ No newline at end of file diff --git a/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/SampleData.java b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/SampleData.java new file mode 100644 index 00000000..1a3c3de1 --- /dev/null +++ b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/SampleData.java @@ -0,0 +1,117 @@ +package org.itsallcode.openfasttrace.report.ux; + +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; +import org.itsallcode.openfasttrace.api.core.ItemStatus; +import org.itsallcode.openfasttrace.api.core.LinkedSpecificationItem; +import org.itsallcode.openfasttrace.api.core.SpecificationItem; +import org.itsallcode.openfasttrace.api.core.SpecificationItemId; +import org.itsallcode.openfasttrace.core.Linker; +import org.itsallcode.openfasttrace.report.ux.model.Coverage; +import org.junit.jupiter.params.provider.Arguments; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class SampleData { + /** + * Coverage types in ordered from based on SAMPLE_ITEM linkage + */ + public static final List ORDERED_SAMPLE_TYPES = List.of("fea", "req", "arch", "utest"); + /** + * Sample for items on all level fea,req,arch,utest with upwards linkes + */ + public static final List SAMPLE_ITEMS = List.of( + item("fea~fea1", ItemStatus.APPROVED, Set.of("req")), + item("req~req1", ItemStatus.APPROVED, Set.of("arch"), Set.of("fea~fea1")), + item("req~req2", ItemStatus.APPROVED, Set.of("arch"), Set.of("fea~fea1")), + item("arch~arch1", ItemStatus.APPROVED, Set.of("utest"), Set.of("req~req1")), + item("arch~arch2", ItemStatus.APPROVED, Set.of("utest"), Set.of("req~req1")), + item("arch~arch3", ItemStatus.APPROVED, Set.of("utest"), Set.of("req~req2")), + item("utest~utest1", ItemStatus.APPROVED, Set.of(), Set.of("arch~arch1")), + item("utest~utest2", ItemStatus.APPROVED, Set.of(), Set.of("arch~arch2")) + ); + public static final List LINKED_SAMPLE_ITEMS = new Linker(SAMPLE_ITEMS).link(); + /** + * Sample for items on all level fea,req,arch,utest with a circular link + */ + public static final List SAMPLE_ITEM_CYCLE = List.of( + item("fea~fea1", ItemStatus.APPROVED, Set.of("req")), + item("req~req1", ItemStatus.APPROVED, Set.of("arch"), Set.of("fea~fea1")), + item("req~req2", ItemStatus.APPROVED, Set.of("arch"), Set.of("fea~fea1")), + item("arch~arch1", ItemStatus.APPROVED, Set.of("utest"), Set.of("req~req1")), + item("arch~arch2", ItemStatus.APPROVED, Set.of("utest"), Set.of("req~req1")), + item("arch~arch3", ItemStatus.APPROVED, Set.of("utest"), Set.of("req~req2")), + item("arch~cycle", ItemStatus.APPROVED, Set.of("utest"), Set.of("utest~cycle")), + item("utest~utest1", ItemStatus.APPROVED, Set.of(), Set.of("arch~arch1")), + item("utest~utest2", ItemStatus.APPROVED, Set.of(), Set.of("arch~arch2")), + item("utest~cycle", ItemStatus.APPROVED, Set.of(), Set.of("arch~cycle")) + ); + public static final List LINKED_SAMPLE_ITEMS_CYCLE = new Linker(SAMPLE_ITEM_CYCLE).link(); + public static final Map fromCoverages = Map.of( + "fea", Coverage.COVERED, + "arch", Coverage.UNCOVERED, + "req", Coverage.NONE + ); + public static final Map toCoverages = Map.of( + "fea", Coverage.COVERED, + "arch", Coverage.COVERED, + "utest", Coverage.NONE + ); + + + // + // Helpers + + /** + * Helper to produce tuples of all permutations of coverage types. + */ + public static Stream provideCoveragePermutations() { + return Arrays.stream(Coverage.values()).flatMap(firstCoverage -> + Arrays.stream(Coverage.values()).map(secondCoverage -> + Arguments.of(firstCoverage, secondCoverage) + )); + } + + public static List>> coverages(Coverage... coverage) { + final List stack = new ArrayList<>(Arrays.stream(coverage).toList()); + return ORDERED_SAMPLE_TYPES.stream().map(type -> + Matchers.hasEntry(type, !stack.isEmpty() ? stack.remove(0) : Coverage.NONE) + ).collect(Collectors.toList()); + } + + public static SpecificationItem item(final String id, ItemStatus status, Set needs) { + final SpecificationItemId specId = id(id); + SpecificationItem.Builder builder = SpecificationItem.builder() + .id(specId) + .title("Title " + id) + .description("Descriptive text for " + id) + .status(status); + needs.forEach(builder::addNeedsArtifactType); + + return builder.build(); + } + + public static SpecificationItem item(final String id, + ItemStatus status, + Set needs, + Set coverages) { + final SpecificationItemId specId = id(id); + SpecificationItem.Builder builder = SpecificationItem.builder() + .id(specId) + .title("Title " + id) + .description("Descriptive text for " + id) + .status(status); + needs.forEach(builder::addNeedsArtifactType); + coverages.forEach(coverage -> builder.addCoveredId(id(coverage))); + + return builder.build(); + } + + public static SpecificationItemId id(final String id) { + return id.matches("/~.*~") ? + new SpecificationItemId.Builder(id).build() : + new SpecificationItemId.Builder(id + "~1").build(); + } +} diff --git a/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/generator/JsGeneratorTest.java b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/generator/JsGeneratorTest.java new file mode 100644 index 00000000..8656384c --- /dev/null +++ b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/generator/JsGeneratorTest.java @@ -0,0 +1,30 @@ +package org.itsallcode.openfasttrace.report.ux.generator; + +import org.itsallcode.openfasttrace.report.ux.Collector; +import org.itsallcode.openfasttrace.report.ux.SampleData; +import org.itsallcode.openfasttrace.report.ux.model.UxModel; +import org.junit.jupiter.api.Test; + +import java.io.*; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.*; + +class JsGeneratorTest { + + @Test + void type() { + final IGenerator generator = new JsGenerator(); + assertThat( generator.type(), equalTo(JsGenerator.TYPE )); + + } + + @Test + void generate() { + final UxModel model = new Collector().collect(SampleData.LINKED_SAMPLE_ITEMS).getUxModel(); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + new JsGenerator().generate(out,model); + System.out.println(out.toString()); + } +} \ No newline at end of file From 60bc1e6c1815638035dbfdc5260c81700f436b88 Mon Sep 17 00:00:00 2001 From: Bernd Haberstumpf Date: Sun, 9 Feb 2025 20:36:14 +0100 Subject: [PATCH 04/20] First running version of the UX generators that outputs a JavaScript model model. --- core/src/main/resources/usage.txt | 2 +- .../openfasttrace/report/ux/UxReporter.java | 3 ++- .../report/ux/generator/IGenerator.java | 3 ++- .../report/ux/generator/JsGenerator.java | 4 ++-- .../openfasttrace/report/ux/model/UxModel.java | 4 ++-- .../openfasttrace/report/ux/CollectorTest.java | 15 +++++++++++++-- .../openfasttrace/report/ux/SampleData.java | 15 ++------------- .../report/ux/generator/JsGeneratorTest.java | 8 ++++---- 8 files changed, 28 insertions(+), 26 deletions(-) diff --git a/core/src/main/resources/usage.txt b/core/src/main/resources/usage.txt index f1616092..90c63c2c 100644 --- a/core/src/main/resources/usage.txt +++ b/core/src/main/resources/usage.txt @@ -8,7 +8,7 @@ Commands: convert Convert to a different requirements format Tracing options: - -o, --output-format Report format, one of "plain", "html", "aspec" + -o, --output-format Report format, one of "plain", "html", "aspec", "ux" Defaults to "plain" -v, --report-verbosity Set how verbose the output is. Ranges from "quiet" to "all". diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporter.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporter.java index 9c7912e6..ba70b18f 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporter.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporter.java @@ -44,4 +44,5 @@ public UxReporter(final Trace trace, final ReporterContext context) final IGenerator generator = new JsGenerator(); generator.generate(outputStream, collector.getUxModel()); } -} + +} // UxReporter diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/IGenerator.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/IGenerator.java index 52292524..c5adaef3 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/IGenerator.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/IGenerator.java @@ -9,4 +9,5 @@ public interface IGenerator { String type(); public void generate(final OutputStream out, final UxModel model); -} + +} // IGenerator diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/JsGenerator.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/JsGenerator.java index 6b580231..1cae763e 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/JsGenerator.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/JsGenerator.java @@ -150,6 +150,6 @@ private String wrap(final String text, final int offset) { indentEnd(); return b.toString(); - } -} + +} // JsGenerator diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxModel.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxModel.java index c91291e1..fb91713d 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxModel.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxModel.java @@ -3,7 +3,6 @@ import org.itsallcode.openfasttrace.api.core.SpecificationItem; import java.util.List; -import java.util.Map; /** * Surrounding model that is used to generate the specitem_data model for OpenFastTrace-UX. @@ -173,4 +172,5 @@ public Builder withProjectName(String projectName) { return this; } } -} + +} // UxModel diff --git a/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/CollectorTest.java b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/CollectorTest.java index 6fbb46ee..186513ee 100644 --- a/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/CollectorTest.java +++ b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/CollectorTest.java @@ -8,10 +8,12 @@ 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; import org.junit.jupiter.params.provider.MethodSource; import java.util.*; import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; @@ -85,6 +87,16 @@ void testInitializedCoverages() { )); } + /** + * Helper to produce tuples of all permutations of coverage types. + */ + public static Stream provideCoveragePermutations() { + return Arrays.stream(Coverage.values()).flatMap(firstCoverage -> + Arrays.stream(Coverage.values()).map(secondCoverage -> + Arguments.of(firstCoverage, secondCoverage) + )); + } + /** * Tests that Collector.mergeCoverType returns the fitting coverage for all permutations of coverage types. */ @@ -230,5 +242,4 @@ void testItemCoveragesWithCycle() { allOf(SampleData.coverages(Coverage.NONE,Coverage.NONE, Coverage.NONE, Coverage.COVERED))); } - -} \ No newline at end of file +} // CollectorTest \ No newline at end of file diff --git a/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/SampleData.java b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/SampleData.java index 1a3c3de1..991d1f70 100644 --- a/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/SampleData.java +++ b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/SampleData.java @@ -8,11 +8,9 @@ import org.itsallcode.openfasttrace.api.core.SpecificationItemId; import org.itsallcode.openfasttrace.core.Linker; import org.itsallcode.openfasttrace.report.ux.model.Coverage; -import org.junit.jupiter.params.provider.Arguments; import java.util.*; import java.util.stream.Collectors; -import java.util.stream.Stream; public class SampleData { /** @@ -64,16 +62,6 @@ public class SampleData { // // Helpers - /** - * Helper to produce tuples of all permutations of coverage types. - */ - public static Stream provideCoveragePermutations() { - return Arrays.stream(Coverage.values()).flatMap(firstCoverage -> - Arrays.stream(Coverage.values()).map(secondCoverage -> - Arguments.of(firstCoverage, secondCoverage) - )); - } - public static List>> coverages(Coverage... coverage) { final List stack = new ArrayList<>(Arrays.stream(coverage).toList()); return ORDERED_SAMPLE_TYPES.stream().map(type -> @@ -114,4 +102,5 @@ public static SpecificationItemId id(final String id) { new SpecificationItemId.Builder(id).build() : new SpecificationItemId.Builder(id + "~1").build(); } -} + +} // SampleData diff --git a/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/generator/JsGeneratorTest.java b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/generator/JsGeneratorTest.java index 8656384c..a123d7b8 100644 --- a/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/generator/JsGeneratorTest.java +++ b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/generator/JsGeneratorTest.java @@ -5,11 +5,10 @@ import org.itsallcode.openfasttrace.report.ux.model.UxModel; import org.junit.jupiter.api.Test; -import java.io.*; +import java.io.ByteArrayOutputStream; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; -import static org.junit.jupiter.api.Assertions.*; class JsGeneratorTest { @@ -25,6 +24,7 @@ void generate() { final UxModel model = new Collector().collect(SampleData.LINKED_SAMPLE_ITEMS).getUxModel(); final ByteArrayOutputStream out = new ByteArrayOutputStream(); new JsGenerator().generate(out,model); - System.out.println(out.toString()); + System.out.println(out); } -} \ No newline at end of file + +} // JsGeneratorTest \ No newline at end of file From 1cffd9ab13737e5e21d621be79de44ee20206d27 Mon Sep 17 00:00:00 2001 From: Bernd Haberstumpf Date: Wed, 26 Feb 2025 10:14:14 +0100 Subject: [PATCH 05/20] Various fixes, initial working version of the UxReporter. --- parent/pom.xml | 4 +- .../openfasttrace/report/ux/Collector.java | 9 +++-- .../openfasttrace/report/ux/UxReporter.java | 11 +++--- .../report/ux/generator/JsGenerator.java | 24 ++++++++---- .../report/ux/model/Coverage.java | 14 +++---- .../report/ux/model/UxModel.java | 11 ++++++ .../report/ux/model/UxReporterSettings.java | 38 +++++++++++++++++++ .../report/ux/generator/JsGeneratorTest.java | 7 ++++ 8 files changed, 92 insertions(+), 26 deletions(-) create mode 100644 reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxReporterSettings.java diff --git a/parent/pom.xml b/parent/pom.xml index 14492b26..51ff11e3 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -10,7 +10,7 @@ Free requirement tracking suite https://github.com/itsallcode/openfasttrace - 4.1.0 + 4.2.0 17 5.11.4 3.5.2 @@ -384,7 +384,7 @@ true true - true + false false -html5 diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java index 2d2aadfe..70febb45 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java @@ -77,7 +77,7 @@ public List getTags() { /** * ItemCoverages provide a shallow coverages for each type of {@link SpecificationItem} type based on the linkage * of a SpecItem. - * The linkage tree is flattended, means the shallows coverages of all types merged. Merged means that the type is + * The linkage tree is flattened, means the shallows coverages of all types merged. Merged means that the type is * not part of the tree {@link Coverage#NONE} is returned, {@link Coverage#UNCOVERED} is returned when at least one * item of the type is uncovered. {@link Coverage#COVERED} is returned when all items of a type are covered. * @@ -111,7 +111,7 @@ private void collectUxModel() { .withProjectName(generateProjectName("")) .withArtifactTypes(orderedTypes) .withNumberOfSpecItems(items.size()) - .withUncoveredSpecItems((int)isCovered.stream().filter(covered -> covered).count()) + .withUncoveredSpecItems(items.size() - (int)isCovered.stream().filter(covered -> covered).count()) .withTags(tags) .withItems(uxItems) .build(); @@ -140,7 +140,7 @@ UxSpecItem createUxSpecItem(final int index) { .withName(toName(item)) .withFullName(item.getId().toString()) .withTagIndex(toTagIndex(item)) - .withNeededTypeIndex(typeToIndex(orderedTypes)) + .withNeededTypeIndex(typeToIndex(item.getNeedsArtifactTypes())) .withCoveredIndex(toCoveragesIds(index)) .withCoveringIndex(toItemIndex(item.getLinksByStatus(LinkStatus.COVERS))) .withCoveredByIndex(toItemIndex(item.getLinksByStatus(LinkStatus.COVERED_SHALLOW))) @@ -167,7 +167,7 @@ private List toCoveragesIds(final int index) { final Map coverages = itemCoverages.get(index); return orderedTypes.stream().map(type -> { final Coverage coverage = coverages.get(type); - return coverage != null ? coverage.ordinal() : Coverage.NONE.ordinal(); + return coverage != null ? coverage.getId() : Coverage.NONE.ordinal(); }).toList(); } @@ -302,6 +302,7 @@ void collectItemCoverages() { // Fill coverages for( int i = 0; i < items.size(); i++ ) { final Map itemCoverage = collectItemCoverage(i); + if( !collectIsCovered(itemCoverage)) System.out.println("not covered " + i + " times " + itemCoverage.values().stream().filter(coverage -> coverage == Coverage.UNCOVERED).count()); isCovered.add(collectIsCovered(itemCoverage)); } } diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporter.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporter.java index ba70b18f..290aaca9 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporter.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporter.java @@ -17,11 +17,8 @@ public class UxReporter implements Reportable private static final Logger LOG = Logger.getLogger(UxReporter.class.getName()); - private final Collector collector; - - public UxReporter() { - collector = new Collector(); - } + private final Trace trace; + private final ReporterContext context; /** * @@ -31,7 +28,8 @@ public UxReporter() { public UxReporter(final Trace trace, final ReporterContext context) { LOG.info(String.format("constructor(context=%s",context.toString())); - collector = new Collector().collect(trace.getItems()); + this.trace = trace; + this.context = context; } /** @@ -41,6 +39,7 @@ public UxReporter(final Trace trace, final ReporterContext context) @Override public void renderToStream(OutputStream outputStream) { LOG.info("renderToStream"); + final Collector collector = new Collector().collect(trace.getItems()); final IGenerator generator = new JsGenerator(); generator.generate(outputStream, collector.getUxModel()); } diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/JsGenerator.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/JsGenerator.java index 1cae763e..d88b151e 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/JsGenerator.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/JsGenerator.java @@ -46,10 +46,11 @@ private void generateHeader(final UxModel model) { } private void generateMetaData(final UxModel model) { - printOpen("meta: {"); + printOpen("project: {"); println("projectName", model.getProjectName()); println("types", model.getArtifactTypes()); println("tags", model.getTags()); + println("status",model.getStatusNames()); println("item_count", model.getItems().size()); println("item_covered", model.getItems().size() - model.getUncoveredSpecItems()); println("item_uncovered", model.getUncoveredSpecItems()); @@ -70,6 +71,7 @@ private void generateSpecItem(final UxSpecItem item) { println("version", item.getItem().getRevision()); println("content", item.getItem().getItem().getDescription()); println("provides", item.getProvidesIndex()); + println("needs", item.getNeededTypeIndex()); println("covered", item.getCoveredIndex()); println("uncovered", item.getUncoveredIndex()); println("covering", item.getCoveringIndex()); @@ -134,22 +136,30 @@ private void indentEnd() { } private String wrap(final String text, final int offset) { - if( text.length() < ( LINE_LENGTH - offset - INDENT - 2 ) ) return "\"" + text + "\","; + final String value = quote(text); + if( value.length() < ( LINE_LENGTH - offset - INDENT - 2 ) ) return "'" + value + "',"; final StringBuilder b = new StringBuilder(); b.append(System.lineSeparator()); indentBegin(); final int fragmentLength = LINE_LENGTH - offset - INDENT - 3; - for( int i = 0; i < text.length(); i += fragmentLength ) { + for( int i = 0; i < value.length(); i += fragmentLength ) { b.append(" ".repeat(indent)); - b.append(i == 0 ? "\"" : "+ \""); - b.append(text, i, Math.min(i + fragmentLength, text.length())); - b.append(( i + fragmentLength ) < text.length() ? "\"" + System.lineSeparator() : "\","); + b.append(i == 0 ? "'" : "+ '"); + b.append(value, i, Math.min(i + fragmentLength, value.length())); + b.append(( i + fragmentLength ) < value.length() ? "'" + System.lineSeparator() : "',"); } indentEnd(); return b.toString(); } -} // JsGenerator + private String quote(final String text) { + return text.replace("'", "\\\'") + .replace("<", "<") + .replace(">", ">") + .replaceAll("\n\r?|\r", "
"); + } + +} // JsGenerator \ No newline at end of file diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/Coverage.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/Coverage.java index 41e20b62..ef3a9dea 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/Coverage.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/Coverage.java @@ -2,18 +2,18 @@ public enum Coverage { - COVERED(0), + COVERED(2), UNCOVERED(1), - NONE(2); + NONE(0); - Coverage(int index) { - this.index = index; + Coverage(int id) { + this.id = id; } - private final int index; + private final int id; - public int getIndex() + public int getId() { - return index; + return id; } } diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxModel.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxModel.java index fb91713d..8da396e7 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxModel.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxModel.java @@ -1,7 +1,9 @@ package org.itsallcode.openfasttrace.report.ux.model; +import org.itsallcode.openfasttrace.api.core.ItemStatus; import org.itsallcode.openfasttrace.api.core.SpecificationItem; +import java.util.Arrays; import java.util.List; /** @@ -12,6 +14,7 @@ public class UxModel private final String projectName; private final List artifactTypes; private final List tags; + private final List statusNames; private final int numberOfSpecItems; private final int uncoveredSpecItems; @@ -25,6 +28,7 @@ private UxModel(Builder builder) { numberOfSpecItems = builder.numberOfSpecItems; uncoveredSpecItems = builder.uncoveredSpecItems; items = builder.items; + statusNames = Arrays.stream(ItemStatus.values()).map(Enum::name).toList(); } /** @@ -66,6 +70,13 @@ public List getTags() { return tags; } + /** + * @return The names of the {@link ItemStatus} enum entries. + */ + public List getStatusNames() { + return statusNames; + } + /** * @return items within the model */ diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxReporterSettings.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxReporterSettings.java new file mode 100644 index 00000000..d84937d7 --- /dev/null +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxReporterSettings.java @@ -0,0 +1,38 @@ +package org.itsallcode.openfasttrace.report.ux.model; + +import org.itsallcode.openfasttrace.api.ReportSettings; + +/** + * Extended settings for the reporter + */ +public class UxReporterSettings extends ReportSettings +{ + /** + * @param builder creates the settings + */ + public UxReporterSettings(final Builder builder) + { + super(builder); + } + + /** + * Builds the setting + */ + public static class Builder extends ReportSettings.Builder { + /** + * New builder + */ + public Builder() + { + super(); + } + + /** + * @return the settings + */ + @Override public UxReporterSettings build() + { + return new UxReporterSettings(this); + } + } +} diff --git a/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/generator/JsGeneratorTest.java b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/generator/JsGeneratorTest.java index a123d7b8..ad38a7a5 100644 --- a/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/generator/JsGeneratorTest.java +++ b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/generator/JsGeneratorTest.java @@ -27,4 +27,11 @@ void generate() { System.out.println(out); } + @Test + void regexp() { + String text = "'Users can extend OFT's features with plugins from third parties.'"; + String o = text.replace("'","\\\'").replaceAll("\n\r?|\r", "
"); + System.out.println(o); + } + } // JsGeneratorTest \ No newline at end of file From 8883f2663d2087b0b682f7887315d337c421dc1a Mon Sep 17 00:00:00 2001 From: Bernd Haberstumpf Date: Wed, 26 Feb 2025 11:24:23 +0100 Subject: [PATCH 06/20] Added support for title --- .../openfasttrace/report/ux/Collector.java | 17 +++++-- .../report/ux/generator/JsGenerator.java | 3 +- .../report/ux/model/UxSpecItem.java | 47 +++++++++++++------ 3 files changed, 48 insertions(+), 19 deletions(-) diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java index 70febb45..d8684b0a 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java @@ -137,8 +137,9 @@ UxSpecItem createUxSpecItem(final int index) { return UxSpecItem.Builder.builder() .withIndex(index) .withTypeIndex(orderedTypes.indexOf(item.getArtifactType())) + .withTitle(toTitle(item)) .withName(toName(item)) - .withFullName(item.getId().toString()) + .withId(toId(item)) .withTagIndex(toTagIndex(item)) .withNeededTypeIndex(typeToIndex(item.getNeedsArtifactTypes())) .withCoveredIndex(toCoveragesIds(index)) @@ -155,12 +156,20 @@ private List typeToIndex(final List types) { return types.stream().map(orderedTypes::indexOf).toList(); } - private String toName(final LinkedSpecificationItem item) { + private String toTitle(final LinkedSpecificationItem item ) { final String title = item.getTitle(); - if( title != null && !title.isEmpty() ) return title; + return title != null && !title.isEmpty() ? title : item.getId().getName(); + } + + private String toName( final LinkedSpecificationItem item ) { + return item.getId().getName(); + } + + private String toId(final LinkedSpecificationItem item) { + final String type = item.getId().getArtifactType(); final String name = item.getId().getName(); final int version = item.getId().getRevision(); - return version > 1 ? name + ":" + version : name; + return version > 1 ? type + ":" + name + ":" + version : type + ":" + name; } private List toCoveragesIds(final int index) { diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/JsGenerator.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/JsGenerator.java index d88b151e..8efdb17e 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/JsGenerator.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/JsGenerator.java @@ -65,8 +65,9 @@ private void generateSpecItem(final UxSpecItem item) { printOpen("{"); println("index", item.getIndex()); println("type", item.getTypeIndex()); + println("title", item.getTitle()); println("name", item.getName()); - println("fullName", item.getFullName()); + println("id", item.getId()); println("tags", item.getTagIndex()); println("version", item.getItem().getRevision()); println("content", item.getItem().getItem().getDescription()); diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxSpecItem.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxSpecItem.java index f9a41735..d2993c5e 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxSpecItem.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxSpecItem.java @@ -9,8 +9,9 @@ public class UxSpecItem { private final int index; private final int typeIndex; + private final String title; private final String name; - private final String fullName; + private final String id; private final List tagIndex; private final List providesIndex; private final List neededTypeIndex; @@ -26,8 +27,9 @@ public class UxSpecItem private UxSpecItem(Builder builder) { index = builder.index; typeIndex = builder.typeIndex; + title = builder.title; name = builder.name; - fullName = builder.fullName; + id = builder.id; tagIndex = builder.tagIndex; providesIndex = builder.providesIndex; neededTypeIndex = builder.neededTypeIndex; @@ -49,12 +51,16 @@ public int getTypeIndex() { return typeIndex; } + public String getTitle() { + return title; + } + public String getName() { return name; } - public String getFullName() { - return fullName; + public String getId() { + return id; } public List getTagIndex() { @@ -107,18 +113,19 @@ public LinkedSpecificationItem getItem() { public static final class Builder { private int index; private int typeIndex; + private String title; private String name; - private String fullName; + private String id; private List tagIndex = new ArrayList<>(); - private List providesIndex = new ArrayList<>(); + private List providesIndex = new ArrayList<>(); private List neededTypeIndex; private List coveredIndex; - private List uncoveredIndex = new ArrayList<>(); + private List uncoveredIndex = new ArrayList<>(); private List coveringIndex; private List coveredByIndex; - private List dependsIndex = new ArrayList<>(); + private List dependsIndex = new ArrayList<>(); private int statusId; - private List path = new ArrayList<>(); + private List path = new ArrayList<>(); private LinkedSpecificationItem item; private Builder() { @@ -152,6 +159,18 @@ public Builder withTypeIndex(int typeIndex) { return this; } + /** + * Sets the {@code title} and returns a reference to this Builder enabling method chaining. + * + * @param title + * the {@code title} to set + * @return a reference to this Builder + */ + public Builder withTitle(String title) { + this.title = title; + return this; + } + /** * Sets the {@code name} and returns a reference to this Builder enabling method chaining. * @@ -165,14 +184,14 @@ public Builder withName(String name) { } /** - * Sets the {@code fullName} and returns a reference to this Builder enabling method chaining. + * Sets the {@code id} and returns a reference to this Builder enabling method chaining. * - * @param fullName - * the {@code fullName} to set + * @param id + * the {@code id} to set * @return a reference to this Builder */ - public Builder withFullName(String fullName) { - this.fullName = fullName; + public Builder withId(String id) { + this.id = id; return this; } From f7cad545e6b903674b0acb5c8fbdda5c6f63f8de Mon Sep 17 00:00:00 2001 From: Bernd Haberstumpf Date: Thu, 27 Feb 2025 16:17:59 +0100 Subject: [PATCH 07/20] Fixed generating missing requirements --- .../openfasttrace/report/ux/Collector.java | 42 +++--- .../report/ux/model/Coverage.java | 1 + .../report/ux/CollectorTest.java | 122 +++++++++++------- 3 files changed, 101 insertions(+), 64 deletions(-) diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java index d8684b0a..49d8d1db 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java @@ -311,7 +311,6 @@ void collectItemCoverages() { // Fill coverages for( int i = 0; i < items.size(); i++ ) { final Map itemCoverage = collectItemCoverage(i); - if( !collectIsCovered(itemCoverage)) System.out.println("not covered " + i + " times " + itemCoverage.values().stream().filter(coverage -> coverage == Coverage.UNCOVERED).count()); isCovered.add(collectIsCovered(itemCoverage)); } } @@ -338,7 +337,7 @@ Map collectItemCoverage(final int index) { // Coverage already collected final Map targetCoverage = itemCoverages.get(index); if( targetCoverage != null ) { - System.out.println("<<< already covered index " + index); + //System.out.println("<<< already covered index " + index); return targetCoverage; } @@ -347,25 +346,33 @@ Map collectItemCoverage(final int index) { // End of the tree final LinkedSpecificationItem item = items.get(index); if( item.getNeedsArtifactTypes().isEmpty() ) { - System.out.println("<<< final " + item.getId()); + //System.out.println("<<< final " + item.getId()); return updateItemCoverage(index, item.getArtifactType(), - item.getStatus() == ItemStatus.APPROVED, + item.getStatus() == ItemStatus.APPROVED ? Coverage.COVERED : Coverage.UNCOVERED, coverages); } // Traverse down for( final LinkedSpecificationItem coveringItem : item.getLinksByStatus(LinkStatus.COVERED_SHALLOW) ) { - int coveringIndex = ids.indexOf(coveringItem.getId()); - System.out.println(">>> coveringItem (" + coveringIndex + ")" + coveringItem.getId()); + final int coveringIndex = ids.indexOf(coveringItem.getId()); + //System.out.println(">>> coveringItem (" + coveringIndex + ")" + coveringItem.getId()); final Map collectedCoverages = collectItemCoverage(coveringIndex); mergeCoverages(collectedCoverages, coverages); } // Refresh this coverage - updateItemCoverage(index, item.getArtifactType(), item.isCoveredShallowWithApprovedItems(), coverages); + updateItemCoverage(index, + item.getArtifactType(), + item.isCoveredShallowWithApprovedItems() ? Coverage.COVERED :Coverage.UNCOVERED, + coverages); + + // Refresh needed uncovered types + for( final String uncoveredType : item.getUncoveredApprovedArtifactTypes() ) { + updateItemCoverage(index,uncoveredType,Coverage.MISSING,coverages); + } - System.out.println("<<< intermediate " + item.getId()); + //System.out.println("<<< intermediate " + item.getId()); return coverages; } @@ -376,7 +383,7 @@ Map collectItemCoverage(final int index) { * The index of the item * @param artifactType * The type of the coverage - * @param covered + * @param coverage * true if the type is covered * @param coverages * the coverages to update @@ -384,9 +391,9 @@ Map collectItemCoverage(final int index) { */ Map updateItemCoverage(final int index, final String artifactType, - final boolean covered, + final Coverage coverage, final Map coverages) { - coverages.put(artifactType, covered ? Coverage.COVERED : Coverage.UNCOVERED); + coverages.put(artifactType, coverage); itemCoverages.set(index, coverages); return coverages; } @@ -423,13 +430,10 @@ static boolean mergeCoverages(final Map fromCoverages, * @return merge input coverage */ static Coverage mergeCoverType(Coverage type1, Coverage type2) { - return type1 == org.itsallcode.openfasttrace.report.ux.model.Coverage.UNCOVERED || type2 == org.itsallcode.openfasttrace.report.ux.model.Coverage.UNCOVERED ? - org.itsallcode.openfasttrace.report.ux.model.Coverage.UNCOVERED - : - type1 == org.itsallcode.openfasttrace.report.ux.model.Coverage.COVERED || type2 == org.itsallcode.openfasttrace.report.ux.model.Coverage.COVERED ? - org.itsallcode.openfasttrace.report.ux.model.Coverage.COVERED - : - org.itsallcode.openfasttrace.report.ux.model.Coverage.NONE; + return type1 == Coverage.MISSING || type2 == Coverage.MISSING ? Coverage.MISSING + : type1 == Coverage.UNCOVERED || type2 == Coverage.UNCOVERED ? Coverage.UNCOVERED + : type1 == Coverage.COVERED || type2 == Coverage.COVERED ? Coverage.COVERED + : Coverage.NONE; } /** @@ -439,7 +443,7 @@ static Coverage mergeCoverType(Coverage type1, Coverage type2) { */ static Map initializedCoverages(final List allTypes) { return allTypes.stream().collect( - Collectors.toMap(type -> type, (any) -> org.itsallcode.openfasttrace.report.ux.model.Coverage.NONE)); + Collectors.toMap(type -> type, (any) -> Coverage.NONE)); } } // Collector diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/Coverage.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/Coverage.java index ef3a9dea..48670972 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/Coverage.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/Coverage.java @@ -2,6 +2,7 @@ public enum Coverage { + MISSING(3), COVERED(2), UNCOVERED(1), NONE(0); diff --git a/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/CollectorTest.java b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/CollectorTest.java index 186513ee..3649d169 100644 --- a/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/CollectorTest.java +++ b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/CollectorTest.java @@ -103,7 +103,11 @@ public static Stream provideCoveragePermutations() { @ParameterizedTest @MethodSource( "provideCoveragePermutations" ) void testMergeCoverageType(final Coverage firstCoverage, final Coverage secondCoverage) { - if( ( firstCoverage == Coverage.UNCOVERED || secondCoverage == Coverage.UNCOVERED ) ) { + System.out.println(firstCoverage + ", " + secondCoverage); + if( ( firstCoverage == Coverage.MISSING || secondCoverage == Coverage.MISSING ) ) { + assertThat(Collector.mergeCoverType(firstCoverage, secondCoverage), equalTo(Coverage.MISSING)); + } + else if( ( firstCoverage == Coverage.UNCOVERED || secondCoverage == Coverage.UNCOVERED ) ) { assertThat(Collector.mergeCoverType(firstCoverage, secondCoverage), equalTo(Coverage.UNCOVERED)); } else if( firstCoverage == Coverage.COVERED || secondCoverage == Coverage.COVERED ) { @@ -159,7 +163,7 @@ void testUpdateItemCoverageAddingFirstEntry() { final Collector collector = new Collector().collect(List.of()); collector.itemCoverages.add(0, sampleCoverages); - collector.updateItemCoverage(0, sampleItem.getArtifactType(), true, sampleCoverages); + collector.updateItemCoverage(0, sampleItem.getArtifactType(), Coverage.COVERED, sampleCoverages); final List> result = collector.getItemCoverages(); assertThat(result.size(), is(1)); @@ -176,31 +180,49 @@ void testUpdateItemCoverageAddingFirstEntry() { */ @Test void testItemCoverages() { - final List> coverages = collector.getItemCoverages(); - System.out.println("0:" + coverages.get(0)); + final Coverage[][] expectedCoverages = { + { Coverage.COVERED, Coverage.COVERED, Coverage.UNCOVERED, Coverage.MISSING }, + { Coverage.NONE, Coverage.COVERED, Coverage.COVERED, Coverage.COVERED }, + { Coverage.NONE, Coverage.COVERED, Coverage.UNCOVERED, Coverage.MISSING }, + { Coverage.NONE, Coverage.NONE, Coverage.COVERED, Coverage.COVERED }, + { Coverage.NONE, Coverage.NONE, Coverage.COVERED, Coverage.COVERED }, + { Coverage.NONE, Coverage.NONE, Coverage.UNCOVERED, Coverage.MISSING }, + { Coverage.NONE, Coverage.NONE, Coverage.NONE, Coverage.COVERED }, + { Coverage.NONE, Coverage.NONE, Coverage.NONE, Coverage.COVERED }, + }; + + final List> coverages = collector + .collect(SampleData.LINKED_SAMPLE_ITEMS) + .getItemCoverages(); + validateCoverages(SampleData.LINKED_SAMPLE_ITEMS, coverages, expectedCoverages); +/* + + System.out.println("0:" + coverages.get(0) + " of " + SAMPLE_ITEMS.get(0).getId()); assertThat(coverages.get(0), allOf(SampleData.coverages(Coverage.COVERED, Coverage.COVERED, Coverage.UNCOVERED, Coverage.COVERED))); - System.out.println("1:" + coverages.get(1)); + System.out.println("1:" + coverages.get(1) + " of " + SAMPLE_ITEMS.get(1).getId()); assertThat(coverages.get(1), allOf(SampleData.coverages(Coverage.NONE, Coverage.COVERED, Coverage.COVERED, Coverage.COVERED))); - System.out.println("2:" + coverages.get(2)); + System.out.println("2:" + coverages.get(2) + " of " + SAMPLE_ITEMS.get(2).getId()); assertThat(coverages.get(2), allOf(SampleData.coverages(Coverage.NONE, Coverage.COVERED, Coverage.UNCOVERED, Coverage.NONE))); - System.out.println("3:" + coverages.get(3)); + System.out.println("3:" + coverages.get(3) + " of " + SAMPLE_ITEMS.get(3).getId()); assertThat(coverages.get(3), allOf(SampleData.coverages(Coverage.NONE, Coverage.NONE, Coverage.COVERED, Coverage.COVERED))); - System.out.println("4:" + coverages.get(4)); + System.out.println("4:" + coverages.get(4) + " of " + SAMPLE_ITEMS.get(4).getId()); assertThat(coverages.get(4), allOf(SampleData.coverages(Coverage.NONE, Coverage.NONE, Coverage.COVERED, Coverage.COVERED))); - System.out.println("5:" + coverages.get(5)); + System.out.println("5:" + coverages.get(5) + " of " + SAMPLE_ITEMS.get(5).getId()); assertThat(coverages.get(5), - allOf(SampleData.coverages(Coverage.NONE, Coverage.NONE, Coverage.UNCOVERED))); - System.out.println("6:" + coverages.get(6)); + allOf(SampleData.coverages(Coverage.NONE, Coverage.NONE, Coverage.UNCOVERED, Coverage.MISSING))); + System.out.println("6:" + coverages.get(6) + " of " + SAMPLE_ITEMS.get(6).getId()); assertThat(coverages.get(6), allOf(SampleData.coverages(Coverage.NONE, Coverage.NONE, Coverage.NONE, Coverage.COVERED))); - System.out.println("7:" + coverages.get(7)); + System.out.println("7:" + coverages.get(7) + " of " + SAMPLE_ITEMS.get(7).getId()); assertThat(coverages.get(7), - allOf(SampleData.coverages(Coverage.NONE,Coverage.NONE, Coverage.NONE, Coverage.COVERED))); + allOf(SampleData.coverages(Coverage.NONE, Coverage.NONE, Coverage.NONE, Coverage.COVERED))); + + */ } /** @@ -208,38 +230,48 @@ void testItemCoverages() { */ @Test void testItemCoveragesWithCycle() { - final List> coverages = collector.collect(SampleData.LINKED_SAMPLE_ITEMS_CYCLE).getItemCoverages(); - System.out.println(coverages.stream().map(Object::toString).collect(Collectors.joining(",\n"))); - System.out.println("0:" + coverages.get(0)); - assertThat(coverages.get(0), - allOf(SampleData.coverages(Coverage.COVERED, Coverage.COVERED, Coverage.UNCOVERED, Coverage.COVERED))); - System.out.println("1:" + coverages.get(1)); - assertThat(coverages.get(1), - allOf(SampleData.coverages(Coverage.NONE, Coverage.COVERED, Coverage.COVERED, Coverage.COVERED))); - System.out.println("2:" + coverages.get(2)); - assertThat(coverages.get(2), - allOf(SampleData.coverages(Coverage.NONE, Coverage.COVERED, Coverage.UNCOVERED, Coverage.NONE))); - System.out.println("3:" + coverages.get(3)); - assertThat(coverages.get(3), - allOf(SampleData.coverages(Coverage.NONE, Coverage.NONE, Coverage.COVERED, Coverage.COVERED))); - System.out.println("4:" + coverages.get(4)); - assertThat(coverages.get(4), - allOf(SampleData.coverages(Coverage.NONE, Coverage.NONE, Coverage.COVERED, Coverage.COVERED))); - System.out.println("5:" + coverages.get(5)); - assertThat(coverages.get(5), - allOf(SampleData.coverages(Coverage.NONE, Coverage.NONE, Coverage.UNCOVERED))); - System.out.println("6:" + coverages.get(6)); - assertThat(coverages.get(6), - allOf(SampleData.coverages(Coverage.NONE, Coverage.NONE, Coverage.COVERED, Coverage.COVERED))); - System.out.println("7:" + coverages.get(7)); - assertThat(coverages.get(7), - allOf(SampleData.coverages(Coverage.NONE,Coverage.NONE, Coverage.NONE, Coverage.COVERED))); - System.out.println("8:" + coverages.get(8)); - assertThat(coverages.get(8), - allOf(SampleData.coverages(Coverage.NONE,Coverage.NONE, Coverage.NONE, Coverage.COVERED))); - System.out.println("9:" + coverages.get(8)); - assertThat(coverages.get(9), - allOf(SampleData.coverages(Coverage.NONE,Coverage.NONE, Coverage.NONE, Coverage.COVERED))); + final Coverage[][] expectedCoverages = { + { Coverage.COVERED, Coverage.COVERED, Coverage.UNCOVERED, Coverage.MISSING }, + { Coverage.NONE, Coverage.COVERED, Coverage.COVERED, Coverage.COVERED }, + { Coverage.NONE, Coverage.COVERED, Coverage.UNCOVERED, Coverage.MISSING }, + { Coverage.NONE, Coverage.NONE, Coverage.COVERED, Coverage.COVERED }, + { Coverage.NONE, Coverage.NONE, Coverage.COVERED, Coverage.COVERED }, + { Coverage.NONE, Coverage.NONE, Coverage.UNCOVERED, Coverage.MISSING }, + { Coverage.NONE, Coverage.NONE, Coverage.COVERED, Coverage.COVERED }, + { Coverage.NONE, Coverage.NONE, Coverage.NONE, Coverage.COVERED }, + { Coverage.NONE, Coverage.NONE, Coverage.NONE, Coverage.COVERED }, + { Coverage.NONE, Coverage.NONE, Coverage.NONE, Coverage.COVERED } + }; + + final List> coverages = collector + .collect(SampleData.LINKED_SAMPLE_ITEMS_CYCLE) + .getItemCoverages(); + validateCoverages(SampleData.LINKED_SAMPLE_ITEMS_CYCLE, coverages, expectedCoverages); + } + + private void validateCoverages(final List specItems, + final List> returnedCoverages, + final Coverage[][] expectedCoverages) { + for( int index = 0; index < expectedCoverages.length; index++ ) { + validateCoverage(index, specItems, returnedCoverages, expectedCoverages[index]); + } + } + + + // + // Helper + + private void validateCoverage(int index, + List specItems, + List> returnedCoverages, + Coverage... expectedCoverages) { + final LinkedSpecificationItem specItem = specItems.get(index); + final Map returnedCoverage = returnedCoverages.get(index); + System.out.printf("%d: %s%s matching {%s}\n", index, + specItem.getArtifactType(), + returnedCoverage, + Arrays.stream(expectedCoverages).map(Coverage::toString).collect(Collectors.joining(","))); + assertThat(returnedCoverage, allOf(SampleData.coverages(expectedCoverages))); } } // CollectorTest \ No newline at end of file From 5edb9433ee45875e8a6891f5d9471d9253dff4d1 Mon Sep 17 00:00:00 2001 From: Bernd Haberstumpf Date: Fri, 28 Feb 2025 19:42:53 +0100 Subject: [PATCH 08/20] Added uncoveredIndex --- .../openfasttrace/report/ux/Collector.java | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java index 49d8d1db..1d529c1a 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java @@ -88,7 +88,7 @@ public List> getItemCoverages() { } /** - * @return the meta model of the collected items. + * @return the metamodel of the collected items. */ public UxModel getUxModel() { return uxModel; @@ -143,6 +143,7 @@ UxSpecItem createUxSpecItem(final int index) { .withTagIndex(toTagIndex(item)) .withNeededTypeIndex(typeToIndex(item.getNeedsArtifactTypes())) .withCoveredIndex(toCoveragesIds(index)) + .withUncoveredIndex(toUncoveredIndexes(index)) .withCoveringIndex(toItemIndex(item.getLinksByStatus(LinkStatus.COVERS))) .withCoveredByIndex(toItemIndex(item.getLinksByStatus(LinkStatus.COVERED_SHALLOW))) .withDependsIndex(toIdIndex(item.getItem().getDependOnIds())) @@ -176,10 +177,24 @@ private List toCoveragesIds(final int index) { final Map coverages = itemCoverages.get(index); return orderedTypes.stream().map(type -> { final Coverage coverage = coverages.get(type); - return coverage != null ? coverage.getId() : Coverage.NONE.ordinal(); + return coverage != null ? coverage.getId() : Coverage.NONE.getId(); }).toList(); } + private List toUncoveredIndexes(final int index) { + final Map coverages = itemCoverages.get(index); + final List uncoveredIndexes = new ArrayList<>(); + int i = 0; + for( final String type : orderedTypes ) { + final Coverage coverage = coverages.get(type); + if( ( coverage == Coverage.UNCOVERED || coverage == Coverage.MISSING ) ) { + uncoveredIndexes.add(i); + } + i++; + } + return uncoveredIndexes; + } + private List toItemIndex(final List items) { return items.stream().map(item -> ids.indexOf(item.getId())).toList(); } From 3411fcf0af8c61473bde8affdd03b5d3c75a63a0 Mon Sep 17 00:00:00 2001 From: Bernd Haberstumpf Date: Sat, 1 Mar 2025 11:28:45 +0100 Subject: [PATCH 09/20] Added uncovered specitem index list to ux-generator output --- .../openfasttrace/report/ux/Collector.java | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java index 1d529c1a..4e784de1 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java @@ -111,7 +111,7 @@ private void collectUxModel() { .withProjectName(generateProjectName("")) .withArtifactTypes(orderedTypes) .withNumberOfSpecItems(items.size()) - .withUncoveredSpecItems(items.size() - (int)isCovered.stream().filter(covered -> covered).count()) + .withUncoveredSpecItems(items.size() - (int) isCovered.stream().filter(covered -> covered).count()) .withTags(tags) .withItems(uxItems) .build(); @@ -141,6 +141,7 @@ UxSpecItem createUxSpecItem(final int index) { .withName(toName(item)) .withId(toId(item)) .withTagIndex(toTagIndex(item)) + .withProvidesIndex(getProvidesTypeIndex(item)) .withNeededTypeIndex(typeToIndex(item.getNeedsArtifactTypes())) .withCoveredIndex(toCoveragesIds(index)) .withUncoveredIndex(toUncoveredIndexes(index)) @@ -157,12 +158,11 @@ private List typeToIndex(final List types) { return types.stream().map(orderedTypes::indexOf).toList(); } - private String toTitle(final LinkedSpecificationItem item ) { - final String title = item.getTitle(); - return title != null && !title.isEmpty() ? title : item.getId().getName(); + private String toTitle(final LinkedSpecificationItem item) { + return item.getTitleWithFallback(); } - private String toName( final LinkedSpecificationItem item ) { + private String toName(final LinkedSpecificationItem item) { return item.getId().getName(); } @@ -173,6 +173,11 @@ private String toId(final LinkedSpecificationItem item) { return version > 1 ? type + ":" + name + ":" + version : type + ":" + name; } + private List getProvidesTypeIndex( final LinkedSpecificationItem item ) { + List uplinks = item.getLinks().getOrDefault(LinkStatus.COVERS,List.of()); + return typeToIndex(uplinks.stream().map(LinkedSpecificationItem::getArtifactType).toList()); + } + private List toCoveragesIds(final int index) { final Map coverages = itemCoverages.get(index); return orderedTypes.stream().map(type -> { @@ -311,10 +316,11 @@ static Map collectDependentTypes(final List collectItemCoverage(final int index) { // Refresh this coverage updateItemCoverage(index, item.getArtifactType(), - item.isCoveredShallowWithApprovedItems() ? Coverage.COVERED :Coverage.UNCOVERED, + item.isCoveredShallowWithApprovedItems() ? Coverage.COVERED : Coverage.UNCOVERED, coverages); // Refresh needed uncovered types for( final String uncoveredType : item.getUncoveredApprovedArtifactTypes() ) { - updateItemCoverage(index,uncoveredType,Coverage.MISSING,coverages); + updateItemCoverage(index, uncoveredType, Coverage.MISSING, coverages); } //System.out.println("<<< intermediate " + item.getId()); @@ -392,7 +398,7 @@ Map collectItemCoverage(final int index) { } /** - * Updates the given coverages by setting the coverage of the goven type and updates the {@link #itemCoverages}. + * Updates the given coverages by setting the coverage of the given type and updates the {@link #itemCoverages}. * * @param index * The index of the item @@ -448,7 +454,7 @@ static Coverage mergeCoverType(Coverage type1, Coverage type2) { return type1 == Coverage.MISSING || type2 == Coverage.MISSING ? Coverage.MISSING : type1 == Coverage.UNCOVERED || type2 == Coverage.UNCOVERED ? Coverage.UNCOVERED : type1 == Coverage.COVERED || type2 == Coverage.COVERED ? Coverage.COVERED - : Coverage.NONE; + : Coverage.NONE; } /** From 4017db8d91a6b27ac70fb2bb6de23694bd3710ed Mon Sep 17 00:00:00 2001 From: Bernd Haberstumpf Date: Thu, 6 Mar 2025 22:25:23 +0100 Subject: [PATCH 10/20] Added OpenFastTrace-UX comment to the main README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 10af627d..3d62aef1 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ Below you see a screenshot of an HTML tracing report where OFT traces itself. Yo OFT HTML tracing report +In addition to the HTML tracing report an interactive requirement browser and analysis tool is integrated into OpenFastTrace. + ## Project Information [![Build](https://github.com/itsallcode/openfasttrace/actions/workflows/build.yml/badge.svg)](https://github.com/itsallcode/openfasttrace/actions/workflows/build.yml) From 8fa494b6607025c418f77b483fa9f886c049fd05 Mon Sep 17 00:00:00 2001 From: Bernd Haberstumpf Date: Thu, 6 Mar 2025 22:41:55 +0100 Subject: [PATCH 11/20] Added OpenFastTrace-UX comment to the user documentation. --- doc/user_guide.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/doc/user_guide.md b/doc/user_guide.md index 89afea1f..2635ec79 100644 --- a/doc/user_guide.md +++ b/doc/user_guide.md @@ -462,6 +462,18 @@ While plain text reports are perfect for debugging your tracing chain, sometimes oft trace -o html ``` +### Interactive requirement analyisis + +Besides a basic HTML visualization of requirements OpenFastTrace also provides an interactive requirement browsing and requirement analysis frontend in the form of a responsive HTML page similar to the HTML report. + +The UX reporter: + +``` +oft trace -o ux +``` + +generates an input file for the OpenFastTrace-UX HTML frontend [OpenFastTrace-UX](https://github.com/poldi2015/openfasttrace-ux). + ### Understanding and Fixing Broken Requirement Branches Requirements — or specification items as we call them more broadly — in OFT are internally organized in a graph. If you haven't heard of that term, don't worry. In most cases it is close enough to think of the relationships between the specification items like a forest where the highest level of the specification are tree trunks from which details branch out into big branches, twigs and eventually leaves. @@ -568,6 +580,7 @@ One of: * `plain` * `html` * `aspec` +* `ux` Defaults to `plain`. From 880ac8dc31f738afbbce728da8ca9d64f017c628 Mon Sep 17 00:00:00 2001 From: Bernd Haberstumpf Date: Sun, 9 Mar 2025 10:59:03 +0100 Subject: [PATCH 12/20] Added support for setting the projectName via an environment variable --- .../openfasttrace/report/ux/Collector.java | 1 + .../openfasttrace/report/ux/UxReporter.java | 27 +++++++- .../report/ux/model/UxModel.java | 62 +++++++++++++++---- 3 files changed, 75 insertions(+), 15 deletions(-) diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java index 4e784de1..60d12074 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java @@ -113,6 +113,7 @@ private void collectUxModel() { .withNumberOfSpecItems(items.size()) .withUncoveredSpecItems(items.size() - (int) isCovered.stream().filter(covered -> covered).count()) .withTags(tags) + .withStatusNames(Arrays.stream(ItemStatus.values()).map(ItemStatus::toString).toList()) .withItems(uxItems) .build(); } diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporter.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporter.java index 290aaca9..2e0aaddd 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporter.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporter.java @@ -1,10 +1,12 @@ package org.itsallcode.openfasttrace.report.ux; +import org.itsallcode.openfasttrace.api.ReportSettings; import org.itsallcode.openfasttrace.api.core.Trace; import org.itsallcode.openfasttrace.api.report.Reportable; import org.itsallcode.openfasttrace.api.report.ReporterContext; import org.itsallcode.openfasttrace.report.ux.generator.IGenerator; import org.itsallcode.openfasttrace.report.ux.generator.JsGenerator; +import org.itsallcode.openfasttrace.report.ux.model.UxModel; import java.io.OutputStream; import java.util.logging.Logger; @@ -18,7 +20,7 @@ public class UxReporter implements Reportable private static final Logger LOG = Logger.getLogger(UxReporter.class.getName()); private final Trace trace; - private final ReporterContext context; + private final ReportSettings settings; /** * @@ -29,7 +31,7 @@ public UxReporter(final Trace trace, final ReporterContext context) { LOG.info(String.format("constructor(context=%s",context.toString())); this.trace = trace; - this.context = context; + this.settings = context.getSettings(); } /** @@ -41,7 +43,26 @@ public UxReporter(final Trace trace, final ReporterContext context) LOG.info("renderToStream"); final Collector collector = new Collector().collect(trace.getItems()); final IGenerator generator = new JsGenerator(); - generator.generate(outputStream, collector.getUxModel()); + generator.generate(outputStream, extendModel(collector.getUxModel(), settings)); + } + + /** + * Adjusts + * + * @param uxModel The collected model + * @return uxModel extended via setting + */ + private static UxModel extendModel( final UxModel uxModel, final ReportSettings settings ) { + final UxModel.Builder uxModelBuilder = UxModel.builder(uxModel); + + // Add project name prefix if set + final String projectName = System.getProperty("oftProjectName"); + if( projectName != null ) + { + uxModelBuilder.withProjectName(projectName + " " + uxModel.getProjectName() ); + } + + return uxModelBuilder.build(); } } // UxReporter diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxModel.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxModel.java index 8da396e7..3745b273 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxModel.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxModel.java @@ -21,14 +21,28 @@ public class UxModel private final List items; - private UxModel(Builder builder) { + private UxModel(Builder builder) + { projectName = builder.projectName; artifactTypes = builder.artifactTypes; tags = builder.tags; + statusNames = builder.statusNames; numberOfSpecItems = builder.numberOfSpecItems; uncoveredSpecItems = builder.uncoveredSpecItems; items = builder.items; - statusNames = Arrays.stream(ItemStatus.values()).map(Enum::name).toList(); + } + + public static Builder builder(UxModel copy) + { + Builder builder = new Builder(); + builder.projectName = copy.getProjectName(); + builder.artifactTypes = copy.getArtifactTypes(); + builder.tags = copy.getTags(); + builder.statusNames = copy.getStatusNames(); + builder.numberOfSpecItems = copy.getNumberOfSpecItems(); + builder.uncoveredSpecItems = copy.getUncoveredSpecItems(); + builder.items = copy.getItems(); + return builder; } /** @@ -87,18 +101,22 @@ public List getItems() { /** * {@code UxModel} builder static inner class. */ - public static final class Builder { + public static final class Builder + { private List artifactTypes; private List tags; + private List statusNames; private int numberOfSpecItems; private int uncoveredSpecItems; private List items; private String projectName; - private Builder() { + private Builder() + { } - public static Builder builder() { + public static Builder builder() + { return new Builder(); } @@ -109,7 +127,8 @@ public static Builder builder() { * the {@code artifactTypes} to set * @return a reference to this Builder */ - public Builder withArtifactTypes(List artifactTypes) { + public Builder withArtifactTypes(List artifactTypes) + { this.artifactTypes = artifactTypes; return this; } @@ -121,11 +140,25 @@ public Builder withArtifactTypes(List artifactTypes) { * the {@code tags} to set * @return a reference to this Builder */ - public Builder withTags(List tags) { + public Builder withTags(List tags) + { this.tags = tags; return this; } + /** + * Sets the {@code statusNames} and returns a reference to this Builder enabling method chaining. + * + * @param statusNames + * the {@code statusNames} to set + * @return a reference to this Builder + */ + public Builder withStatusNames(List statusNames) + { + this.statusNames = statusNames; + return this; + } + /** * Sets the {@code numberOfSpecItems} and returns a reference to this Builder enabling method chaining. * @@ -133,7 +166,8 @@ public Builder withTags(List tags) { * the {@code numberOfSpecItems} to set * @return a reference to this Builder */ - public Builder withNumberOfSpecItems(int numberOfSpecItems) { + public Builder withNumberOfSpecItems(int numberOfSpecItems) + { this.numberOfSpecItems = numberOfSpecItems; return this; } @@ -145,7 +179,8 @@ public Builder withNumberOfSpecItems(int numberOfSpecItems) { * the {@code uncoveredSpecItems} to set * @return a reference to this Builder */ - public Builder withUncoveredSpecItems(int uncoveredSpecItems) { + public Builder withUncoveredSpecItems(int uncoveredSpecItems) + { this.uncoveredSpecItems = uncoveredSpecItems; return this; } @@ -157,7 +192,8 @@ public Builder withUncoveredSpecItems(int uncoveredSpecItems) { * the {@code items} to set * @return a reference to this Builder */ - public Builder withItems(List items) { + public Builder withItems(List items) + { this.items = items; return this; } @@ -167,7 +203,8 @@ public Builder withItems(List items) { * * @return a {@code UxModel} built with parameters of this {@code UxModel.Builder} */ - public UxModel build() { + public UxModel build() + { return new UxModel(this); } @@ -178,7 +215,8 @@ public UxModel build() { * the {@code projectName} to set * @return a reference to this Builder */ - public Builder withProjectName(String projectName) { + public Builder withProjectName(String projectName) + { this.projectName = projectName; return this; } From f11db4ef90fc2d79533690241b9b87f01a35e829 Mon Sep 17 00:00:00 2001 From: Bernd Haberstumpf Date: Sun, 9 Mar 2025 11:02:57 +0100 Subject: [PATCH 13/20] Added support for setting the projectName via an environment variable or Java property --- .../itsallcode/openfasttrace/report/ux/UxReporter.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporter.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporter.java index 2e0aaddd..dc8054c7 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporter.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporter.java @@ -56,10 +56,12 @@ private static UxModel extendModel( final UxModel uxModel, final ReportSettings final UxModel.Builder uxModelBuilder = UxModel.builder(uxModel); // Add project name prefix if set - final String projectName = System.getProperty("oftProjectName"); - if( projectName != null ) + final String projectNameEnvironment = System.getenv("oftProjectName"); + final String projectNameProperty = System.getProperty("oftProjectName"); + if (projectNameEnvironment != null || projectNameProperty != null) { - uxModelBuilder.withProjectName(projectName + " " + uxModel.getProjectName() ); + final String projectName = projectNameEnvironment != null ? projectNameEnvironment : projectNameProperty; + uxModelBuilder.withProjectName(projectName + " " + uxModel.getProjectName()); } return uxModelBuilder.build(); From 4bd126d90915bb5907d7bd8b6e836431c55989d3 Mon Sep 17 00:00:00 2001 From: Bernd Haberstumpf Date: Sun, 9 Mar 2025 12:14:54 +0100 Subject: [PATCH 14/20] Added support for project name to the UX reporter --- .../org/itsallcode/openfasttrace/report/ux/UxReporter.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporter.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporter.java index dc8054c7..9880a092 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporter.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporter.java @@ -56,12 +56,13 @@ private static UxModel extendModel( final UxModel uxModel, final ReportSettings final UxModel.Builder uxModelBuilder = UxModel.builder(uxModel); // Add project name prefix if set - final String projectNameEnvironment = System.getenv("oftProjectName"); + String projectNameEnvironment = System.getenv("oftProjectName"); + if( "".equals(projectNameEnvironment)) projectNameEnvironment = null; final String projectNameProperty = System.getProperty("oftProjectName"); - if (projectNameEnvironment != null || projectNameProperty != null) + if ( projectNameEnvironment != null || projectNameProperty != null) { final String projectName = projectNameEnvironment != null ? projectNameEnvironment : projectNameProperty; - uxModelBuilder.withProjectName(projectName + " " + uxModel.getProjectName()); + uxModelBuilder.withProjectName(projectName + " (" + uxModel.getProjectName() + ")"); } return uxModelBuilder.build(); From 30f6419f3c71055877af013d6c7940bdfd59f5c4 Mon Sep 17 00:00:00 2001 From: Bernd Haberstumpf Date: Thu, 13 Mar 2025 23:11:49 +0100 Subject: [PATCH 15/20] Added calculation of type, uncovered status and tag Count --- .../openfasttrace/report/ux/Collector.java | 113 ++++++++++++++++-- .../report/ux/generator/JsGenerator.java | 4 + .../report/ux/model/UxModel.java | 102 ++++++++++++++++ 3 files changed, 209 insertions(+), 10 deletions(-) diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java index 60d12074..feaf9e03 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java @@ -23,13 +23,20 @@ public class Collector { private final List orderedTypes = new ArrayList<>(); private final List tags = new ArrayList<>(); - private final Set uniqueTags = new HashSet<>(); + private final List tagCount = new ArrayList<>(); final List> itemCoverages = new ArrayList<>(); - private final List isCovered = new ArrayList<>(); + private final List isDeepCovered = new ArrayList<>(); private final List uxItems = new ArrayList<>(); + + private final List typeCount = new ArrayList<>(); + + private final List uncoveredCounts = new ArrayList<>(); + + private final List statusCount = new ArrayList<>(); + private UxModel uxModel = null; public Collector() { @@ -111,9 +118,13 @@ private void collectUxModel() { .withProjectName(generateProjectName("")) .withArtifactTypes(orderedTypes) .withNumberOfSpecItems(items.size()) - .withUncoveredSpecItems(items.size() - (int) isCovered.stream().filter(covered -> covered).count()) + .withUncoveredSpecItems(items.size() - (int) isDeepCovered.stream().filter(covered -> covered).count()) .withTags(tags) .withStatusNames(Arrays.stream(ItemStatus.values()).map(ItemStatus::toString).toList()) + .withTypeCount(typeCount) + .withUncoveredCount(uncoveredCounts) + .withStatusCount(statusCount) + .withTagCount(tagCount) .withItems(uxItems) .build(); } @@ -223,13 +234,21 @@ private void initializeIndexes() { allTypes.addAll(collectAllTypes(items)); orderedTypes.clear(); orderedTypes.addAll(createOrderedTypes(items)); + typeCount.clear(); + typeCount.addAll(collectTypeCount(items, orderedTypes)); ids.clear(); ids.addAll(items.stream().map(LinkedSpecificationItem::getId).toList()); tags.clear(); - uniqueTags.clear(); - tags.addAll(collectTags(items, uniqueTags)); + tagCount.clear(); + final Map tagMap = collectTagCount(items); + final List tagList = new ArrayList<>(tagMap.keySet()); + tags.addAll(tagList); + tagList.forEach(tag -> tagCount.add(tagMap.get(tag))); + + statusCount.clear(); + statusCount.addAll(collectStatusCount(items)); } /** @@ -239,10 +258,22 @@ static Set collectAllTypes(final List items) { return items.stream().map(LinkedSpecificationItem::getArtifactType).collect(Collectors.toSet()); } - static List collectTags(final List items, Set uniqueTags) { - final List tags = new ArrayList<>(); - for( final LinkedSpecificationItem item : items ) { - item.getTags().stream().filter(tag -> !uniqueTags.contains(tag)).forEach(tags::add); + /** + * Provides a list of tags accompanied by the number of items that provides a specific tags. + * + * @param items + * The items to process + * @return tag to count mapping + */ + static Map collectTagCount(final List items) + { + final Map tags = new HashMap<>(); + for (final LinkedSpecificationItem item : items) + { + for (final String tag : item.getTags()) + { + tags.put(tag, tags.getOrDefault(tag, 0) + 1); + } } return tags; @@ -317,6 +348,39 @@ static Map collectDependentTypes(final List collectTypeCount(final List items, + final List orderedTypes) + { + final List typeCount = new ArrayList<>(Collections.nCopies(orderedTypes.size(), 0)); + for (final LinkedSpecificationItem item : items) + { + final int typeIndex = orderedTypes.indexOf(item.getArtifactType()); + typeCount.set(typeIndex, typeCount.get(typeIndex) + 1); + } + + return typeCount; + } + + private static List collectStatusCount(final List items) + { + final List statusCount = new ArrayList<>(Collections.nCopies(ItemStatus.values().length, 0)); + for (final LinkedSpecificationItem item : items) + { + final int statusIndex = item.getStatus().ordinal(); + statusCount.set(statusIndex, statusIndex < statusCount.size() ? statusCount.get(statusIndex) + 1 : 1); + } + + return statusCount; + } + // Covered Status @@ -330,13 +394,42 @@ void collectItemCoverages() { itemCoverages.add(null); } + // Initialize uncoveredCounts + uncoveredCounts.clear(); + for (int i = 0; i < orderedTypes.size(); i++) + { + uncoveredCounts.add(0); + } + // Fill coverages for( int i = 0; i < items.size(); i++ ) { final Map itemCoverage = collectItemCoverage(i); - isCovered.add(collectIsCovered(itemCoverage)); + isDeepCovered.add(collectIsCovered(itemCoverage)); + updateUncoveredCount(i,items.get(i).isCoveredShallowWithApprovedItems()); } } + /** + * Update {@link #uncoveredCounts} by incrementing the corresponding entry if the item with the given index is + * uncovered. + * + * @param index + * The index of the processed item + * @param isCovered + * true of the item is covered + * @return true of the item is covered + */ + private boolean updateUncoveredCount(final int index, final boolean isCovered) + { + if (!isCovered) + { + final int uncoveredIndex = orderedTypes.indexOf(items.get(index).getArtifactType()); + uncoveredCounts.set(uncoveredIndex, uncoveredCounts.get(uncoveredIndex) + 1); + } + + return isCovered; + } + /** * @param itemCoverage * collected coverages for an item diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/JsGenerator.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/JsGenerator.java index 8efdb17e..eb5f58d8 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/JsGenerator.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/JsGenerator.java @@ -54,6 +54,10 @@ private void generateMetaData(final UxModel model) { println("item_count", model.getItems().size()); println("item_covered", model.getItems().size() - model.getUncoveredSpecItems()); println("item_uncovered", model.getUncoveredSpecItems()); + println("type_count",model.getTypeCount()); + println("uncovered_count", model.getUncoveredCount()); + println("status_count",model.getStatusCount()); + println("tag_count", model.getTagCount()); printClose("},"); } diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxModel.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxModel.java index 3745b273..cf2eb369 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxModel.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxModel.java @@ -5,6 +5,7 @@ import java.util.Arrays; import java.util.List; +import java.util.Map; /** * Surrounding model that is used to generate the specitem_data model for OpenFastTrace-UX. @@ -21,6 +22,11 @@ public class UxModel private final List items; + private final List typeCount; + private final List uncoveredCount; + private final List statusCount; + private final List tagCount; + private UxModel(Builder builder) { projectName = builder.projectName; @@ -30,6 +36,10 @@ private UxModel(Builder builder) numberOfSpecItems = builder.numberOfSpecItems; uncoveredSpecItems = builder.uncoveredSpecItems; items = builder.items; + typeCount = builder.typeCount; + uncoveredCount = builder.uncoveredCount; + statusCount = builder.statusCount; + tagCount = builder.tagCount; } public static Builder builder(UxModel copy) @@ -42,6 +52,10 @@ public static Builder builder(UxModel copy) builder.numberOfSpecItems = copy.getNumberOfSpecItems(); builder.uncoveredSpecItems = copy.getUncoveredSpecItems(); builder.items = copy.getItems(); + builder.typeCount = copy.getTypeCount(); + builder.uncoveredCount = copy.getUncoveredCount(); + builder.statusCount = copy.getStatusCount(); + builder.tagCount = copy.getTagCount(); return builder; } @@ -98,6 +112,38 @@ public List getItems() { return items; } + /** + * @return number of items by type index + */ + public List getTypeCount() + { + return typeCount; + } + + /** + * @return covered count per soecObject type + */ + public List getUncoveredCount() + { + return uncoveredCount; + } + + /** + * @return number of items by status index + */ + public List getStatusCount() + { + return statusCount; + } + + /** + * @return number of items by status index + */ + public List getTagCount() + { + return tagCount; + } + /** * {@code UxModel} builder static inner class. */ @@ -109,6 +155,10 @@ public static final class Builder private int numberOfSpecItems; private int uncoveredSpecItems; private List items; + private List typeCount; + private List uncoveredCount; + private List statusCount; + private List tagCount; private String projectName; private Builder() @@ -198,6 +248,58 @@ public Builder withItems(List items) return this; } + /** + * Sets the {@code typeCount} and returns a reference to this Builder enabling method chaining. + * + * @param typeCount + * the {@code typeCount} to set + * @return a reference to this Builder + */ + public Builder withTypeCount(List typeCount) + { + this.typeCount = typeCount; + return this; + } + + /** + * Sets the {@code uncoveredCount} and returns a reference to this Builder enabling method chaining. + * + * @param uncoveredCount + * the {@code uncoveredCount} to set + * @return a reference to this Builder + */ + public Builder withUncoveredCount(List uncoveredCount) + { + this.uncoveredCount = uncoveredCount; + return this; + } + + /** + * Sets the {@code statusCount} and returns a reference to this Builder enabling method chaining. + * + * @param statusCount + * the {@code statusCount} to set + * @return a reference to this Builder + */ + public Builder withStatusCount(List statusCount) + { + this.statusCount = statusCount; + return this; + } + + /** + * Sets the {@code tagCount} and returns a reference to this Builder enabling method chaining. + * + * @param tagCount + * the {@code tagCount} to set + * @return a reference to this Builder + */ + public Builder withTagCount(List tagCount) + { + this.tagCount = tagCount; + return this; + } + /** * Returns a {@code UxModel} built from the parameters previously set. * From b328118cc8e304d9e305397d9ec43c3f244aee8a Mon Sep 17 00:00:00 2001 From: Bernd Haberstumpf Date: Sun, 16 Mar 2025 20:05:41 +0100 Subject: [PATCH 16/20] Increased covered for UxReporter to 97% --- .../openfasttrace/report/ux/UxReporter.java | 5 +- .../report/ux/generator/JsGenerator.java | 4 +- .../report/ux/model/UxReporterSettings.java | 38 ---- .../openfasttrace/report/ux/SampleData.java | 52 ++++- .../openfasttrace/report/ux/TestHelper.java | 68 ++++++ .../report/ux/UxReporterFactoryTest.java | 25 ++- .../report/ux/UxReporterTest.java | 28 +++ .../report/ux/generator/JsGeneratorTest.java | 9 +- .../resources/sample_jsgenerator_result.js | 202 ++++++++++++++++++ 9 files changed, 380 insertions(+), 51 deletions(-) delete mode 100644 reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxReporterSettings.java create mode 100644 reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/TestHelper.java create mode 100644 reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/UxReporterTest.java create mode 100644 reporter/ux/src/test/resources/sample_jsgenerator_result.js diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporter.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporter.java index 9880a092..4a4f9a16 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporter.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/UxReporter.java @@ -43,7 +43,7 @@ public UxReporter(final Trace trace, final ReporterContext context) LOG.info("renderToStream"); final Collector collector = new Collector().collect(trace.getItems()); final IGenerator generator = new JsGenerator(); - generator.generate(outputStream, extendModel(collector.getUxModel(), settings)); + generator.generate(outputStream, extendModel(collector.getUxModel())); } /** @@ -52,7 +52,8 @@ public UxReporter(final Trace trace, final ReporterContext context) * @param uxModel The collected model * @return uxModel extended via setting */ - private static UxModel extendModel( final UxModel uxModel, final ReportSettings settings ) { + private static UxModel extendModel(final UxModel uxModel) + { final UxModel.Builder uxModelBuilder = UxModel.builder(uxModel); // Add project name prefix if set diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/JsGenerator.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/JsGenerator.java index eb5f58d8..2a5a354c 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/JsGenerator.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/JsGenerator.java @@ -111,7 +111,7 @@ private void println(final String name, final int value) { } private void println(final String name, final String value) { - printf("%s: %s", name, wrap(value, name.length())); + printf("%s:%s", name, wrap(value, name.length())); } private void println(final String name, final List values) { @@ -142,7 +142,7 @@ private void indentEnd() { private String wrap(final String text, final int offset) { final String value = quote(text); - if( value.length() < ( LINE_LENGTH - offset - INDENT - 2 ) ) return "'" + value + "',"; + if( value.length() < ( LINE_LENGTH - offset - INDENT - 2 ) ) return " '" + value + "',"; final StringBuilder b = new StringBuilder(); b.append(System.lineSeparator()); diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxReporterSettings.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxReporterSettings.java deleted file mode 100644 index d84937d7..00000000 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxReporterSettings.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.itsallcode.openfasttrace.report.ux.model; - -import org.itsallcode.openfasttrace.api.ReportSettings; - -/** - * Extended settings for the reporter - */ -public class UxReporterSettings extends ReportSettings -{ - /** - * @param builder creates the settings - */ - public UxReporterSettings(final Builder builder) - { - super(builder); - } - - /** - * Builds the setting - */ - public static class Builder extends ReportSettings.Builder { - /** - * New builder - */ - public Builder() - { - super(); - } - - /** - * @return the settings - */ - @Override public UxReporterSettings build() - { - return new UxReporterSettings(this); - } - } -} diff --git a/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/SampleData.java b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/SampleData.java index 991d1f70..8facd575 100644 --- a/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/SampleData.java +++ b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/SampleData.java @@ -13,10 +13,31 @@ import java.util.stream.Collectors; public class SampleData { + + public static final String LONG_SAMPLE_CONTENT = + "Officia voluptate aliquip ullamco dolore irure sint occaecat dolore eu proident." + + " Lorem cupidatat dolore voluptate non nulla commodo sint. Aliquip velit anim tem" + + "por magna culpa in esse. Excepteur anim ea ex est anim minim esse ut. Deserunt e" + + "nim veniam amet quis veniam amet in velit esse. Pariatur ut aliquip ipsum dolore" + + " quis reprehenderit excepteur adipisicing.Reprehenderit laboris reprehenderit re" + + "prehenderit irure aute eiusmod fugiat dolore ipsum velit mollit cillum. Commodo " + + "minim dolore nisi nostrud enim nisi reprehenderit aliqua anim deserunt ea ut eli" + + "t. Aute Lorem quis elit proident veniam sunt duis aliquip. Duis duis ad nostrud " + + "adipisicing. Consequat laboris qui aute cillum do eu non. Tempor commodo adipisi" + + "cing eu exercitation laboris."; + + public static final List SAMPLE_TAGS = List.of( "v1", "v2", "v3" ); + /** * Coverage types in ordered from based on SAMPLE_ITEM linkage */ public static final List ORDERED_SAMPLE_TYPES = List.of("fea", "req", "arch", "utest"); + + /** + * Generated samples data with project name removed. + */ + public static final String SAMPLE_OUTPUT_RESOURCE = "sample_jsgenerator_result.js"; + /** * Sample for items on all level fea,req,arch,utest with upwards linkes */ @@ -26,7 +47,7 @@ public class SampleData { item("req~req2", ItemStatus.APPROVED, Set.of("arch"), Set.of("fea~fea1")), item("arch~arch1", ItemStatus.APPROVED, Set.of("utest"), Set.of("req~req1")), item("arch~arch2", ItemStatus.APPROVED, Set.of("utest"), Set.of("req~req1")), - item("arch~arch3", ItemStatus.APPROVED, Set.of("utest"), Set.of("req~req2")), + item("arch~arch3", ItemStatus.APPROVED, Set.of("utest"), Set.of("req~req2"),LONG_SAMPLE_CONTENT, SAMPLE_TAGS), item("utest~utest1", ItemStatus.APPROVED, Set.of(), Set.of("arch~arch1")), item("utest~utest2", ItemStatus.APPROVED, Set.of(), Set.of("arch~arch2")) ); @@ -62,14 +83,14 @@ public class SampleData { // // Helpers - public static List>> coverages(Coverage... coverage) { + public static List>> coverages(final Coverage... coverage) { final List stack = new ArrayList<>(Arrays.stream(coverage).toList()); return ORDERED_SAMPLE_TYPES.stream().map(type -> Matchers.hasEntry(type, !stack.isEmpty() ? stack.remove(0) : Coverage.NONE) ).collect(Collectors.toList()); } - public static SpecificationItem item(final String id, ItemStatus status, Set needs) { + public static SpecificationItem item(final String id, final ItemStatus status, final Set needs) { final SpecificationItemId specId = id(id); SpecificationItem.Builder builder = SpecificationItem.builder() .id(specId) @@ -82,9 +103,9 @@ public static SpecificationItem item(final String id, ItemStatus status, Set needs, - Set coverages) { + final ItemStatus status, + final Set needs, + final Set coverages) { final SpecificationItemId specId = id(id); SpecificationItem.Builder builder = SpecificationItem.builder() .id(specId) @@ -97,6 +118,25 @@ public static SpecificationItem item(final String id, return builder.build(); } + public static SpecificationItem item(final String id, + final ItemStatus status, + final Set needs, + final Set coverages, + final String content, + final List tags) { + final SpecificationItemId specId = id(id); + SpecificationItem.Builder builder = SpecificationItem.builder() + .id(specId) + .title("Title " + id) + .description(content) + .status(status); + needs.forEach(builder::addNeedsArtifactType); + tags.forEach(builder::addTag); + coverages.forEach(coverage -> builder.addCoveredId(id(coverage))); + + return builder.build(); + } + public static SpecificationItemId id(final String id) { return id.matches("/~.*~") ? new SpecificationItemId.Builder(id).build() : diff --git a/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/TestHelper.java b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/TestHelper.java new file mode 100644 index 00000000..585a1725 --- /dev/null +++ b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/TestHelper.java @@ -0,0 +1,68 @@ +package org.itsallcode.openfasttrace.report.ux; + +import org.hamcrest.Matcher; +import org.itsallcode.openfasttrace.api.ReportSettings; +import org.itsallcode.openfasttrace.api.core.LinkedSpecificationItem; +import org.itsallcode.openfasttrace.api.core.Trace; +import org.itsallcode.openfasttrace.api.report.Reportable; +import org.itsallcode.openfasttrace.api.report.ReporterContext; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +import static org.hamcrest.Matchers.equalTo; + +public class TestHelper +{ + /** + * Creates a {@link UxReporter} with a default Context. + * + * @param items The items to process + * @return a UxReporter + */ + public static Reportable createReporter(final List items) { + final UxReporterFactory factory = new UxReporterFactory(); + factory.init(new ReporterContext(ReportSettings.createDefault())); + final Trace trace = createTrace(items); + return factory.createImporter(trace); + } + + /** + * Create a {@link Trace} from a List of {@link LinkedSpecificationItem}. + * + * @param items The items to trace + * @return The Trace + */ + public static Trace createTrace(final List items) { + return Trace.builder() + .items(items) + .defectItems(new ArrayList<>()) + .build(); + } + + /** + * Matcher that equals against a test resource file. + * + * @param fileName The file beneath test/resources + * @return A matcher + * @throws IOException file does not exist + */ + public static Matcher equalsToResource( final String fileName ) throws IOException + { + return equalTo(new String(Files.readAllBytes(Paths.get("src/test/resources", fileName )))); + } + + /** + * Removes the generated project Name from the generated js file as it includes a timestamp. + * + * @param generatedText The generated js + * @return The generated js without the project name + */ + public static String removeProjectNameFromJs( final String generatedText ) { + return generatedText.replaceFirst("(?m)projectName: '[^']*',","projectName: '',"); + } + +} // TestHelper diff --git a/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/UxReporterFactoryTest.java b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/UxReporterFactoryTest.java index bd9fc59c..b9e8988b 100644 --- a/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/UxReporterFactoryTest.java +++ b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/UxReporterFactoryTest.java @@ -1,14 +1,35 @@ package org.itsallcode.openfasttrace.report.ux; +import org.itsallcode.openfasttrace.api.core.LinkedSpecificationItem; +import org.itsallcode.openfasttrace.api.core.Trace; +import org.itsallcode.openfasttrace.api.report.Reportable; import org.junit.jupiter.api.Test; +import java.util.ArrayList; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.itsallcode.openfasttrace.report.ux.TestHelper.createReporter; import static org.junit.jupiter.api.Assertions.*; class UxReporterFactoryTest { @Test - public void test() + public void testFormat() + { + final UxReporterFactory factory = new UxReporterFactory(); + assertTrue(factory.supportsFormat("ux")); + assertFalse(factory.supportsFormat("plain")); + assertFalse(factory.supportsFormat("html")); + assertFalse(factory.supportsFormat("aspec")); + } + + @Test + void factoryCreatesUxReporter() { - assertTrue(true); + final Reportable reporter = createReporter(SampleData.LINKED_SAMPLE_ITEMS); + assertThat(reporter, instanceOf(UxReporter.class)); } + } \ No newline at end of file diff --git a/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/UxReporterTest.java b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/UxReporterTest.java new file mode 100644 index 00000000..ac21a6c0 --- /dev/null +++ b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/UxReporterTest.java @@ -0,0 +1,28 @@ +package org.itsallcode.openfasttrace.report.ux; + +import org.hamcrest.Matchers; +import org.itsallcode.openfasttrace.api.report.Reportable; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; + +import static net.bytebuddy.matcher.ElementMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.matchesPattern; +import static org.itsallcode.openfasttrace.report.ux.TestHelper.createReporter; +import static org.itsallcode.openfasttrace.report.ux.TestHelper.createTrace; + +class UxReporterTest +{ + @Test + void generatedModelContainsProjectNameFromProperty() + { + System.setProperty("oftProjectName", "TestProject"); + final Reportable reporter = createReporter(SampleData.LINKED_SAMPLE_ITEMS); + final OutputStream outputStream = new ByteArrayOutputStream(); + reporter.renderToStream(outputStream); + final String output = outputStream.toString(); + assertThat(output, matchesPattern("(?s).*projectName *: *['\"]TestProject.*")); + } +} \ No newline at end of file diff --git a/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/generator/JsGeneratorTest.java b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/generator/JsGeneratorTest.java index ad38a7a5..ea8a7bcd 100644 --- a/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/generator/JsGeneratorTest.java +++ b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/generator/JsGeneratorTest.java @@ -6,9 +6,13 @@ import org.junit.jupiter.api.Test; import java.io.ByteArrayOutputStream; +import java.io.IOException; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; +import static org.itsallcode.openfasttrace.report.ux.SampleData.SAMPLE_OUTPUT_RESOURCE; +import static org.itsallcode.openfasttrace.report.ux.TestHelper.equalsToResource; +import static org.itsallcode.openfasttrace.report.ux.TestHelper.removeProjectNameFromJs; class JsGeneratorTest { @@ -20,11 +24,14 @@ void type() { } @Test - void generate() { + void generate() throws IOException + { final UxModel model = new Collector().collect(SampleData.LINKED_SAMPLE_ITEMS).getUxModel(); final ByteArrayOutputStream out = new ByteArrayOutputStream(); new JsGenerator().generate(out,model); System.out.println(out); + final String outWithoutProjectName = removeProjectNameFromJs(out.toString()); + assertThat(outWithoutProjectName, equalsToResource(SAMPLE_OUTPUT_RESOURCE)); } @Test diff --git a/reporter/ux/src/test/resources/sample_jsgenerator_result.js b/reporter/ux/src/test/resources/sample_jsgenerator_result.js new file mode 100644 index 00000000..c2879dca --- /dev/null +++ b/reporter/ux/src/test/resources/sample_jsgenerator_result.js @@ -0,0 +1,202 @@ +(function (window,undefined) { + window.specitem = { + project: { + projectName: '', + types: ["fea", "req", "arch", "utest"], + tags: ["v1", "v2", "v3"], + status: ["approved", "proposed", "draft", "rejected"], + item_count: 8, + item_covered: 5, + item_uncovered: 3, + type_count: [1, 2, 3, 2], + uncovered_count: [0, 0, 1, 0], + status_count: [8, 0, 0, 0], + tag_count: [1, 1, 1], + }, + specitems: [ + { + index: 0, + type: 0, + title: 'Title fea~fea1', + name: 'fea1', + id: 'fea:fea1', + tags: [], + version: 1, + content: 'Descriptive text for fea~fea1', + provides: [], + needs: [1], + covered: [2, 2, 1, 3], + uncovered: [2, 3], + covering: [], + coveredBy: [1, 2], + depends: [], + status: 0, + path: [], + sourceFile: '', + sourceLine: 0, + comments: '', + }, + { + index: 1, + type: 1, + title: 'Title req~req1', + name: 'req1', + id: 'req:req1', + tags: [], + version: 1, + content: 'Descriptive text for req~req1', + provides: [0], + needs: [2], + covered: [0, 2, 2, 2], + uncovered: [], + covering: [0], + coveredBy: [3, 4], + depends: [], + status: 0, + path: [], + sourceFile: '', + sourceLine: 0, + comments: '', + }, + { + index: 2, + type: 1, + title: 'Title req~req2', + name: 'req2', + id: 'req:req2', + tags: [], + version: 1, + content: 'Descriptive text for req~req2', + provides: [0], + needs: [2], + covered: [0, 2, 1, 3], + uncovered: [2, 3], + covering: [0], + coveredBy: [5], + depends: [], + status: 0, + path: [], + sourceFile: '', + sourceLine: 0, + comments: '', + }, + { + index: 3, + type: 2, + title: 'Title arch~arch1', + name: 'arch1', + id: 'arch:arch1', + tags: [], + version: 1, + content: 'Descriptive text for arch~arch1', + provides: [1], + needs: [3], + covered: [0, 0, 2, 2], + uncovered: [], + covering: [1], + coveredBy: [6], + depends: [], + status: 0, + path: [], + sourceFile: '', + sourceLine: 0, + comments: '', + }, + { + index: 4, + type: 2, + title: 'Title arch~arch2', + name: 'arch2', + id: 'arch:arch2', + tags: [], + version: 1, + content: 'Descriptive text for arch~arch2', + provides: [1], + needs: [3], + covered: [0, 0, 2, 2], + uncovered: [], + covering: [1], + coveredBy: [7], + depends: [], + status: 0, + path: [], + sourceFile: '', + sourceLine: 0, + comments: '', + }, + { + index: 5, + type: 2, + title: 'Title arch~arch3', + name: 'arch3', + id: 'arch:arch3', + tags: [0, 1, 2], + version: 1, + content: + 'Officia voluptate aliquip ullamco dolore irure sint occaecat dolore eu proident. Lorem cupidatat dolore volu' + + 'ptate non nulla commodo sint. Aliquip velit anim tempor magna culpa in esse. Excepteur anim ea ex est anim m' + + 'inim esse ut. Deserunt enim veniam amet quis veniam amet in velit esse. Pariatur ut aliquip ipsum dolore qui' + + 's reprehenderit excepteur adipisicing.Reprehenderit laboris reprehenderit reprehenderit irure aute eiusmod f' + + 'ugiat dolore ipsum velit mollit cillum. Commodo minim dolore nisi nostrud enim nisi reprehenderit aliqua ani' + + 'm deserunt ea ut elit. Aute Lorem quis elit proident veniam sunt duis aliquip. Duis duis ad nostrud adipisic' + + 'ing. Consequat laboris qui aute cillum do eu non. Tempor commodo adipisicing eu exercitation laboris.', + provides: [1], + needs: [3], + covered: [0, 0, 1, 3], + uncovered: [2, 3], + covering: [2], + coveredBy: [], + depends: [], + status: 0, + path: [], + sourceFile: '', + sourceLine: 0, + comments: '', + }, + { + index: 6, + type: 3, + title: 'Title utest~utest1', + name: 'utest1', + id: 'utest:utest1', + tags: [], + version: 1, + content: 'Descriptive text for utest~utest1', + provides: [2], + needs: [], + covered: [0, 0, 0, 2], + uncovered: [], + covering: [3], + coveredBy: [], + depends: [], + status: 0, + path: [], + sourceFile: '', + sourceLine: 0, + comments: '', + }, + { + index: 7, + type: 3, + title: 'Title utest~utest2', + name: 'utest2', + id: 'utest:utest2', + tags: [], + version: 1, + content: 'Descriptive text for utest~utest2', + provides: [2], + needs: [], + covered: [0, 0, 0, 2], + uncovered: [], + covering: [4], + coveredBy: [], + depends: [], + status: 0, + path: [], + sourceFile: '', + sourceLine: 0, + comments: '', + }, + ] + } +})(window); From 85be76a8b1f0884af311d4f1cf31e12644ba90dc Mon Sep 17 00:00:00 2001 From: Bernd Haberstumpf Date: Thu, 25 Dec 2025 22:38:34 +0100 Subject: [PATCH 17/20] Fixed imports in module-info.java of ux reporter. --- reporter/ux/src/main/java/module-info.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/reporter/ux/src/main/java/module-info.java b/reporter/ux/src/main/java/module-info.java index 319cf608..7a3bb1a1 100644 --- a/reporter/ux/src/main/java/module-info.java +++ b/reporter/ux/src/main/java/module-info.java @@ -7,6 +7,10 @@ { requires transitive org.itsallcode.openfasttrace.api; requires java.logging; + requires java.desktop; + requires jdk.jfr; + requires java.xml.crypto; + requires java.xml; provides org.itsallcode.openfasttrace.api.report.ReporterFactory with org.itsallcode.openfasttrace.report.ux.UxReporterFactory; From 962a0bbd51a50fcb5e6a411344e3c01ac32355e9 Mon Sep 17 00:00:00 2001 From: Bernd Haberstumpf Date: Thu, 25 Dec 2025 22:39:39 +0100 Subject: [PATCH 18/20] Added package-info.java to ux reporter describing the whole output format of the ux reporter. --- .../openfasttrace/report/ux/package-info.java | 196 ++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/package-info.java diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/package-info.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/package-info.java new file mode 100644 index 00000000..4cf368be --- /dev/null +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/package-info.java @@ -0,0 +1,196 @@ +/** + * OpenFastTrace UX Reporter Package. + * + *

This package provides the UX (User Experience) reporter functionality for OpenFastTrace, + * which generates JavaScript data structures for interactive web-based visualization and analysis of specification + * traceability.

+ * + *

Overview

+ * + *

The UX reporter transforms OpenFastTrace's internal specification item model into a + * JavaScript data structure that can be consumed by web applications for interactive traceability visualization. The + * main components are:

+ * + *
    + *
  • {@link org.itsallcode.openfasttrace.report.ux.UxReporter} - Main reporter implementation
  • + *
  • {@link org.itsallcode.openfasttrace.report.ux.Collector} - Transforms specification items into UX model
  • + *
  • {@link org.itsallcode.openfasttrace.report.ux.generator.JsGenerator} - Generates JavaScript output
  • + *
  • {@link org.itsallcode.openfasttrace.report.ux.model.UxModel} - Container for project metadata and items
  • + *
  • {@link org.itsallcode.openfasttrace.report.ux.model.UxSpecItem} - Individual specification item data
  • + *
+ * + *

Generated JavaScript Data Structure

+ * + *

The {@link org.itsallcode.openfasttrace.report.ux.generator.JsGenerator} produces a JavaScript object + * with the following structure:

+ * + *
{@code
+ * window.specitem = {
+ *   project: { project metadata  },
+ *   specitems: [ array of specification items
+ * }
+ * }
+ * + *

The {@code project} object contains high-level information about the specification project:

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Project Object Fields
FieldTypeDescription
projectNamestringName of the project
typesstring[]Array of artifact types (e.g., "itest", "feat", "req", "arch", "utest")
tagsstring[]Array of all tags used in the project
statusstring[]Array of possible item statuses ("approved", "proposed", "draft", "rejected")
wrongLinkNamesstring[]Array of wrong link type names ("version", "orphaned", "unwanted")
item_countnumberTotal number of specification items
item_coverednumberNumber of items that are covered
item_uncoverednumberNumber of items that are uncovered
type_countnumber[]Count of items per type (indexed by types array)
uncovered_countnumber[]Count of uncovered items per type
status_countnumber[]Count of items per status
tag_countnumber[]Count of items per tag
+ * + *

Specification Items Array

+ * + *

Each entry in the {@code specitems} array represents a single specification item with the following structure:

+ * + *

Basic Properties

+ * + * + * + * + * + * + * + * + * + * + * + *
Basic Properties of Specification Items
FieldTypeDescription
indexnumberUnique sequential index of the item in the array
typenumberIndex into the types array indicating the item type
titlestringFull title of the specification item
namestringShort name identifier of the item
idstringFull unique identifier in format "type:name[:version]"
tagsnumber[]Array of indices into the tags array
versionnumberRevision number of the specification item
statusnumberIndex into the status array
+ * + *

Content Properties

+ * + * + * + * + * + * + * + * + *
Content Properties of Specification Items
FieldTypeDescription
contentstringDescription/content of the specification item
commentsstringAdditional comments for the item
pathstring[] File path components where the item is defined
sourceFilestring Source file path where the item is located
sourceLinenumber Line number in the source file (0 if not available)
+ * + *

Traceability Properties

+ * + * + * + * + * + * + * + * + * + * + *
Traceability Properties of Specification Items
FieldTypeDescription
providesnumber[]Array of type indices that this item provides coverage for
needsnumber[]Array of type indices that this item needs coverage from
coverednumber[]Array of {@link org.itsallcode.openfasttrace.report.ux.model.Coverage} enum IDs per type (0=NONE, 1=UNCOVERED, 2=COVERED, 3=MISSING)
uncoverednumber[]Array of type indices that are uncovered or missing for this item
coveringnumber[]Array of indices of items that this item covers
coveredBynumber[]Array of indices of items that cover this item
dependsnumber[]Array of indices of items that this item depends on
+ * + *

Link Validation Properties

+ * + * + * + * + * + *
Link Validation Properties of Specification Items
FieldTypeDescription
wrongLinkTypesnumber[]Array of indices into wrongLinkNames for invalid link types
wrongLinkTargetsstring[]Array of invalid link targets with format "target[reason]"
+ * + *

Data Interpretation

+ * + *

Index-Based References

+ *

Most numeric arrays in the specitems use indices to reference:

+ *
    + *
  • Type indices: Reference the {@code project.types} array
  • + *
  • Tag indices: Reference the {@code project.tags} array
  • + *
  • Status indices: Reference the {@code project.status} array
  • + *
  • Item indices: Reference other items in the {@code specitems} array
  • + *
  • Wrong link type indices: Reference the {@code project.wrongLinkNames} array
  • + *
+ * + *

Coverage Arrays

+ *

The {@code covered} array contains {@link org.itsallcode.openfasttrace.report.ux.model.Coverage} enum IDs per type, + * where each position corresponds to a type in the {@code project.types} array. + * The {@link org.itsallcode.openfasttrace.report.ux.model.Coverage} enum values are:

+ *
    + *
  • 0 = NONE: No coverage relationship exists for this type
  • + *
  • 1 = UNCOVERED: Coverage is required but missing for this type
  • + *
  • 2 = COVERED: Coverage is complete for this type
  • + *
  • 3 = MISSING: Coverage exists but has issues (e.g., missing target items)
  • + *
+ * + *

For example, if {@code project.types = ["itest", "feat", "fea", "req", "arch", "utest"]} and + * {@code covered = [0, 0, 2, 2, 1, 3]}, this means:

+ *
    + *
  • 0 = NONE: No coverage relationship exists for this type
  • + *
  • 1 = UNCOVERED: The specItem is not fully covered
  • + *
  • 2 = COVERED: The specItem is fully covered
  • + *
  • 3 = MISSING: These coverage type are needed by this specItems or specItems that cover this specItem + * (deep coverage) but are not covered
  • + *
+ * + *

Uncovered Arrays

+ *

The {@code uncovered} array is calculated by {@link org.itsallcode.openfasttrace.report.ux.Collector} toUncoveredIndexes() + * and contains type indices where the coverage state is either {@code UNCOVERED} or {@code MISSING}.

+ * + *

Wrong Link Targets Format

+ *

Wrong link targets are formatted as {@code "target[reason]"} where:

+ *
    + *
  • {@code target} is the invalid link target specification
  • + *
  • {@code reason} explains why the link is invalid (e.g., "orphaned", "unwanted coverage", "outdated coverage")
  • + *
+ *

Example: {@code "itest:itest_wrong_type[unwanted coverage]"}

+ * + *

Example JavaScript Output

+ * + *
{@code
+ * {
+ *   index: 0,
+ *   type: 2,
+ *   title: 'Title fea~fea1',
+ *   name: 'fea1',
+ *   id: 'fea:fea1',
+ *   tags: [],
+ *   version: 1,
+ *   content: 'Descriptive text for fea~fea1',
+ *   provides: [],
+ *   needs: [3],
+ *   covered: [0, 0, 2, 2, 1, 3],
+ *   uncovered: [4, 5],
+ *   covering: [],
+ *   coveredBy: [1, 2],
+ *   depends: [],
+ *   status: 0,
+ *   path: [],
+ *   sourceFile: '',
+ *   sourceLine: 0,
+ *   comments: '',
+ *   wrongLinkTypes: [],
+ *   wrongLinkTargets: []
+ * }
+ * }
+ * + *

Implementation Notes

+ * + *
    + *
  • All string values are properly escaped for JavaScript (quotes, HTML entities)
  • + *
  • Long content strings are automatically wrapped across multiple lines with string concatenation
  • + *
  • Empty arrays and strings indicate no data for that property
  • + *
  • The data structure is designed for efficient lookup and filtering in web interfaces
  • + *
  • Indices provide memory-efficient references while maintaining data integrity
  • + *
+ * + *

Usage

+ * + *

This data structure enables comprehensive traceability analysis and visualization in + * OpenFastTrace-UX web applications. The generated JavaScript can be consumed by frontend + * frameworks to create interactive dashboards, coverage reports, and traceability matrices.

+ * + * @since 4.2.0 + */ + +package org.itsallcode.openfasttrace.report.ux; From c4a2a53d75a42390555ff45e3061ab49f88e4df3 Mon Sep 17 00:00:00 2001 From: Bernd Haberstumpf Date: Thu, 25 Dec 2025 22:40:54 +0100 Subject: [PATCH 19/20] Enhanced the ux reporter by including wrong links information in the output format. --- .../openfasttrace/report/ux/Collector.java | 93 ++++- .../report/ux/generator/JsGenerator.java | 10 +- .../report/ux/model/UxModel.java | 60 ++- .../report/ux/model/UxSpecItem.java | 130 +++++- .../report/ux/model/WrongLinkType.java | 67 +++ .../report/ux/CollectorTest.java | 382 ++++++++++++++++-- .../openfasttrace/report/ux/SampleData.java | 20 +- .../report/ux/generator/JsGeneratorTest.java | 2 +- .../resources/sample_jsgenerator_result.js | 214 ++++++++-- 9 files changed, 863 insertions(+), 115 deletions(-) create mode 100644 reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/WrongLinkType.java diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java index feaf9e03..8a6852ad 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/Collector.java @@ -4,12 +4,16 @@ import org.itsallcode.openfasttrace.report.ux.model.Coverage; import org.itsallcode.openfasttrace.report.ux.model.UxModel; import org.itsallcode.openfasttrace.report.ux.model.UxSpecItem; +import org.itsallcode.openfasttrace.report.ux.model.WrongLinkType; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; +import java.util.Map.Entry; import java.util.stream.Collectors; +import static java.util.Map.entry; + /** * Collector traverses a {@link LinkedSpecificationItem} tree and provides a {@link UxSpecItem} and * a {@link UxModel} based on the parsed items. @@ -22,6 +26,8 @@ public class Collector { private final List allTypes = new ArrayList<>(); private final List orderedTypes = new ArrayList<>(); + private final List wrongLinkTypes = new ArrayList<>(); + private final List tags = new ArrayList<>(); private final List tagCount = new ArrayList<>(); @@ -37,6 +43,8 @@ public class Collector { private final List statusCount = new ArrayList<>(); + private final List wrongLinkCount = new ArrayList<>(); + private UxModel uxModel = null; public Collector() { @@ -121,10 +129,12 @@ private void collectUxModel() { .withUncoveredSpecItems(items.size() - (int) isDeepCovered.stream().filter(covered -> covered).count()) .withTags(tags) .withStatusNames(Arrays.stream(ItemStatus.values()).map(ItemStatus::toString).toList()) + .withWrongLinkType(wrongLinkTypes) .withTypeCount(typeCount) .withUncoveredCount(uncoveredCounts) .withStatusCount(statusCount) .withTagCount(tagCount) + .withWrongLinkCount(wrongLinkCount) .withItems(uxItems) .build(); } @@ -161,6 +171,8 @@ UxSpecItem createUxSpecItem(final int index) { .withCoveredByIndex(toItemIndex(item.getLinksByStatus(LinkStatus.COVERED_SHALLOW))) .withDependsIndex(toIdIndex(item.getItem().getDependOnIds())) .withStatusId(item.getItem().getStatus().ordinal()) + .withWrongLinkTypes(getWrongLinkTypeIndexes(item)) + .withWrongLinkTargets(getWrongLinkTypeByTargets(item)) //.withPath() .withItem(item) .build(); @@ -212,6 +224,30 @@ private List toUncoveredIndexes(final int index) { return uncoveredIndexes; } + private List getWrongLinkTypeIndexes(final LinkedSpecificationItem item) + { + return item.getLinks().keySet().stream() + .map(WrongLinkType::toWrongLinkType) + .filter(WrongLinkType::isValid).distinct() + .map(wrongLinkTypes::indexOf) + .toList(); + } + + private Map getWrongLinkTypeByTargets(final LinkedSpecificationItem item) + { + final Set acceptedStatusTypes = Set.of(LinkStatus.ORPHANED, LinkStatus.AMBIGUOUS, + LinkStatus.COVERED_UNWANTED, LinkStatus.COVERED_OUTDATED, LinkStatus.COVERED_PREDATED); + + final Map, LinkStatus> statusByLinkTargets = item.getLinks().entrySet().stream() + .filter(entry -> acceptedStatusTypes.contains(entry.getKey())) + .collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey)); + + return statusByLinkTargets.entrySet().stream() + .flatMap(entry -> entry.getKey().stream() + .map(targetItem -> entry(toId(targetItem), entry.getValue().toString()))) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + } + private List toItemIndex(final List items) { return items.stream().map(item -> ids.indexOf(item.getId())).toList(); } @@ -236,6 +272,8 @@ private void initializeIndexes() { orderedTypes.addAll(createOrderedTypes(items)); typeCount.clear(); typeCount.addAll(collectTypeCount(items, orderedTypes)); + wrongLinkTypes.clear(); + wrongLinkTypes.addAll(collectWrongLinkTypes(items)); ids.clear(); ids.addAll(items.stream().map(LinkedSpecificationItem::getId).toList()); @@ -249,6 +287,9 @@ private void initializeIndexes() { statusCount.clear(); statusCount.addAll(collectStatusCount(items)); + + wrongLinkCount.clear(); + wrongLinkCount.addAll(collectWrongLinkCount(items, wrongLinkTypes)); } /** @@ -258,6 +299,24 @@ static Set collectAllTypes(final List items) { return items.stream().map(LinkedSpecificationItem::getArtifactType).collect(Collectors.toSet()); } + /** + * Collects all wrongLinkTypes that exist in the model. + * + * @param items + * All {@link LinkedSpecificationItem} + * @return A list of used types + */ + static List collectWrongLinkTypes(final List items) + { + return items.stream() + .map(item -> item.getLinks().keySet()) + .flatMap(Collection::stream) + .map(WrongLinkType::toWrongLinkType) + .filter(WrongLinkType::isValid) + .distinct() + .toList(); + } + /** * Provides a list of tags accompanied by the number of items that provides a specific tags. * @@ -294,7 +353,8 @@ static List createOrderedTypes(final List items while( !dependenciesByType.isEmpty() ) { final Map previousDependenciesByType = new HashMap<>(dependenciesByType); - for( final Map.Entry neededTypeEntry : previousDependenciesByType.entrySet() ) { + for (final Entry neededTypeEntry : previousDependenciesByType.entrySet()) + { final String type = neededTypeEntry.getKey(); final TypeDependencies dependencies = neededTypeEntry.getValue(); if( dependencies.needs.isEmpty() ) { @@ -356,7 +416,7 @@ static Map collectDependentTypes(final List collectTypeCount(final List items, + static List collectTypeCount(final List items, final List orderedTypes) { final List typeCount = new ArrayList<>(Collections.nCopies(orderedTypes.size(), 0)); @@ -369,7 +429,7 @@ private static List collectTypeCount(final List collectStatusCount(final List items) + static List collectStatusCount(final List items) { final List statusCount = new ArrayList<>(Collections.nCopies(ItemStatus.values().length, 0)); for (final LinkedSpecificationItem item : items) @@ -381,6 +441,27 @@ private static List collectStatusCount(final List collectWrongLinkCount(final List items, + final List wrongLinkTypes) + { + return wrongLinkTypes.stream() + .map(wrongLinkType -> items.stream() + .flatMap(item -> item.getLinks().entrySet().stream()) + .filter(entry -> WrongLinkType.toWrongLinkType(entry.getKey()) == wrongLinkType) + .mapToInt(entry -> entry.getValue().size()) + .sum()) + .toList(); + } + // Covered Status @@ -524,8 +605,10 @@ Map updateItemCoverage(final int index, */ static boolean mergeCoverages(final Map fromCoverages, final Map toCoverages) { - if( fromCoverages == null ) return false; - for( final Map.Entry fromCoverage : fromCoverages.entrySet() ) { + if (fromCoverages == null) + return false; + for (final Entry fromCoverage : fromCoverages.entrySet()) + { final Coverage fromCoverageValue = fromCoverage.getValue(); final Coverage toCoverageVales = toCoverages.get(fromCoverage.getKey()); toCoverages.put(fromCoverage.getKey(), mergeCoverType(fromCoverageValue, toCoverageVales)); diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/JsGenerator.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/JsGenerator.java index 2a5a354c..4a35ed31 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/JsGenerator.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/generator/JsGenerator.java @@ -3,6 +3,7 @@ import org.itsallcode.openfasttrace.api.core.Location; import org.itsallcode.openfasttrace.report.ux.model.UxModel; import org.itsallcode.openfasttrace.report.ux.model.UxSpecItem; +import org.itsallcode.openfasttrace.report.ux.model.WrongLinkType; import java.io.OutputStream; import java.io.PrintStream; @@ -51,6 +52,7 @@ private void generateMetaData(final UxModel model) { println("types", model.getArtifactTypes()); println("tags", model.getTags()); println("status",model.getStatusNames()); + println("wronglinkNames", model.getWrongLinkTypes().stream().map(WrongLinkType::toString).toList()); println("item_count", model.getItems().size()); println("item_covered", model.getItems().size() - model.getUncoveredSpecItems()); println("item_uncovered", model.getUncoveredSpecItems()); @@ -58,6 +60,7 @@ private void generateMetaData(final UxModel model) { println("uncovered_count", model.getUncoveredCount()); println("status_count",model.getStatusCount()); println("tag_count", model.getTagCount()); + println("wronglink_count", model.getWrongLinkCount()); printClose("},"); } @@ -88,6 +91,11 @@ private void generateSpecItem(final UxSpecItem item) { println("sourceFile", location != null ? location.getPath() : ""); println("sourceLine", location != null ? location.getLine() : 0); println("comments", item.getItem().getItem().getComment()); + println("wrongLinkTypes", item.getWrongLinkTypes()); + println("wrongLinkTargets", + item.getWrongLinkTargets().entrySet().stream().map(entry -> + String.format("%s[%s]", entry.getKey(), entry.getValue())).toList() + ); printClose("},"); } @@ -161,7 +169,7 @@ private String wrap(final String text, final int offset) { } private String quote(final String text) { - return text.replace("'", "\\\'") + return text.replace("'", "\\'") .replace("<", "<") .replace(">", ">") .replaceAll("\n\r?|\r", "
"); diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxModel.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxModel.java index cf2eb369..4177807e 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxModel.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxModel.java @@ -3,9 +3,7 @@ import org.itsallcode.openfasttrace.api.core.ItemStatus; import org.itsallcode.openfasttrace.api.core.SpecificationItem; -import java.util.Arrays; import java.util.List; -import java.util.Map; /** * Surrounding model that is used to generate the specitem_data model for OpenFastTrace-UX. @@ -16,6 +14,7 @@ public class UxModel private final List artifactTypes; private final List tags; private final List statusNames; + private final List wrongLinkTypes; private final int numberOfSpecItems; private final int uncoveredSpecItems; @@ -26,6 +25,7 @@ public class UxModel private final List uncoveredCount; private final List statusCount; private final List tagCount; + private final List wrongLinkCount; private UxModel(Builder builder) { @@ -33,6 +33,7 @@ private UxModel(Builder builder) artifactTypes = builder.artifactTypes; tags = builder.tags; statusNames = builder.statusNames; + wrongLinkTypes = builder.wrongLinkTypes; numberOfSpecItems = builder.numberOfSpecItems; uncoveredSpecItems = builder.uncoveredSpecItems; items = builder.items; @@ -40,6 +41,7 @@ private UxModel(Builder builder) uncoveredCount = builder.uncoveredCount; statusCount = builder.statusCount; tagCount = builder.tagCount; + wrongLinkCount = builder.wrongLinkCount; } public static Builder builder(UxModel copy) @@ -49,6 +51,7 @@ public static Builder builder(UxModel copy) builder.artifactTypes = copy.getArtifactTypes(); builder.tags = copy.getTags(); builder.statusNames = copy.getStatusNames(); + builder.wrongLinkTypes = copy.getWrongLinkTypes(); builder.numberOfSpecItems = copy.getNumberOfSpecItems(); builder.uncoveredSpecItems = copy.getUncoveredSpecItems(); builder.items = copy.getItems(); @@ -56,6 +59,7 @@ public static Builder builder(UxModel copy) builder.uncoveredCount = copy.getUncoveredCount(); builder.statusCount = copy.getStatusCount(); builder.tagCount = copy.getTagCount(); + builder.wrongLinkCount = copy.getWrongLinkCount(); return builder; } @@ -105,6 +109,14 @@ public List getStatusNames() { return statusNames; } + /** + * @return The names of the wrongLink type names find in specItems. + */ + public List getWrongLinkTypes() + { + return wrongLinkTypes; + } + /** * @return items within the model */ @@ -113,7 +125,7 @@ public List getItems() { } /** - * @return number of items by type index + * @return numbers of items by type index */ public List getTypeCount() { @@ -121,7 +133,7 @@ public List getTypeCount() } /** - * @return covered count per soecObject type + * @return covered count per specObject type */ public List getUncoveredCount() { @@ -129,7 +141,7 @@ public List getUncoveredCount() } /** - * @return number of items by status index + * @return numbers of items by status index */ public List getStatusCount() { @@ -137,13 +149,21 @@ public List getStatusCount() } /** - * @return number of items by status index + * @return numbers of items by status index */ public List getTagCount() { return tagCount; } + /** + * @return numbers of wrong links + */ + public List getWrongLinkCount() + { + return wrongLinkCount; + } + /** * {@code UxModel} builder static inner class. */ @@ -152,6 +172,7 @@ public static final class Builder private List artifactTypes; private List tags; private List statusNames; + private List wrongLinkTypes; private int numberOfSpecItems; private int uncoveredSpecItems; private List items; @@ -159,6 +180,7 @@ public static final class Builder private List uncoveredCount; private List statusCount; private List tagCount; + private List wrongLinkCount; private String projectName; private Builder() @@ -209,6 +231,19 @@ public Builder withStatusNames(List statusNames) return this; } + /** + * Sets the {@code wrongLinkTypeNames} and returns a reference to this Builder enabling method chaining. + * + * @param wrongLinkType + * the {@code wrongLinkTypeNames} to set + * @return a reference to this Builder + */ + public Builder withWrongLinkType(List wrongLinkType) + { + this.wrongLinkTypes = wrongLinkType; + return this; + } + /** * Sets the {@code numberOfSpecItems} and returns a reference to this Builder enabling method chaining. * @@ -300,6 +335,19 @@ public Builder withTagCount(List tagCount) return this; } + /** + * Sets the {@code wrongLinkCount} and returns a reference to this Builder enabling method chaining. + * + * @param wrongLinkCount + * the {@code wrongLinkCount} to set + * @return a reference to this Builder + */ + public Builder withWrongLinkCount(List wrongLinkCount) + { + this.wrongLinkCount = wrongLinkCount; + return this; + } + /** * Returns a {@code UxModel} built from the parameters previously set. * diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxSpecItem.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxSpecItem.java index d2993c5e..350b78c5 100644 --- a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxSpecItem.java +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/UxSpecItem.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; public class UxSpecItem { @@ -21,10 +22,13 @@ public class UxSpecItem private final List coveredByIndex; private final List dependsIndex; private final int statusId; + private final List wrongLinkTypes; + private final Map wrongLinkTargets; private final List path; private final LinkedSpecificationItem item; - private UxSpecItem(Builder builder) { + private UxSpecItem(Builder builder) + { index = builder.index; typeIndex = builder.typeIndex; title = builder.title; @@ -39,10 +43,36 @@ private UxSpecItem(Builder builder) { coveredByIndex = builder.coveredByIndex; dependsIndex = builder.dependsIndex; statusId = builder.statusId; + wrongLinkTypes = builder.wrongLinkTypes; + wrongLinkTargets = builder.wrongLinkTargets; path = builder.path; item = builder.item; } + public static Builder builder(UxSpecItem copy) + { + Builder builder = new Builder(); + builder.index = copy.getIndex(); + builder.typeIndex = copy.getTypeIndex(); + builder.title = copy.getTitle(); + builder.name = copy.getName(); + builder.id = copy.getId(); + builder.tagIndex = copy.getTagIndex(); + builder.providesIndex = copy.getProvidesIndex(); + builder.neededTypeIndex = copy.getNeededTypeIndex(); + builder.coveredIndex = copy.getCoveredIndex(); + builder.uncoveredIndex = copy.getUncoveredIndex(); + builder.coveringIndex = copy.getCoveringIndex(); + builder.coveredByIndex = copy.getCoveredByIndex(); + builder.dependsIndex = copy.getDependsIndex(); + builder.statusId = copy.getStatusId(); + builder.wrongLinkTypes = copy.getWrongLinkTypes(); + builder.wrongLinkTargets = copy.getWrongLinkTargets(); + builder.path = copy.getPath(); + builder.item = copy.getItem(); + return builder; + } + public int getIndex() { return index; } @@ -99,6 +129,16 @@ public int getStatusId() { return statusId; } + public List getWrongLinkTypes() + { + return wrongLinkTypes; + } + + public Map getWrongLinkTargets() + { + return wrongLinkTargets; + } + public List getPath() { return path; } @@ -110,7 +150,8 @@ public LinkedSpecificationItem getItem() { /** * {@code UxSpecItem} builder static inner class. */ - public static final class Builder { + public static final class Builder + { private int index; private int typeIndex; private String title; @@ -125,13 +166,17 @@ public static final class Builder { private List coveredByIndex; private List dependsIndex = new ArrayList<>(); private int statusId; + private List wrongLinkTypes; + private Map wrongLinkTargets; private List path = new ArrayList<>(); private LinkedSpecificationItem item; - private Builder() { + private Builder() + { } - public static Builder builder() { + public static Builder builder() + { return new Builder(); } @@ -142,7 +187,8 @@ public static Builder builder() { * the {@code index} to set * @return a reference to this Builder */ - public Builder withIndex(int index) { + public Builder withIndex(int index) + { this.index = index; return this; } @@ -154,7 +200,8 @@ public Builder withIndex(int index) { * the {@code typeIndex} to set * @return a reference to this Builder */ - public Builder withTypeIndex(int typeIndex) { + public Builder withTypeIndex(int typeIndex) + { this.typeIndex = typeIndex; return this; } @@ -166,7 +213,8 @@ public Builder withTypeIndex(int typeIndex) { * the {@code title} to set * @return a reference to this Builder */ - public Builder withTitle(String title) { + public Builder withTitle(String title) + { this.title = title; return this; } @@ -178,7 +226,8 @@ public Builder withTitle(String title) { * the {@code name} to set * @return a reference to this Builder */ - public Builder withName(String name) { + public Builder withName(String name) + { this.name = name; return this; } @@ -190,7 +239,8 @@ public Builder withName(String name) { * the {@code id} to set * @return a reference to this Builder */ - public Builder withId(String id) { + public Builder withId(String id) + { this.id = id; return this; } @@ -202,7 +252,8 @@ public Builder withId(String id) { * the {@code tagIndex} to set * @return a reference to this Builder */ - public Builder withTagIndex(List tagIndex) { + public Builder withTagIndex(List tagIndex) + { this.tagIndex = tagIndex; return this; } @@ -214,7 +265,8 @@ public Builder withTagIndex(List tagIndex) { * the {@code providesIndex} to set * @return a reference to this Builder */ - public Builder withProvidesIndex(List providesIndex) { + public Builder withProvidesIndex(List providesIndex) + { this.providesIndex = providesIndex; return this; } @@ -226,7 +278,8 @@ public Builder withProvidesIndex(List providesIndex) { * the {@code neededTypeIndex} to set * @return a reference to this Builder */ - public Builder withNeededTypeIndex(List neededTypeIndex) { + public Builder withNeededTypeIndex(List neededTypeIndex) + { this.neededTypeIndex = neededTypeIndex; return this; } @@ -238,7 +291,8 @@ public Builder withNeededTypeIndex(List neededTypeIndex) { * the {@code coveredIndex} to set * @return a reference to this Builder */ - public Builder withCoveredIndex(List coveredIndex) { + public Builder withCoveredIndex(List coveredIndex) + { this.coveredIndex = coveredIndex; return this; } @@ -250,7 +304,8 @@ public Builder withCoveredIndex(List coveredIndex) { * the {@code uncoveredIndex} to set * @return a reference to this Builder */ - public Builder withUncoveredIndex(List uncoveredIndex) { + public Builder withUncoveredIndex(List uncoveredIndex) + { this.uncoveredIndex = uncoveredIndex; return this; } @@ -262,7 +317,8 @@ public Builder withUncoveredIndex(List uncoveredIndex) { * the {@code coveringIndex} to set * @return a reference to this Builder */ - public Builder withCoveringIndex(List coveringIndex) { + public Builder withCoveringIndex(List coveringIndex) + { this.coveringIndex = coveringIndex; return this; } @@ -274,7 +330,8 @@ public Builder withCoveringIndex(List coveringIndex) { * the {@code coveredByIndex} to set * @return a reference to this Builder */ - public Builder withCoveredByIndex(List coveredByIndex) { + public Builder withCoveredByIndex(List coveredByIndex) + { this.coveredByIndex = coveredByIndex; return this; } @@ -286,7 +343,8 @@ public Builder withCoveredByIndex(List coveredByIndex) { * the {@code dependsIndex} to set * @return a reference to this Builder */ - public Builder withDependsIndex(List dependsIndex) { + public Builder withDependsIndex(List dependsIndex) + { this.dependsIndex = dependsIndex; return this; } @@ -298,11 +356,38 @@ public Builder withDependsIndex(List dependsIndex) { * the {@code statusId} to set * @return a reference to this Builder */ - public Builder withStatusId(int statusId) { + public Builder withStatusId(int statusId) + { this.statusId = statusId; return this; } + /** + * Sets the {@code wrongLinkTypes} and returns a reference to this Builder enabling method chaining. + * + * @param wrongLinkTypes + * the {@code wrongLinkTypes} to set + * @return a reference to this Builder + */ + public Builder withWrongLinkTypes(List wrongLinkTypes) + { + this.wrongLinkTypes = wrongLinkTypes; + return this; + } + + /** + * Sets the {@code wrongLinkTargets} and returns a reference to this Builder enabling method chaining. + * + * @param wrongLinkTargets + * the {@code wrongLinkTargets} to set + * @return a reference to this Builder + */ + public Builder withWrongLinkTargets(Map wrongLinkTargets) + { + this.wrongLinkTargets = wrongLinkTargets; + return this; + } + /** * Sets the {@code path} and returns a reference to this Builder enabling method chaining. * @@ -310,7 +395,8 @@ public Builder withStatusId(int statusId) { * the {@code path} to set * @return a reference to this Builder */ - public Builder withPath(List path) { + public Builder withPath(List path) + { this.path = path; return this; } @@ -322,7 +408,8 @@ public Builder withPath(List path) { * the {@code item} to set * @return a reference to this Builder */ - public Builder withItem(LinkedSpecificationItem item) { + public Builder withItem(LinkedSpecificationItem item) + { this.item = item; return this; } @@ -332,7 +419,8 @@ public Builder withItem(LinkedSpecificationItem item) { * * @return a {@code UxSpecItem} built with parameters of this {@code UxSpecItem.Builder} */ - public UxSpecItem build() { + public UxSpecItem build() + { return new UxSpecItem(this); } } diff --git a/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/WrongLinkType.java b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/WrongLinkType.java new file mode 100644 index 00000000..b12a6424 --- /dev/null +++ b/reporter/ux/src/main/java/org/itsallcode/openfasttrace/report/ux/model/WrongLinkType.java @@ -0,0 +1,67 @@ +package org.itsallcode.openfasttrace.report.ux.model; + +import org.itsallcode.openfasttrace.api.core.LinkStatus; + +/** + * {@link LinkStatus} used to filter SpecItems by bad link within OpenFastTrace UX. + */ +public enum WrongLinkType +{ + /** + * PREDATED or OUTDATED reference. + */ + WRONG_VERSION("version"), + + /** + * Unknown ID. + */ + ORPHANED("orphaned"), + + /** + * Not needed coverage type. + */ + UNWANTED("unwanted"), + + /** + * Not relevant + */ + NONE(""); + + private final String text; + + /** + * Tranform a {@link LinkStatus} to a WrongLinkType + * + * @param linkStatus The status to convert + * @return This WrongLinkType + */ + public static WrongLinkType toWrongLinkType( final LinkStatus linkStatus ) { + return switch (linkStatus) + { + case PREDATED, OUTDATED -> WRONG_VERSION; + case ORPHANED, AMBIGUOUS -> ORPHANED; + case UNWANTED -> UNWANTED; + default -> NONE; + }; + } + + WrongLinkType(final String text) { + this.text = text; + } + + public boolean isValid() { + return this != NONE; + } + + public String getText() + { + return text; + } + + @Override + public String toString() + { + return text; + } + +} // WrongLinkType diff --git a/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/CollectorTest.java b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/CollectorTest.java index 3649d169..b6d27a22 100644 --- a/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/CollectorTest.java +++ b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/CollectorTest.java @@ -2,10 +2,12 @@ import org.itsallcode.openfasttrace.api.core.ItemStatus; import org.itsallcode.openfasttrace.api.core.LinkedSpecificationItem; +import org.itsallcode.openfasttrace.api.core.SpecificationItem; import org.itsallcode.openfasttrace.core.Linker; import org.itsallcode.openfasttrace.report.ux.model.Coverage; +import org.itsallcode.openfasttrace.report.ux.model.UxSpecItem; +import org.itsallcode.openfasttrace.report.ux.model.WrongLinkType; import org.junit.jupiter.api.AfterEach; -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; @@ -18,16 +20,11 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; import static org.itsallcode.matcher.auto.AutoMatcher.containsInAnyOrder; +import static org.itsallcode.openfasttrace.report.ux.SampleData.LINKED_SAMPLE_WRONG_LINKS; +import static org.itsallcode.openfasttrace.report.ux.model.WrongLinkType.*; class CollectorTest { - private Collector collector = null; - - @BeforeEach - void setUp() { - collector = new Collector().collect(SampleData.LINKED_SAMPLE_ITEMS); - } - @AfterEach void tearDown() { } @@ -191,38 +188,10 @@ void testItemCoverages() { { Coverage.NONE, Coverage.NONE, Coverage.NONE, Coverage.COVERED }, }; - final List> coverages = collector + final List> coverages = new Collector() .collect(SampleData.LINKED_SAMPLE_ITEMS) .getItemCoverages(); validateCoverages(SampleData.LINKED_SAMPLE_ITEMS, coverages, expectedCoverages); -/* - - System.out.println("0:" + coverages.get(0) + " of " + SAMPLE_ITEMS.get(0).getId()); - assertThat(coverages.get(0), - allOf(SampleData.coverages(Coverage.COVERED, Coverage.COVERED, Coverage.UNCOVERED, Coverage.COVERED))); - System.out.println("1:" + coverages.get(1) + " of " + SAMPLE_ITEMS.get(1).getId()); - assertThat(coverages.get(1), - allOf(SampleData.coverages(Coverage.NONE, Coverage.COVERED, Coverage.COVERED, Coverage.COVERED))); - System.out.println("2:" + coverages.get(2) + " of " + SAMPLE_ITEMS.get(2).getId()); - assertThat(coverages.get(2), - allOf(SampleData.coverages(Coverage.NONE, Coverage.COVERED, Coverage.UNCOVERED, Coverage.NONE))); - System.out.println("3:" + coverages.get(3) + " of " + SAMPLE_ITEMS.get(3).getId()); - assertThat(coverages.get(3), - allOf(SampleData.coverages(Coverage.NONE, Coverage.NONE, Coverage.COVERED, Coverage.COVERED))); - System.out.println("4:" + coverages.get(4) + " of " + SAMPLE_ITEMS.get(4).getId()); - assertThat(coverages.get(4), - allOf(SampleData.coverages(Coverage.NONE, Coverage.NONE, Coverage.COVERED, Coverage.COVERED))); - System.out.println("5:" + coverages.get(5) + " of " + SAMPLE_ITEMS.get(5).getId()); - assertThat(coverages.get(5), - allOf(SampleData.coverages(Coverage.NONE, Coverage.NONE, Coverage.UNCOVERED, Coverage.MISSING))); - System.out.println("6:" + coverages.get(6) + " of " + SAMPLE_ITEMS.get(6).getId()); - assertThat(coverages.get(6), - allOf(SampleData.coverages(Coverage.NONE, Coverage.NONE, Coverage.NONE, Coverage.COVERED))); - System.out.println("7:" + coverages.get(7) + " of " + SAMPLE_ITEMS.get(7).getId()); - assertThat(coverages.get(7), - allOf(SampleData.coverages(Coverage.NONE, Coverage.NONE, Coverage.NONE, Coverage.COVERED))); - - */ } /** @@ -243,12 +212,345 @@ void testItemCoveragesWithCycle() { { Coverage.NONE, Coverage.NONE, Coverage.NONE, Coverage.COVERED } }; - final List> coverages = collector + final List> coverages = new Collector() .collect(SampleData.LINKED_SAMPLE_ITEMS_CYCLE) .getItemCoverages(); validateCoverages(SampleData.LINKED_SAMPLE_ITEMS_CYCLE, coverages, expectedCoverages); } + @Test + void testLinkTypes() + { + final Collector collector = new Collector().collect(LINKED_SAMPLE_WRONG_LINKS); + assertThat(collector.getUxModel().getWrongLinkTypes(), containsInAnyOrder(WRONG_VERSION, UNWANTED, ORPHANED)); + }@Test + void testCollectWrongLinkTypesWithWrongLinks() + { + final List wrongLinkTypes = Collector.collectWrongLinkTypes(LINKED_SAMPLE_WRONG_LINKS); + + assertThat(wrongLinkTypes, hasSize(3)); + assertThat(wrongLinkTypes, containsInAnyOrder(WRONG_VERSION, ORPHANED, UNWANTED)); + } + + @Test + void testCollectWrongLinkTypesWithNoWrongLinks() + { + final List wrongLinkTypes = Collector.collectWrongLinkTypes(SampleData.LINKED_SAMPLE_ITEMS); + + assertThat(wrongLinkTypes, hasSize(0)); + } + + @Test + void testCollectWrongLinkTypesEmptyItems() + { + final List wrongLinkTypes = Collector.collectWrongLinkTypes(List.of()); + + assertThat(wrongLinkTypes, hasSize(0)); + } + + @Test + void testWrongLinks() + { + final Map goldenWrongLinkTypeMapping = Map.of( + "req~req_lower_version~1", WRONG_VERSION, + "req~req_higher_version~1", WRONG_VERSION, + "itest~itest_wrong_type~1", UNWANTED, + "itest~itest_dead_type~1", ORPHANED + ); + final Collector collector = new Collector().collect(LINKED_SAMPLE_WRONG_LINKS); + final List wrongLinkTypeMap = collector.getUxModel().getWrongLinkTypes(); + + for (final UxSpecItem item : collector.getUxItems()) + { + if ("feat".equals(item.getItem().getId().getArtifactType())) + continue; + + final String id = item.getItem().getId().toString(); + final int expectedTypeIndex = wrongLinkTypeMap.indexOf(goldenWrongLinkTypeMapping.get(id)); + System.out.printf("Item links %s %s%n", item.getId(), + item.getWrongLinkTypes().stream() + .map(index -> wrongLinkTypeMap.get(index).getText()) + .collect(Collectors.joining(",")) + ); + assertThat(item.getWrongLinkTypes(), hasItem(expectedTypeIndex)); + } + } + + @Test + void testWrongLinksByType() + { + final Map> goldenWrongLinkTypeMapping = Map.of( + "req:req_lower_version", List.of("feat:feat1:2[outdated]", "feat:feat1[orphaned]"), + "req:req_higher_version", List.of("feat:feat1:2[predated]", "feat:feat1:3[orphaned]"), + "itest:itest_wrong_type", List.of("feat:feat1:2[unwanted]"), + "itest:itest_dead_type", List.of("feat:feat2:3[orphaned]") + ); + + final Collector collector = new Collector().collect(LINKED_SAMPLE_WRONG_LINKS); + for (final UxSpecItem item : collector.getUxItems()) + { + if ("feat".equals(item.getItem().getId().getArtifactType())) + continue; + + final List goldenSample = goldenWrongLinkTypeMapping.get(item.getId()); + assertThat(goldenSample,notNullValue()); + Map targets = item.getWrongLinkTargets(); + for (Map.Entry target : targets.entrySet()) + { + final String targetValue = String.format("%s[%s]", target.getKey(), target.getValue()); + assertThat(targetValue,is(in(goldenSample))); + } + } + } + + @Test + void testCollectWrongLinkCountWithNoWrongLinks() + { + final List wrongLinkTypes = List.of(WRONG_VERSION, ORPHANED, UNWANTED); + final List wrongLinkCount = Collector.collectWrongLinkCount( + SampleData.LINKED_SAMPLE_ITEMS, wrongLinkTypes); + + assertThat(wrongLinkCount, hasSize(3)); + assertThat(wrongLinkCount, contains(0, 0, 0)); + } + + @Test + void testCollectWrongLinkCountWithWrongLinks() + { + final List wrongLinkTypes = Collector.collectWrongLinkTypes(LINKED_SAMPLE_WRONG_LINKS); + final List wrongLinkCount = Collector.collectWrongLinkCount( + LINKED_SAMPLE_WRONG_LINKS, wrongLinkTypes); + + // wrongLinkTypes should be in the order: [WRONG_VERSION, ORPHANED, UNWANTED] + // or could be in a different order, so we need to check based on the actual order + final int versionIndex = wrongLinkTypes.indexOf(WRONG_VERSION); + final int orphanedIndex = wrongLinkTypes.indexOf(ORPHANED); + final int unwantedIndex = wrongLinkTypes.indexOf(UNWANTED); + + assertThat(wrongLinkCount, hasSize(3)); + // 2 version wrong links (1 outdated + 1 predated) + assertThat(wrongLinkCount.get(versionIndex), is(2)); + // 3 orphaned links (1 from req_lower_version + 1 from req_higher_version + 1 from itest_dead_type) + assertThat(wrongLinkCount.get(orphanedIndex), is(3)); + // 1 unwanted link (from itest_wrong_type) + assertThat(wrongLinkCount.get(unwantedIndex), is(1)); + } + + @Test + void testCollectWrongLinkCountEmptyItems() + { + final List wrongLinkTypes = List.of(WRONG_VERSION, ORPHANED, UNWANTED); + final List wrongLinkCount = Collector.collectWrongLinkCount( + List.of(), wrongLinkTypes); + + assertThat(wrongLinkCount, hasSize(3)); + assertThat(wrongLinkCount, contains(0, 0, 0)); + } + + @Test + void testCollectWrongLinkCountEmptyWrongLinkTypes() + { + final List wrongLinkCount = Collector.collectWrongLinkCount( + LINKED_SAMPLE_WRONG_LINKS, List.of()); + + assertThat(wrongLinkCount, hasSize(0)); + } + + @Test + void testCollectWrongLinkCountIntegration() + { + final Collector collector = new Collector().collect(LINKED_SAMPLE_WRONG_LINKS); + final List wrongLinkCount = collector.getUxModel().getWrongLinkCount(); + final List wrongLinkTypes = collector.getUxModel().getWrongLinkTypes(); + + assertThat(wrongLinkCount, notNullValue()); + assertThat(wrongLinkCount, hasSize(wrongLinkTypes.size())); + + // Verify the total count matches expected + final int totalWrongLinks = wrongLinkCount.stream().mapToInt(Integer::intValue).sum(); + assertThat(totalWrongLinks, is(6)); // 2 version + 3 orphaned + 1 unwanted = 6 total + } + + // Collect Tag Count + + @Test + void testCollectTagCountWithTags() + { + final Map tagCount = Collector.collectTagCount(SampleData.LINKED_SAMPLE_ITEMS); + + assertThat(tagCount, notNullValue()); + assertThat(tagCount, hasEntry("v1", 1)); + assertThat(tagCount, hasEntry("v2", 1)); + assertThat(tagCount, hasEntry("v3", 1)); + assertThat(tagCount.size(), is(3)); + } + + @Test + void testCollectTagCountNoTags() + { + final List itemsWithoutTags = List.of( + SampleData.item("req~req1", ItemStatus.APPROVED, Set.of("arch")) + ); + final List linkedItems = new Linker(itemsWithoutTags).link(); + final Map tagCount = Collector.collectTagCount(linkedItems); + + assertThat(tagCount, notNullValue()); + assertThat(tagCount.isEmpty(), is(true)); + } + + @Test + void testCollectTagCountMultipleItemsWithSameTags() + { + final List itemsWithTags = List.of( + SampleData.item("req~req1", ItemStatus.APPROVED, Set.of("arch"), Set.of(), + "content", List.of("tag1", "tag2")), + SampleData.item("req~req2", ItemStatus.APPROVED, Set.of("arch"), Set.of(), + "content", List.of("tag1")), + SampleData.item("req~req3", ItemStatus.APPROVED, Set.of("arch"), Set.of(), + "content", List.of("tag2", "tag3")) + ); + final List linkedItems = new Linker(itemsWithTags).link(); + final Map tagCount = Collector.collectTagCount(linkedItems); + + assertThat(tagCount, hasEntry("tag1", 2)); + assertThat(tagCount, hasEntry("tag2", 2)); + assertThat(tagCount, hasEntry("tag3", 1)); + assertThat(tagCount.size(), is(3)); + } + + @Test + void testCollectTagCountEmptyItems() + { + final Map tagCount = Collector.collectTagCount(List.of()); + + assertThat(tagCount, notNullValue()); + assertThat(tagCount.isEmpty(), is(true)); + } + + @Test + void testCollectTagCountIntegration() + { + final Collector collector = new Collector().collect(SampleData.LINKED_SAMPLE_ITEMS); + final List tags = collector.getUxModel().getTags(); + final List tagCounts = collector.getUxModel().getTagCount(); + + assertThat(tags, hasSize(3)); + assertThat(tagCounts, hasSize(3)); + assertThat(tagCounts, everyItem(is(1))); // Each tag appears once in sample data + } + + // Collect Type Count + + @Test + void testCollectTypeCountWithTypes() + { + final List orderedTypes = SampleData.ORDERED_SAMPLE_TYPES; + final List typeCount = Collector.collectTypeCount( + SampleData.LINKED_SAMPLE_ITEMS, orderedTypes); + + assertThat(typeCount, hasSize(4)); + // fea: 1, req: 2, arch: 3, utest: 2 + assertThat(typeCount.get(orderedTypes.indexOf("fea")), is(1)); + assertThat(typeCount.get(orderedTypes.indexOf("req")), is(2)); + assertThat(typeCount.get(orderedTypes.indexOf("arch")), is(3)); + assertThat(typeCount.get(orderedTypes.indexOf("utest")), is(2)); + } + + @Test + void testCollectTypeCountEmptyItems() + { + final List orderedTypes = SampleData.ORDERED_SAMPLE_TYPES; + final List typeCount = Collector.collectTypeCount(List.of(), orderedTypes); + + assertThat(typeCount, hasSize(4)); + assertThat(typeCount, everyItem(is(0))); + } + + @Test + void testCollectTypeCountCountsForAllTypes() + { + final List typeCount = Collector.collectTypeCount( + SampleData.LINKED_SAMPLE_ITEMS, List.of("fea","req","arch","utest")); + + assertThat(typeCount, hasSize(4)); + } + + @Test + void testCollectTypeCountIntegration() + { + final Collector collector = new Collector().collect(SampleData.LINKED_SAMPLE_ITEMS); + final List types = collector.getUxModel().getArtifactTypes(); + final List typeCounts = collector.getUxModel().getTypeCount(); + + assertThat(types, hasSize(4)); + assertThat(typeCounts, hasSize(4)); + + final int totalItems = typeCounts.stream().mapToInt(Integer::intValue).sum(); + assertThat(totalItems, is(SampleData.LINKED_SAMPLE_ITEMS.size())); + } + + // Collect Status Count + + @Test + void testCollectStatusCountWithApprovedItems() + { + final List statusCount = Collector.collectStatusCount(SampleData.LINKED_SAMPLE_ITEMS); + + assertThat(statusCount, hasSize(ItemStatus.values().length)); + // All sample items are APPROVED (index 0) + assertThat(statusCount.get(ItemStatus.APPROVED.ordinal()), is(SampleData.LINKED_SAMPLE_ITEMS.size())); + assertThat(statusCount.get(ItemStatus.PROPOSED.ordinal()), is(0)); + assertThat(statusCount.get(ItemStatus.DRAFT.ordinal()), is(0)); + assertThat(statusCount.get(ItemStatus.REJECTED.ordinal()), is(0)); + } + + @Test + void testCollectStatusCountWithMixedStatuses() + { + final List mixedStatusItems = List.of( + SampleData.item("req~req1", ItemStatus.APPROVED, Set.of("arch")), + SampleData.item("req~req2", ItemStatus.PROPOSED, Set.of("arch")), + SampleData.item("req~req3", ItemStatus.DRAFT, Set.of("arch")), + SampleData.item("req~req4", ItemStatus.REJECTED, Set.of("arch")), + SampleData.item("req~req5", ItemStatus.APPROVED, Set.of("arch")) + ); + final List linkedItems = new Linker(mixedStatusItems).link(); + final List statusCount = Collector.collectStatusCount(linkedItems); + + assertThat(statusCount, hasSize(ItemStatus.values().length)); + assertThat(statusCount.get(ItemStatus.APPROVED.ordinal()), is(2)); + assertThat(statusCount.get(ItemStatus.PROPOSED.ordinal()), is(1)); + assertThat(statusCount.get(ItemStatus.DRAFT.ordinal()), is(1)); + assertThat(statusCount.get(ItemStatus.REJECTED.ordinal()), is(1)); + } + + @Test + void testCollectStatusCountEmptyItems() + { + final List statusCount = Collector.collectStatusCount(List.of()); + + assertThat(statusCount, hasSize(ItemStatus.values().length)); + assertThat(statusCount, everyItem(is(0))); + } + + @Test + void testCollectStatusCountIntegration() + { + final Collector collector = new Collector().collect(SampleData.LINKED_SAMPLE_ITEMS); + final List statusNames = collector.getUxModel().getStatusNames(); + final List statusCounts = collector.getUxModel().getStatusCount(); + + assertThat(statusNames, hasSize(ItemStatus.values().length)); + assertThat(statusCounts, hasSize(ItemStatus.values().length)); + + final int totalItems = statusCounts.stream().mapToInt(Integer::intValue).sum(); + assertThat(totalItems, is(SampleData.LINKED_SAMPLE_ITEMS.size())); + } + + // + // Helper + + private void validateCoverages(final List specItems, final List> returnedCoverages, final Coverage[][] expectedCoverages) { @@ -257,10 +559,6 @@ private void validateCoverages(final List specItems, } } - - // - // Helper - private void validateCoverage(int index, List specItems, List> returnedCoverages, diff --git a/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/SampleData.java b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/SampleData.java index 8facd575..8a587783 100644 --- a/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/SampleData.java +++ b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/SampleData.java @@ -11,6 +11,7 @@ import java.util.*; import java.util.stream.Collectors; +import java.util.stream.Stream; public class SampleData { @@ -79,6 +80,23 @@ public class SampleData { "utest", Coverage.NONE ); + public static final List SAMPLE_WRONG_LINKS = List.of( + item("feat~feat1~2", ItemStatus.APPROVED, Set.of("req")), + item("req~req_lower_version~1", ItemStatus.APPROVED, Set.of("arch"), Set.of("feat~feat1~1")), + item("req~req_higher_version~1", ItemStatus.APPROVED, Set.of("arch"), Set.of("feat~feat1~3")), + item("itest~itest_wrong_type~1", ItemStatus.APPROVED, Set.of("arch"), Set.of("feat~feat1~2")), + item("itest~itest_dead_type~1", ItemStatus.APPROVED, Set.of("arch"), Set.of("feat~feat2~3")) + ); + + public static final List LINKED_SAMPLE_WRONG_LINKS = new Linker(SAMPLE_WRONG_LINKS).link(); + + public static final List SAMPLE_ITEMS_WRONG_LINKS = Stream.concat( + SAMPLE_ITEMS.stream(), + SAMPLE_WRONG_LINKS.stream() + ).toList(); + + public static final List LINKED_SAMPLE_ITEMS_WRONG_LINK = + new Linker(SAMPLE_ITEMS_WRONG_LINKS).link(); // // Helpers @@ -140,7 +158,7 @@ public static SpecificationItem item(final String id, public static SpecificationItemId id(final String id) { return id.matches("/~.*~") ? new SpecificationItemId.Builder(id).build() : - new SpecificationItemId.Builder(id + "~1").build(); + new SpecificationItemId.Builder(id.matches(".*~[0-9]+$") ? id : id + "~1").build(); } } // SampleData diff --git a/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/generator/JsGeneratorTest.java b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/generator/JsGeneratorTest.java index ea8a7bcd..67ae63e0 100644 --- a/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/generator/JsGeneratorTest.java +++ b/reporter/ux/src/test/java/org/itsallcode/openfasttrace/report/ux/generator/JsGeneratorTest.java @@ -26,7 +26,7 @@ void type() { @Test void generate() throws IOException { - final UxModel model = new Collector().collect(SampleData.LINKED_SAMPLE_ITEMS).getUxModel(); + final UxModel model = new Collector().collect(SampleData.LINKED_SAMPLE_ITEMS_WRONG_LINK).getUxModel(); final ByteArrayOutputStream out = new ByteArrayOutputStream(); new JsGenerator().generate(out,model); System.out.println(out); diff --git a/reporter/ux/src/test/resources/sample_jsgenerator_result.js b/reporter/ux/src/test/resources/sample_jsgenerator_result.js index c2879dca..a7b23039 100644 --- a/reporter/ux/src/test/resources/sample_jsgenerator_result.js +++ b/reporter/ux/src/test/resources/sample_jsgenerator_result.js @@ -2,21 +2,23 @@ window.specitem = { project: { projectName: '', - types: ["fea", "req", "arch", "utest"], + types: ["itest", "feat", "fea", "req", "arch", "utest"], tags: ["v1", "v2", "v3"], status: ["approved", "proposed", "draft", "rejected"], - item_count: 8, + wronglinkNames: ["version", "orphaned", "unwanted"], + item_count: 13, item_covered: 5, - item_uncovered: 3, - type_count: [1, 2, 3, 2], - uncovered_count: [0, 0, 1, 0], - status_count: [8, 0, 0, 0], + item_uncovered: 8, + type_count: [2, 1, 1, 4, 3, 2], + uncovered_count: [2, 0, 0, 2, 1, 0], + status_count: [13, 0, 0, 0], tag_count: [1, 1, 1], + wronglink_count: [2, 3, 1], }, specitems: [ { index: 0, - type: 0, + type: 2, title: 'Title fea~fea1', name: 'fea1', id: 'fea:fea1', @@ -24,9 +26,9 @@ version: 1, content: 'Descriptive text for fea~fea1', provides: [], - needs: [1], - covered: [2, 2, 1, 3], - uncovered: [2, 3], + needs: [3], + covered: [0, 0, 2, 2, 1, 3], + uncovered: [4, 5], covering: [], coveredBy: [1, 2], depends: [], @@ -35,19 +37,21 @@ sourceFile: '', sourceLine: 0, comments: '', + wrongLinkTypes: [], + wrongLinkTargets: [], }, { index: 1, - type: 1, + type: 3, title: 'Title req~req1', name: 'req1', id: 'req:req1', tags: [], version: 1, content: 'Descriptive text for req~req1', - provides: [0], - needs: [2], - covered: [0, 2, 2, 2], + provides: [2], + needs: [4], + covered: [0, 0, 0, 2, 2, 2], uncovered: [], covering: [0], coveredBy: [3, 4], @@ -57,20 +61,22 @@ sourceFile: '', sourceLine: 0, comments: '', + wrongLinkTypes: [], + wrongLinkTargets: [], }, { index: 2, - type: 1, + type: 3, title: 'Title req~req2', name: 'req2', id: 'req:req2', tags: [], version: 1, content: 'Descriptive text for req~req2', - provides: [0], - needs: [2], - covered: [0, 2, 1, 3], - uncovered: [2, 3], + provides: [2], + needs: [4], + covered: [0, 0, 0, 2, 1, 3], + uncovered: [4, 5], covering: [0], coveredBy: [5], depends: [], @@ -79,19 +85,21 @@ sourceFile: '', sourceLine: 0, comments: '', + wrongLinkTypes: [], + wrongLinkTargets: [], }, { index: 3, - type: 2, + type: 4, title: 'Title arch~arch1', name: 'arch1', id: 'arch:arch1', tags: [], version: 1, content: 'Descriptive text for arch~arch1', - provides: [1], - needs: [3], - covered: [0, 0, 2, 2], + provides: [3], + needs: [5], + covered: [0, 0, 0, 0, 2, 2], uncovered: [], covering: [1], coveredBy: [6], @@ -101,19 +109,21 @@ sourceFile: '', sourceLine: 0, comments: '', + wrongLinkTypes: [], + wrongLinkTargets: [], }, { index: 4, - type: 2, + type: 4, title: 'Title arch~arch2', name: 'arch2', id: 'arch:arch2', tags: [], version: 1, content: 'Descriptive text for arch~arch2', - provides: [1], - needs: [3], - covered: [0, 0, 2, 2], + provides: [3], + needs: [5], + covered: [0, 0, 0, 0, 2, 2], uncovered: [], covering: [1], coveredBy: [7], @@ -123,10 +133,12 @@ sourceFile: '', sourceLine: 0, comments: '', + wrongLinkTypes: [], + wrongLinkTargets: [], }, { index: 5, - type: 2, + type: 4, title: 'Title arch~arch3', name: 'arch3', id: 'arch:arch3', @@ -140,10 +152,10 @@ + 'ugiat dolore ipsum velit mollit cillum. Commodo minim dolore nisi nostrud enim nisi reprehenderit aliqua ani' + 'm deserunt ea ut elit. Aute Lorem quis elit proident veniam sunt duis aliquip. Duis duis ad nostrud adipisic' + 'ing. Consequat laboris qui aute cillum do eu non. Tempor commodo adipisicing eu exercitation laboris.', - provides: [1], - needs: [3], - covered: [0, 0, 1, 3], - uncovered: [2, 3], + provides: [3], + needs: [5], + covered: [0, 0, 0, 0, 1, 3], + uncovered: [4, 5], covering: [2], coveredBy: [], depends: [], @@ -152,19 +164,21 @@ sourceFile: '', sourceLine: 0, comments: '', + wrongLinkTypes: [], + wrongLinkTargets: [], }, { index: 6, - type: 3, + type: 5, title: 'Title utest~utest1', name: 'utest1', id: 'utest:utest1', tags: [], version: 1, content: 'Descriptive text for utest~utest1', - provides: [2], + provides: [4], needs: [], - covered: [0, 0, 0, 2], + covered: [0, 0, 0, 0, 0, 2], uncovered: [], covering: [3], coveredBy: [], @@ -174,19 +188,21 @@ sourceFile: '', sourceLine: 0, comments: '', + wrongLinkTypes: [], + wrongLinkTargets: [], }, { index: 7, - type: 3, + type: 5, title: 'Title utest~utest2', name: 'utest2', id: 'utest:utest2', tags: [], version: 1, content: 'Descriptive text for utest~utest2', - provides: [2], + provides: [4], needs: [], - covered: [0, 0, 0, 2], + covered: [0, 0, 0, 0, 0, 2], uncovered: [], covering: [4], coveredBy: [], @@ -196,6 +212,128 @@ sourceFile: '', sourceLine: 0, comments: '', + wrongLinkTypes: [], + wrongLinkTargets: [], + }, + { + index: 8, + type: 1, + title: 'Title feat~feat1~2', + name: 'feat1', + id: 'feat:feat1:2', + tags: [], + version: 2, + content: 'Descriptive text for feat~feat1~2', + provides: [], + needs: [3], + covered: [0, 2, 0, 1, 3, 0], + uncovered: [3, 4], + covering: [], + coveredBy: [9, 10], + depends: [], + status: 0, + path: [], + sourceFile: '', + sourceLine: 0, + comments: '', + wrongLinkTypes: [], + wrongLinkTargets: ["itest:itest_wrong_type[unwanted coverage]", "req:req_lower_version[outdated coverage]", "req:req_higher_version[predated coverage]"], + }, + { + index: 9, + type: 3, + title: 'Title req~req_lower_version~1', + name: 'req_lower_version', + id: 'req:req_lower_version', + tags: [], + version: 1, + content: 'Descriptive text for req~req_lower_version~1', + provides: [1], + needs: [4], + covered: [0, 0, 0, 1, 3, 0], + uncovered: [3, 4], + covering: [8], + coveredBy: [], + depends: [], + status: 0, + path: [], + sourceFile: '', + sourceLine: 0, + comments: '', + wrongLinkTypes: [0, 1], + wrongLinkTargets: ["feat:feat1[orphaned]"], + }, + { + index: 10, + type: 3, + title: 'Title req~req_higher_version~1', + name: 'req_higher_version', + id: 'req:req_higher_version', + tags: [], + version: 1, + content: 'Descriptive text for req~req_higher_version~1', + provides: [1], + needs: [4], + covered: [0, 0, 0, 1, 3, 0], + uncovered: [3, 4], + covering: [8], + coveredBy: [], + depends: [], + status: 0, + path: [], + sourceFile: '', + sourceLine: 0, + comments: '', + wrongLinkTypes: [0, 1], + wrongLinkTargets: ["feat:feat1:3[orphaned]"], + }, + { + index: 11, + type: 0, + title: 'Title itest~itest_wrong_type~1', + name: 'itest_wrong_type', + id: 'itest:itest_wrong_type', + tags: [], + version: 1, + content: 'Descriptive text for itest~itest_wrong_type~1', + provides: [], + needs: [4], + covered: [1, 0, 0, 0, 3, 0], + uncovered: [0, 4], + covering: [], + coveredBy: [], + depends: [], + status: 0, + path: [], + sourceFile: '', + sourceLine: 0, + comments: '', + wrongLinkTypes: [2], + wrongLinkTargets: [], + }, + { + index: 12, + type: 0, + title: 'Title itest~itest_dead_type~1', + name: 'itest_dead_type', + id: 'itest:itest_dead_type', + tags: [], + version: 1, + content: 'Descriptive text for itest~itest_dead_type~1', + provides: [], + needs: [4], + covered: [1, 0, 0, 0, 3, 0], + uncovered: [0, 4], + covering: [], + coveredBy: [], + depends: [], + status: 0, + path: [], + sourceFile: '', + sourceLine: 0, + comments: '', + wrongLinkTypes: [1], + wrongLinkTargets: ["feat:feat2:3[orphaned]"], }, ] } From b4f3bfe80cc91810e6cafbab51cc8abdd488fa25 Mon Sep 17 00:00:00 2001 From: Bernd Haberstumpf Date: Thu, 25 Dec 2025 22:42:09 +0100 Subject: [PATCH 20/20] Increased to openfasttrace version to 4.3.0 to reflect the changes in the ux reporter. --- parent/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parent/pom.xml b/parent/pom.xml index 51ff11e3..e3a8fb3c 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -10,7 +10,7 @@ Free requirement tracking suite https://github.com/itsallcode/openfasttrace - 4.2.0 + 4.3.0 17 5.11.4 3.5.2