Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion doc/changes/changes_0.10.0.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# OpenFastTrace IntelliJ Plugin 0.10.0, released 2026-07-04

We also updated the bundled OFT to 4.8.0 so tag filters now work end to end instead of being ignored.
The Test Runner UI now marks transitive defects with a leading `↳` so they stand out from direct defects in the result tree.

## Bundled OpenFastTrace

OpenFastTrace 4.8.0

## Bugfix

* #39: Re-enabled tag filter test
* #39: Re-enabled tag filter test
* #66: Transitive defects now use a visible `↳` prefix in the Test Runner UI
3 changes: 2 additions & 1 deletion doc/design/building_block_view.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ Needs: impl, itest
### Trace Test Runner Presentation
`dsn~trace-test-runner-presentation~1`

The plugin provides a trace test-runner presentation component that maps the structured OpenFastTrace trace result to IntelliJ SM test runner nodes. It creates project-local source-file suites, sorted specification-item tests, and incoming or outgoing trace-link sub-tests; derives compact title-aware labels, Unicode direction markers, pass/fail status, status roll-up, and item/link details from the OpenFastTrace trace status; and connects source-file, item, and link node navigation to the existing OpenFastTrace trace navigation support.
The plugin provides a trace test-runner presentation component that maps the structured OpenFastTrace trace result to IntelliJ SM test runner nodes. It creates project-local source-file suites, sorted specification-item tests, and incoming or outgoing trace-link sub-tests; derives compact title-aware labels, Unicode direction markers, transitive-defect markers, pass/fail status, status roll-up, and item/link details from the OpenFastTrace trace status; and connects source-file, item, and link node navigation to the existing OpenFastTrace trace navigation support.

Covers:
- `scn~show-trace-source-files-as-test-runner-suites~1`
Expand All @@ -392,6 +392,7 @@ Covers:
- `scn~sort-specification-items-in-test-runner-ui~1`
- `scn~show-trace-links-as-test-runner-sub-tests~1`
- `scn~show-specification-item-status-in-test-runner-ui~2`
- `scn~mark-transitive-defects-in-test-runner~1`
- `scn~show-trace-link-status-in-test-runner-ui~2`
- `scn~show-trace-link-direction-in-test-runner-ui~1`
- `scn~show-unicode-trace-link-direction-in-test-runner-ui~1`
Expand Down
12 changes: 12 additions & 0 deletions doc/design/runtime_view.md
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,18 @@ Covers:

Needs: impl, itest

No### Mark Transitive Defects in Test Runner UI
`dsn~mark-transitive-defects-in-test-runner~1`

**Given** the trace test-runner presentation creates a specification-item test node for a defective item that is identified as a transitive defect
**When** it derives the node name from the OpenFastTrace trace result
**Then** it prefixes the visible node name with `↳` so transitive defects are visually distinct in the tree.

Covers:
- `scn~mark-transitive-defects-in-test-runner~1`

Needs: impl, itest

### Show Trace Link Status in Test Runner UI
`dsn~show-trace-link-status-in-test-runner-ui~2`

Expand Down
140 changes: 79 additions & 61 deletions doc/system_requirements.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions doc/user_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ Use it to:

Clean items are shown as passed tests. Trace defects are shown as failed tests. The top-level trace result is marked failed when OFT reports trace defects.

Transitive defects in the tree are prefixed with `↳` so they are easy to distinguish from direct defects.

Plain text output is useful when you want the raw OFT report. Specification item IDs in the plain text output are clickable when the plugin can resolve them to project files.

![OpenFastTrace trace result in the IntelliJ Test Runner UI](user_guide/images/test-runner-results.png)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,11 @@ private static String traceLinkDetailText(
+ "Trace-link status: " + visibleStatus;
}

// [impl -> dsn~mark-transitive-defects-in-test-runner~1]
private static String itemFailureMessage(final LinkedSpecificationItem item, final String visibleStatus) {
if (item.isTransitiveDefect()) {
return "Transitive trace defect. The problem is not in this item but in one it depends on.";
}
return switch (visibleStatus) {
case "duplicate" -> "Duplicate OpenFastTrace specification item.";
case "cycle" -> "OpenFastTrace coverage cycle.";
Expand All @@ -137,7 +141,13 @@ private static String itemFailureMessage(final LinkedSpecificationItem item, fin
};
}

// [impl -> dsn~mark-transitive-defects-in-test-runner~1]
private static String itemFailureExplanation(final LinkedSpecificationItem item, final String visibleStatus) {
if (item.isTransitiveDefect()) {
return "This transitive error is caused by the specification items this one depends on."
+ LINE_SEPARATOR
+ "Fix the specification items this one depends on.";
}
return switch (visibleStatus) {
case "duplicate" -> "The trace contains more than one specification item with this ID.";
case "cycle" -> "Deep coverage cannot be proven because the coverage graph contains a cycle.";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,10 @@
) {
final String itemStatus = itemStatus(item);
final String itemId = item.getId().toString();
final String itemName = itemName(item);
final String visibleItemName = getVisibleItemName(item, itemName);
return new OftTraceItemNode(
nodeName(itemName(item), itemStatus, !item.isDefect()),
nodeName(visibleItemName, itemStatus, !item.isDefect()),
itemId,
item.isDefect(),
item.isDefect()
Expand All @@ -104,6 +106,11 @@
);
}

// [impl -> dsn~mark-transitive-defects-in-test-runner~1]
private static String getVisibleItemName(final LinkedSpecificationItem item, final String itemName) {
return item.isTransitiveDefect() ? "↳ " + itemName : itemName;

Check warning on line 111 in src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTreeMapper.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add parentheses to make the operator precedence explicit.

See more on https://sonarcloud.io/project/issues?id=org.itsallcode.openfasttrace%3Aopenfasttrace-intellij-plugin&issues=AZ_VVJa8zyrB3mpw9RHU&open=AZ_VVJa8zyrB3mpw9RHU&pullRequest=67
}

private static List<TraceItemLinks> visibleLinksByItem(
final List<LinkedSpecificationItem> items
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,20 @@
import org.itsallcode.openfasttrace.intellijplugin.trace.runconfig.OftRunConfigurationType;
import org.jspecify.annotations.NonNull;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicReference;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.jupiter.api.Assertions.assertAll;

// [itest->dsn~trace-test-runner-presentation~1]
public class OftTraceTestRunnerOutputPresenterTest extends AbstractOftPlatformTestCase {
Expand Down Expand Up @@ -128,6 +132,39 @@ public void testGivenDefectiveTraceResultWhenPresentedThenItCreatesFailedItemAnd
assertThat(resultsViewer.getFailedTestCount(), is(2));
}

// [itest->dsn~mark-transitive-defects-in-test-runner~1]
// [itest->dsn~trace-test-runner-presentation~1]
// [itest->dsn~show-specification-item-status-in-test-runner-ui~2]
// [itest->dsn~show-specification-item-defect-details-in-test-runner-ui~1]
// [itest->dsn~roll-up-source-file-suite-trace-status~1]
// [itest->dsn~roll-up-top-level-trace-status~1]
public void testGivenTransitiveDefectTraceResultWhenPresentedThenItPrefixesTheNodeNameAndExplainsTheError()
throws IOException {
writeUncleanTraceChainProject(Path.of(Objects.requireNonNull(getProject().getBasePath())));

final OftTraceResult result = new OftTraceService().traceProject(
OftTraceInputs.wholeProject(Path.of(Objects.requireNonNull(getProject().getBasePath())), List.of(), List.of()),
OftTraceProgress.NONE
);
final SMTRunnerConsoleView console = present(result);
final SMTestRunnerResultsForm resultsViewer = console.getResultsViewer();
final SMTestProxy suite = resultsViewer.getTestsRootNode().getChildren().getFirst();
final SMTestProxy transitiveFeature = suite.getChildren().stream()
.filter(child -> child.getName().startsWith("↳ Feature"))
.findFirst()
.orElseThrow();

assertAll(
() -> assertThat(transitiveFeature.isSuite(), is(true)),
() -> assertThat(transitiveFeature.isDefect(), is(true)),
() -> assertThat(transitiveFeature.getErrorMessage(),
is("Transitive trace defect. The problem is not in this item but in one it depends on.")),
() -> assertThat(transitiveFeature.getStacktrace(), containsString("Specification item ID: feat~chain_feature~1")),
() -> assertThat(transitiveFeature.getStacktrace(), containsString("Trace status: uncovered")),
() -> assertThat(transitiveFeature.getStacktrace(), containsString("Fix the specification items this one depends on.")),
() -> assertThat(resultsViewer.getTestsRootNode().isDefect(), is(true)));
}

public void testGivenResultWithoutStructuredTraceWhenPresentedThenItCreatesFailedFallbackNode() {
final SMTRunnerConsoleView console = present(OftTraceResult.invalidInput("invalid configuration"));
final SMTestRunnerResultsForm resultsViewer = console.getResultsViewer();
Expand Down Expand Up @@ -234,12 +271,47 @@ private static Trace trace(final LinkedSpecificationItem... items) {
.build();
}

private static LinkedSpecificationItem titledItem(final String id, final String locationPath, final String title) {
return new LinkedSpecificationItem(SpecificationItem.builder()
private static LinkedSpecificationItem titledItem(
final String id,
final String locationPath,
final String title,
final String... needsArtifactTypes
) {
final SpecificationItem.Builder builder = SpecificationItem.builder()
.id(SpecificationItemId.parseId(id))
.title(title)
.status(ItemStatus.APPROVED)
.location(locationPath, 1)
.build());
.location(locationPath, 1);
Arrays.stream(needsArtifactTypes).forEach(builder::addNeedsArtifactType);
return new LinkedSpecificationItem(builder.build());
}

private void writeUncleanTraceChainProject(final Path projectRoot) throws IOException {
final Path docDirectory = Files.createDirectories(projectRoot.resolve("doc"));
Files.writeString(
docDirectory.resolve("trace.md"),
"""
### Feature
`feat~chain_feature~1`

Needs: req

### Requirement
`req~chain_requirement~1`

Covers:
- `feat~chain_feature~1`

Needs: dsn

### Design
`dsn~chain_design~1`

Covers:
- `req~chain_requirement~1`

Needs: impl
"""
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@
import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceTestTree.OftTraceSuiteNode;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;

Expand All @@ -20,6 +24,7 @@
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;

class OftTraceTestTreeMapperTest {
private static final String PROJECT_BASE = "/workspace/openfasttrace-intellij-plugin";
Expand Down Expand Up @@ -275,6 +280,44 @@ void testGivenOutgoingOrphanedTraceLinkWhenMappingThenItCreatesFailedItemAndLink
);
}

// [itest->dsn~mark-transitive-defects-in-test-runner~1]
// [itest->dsn~trace-test-runner-presentation~1]
// [itest->dsn~mark-transitive-defects-in-test-runner~1]
// [itest->dsn~trace-test-runner-presentation~1]
// [itest->dsn~show-specification-item-status-in-test-runner-ui~2]
// [itest->dsn~show-specification-item-defect-details-in-test-runner-ui~1]
// [itest->dsn~roll-up-source-file-suite-trace-status~1]
// [itest->dsn~roll-up-top-level-trace-status~1]
@Test
void testGivenTransitiveDefectsWhenMappingThenItPrefixesTheirNodeNames(@TempDir final Path temporaryDirectory)
throws IOException {
writeUncleanTraceChainProject(temporaryDirectory);

final Trace trace = new OftTraceService().traceProject(
OftTraceInputs.wholeProject(temporaryDirectory, List.of(), List.of()),
OftTraceProgress.NONE
).trace().orElseThrow();
final OftTraceTestTree tree = OftTraceTestTreeMapper.map(trace);
final OftTraceSuiteNode suite = tree.suites().getFirst();

Assertions.assertAll(
() -> assertThat(
suite.items().stream()
.map(OftTraceItemNode::name)
.toList(),
hasSize(3)
),
() -> assertThat(suite.items().get(0).name(), is("Design (uncovered)")),
() -> assertThat(suite.items().get(1).name(), startsWith("↳ Feature")),
() -> assertThat(suite.items().get(2).name(), startsWith("↳ Requirement")),
() -> assertThat(itemNamedStartingWith(suite, "↳ Requirement").details().failureMessage(),
is("Transitive trace defect. The problem is not in this item but in one it depends on.")),
() -> assertThat(itemNamedStartingWith(suite, "↳ Requirement").details().detailText(),
containsString("Fix the specification items this one depends on.")),
() -> assertThat(tree.failed(), is(true))
);
}

// [itest->dsn~trace-test-runner-presentation~1]
// [itest->dsn~show-specification-item-status-in-test-runner-ui~2]
// [itest->dsn~map-specification-item-trace-status-to-test-runner-status~1]
Expand Down Expand Up @@ -315,6 +358,13 @@ private static OftTraceItemNode onlyItem(final OftTraceTestTree tree, final Stri
return items.getFirst();
}

private static OftTraceItemNode itemNamedStartingWith(final OftTraceSuiteNode suite, final String itemNamePrefix) {
return suite.items().stream()
.filter(item -> item.name().startsWith(itemNamePrefix))
.findFirst()
.orElseThrow();
}

private static Trace trace(final LinkedSpecificationItem... items) {
final List<LinkedSpecificationItem> traceItems = Arrays.asList(items);
return Trace.builder()
Expand All @@ -325,6 +375,35 @@ private static Trace trace(final LinkedSpecificationItem... items) {
.build();
}

private static void writeUncleanTraceChainProject(final Path projectRoot) throws IOException {
final Path docDirectory = Files.createDirectories(projectRoot.resolve("doc"));
Files.writeString(
docDirectory.resolve("trace.md"),
"""
### Feature
`feat~chain_feature~1`

Needs: req

### Requirement
`req~chain_requirement~1`

Covers:
- `feat~chain_feature~1`

Needs: dsn

### Design
`dsn~chain_design~1`

Covers:
- `req~chain_requirement~1`

Needs: impl
"""
);
}

private static LinkedSpecificationItem item(
final String id,
final String locationPath,
Expand Down