From d37963d59377fd55e1f93e3fb67b5dbe88059c89 Mon Sep 17 00:00:00 2001 From: redcatbear Date: Tue, 9 Jun 2026 10:22:09 +0200 Subject: [PATCH 01/10] #40: Basic UI test result view for trace. --- README.md | 3 + doc/changes/changelog.md | 1 + ...integrate-oft-trace-into-test-runner-ui.md | 70 +++++++++--------- gradle.properties | 2 +- .../intellijplugin/trace/OftTraceResult.java | 30 ++++++-- .../OftTraceRunContentOutputPresenter.java | 10 +-- .../intellijplugin/trace/OftTraceService.java | 2 +- .../trace/OftTraceSettingsComponent.java | 37 +++++++++- .../trace/OftTraceSettingsSnapshot.java | 25 ++++++- .../trace/runconfig/OftRunConfiguration.java | 25 ++++++- .../OftRunConfigurationSettingsEditor.java | 2 +- .../trace/runconfig/OftRunProfileState.java | 62 +++++++++++++--- .../trace/OftTraceResultTest.java | 34 +++++++++ ...OftTraceRunContentOutputPresenterTest.java | 5 +- .../trace/OftTraceServiceTest.java | 12 ++- ...OftRunConfigurationSettingsEditorTest.java | 17 ++++- .../runconfig/OftRunConfigurationTest.java | 43 ++++++++++- .../runconfig/OftRunProfileStateTest.java | 73 ++++++++++++++++--- 18 files changed, 359 insertions(+), 94 deletions(-) diff --git a/README.md b/README.md index c4d504a..528723d 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ The plugin currently focuses on: * bundled OFT live templates for common specification items, including a scenario template * project-wide or selected-resource OFT tracing from `Tools | OpenFastTrace | Trace Project` or through custom OpenFastTrace run configurations * clickable specification item IDs directly from the trace output window, including generated IDs created from coverage tags +* optional IntelliJ Test Runner UI output for OpenFastTrace run configurations, grouped by source file, specification item, and trace link * quick access to the OpenFastTrace user guide from the IDE help menu For expected behavior and scope details, see: @@ -75,6 +76,8 @@ After the sandbox IDE opens a project, configure trace scope under `Settings | T Run an OFT trace from `Tools | OpenFastTrace | Trace Project`, use the default shortcut `Ctrl+Alt+Shift+O`, or create and run a dedicated `OpenFastTrace` run configuration from the IDE's run/debug toolbar. The plugin traces the configured inputs in the background and shows the plain text result in an IDE output tab with ANSI colors preserved. You can click specification item IDs in the report to jump to their declarations, including source-side items generated from coverage tags. +In an `OpenFastTrace` run configuration, keep `Plain text output` for the existing report view or select `IntelliJ Test Runner UI` to inspect the structured trace as source-file suites, specification-item tests, and trace-link sub-tests. Passed and failed nodes reflect the OpenFastTrace trace status, and source navigation from item and link nodes opens the corresponding specification declaration or source-side coverage tag. + The plugin also bundles an `OpenFastTrace` live-template group under `Settings | Editor | Live Templates`. Use abbreviations such as `feat`, `req`, `dsn`, and `scn` in a supported editing context, then press `Tab` to insert an OFT item skeleton. The `scn` template inserts a scenario stub with placeholders for `Given`, `When`, `Then`, and the covered requirement. While the caret is still in a template's covered-item field, use basic completion to select an existing specification item ID from the project index. When editing a `Covers:` section in a supported specification document, invoke basic completion on a partial specification item ID to get existing declarations from the project index. The same completion is available on the target side of likely coverage tags in files supported by the OpenFastTrace Tag Importer after the left-hand artifact type and arrow, for example `// [impl->dsn~partial]`. Suggestions prefer full-ID prefix matches first, then name-prefix matches, then name-substring matches, and finally artifact-type prefix matches. diff --git a/doc/changes/changelog.md b/doc/changes/changelog.md index ccd0fde..3977f78 100644 --- a/doc/changes/changelog.md +++ b/doc/changes/changelog.md @@ -1,5 +1,6 @@ # Changelog +* [0.8.0](changes_0.8.0.md) * [0.7.0](changes_0.7.0.md) * [0.6.0](changes_0.6.0.md) * [0.5.2](changes_0.5.2.md) diff --git a/doc/changesets/40-integrate-oft-trace-into-test-runner-ui.md b/doc/changesets/40-integrate-oft-trace-into-test-runner-ui.md index f5b372d..4632f51 100644 --- a/doc/changesets/40-integrate-oft-trace-into-test-runner-ui.md +++ b/doc/changesets/40-integrate-oft-trace-into-test-runner-ui.md @@ -46,7 +46,7 @@ Keep presentation selection at the run-configuration layer. The global `Trace Pr ## Task List -- [ ] Create and checkout a new Git branch `feat/40-integrate-oft-trace-into-test-runner-ui` +- [x] Create and checkout a new Git branch `feat/40-integrate-oft-trace-into-test-runner-ui` ### Requirements And Design @@ -58,52 +58,52 @@ Keep presentation selection at the run-configuration layer. The global `Trace Pr - [x] Update `doc/design/solution_strategy.md` to document reuse of IntelliJ's SM test runner infrastructure for structured trace result presentation. - [x] Update `doc/design/building_block_view.md` with a test-runner trace presentation building block and the run-configuration result-view option. - [x] Update `doc/design/runtime_view.md` with design items for presenter selection, test-runner hierarchy construction, status mapping, and navigation. -- [ ] Stop and ask user for a review of the design. +- [x] Stop and ask user for a review of the design. ### Implementation -- [ ] Add a run-configuration presentation mode, defaulting existing configurations to plain text output. -- [ ] Extend `OftRunConfiguration` persistence and `OftRunConfigurationSettingsEditor` so users can choose plain text output or test runner output. -- [ ] Update `OftRunProfileState` to select `OftTraceRunContentOutputPresenter` or the new test-runner presenter from the saved presentation mode. -- [ ] Keep `OftTraceProjectAction` wired to the existing plain text presenter. -- [ ] Extend `OftTraceResult` so successful and defective trace runs retain the structured `Trace` in addition to the rendered text report. -- [ ] Update `OftTraceService` to return structured trace data without changing importer, linker, tracer, class-loader, filter, or text-rendering behavior. -- [ ] Implement a trace-to-test-tree mapper that groups results by source file, creates specification-item test nodes, and creates incoming and outgoing link sub-test nodes. -- [ ] Implement status-label derivation for specification items and links from OpenFastTrace trace data. -- [ ] Implement pass/fail mapping for clean and defective specification items and links. -- [ ] Implement `OftTraceTestRunnerOutputPresenter` using `SMTRunnerConsoleView` and `SMTestProxy`. -- [ ] Connect test-runner node navigation through `OftTraceNavigationResolver`. -- [ ] Present invalid input, cancellation, and unexpected errors coherently when the test-runner output mode is selected. -- [ ] Preserve existing ANSI-colored plain text output and console hyperlink behavior. -- [ ] Avoid adding third-party dependencies unless a separate design decision is approved. +- [x] Add a run-configuration presentation mode, defaulting existing configurations to plain text output. +- [x] Extend `OftRunConfiguration` persistence and `OftRunConfigurationSettingsEditor` so users can choose plain text output or test runner output. +- [x] Update `OftRunProfileState` to select `OftTraceRunContentOutputPresenter` or the new test-runner presenter from the saved presentation mode. +- [x] Keep `OftTraceProjectAction` wired to the existing plain text presenter. +- [x] Extend `OftTraceResult` so successful and defective trace runs retain the structured `Trace` in addition to the rendered text report. +- [x] Update `OftTraceService` to return structured trace data without changing importer, linker, tracer, class-loader, filter, or text-rendering behavior. +- [x] Implement a trace-to-test-tree mapper that groups results by source file, creates specification-item test nodes, and creates incoming and outgoing link sub-test nodes. +- [x] Implement status-label derivation for specification items and links from OpenFastTrace trace data. +- [x] Implement pass/fail mapping for clean and defective specification items and links. +- [x] Implement `OftTraceTestRunnerOutputPresenter` using `SMTRunnerConsoleView` and `SMTestProxy`. +- [x] Connect test-runner node navigation through `OftTraceNavigationResolver`. +- [x] Present invalid input, cancellation, and unexpected errors coherently when the test-runner output mode is selected. +- [x] Preserve existing ANSI-colored plain text output and console hyperlink behavior. +- [x] Avoid adding third-party dependencies unless a separate design decision is approved. ### Verification -- [ ] Add tests for run-configuration presentation-mode defaults, persistence, and backward-compatible XML reading. -- [ ] Add settings-editor tests for selecting and applying the result-view option. -- [ ] Add `OftTraceResult` and `OftTraceService` tests proving structured `Trace` data is retained for clean and defective runs and absent for invalid-input, cancelled, and unexpected-error results. -- [ ] Add focused mapper tests for source-file grouping, specification-item nodes, incoming/outgoing link nodes, bracketed status labels, and pass/fail status. -- [ ] Add presenter tests for `SMTRunnerConsoleView` creation and event emission for successful and defective traces. -- [ ] Add run-profile tests proving plain text output remains the default and test-runner output is selected only when configured. -- [ ] Add navigation tests proving test-runner nodes navigate to specification declarations and source-side coverage tags through `OftTraceNavigationResolver`. -- [ ] Keep existing trace action, trace service, console presenter, run-configuration, and navigation tests green. -- [ ] Keep the OpenFastTrace trace clean for `feat`, `req`, `scn`, `dsn`, `impl`, `utest`, and `itest` artifacts in scope. -- [ ] Keep path coverage at or above the documented 80% threshold. -- [ ] Keep dependency policy unchanged and dependency verification compatible with the build. -- [ ] Run `./gradlew test`. -- [ ] Run `./gradlew check`. -- [ ] Run `./gradlew verifyPlugin`. +- [x] Add tests for run-configuration presentation-mode defaults, persistence, and backward-compatible XML reading. +- [x] Add settings-editor tests for selecting and applying the result-view option. +- [x] Add `OftTraceResult` and `OftTraceService` tests proving structured `Trace` data is retained for clean and defective runs and absent for invalid-input, cancelled, and unexpected-error results. +- [x] Add focused mapper tests for source-file grouping, specification-item nodes, incoming/outgoing link nodes, bracketed status labels, and pass/fail status. +- [x] Add presenter tests for `SMTRunnerConsoleView` creation and event emission for successful and defective traces. +- [x] Add run-profile tests proving plain text output remains the default and test-runner output is selected only when configured. +- [x] Add navigation tests proving test-runner nodes navigate to specification declarations and source-side coverage tags through `OftTraceNavigationResolver`. +- [x] Keep existing trace action, trace service, console presenter, run-configuration, and navigation tests green. +- [x] Keep the OpenFastTrace trace clean for `feat`, `req`, `scn`, `dsn`, `impl`, `utest`, and `itest` artifacts in scope. +- [x] Keep path coverage at or above the documented 80% threshold. +- [x] Keep dependency policy unchanged and dependency verification compatible with the build. +- [x] Run `./gradlew test`. +- [x] Run `./gradlew check`. +- [x] Run `./gradlew verifyPlugin`. - [ ] Keep the SonarQube Cloud quality gate green after CI analysis. ### Update user documentation -- [ ] Update `README.md` to describe selecting the test runner result view from an OpenFastTrace run configuration and using navigation from the structured result tree. +- [x] Update `README.md` to describe selecting the test runner result view from an OpenFastTrace run configuration and using navigation from the structured result tree. ## Version and Changelog Update - [x] Check if the current version mentioned in the build scripts and code parameters is the same as the latest GitHub release. Current `gradle.properties` version is `0.7.0`; latest GitHub release is `0.7.0` as of 2026-06-09. - [x] Decide if and which part of the version needs to be incremented. GH-40 is a feature, so the minor version needs to be incremented. -- [ ] Raise the version to `0.8.0` (this is a feature release). -- [ ] Write the changelog entry for `0.8.0`. -- [ ] Update release date to `2026-06-09`. -- [ ] Ensure that the issue list contains `#40: Integrate OFT Trace into Test Runner UI`. +- [x] Raise the version to `0.8.0` (this is a feature release). +- [x] Write the changelog entry for `0.8.0`. +- [x] Update release date to `2026-06-09`. +- [x] Ensure that the issue list contains `#40: Integrate OFT Trace into Test Runner UI`. diff --git a/gradle.properties b/gradle.properties index 6e16a7c..43eefca 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group=org.itsallcode.openfasttrace -version=0.7.0 +version=0.8.0 platformType=IC platformVersion=2026.1 diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceResult.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceResult.java index 320da0a..fa75ccb 100644 --- a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceResult.java +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceResult.java @@ -1,32 +1,46 @@ package org.itsallcode.openfasttrace.intellijplugin.trace; +import org.itsallcode.openfasttrace.api.core.Trace; + +import java.util.Optional; + public final class OftTraceResult { private final Status status; private final String output; + private final Trace trace; - private OftTraceResult(final Status status, final String output) { + private OftTraceResult(final Status status, final String output, final Trace trace) { this.status = status; this.output = output; + this.trace = trace; } public static OftTraceResult success(final String output) { - return new OftTraceResult(Status.SUCCESS, output); + return success(output, null); + } + + public static OftTraceResult success(final String output, final Trace trace) { + return new OftTraceResult(Status.SUCCESS, output, trace); } public static OftTraceResult failure(final String output) { - return new OftTraceResult(Status.FAILURE, output); + return failure(output, null); + } + + public static OftTraceResult failure(final String output, final Trace trace) { + return new OftTraceResult(Status.FAILURE, output, trace); } public static OftTraceResult error(final String output) { - return new OftTraceResult(Status.ERROR, output); + return new OftTraceResult(Status.ERROR, output, null); } public static OftTraceResult invalidInput(final String output) { - return new OftTraceResult(Status.INVALID_INPUT, output); + return new OftTraceResult(Status.INVALID_INPUT, output, null); } public static OftTraceResult cancelled() { - return new OftTraceResult(Status.CANCELLED, "OpenFastTrace trace was cancelled."); + return new OftTraceResult(Status.CANCELLED, "OpenFastTrace trace was cancelled.", null); } boolean isSuccessful() { @@ -51,6 +65,10 @@ public String output() { return output; } + public Optional trace() { + return Optional.ofNullable(trace); + } + private enum Status { SUCCESS, FAILURE, diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceRunContentOutputPresenter.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceRunContentOutputPresenter.java index bea1f5b..01ffbd0 100644 --- a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceRunContentOutputPresenter.java +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceRunContentOutputPresenter.java @@ -1,6 +1,5 @@ package org.itsallcode.openfasttrace.intellijplugin.trace; -import com.intellij.execution.RunContentDescriptorId; import com.intellij.execution.executors.DefaultRunExecutor; import com.intellij.execution.impl.ConsoleViewImpl; import com.intellij.execution.ui.ConsoleView; @@ -51,7 +50,7 @@ public void show(final Project project, final String contentTitle, final OftTrac } final RunContentDescriptor descriptor = new RunContentDescriptor(console, null, console.getComponent(), contentTitle); - descriptor.setId(new OftRunContentDescriptorId(NEXT_DESCRIPTOR_ID.getAndIncrement())); + descriptor.setExecutionId(NEXT_DESCRIPTOR_ID.getAndIncrement()); Disposer.register(descriptor, console); runContentShower.accept(project, descriptor); } @@ -83,11 +82,4 @@ private static void restoreCycleBufferSize(final String previousBufferSize) { System.setProperty(IDEA_CYCLE_BUFFER_SIZE, previousBufferSize); } } - - private record OftRunContentDescriptorId(int uid) implements RunContentDescriptorId { - @Override - public int getUid() { - return uid; - } - } } diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceService.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceService.java index fafe54a..ae79e92 100644 --- a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceService.java +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceService.java @@ -66,7 +66,7 @@ public OftTraceResult traceProject(final OftTraceInputs inputs, final OftTracePr progress.checkCanceled(); final String output = buildTraceOutput(inputs, trace); progress.phase("Finished OpenFastTrace trace.", 1.0D); - return trace.hasNoDefects() ? OftTraceResult.success(output) : OftTraceResult.failure(output); + return trace.hasNoDefects() ? OftTraceResult.success(output, trace) : OftTraceResult.failure(output, trace); } catch (final ProcessCanceledException exception) { throw exception; } catch (final RuntimeException exception) { diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceSettingsComponent.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceSettingsComponent.java index bf11062..6fb88b2 100644 --- a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceSettingsComponent.java +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceSettingsComponent.java @@ -30,16 +30,29 @@ public final class OftTraceSettingsComponent { private final JBTextArea additionalPathsTextArea = new JBTextArea(); private final JBTextField artifactTypesField = new JBTextField(); private final JBTextField tagsField = new JBTextField(); + private final JBRadioButton plainTextResultViewRadioButton = + new JBRadioButton("Plain text output"); + private final JBRadioButton testRunnerResultViewRadioButton = + new JBRadioButton("IntelliJ Test Runner UI"); private final JBLabel resolvedRelativeToLabel = new JBLabel(); private final JBTextArea validationMessagesArea = new JBTextArea(); private final Path projectRoot; + private final boolean showResultViewSelection; private final JPanel panel; public OftTraceSettingsComponent(final Path projectRoot) { + this(projectRoot, false); + } + + public OftTraceSettingsComponent(final Path projectRoot, final boolean showResultViewSelection) { this.projectRoot = projectRoot; + this.showResultViewSelection = showResultViewSelection; final ButtonGroup traceScopeGroup = new ButtonGroup(); traceScopeGroup.add(wholeProjectRadioButton); traceScopeGroup.add(selectedResourcesRadioButton); + final ButtonGroup resultViewGroup = new ButtonGroup(); + resultViewGroup.add(plainTextResultViewRadioButton); + resultViewGroup.add(testRunnerResultViewRadioButton); selectedResourcesRadioButton.addActionListener(event -> updateSelectedResourcesEnabledState()); wholeProjectRadioButton.addActionListener(event -> updateSelectedResourcesEnabledState()); additionalPathsTextArea.setLineWrap(false); @@ -78,7 +91,7 @@ public void changedUpdate(final DocumentEvent event) { additionalPathsFeedbackPanel.add(resolvedRelativeToLabel, BorderLayout.NORTH); additionalPathsFeedbackPanel.add(validationMessagesArea, BorderLayout.CENTER); additionalPathsPanel.add(additionalPathsFeedbackPanel, BorderLayout.SOUTH); - panel = FormBuilder.createFormBuilder() + final FormBuilder formBuilder = FormBuilder.createFormBuilder() .addComponent(wholeProjectRadioButton) .addComponent(selectedResourcesRadioButton) .addComponent(includeSourceRootsCheckBox, 1) @@ -88,7 +101,15 @@ public void changedUpdate(final DocumentEvent event) { .addLabeledComponent("Artifact types:", artifactTypesField) .addTooltip("comma-separated, empty = all") .addLabeledComponent("Tags:", tagsField) - .addTooltip("comma-separated, empty = all") + .addTooltip("comma-separated, empty = all"); + if (showResultViewSelection) { + formBuilder + .addSeparator() + .addComponent(new JBLabel("Result view")) + .addComponent(plainTextResultViewRadioButton, 1) + .addComponent(testRunnerResultViewRadioButton, 1); + } + panel = formBuilder .addComponentFillVertically(new JPanel(), 0) .getPanel(); setSettings(OftTraceSettingsSnapshot.DEFAULT); @@ -107,7 +128,8 @@ public OftTraceSettingsSnapshot getSettings() { includeTestRootsCheckBox.isSelected(), additionalPathsTextArea.getText(), artifactTypesField.getText(), - tagsField.getText() + tagsField.getText(), + selectedResultView() ); } @@ -119,6 +141,8 @@ public void setSettings(final OftTraceSettingsSnapshot settings) { additionalPathsTextArea.setText(settings.additionalPathsText()); artifactTypesField.setText(settings.artifactTypesText()); tagsField.setText(settings.tagsText()); + plainTextResultViewRadioButton.setSelected(settings.resultView() == OftTraceResultView.PLAIN_TEXT); + testRunnerResultViewRadioButton.setSelected(settings.resultView() == OftTraceResultView.TEST_RUNNER); updateSelectedResourcesEnabledState(); } @@ -146,6 +170,13 @@ private void updateSelectedResourcesEnabledState() { updateValidationFeedback(); } + private OftTraceResultView selectedResultView() { + if (!showResultViewSelection || plainTextResultViewRadioButton.isSelected()) { + return OftTraceResultView.PLAIN_TEXT; + } + return OftTraceResultView.TEST_RUNNER; + } + // [impl->dsn~show-per-line-validation-for-additional-trace-paths~1] private void updateValidationFeedback() { if (!selectedResourcesRadioButton.isSelected() || projectRoot == null) { diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceSettingsSnapshot.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceSettingsSnapshot.java index d215639..feef386 100644 --- a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceSettingsSnapshot.java +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceSettingsSnapshot.java @@ -9,7 +9,8 @@ public record OftTraceSettingsSnapshot( boolean includeTestRoots, String additionalPathsText, String artifactTypesText, - String tagsText + String tagsText, + OftTraceResultView resultView ) { public static final OftTraceSettingsSnapshot DEFAULT = new OftTraceSettingsSnapshot( OftTraceScopeMode.WHOLE_PROJECT, @@ -17,12 +18,32 @@ public record OftTraceSettingsSnapshot( true, "doc/", "", - "" + "", + OftTraceResultView.PLAIN_TEXT ); private static final Pattern LINE_SEPARATOR = Pattern.compile("\\R"); public static final Pattern COMMA = Pattern.compile(","); + public OftTraceSettingsSnapshot( + final OftTraceScopeMode scopeMode, + final boolean includeSourceRoots, + final boolean includeTestRoots, + final String additionalPathsText, + final String artifactTypesText, + final String tagsText + ) { + this( + scopeMode, + includeSourceRoots, + includeTestRoots, + additionalPathsText, + artifactTypesText, + tagsText, + OftTraceResultView.PLAIN_TEXT + ); + } + public List additionalPaths() { return LINE_SEPARATOR.splitAsStream(additionalPathsText) .map(String::trim) diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunConfiguration.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunConfiguration.java index d158212..bb3f13c 100644 --- a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunConfiguration.java +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunConfiguration.java @@ -11,6 +11,7 @@ import com.intellij.openapi.util.InvalidDataException; import com.intellij.openapi.util.WriteExternalException; import com.intellij.util.xmlb.XmlSerializer; +import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceResultView; import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceScopeMode; import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceSettingsSnapshot; import org.jetbrains.annotations.NotNull; @@ -46,7 +47,8 @@ public OftTraceSettingsSnapshot snapshot() { state.isIncludeTestRoots(), state.getAdditionalPathsText(), state.getArtifactTypesText(), - state.getTagsText() + state.getTagsText(), + parseResultView(state.getResultView()) ); } @@ -57,6 +59,7 @@ public void updateFrom(final OftTraceSettingsSnapshot snapshot) { state.setAdditionalPathsText(snapshot.additionalPathsText()); state.setArtifactTypesText(snapshot.artifactTypesText()); state.setTagsText(snapshot.tagsText()); + state.setResultView(snapshot.resultView().name()); } @Override @@ -82,6 +85,17 @@ private static OftTraceScopeMode parseScopeMode(final String value) { } } + private static OftTraceResultView parseResultView(final String value) { + if (value == null || value.isBlank()) { + return OftTraceSettingsSnapshot.DEFAULT.resultView(); + } + try { + return OftTraceResultView.valueOf(value); + } catch (final IllegalArgumentException ignored) { + return OftTraceSettingsSnapshot.DEFAULT.resultView(); + } + } + private static final class State implements Serializable { private String traceScopeMode = OftTraceSettingsSnapshot.DEFAULT.scopeMode().name(); private boolean includeSourceRoots = OftTraceSettingsSnapshot.DEFAULT.includeSourceRoots(); @@ -89,6 +103,7 @@ private static final class State implements Serializable { private String additionalPathsText = OftTraceSettingsSnapshot.DEFAULT.additionalPathsText(); private String artifactTypesText = OftTraceSettingsSnapshot.DEFAULT.artifactTypesText(); private String tagsText = OftTraceSettingsSnapshot.DEFAULT.tagsText(); + private String resultView = OftTraceSettingsSnapshot.DEFAULT.resultView().name(); public String getTraceScopeMode() { return traceScopeMode; @@ -137,5 +152,13 @@ public String getTagsText() { public void setTagsText(final String tagsText) { this.tagsText = tagsText; } + + public String getResultView() { + return resultView; + } + + public void setResultView(final String resultView) { + this.resultView = resultView; + } } } diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunConfigurationSettingsEditor.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunConfigurationSettingsEditor.java index 5de863a..17c6bd9 100644 --- a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunConfigurationSettingsEditor.java +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunConfigurationSettingsEditor.java @@ -34,7 +34,7 @@ protected void applyEditorTo(@NotNull final OftRunConfiguration configuration) { protected @NotNull JComponent createEditor() { final String basePath = project.getBasePath(); final Path projectRoot = basePath != null ? Paths.get(basePath) : null; - component = new OftTraceSettingsComponent(projectRoot); + component = new OftTraceSettingsComponent(projectRoot, true); return component.getPanel(); } } diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunProfileState.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunProfileState.java index 0bcc217..d278914 100644 --- a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunProfileState.java +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunProfileState.java @@ -1,6 +1,7 @@ package org.itsallcode.openfasttrace.intellijplugin.trace.runconfig; import com.intellij.execution.DefaultExecutionResult; +import com.intellij.execution.ExecutionException; import com.intellij.execution.ExecutionResult; import com.intellij.execution.Executor; import com.intellij.execution.configurations.RunProfileState; @@ -8,6 +9,9 @@ import com.intellij.execution.process.ProcessHandler; import com.intellij.execution.runners.ExecutionEnvironment; import com.intellij.execution.runners.ProgramRunner; +import com.intellij.execution.testframework.sm.runner.SMTRunnerConsoleProperties; +import com.intellij.execution.testframework.sm.runner.ui.SMTRunnerConsoleView; +import com.intellij.execution.ui.ExecutionConsole; import com.intellij.execution.ui.ConsoleView; import com.intellij.openapi.project.Project; import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceBackgroundRunner; @@ -15,10 +19,12 @@ import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceInputResolver; import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceOutputPresenter; import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceResult; +import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceResultView; import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceRunContentOutputPresenter; import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceRunner; import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceService; import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceSettingsSnapshot; +import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceTestRunnerOutputPresenter; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -32,32 +38,64 @@ public OftRunProfileState(final ExecutionEnvironment environment, final OftTrace } @Override - public @Nullable ExecutionResult execute(final Executor executor, @NotNull final ProgramRunner runner) { + public @Nullable ExecutionResult execute(final Executor executor, @NotNull final ProgramRunner runner) + throws ExecutionException { final Project project = environment.getProject(); final OftTraceInputResolution resolution = OftTraceInputResolver.resolve(project, settings); final String contentTitle = "OpenFastTrace Trace: " + environment.getRunProfile().getName(); - final ConsoleView console = OftTraceRunContentOutputPresenter.createTraceConsole(project); - final OftTraceOutputPresenter outputPresenter = new OftTraceRunContentOutputPresenter( - p -> console, - (p, descriptor) -> { - // The descriptor is handled by the execution framework. - } - ); + final ExecutionPresentation executionPresentation = createExecutionPresentation(project, executor); if (!resolution.isValid()) { - outputPresenter.show(project, contentTitle, OftTraceResult.invalidInput(resolution.errorMessage())); - return new DefaultExecutionResult(console, new NopProcessHandler()); + executionPresentation.outputPresenter() + .show(project, contentTitle, OftTraceResult.invalidInput(resolution.errorMessage())); + return new DefaultExecutionResult(executionPresentation.console(), new NopProcessHandler()); } final ProcessHandler processHandler = new NopProcessHandler(); final OftTraceRunner traceRunner = new OftTraceBackgroundRunner( new OftTraceService(), - outputPresenter, + executionPresentation.outputPresenter(), processHandler ); traceRunner.run(project, resolution.inputs(), contentTitle); - return new DefaultExecutionResult(console, processHandler); + return new DefaultExecutionResult(executionPresentation.console(), processHandler); + } + + private ExecutionPresentation createExecutionPresentation(final Project project, final Executor executor) { + if (settings.resultView() == OftTraceResultView.TEST_RUNNER) { + return createTestRunnerExecutionPresentation(project, executor); + } + return createPlainTextExecutionPresentation(project); + } + + // [impl->dsn~plain-text-as-default-run-configuration-result-view~1] + private static ExecutionPresentation createPlainTextExecutionPresentation(final Project project) { + final ConsoleView console = OftTraceRunContentOutputPresenter.createTraceConsole(project); + final OftTraceOutputPresenter outputPresenter = new OftTraceRunContentOutputPresenter( + p -> console, + (p, descriptor) -> { + // The descriptor is handled by the execution framework. + } + ); + return new ExecutionPresentation(console, outputPresenter); + } + + // [impl->dsn~select-test-runner-trace-result-view~1] + private ExecutionPresentation createTestRunnerExecutionPresentation(final Project project, final Executor executor) { + final SMTRunnerConsoleProperties properties = new SMTRunnerConsoleProperties( + project, + environment.getRunProfile(), + "OpenFastTrace", + executor + ); + final SMTRunnerConsoleView console = new SMTRunnerConsoleView(properties); + console.initUI(); + final OftTraceOutputPresenter outputPresenter = new OftTraceTestRunnerOutputPresenter(p -> console); + return new ExecutionPresentation(console, outputPresenter); + } + + private record ExecutionPresentation(ExecutionConsole console, OftTraceOutputPresenter outputPresenter) { } } diff --git a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceResultTest.java b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceResultTest.java index 3724dd4..d1d159c 100644 --- a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceResultTest.java +++ b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceResultTest.java @@ -1,10 +1,14 @@ package org.itsallcode.openfasttrace.intellijplugin.trace; +import org.itsallcode.openfasttrace.api.core.Trace; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import java.util.List; + import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.sameInstance; class OftTraceResultTest { @Test @@ -22,6 +26,29 @@ void testGivenAllResultKindsThenTheyExposeTheExpectedStatus() { ); } + // [itest->dsn~trace-test-runner-presentation~1] + @Test + void testGivenSuccessfulOrDefectiveTraceResultThenItExposesStructuredTraceData() { + final Trace trace = emptyTrace(); + + Assertions.assertAll( + () -> assertThat(OftTraceResult.success("ok", trace).trace().orElseThrow(), sameInstance(trace)), + () -> assertThat(OftTraceResult.failure("defects", trace).trace().orElseThrow(), sameInstance(trace)) + ); + } + + // [itest->dsn~trace-test-runner-presentation~1] + @Test + void testGivenResultWithoutACompletedTraceThenItDoesNotExposeStructuredTraceData() { + Assertions.assertAll( + () -> assertThat(OftTraceResult.success("ok").trace().isEmpty(), is(true)), + () -> assertThat(OftTraceResult.failure("defects").trace().isEmpty(), is(true)), + () -> assertThat(OftTraceResult.error("error").trace().isEmpty(), is(true)), + () -> assertThat(OftTraceResult.invalidInput("invalid").trace().isEmpty(), is(true)), + () -> assertThat(OftTraceResult.cancelled().trace().isEmpty(), is(true)) + ); + } + private static void assertResult( final OftTraceResult result, final boolean successful, @@ -36,4 +63,11 @@ private static void assertResult( () -> assertThat(result.output(), is(output)) ); } + + private static Trace emptyTrace() { + return Trace.builder() + .items(List.of()) + .defectItems(List.of()) + .build(); + } } diff --git a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceRunContentOutputPresenterTest.java b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceRunContentOutputPresenterTest.java index ab9467e..90b0275 100644 --- a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceRunContentOutputPresenterTest.java +++ b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceRunContentOutputPresenterTest.java @@ -178,7 +178,7 @@ abstract class AbstractOftPlatformTestCase { } } - public void testGivenTraceOutputWhenPresentedThenRunContentDescriptorGetsAnId() { + public void testGivenTraceOutputWhenPresentedThenRunContentDescriptorGetsAnExecutionId() { final AtomicReference descriptorRef = new AtomicReference<>(); final OftTraceRunContentOutputPresenter presenter = new OftTraceRunContentOutputPresenter( OftTraceRunContentOutputPresenter::createTraceConsole, @@ -195,8 +195,7 @@ public void testGivenTraceOutputWhenPresentedThenRunContentDescriptorGetsAnId() )); assertThat(descriptorRef.get(), notNullValue()); - assertThat(descriptorRef.get().getId(), notNullValue()); - assertThat(descriptorRef.get().getId().getUid(), Matchers.greaterThan(0)); + assertThat(descriptorRef.get().getExecutionId(), Matchers.greaterThan(0L)); } private String readConsoleText(final ConsoleViewImpl console) { diff --git a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceServiceTest.java b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceServiceTest.java index 72f4964..d5b69f3 100644 --- a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceServiceTest.java +++ b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceServiceTest.java @@ -44,7 +44,9 @@ void testGivenCleanTraceInputWhenTracingThenItReturnsSuccessfulPlainTextOutput(@ ), () -> assertThat(result.output(), Matchers.containsString("\u001B[")), () -> assertThat(renderedOutput, Matchers.containsString("ok -")), - () -> assertThat(renderedOutput, Matchers.not(Matchers.containsString("not ok"))) + () -> assertThat(renderedOutput, Matchers.not(Matchers.containsString("not ok"))), + // [itest->dsn~trace-test-runner-presentation~1] + () -> assertThat(result.trace().isPresent(), is(true)) ); } @@ -99,7 +101,9 @@ void testGivenDefectiveTraceInputWhenTracingThenItReturnsFailingPlainTextOutput( ), () -> assertThat(renderedOutput, Matchers.containsString("not ok")), () -> assertThat(renderedOutput, Matchers.containsString("req~trace_output_requirement~1")), - () -> assertThat(result.output(), Matchers.containsString("\u001B[")) + () -> assertThat(result.output(), Matchers.containsString("\u001B[")), + // [itest->dsn~trace-test-runner-presentation~1] + () -> assertThat(result.trace().isPresent(), is(true)) ); } @@ -180,7 +184,9 @@ void testGivenRuntimeExceptionWhenTracingThenItReturnsAnErrorResult(@TempDir fin () -> assertThat(result.isSuccessful(), is(false)), () -> assertThat(result.statusMessage(), is("OpenFastTrace trace failed unexpectedly.")), () -> assertThat(result.output(), Matchers.containsString("OpenFastTrace trace failed for input path")), - () -> assertThat(result.output(), Matchers.containsString("IllegalStateException: boom")) + () -> assertThat(result.output(), Matchers.containsString("IllegalStateException: boom")), + // [itest->dsn~trace-test-runner-presentation~1] + () -> assertThat(result.trace().isEmpty(), is(true)) ); } diff --git a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunConfigurationSettingsEditorTest.java b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunConfigurationSettingsEditorTest.java index 3390461..57c9631 100644 --- a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunConfigurationSettingsEditorTest.java +++ b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunConfigurationSettingsEditorTest.java @@ -1,6 +1,7 @@ package org.itsallcode.openfasttrace.intellijplugin.trace.runconfig; import org.itsallcode.openfasttrace.intellijplugin.AbstractOftPlatformTestCase; +import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceResultView; import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceScopeMode; import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceSettingsSnapshot; import org.junit.jupiter.api.Assertions; @@ -25,6 +26,8 @@ public void testGivenEditorWhenCreatingEditorThenItReturnsNonNullComponent() { assertThat(component, is(notNullValue())); } + // [itest->dsn~select-test-runner-trace-result-view~1] + // [itest->dsn~trace-configuration-integration~1] public void testGivenEditorWhenResettingFromConfigurationThenItUpdatesUI() { editor.createEditor(); // Initialize component final OftRunConfiguration configuration = createConfiguration("Test"); @@ -34,7 +37,8 @@ public void testGivenEditorWhenResettingFromConfigurationThenItUpdatesUI() { true, "additional", "dsn", - "mvp" + "mvp", + OftTraceResultView.TEST_RUNNER ); configuration.updateFrom(snapshot); @@ -48,10 +52,13 @@ public void testGivenEditorWhenResettingFromConfigurationThenItUpdatesUI() { () -> assertThat(uiSettings.includeTestRoots(), is(snapshot.includeTestRoots())), () -> assertThat(uiSettings.additionalPathsText(), is(snapshot.additionalPathsText())), () -> assertThat(uiSettings.artifactTypesText(), is(snapshot.artifactTypesText())), - () -> assertThat(uiSettings.tagsText(), is(snapshot.tagsText())) + () -> assertThat(uiSettings.tagsText(), is(snapshot.tagsText())), + () -> assertThat(uiSettings.resultView(), is(snapshot.resultView())) ); } + // [itest->dsn~select-test-runner-trace-result-view~1] + // [itest->dsn~trace-configuration-integration~1] public void testGivenEditorWhenApplyingToConfigurationThenItUpdatesConfiguration() { editor.createEditor(); // Initialize component final OftTraceSettingsSnapshot snapshot = new OftTraceSettingsSnapshot( @@ -60,7 +67,8 @@ public void testGivenEditorWhenApplyingToConfigurationThenItUpdatesConfiguration false, "more paths", "req", - "tag" + "tag", + OftTraceResultView.TEST_RUNNER ); editor.component.setSettings(snapshot); @@ -74,7 +82,8 @@ public void testGivenEditorWhenApplyingToConfigurationThenItUpdatesConfiguration () -> assertThat(stored.includeTestRoots(), is(snapshot.includeTestRoots())), () -> assertThat(stored.additionalPathsText(), is(snapshot.additionalPathsText())), () -> assertThat(stored.artifactTypesText(), is(snapshot.artifactTypesText())), - () -> assertThat(stored.tagsText(), is(snapshot.tagsText())) + () -> assertThat(stored.tagsText(), is(snapshot.tagsText())), + () -> assertThat(stored.resultView(), is(snapshot.resultView())) ); } diff --git a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunConfigurationTest.java b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunConfigurationTest.java index a36fa0b..980a187 100644 --- a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunConfigurationTest.java +++ b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunConfigurationTest.java @@ -3,6 +3,7 @@ import com.intellij.openapi.util.InvalidDataException; import com.intellij.openapi.util.WriteExternalException; import org.itsallcode.openfasttrace.intellijplugin.AbstractOftPlatformTestCase; +import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceResultView; import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceScopeMode; import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceSettingsSnapshot; import org.jdom.Element; @@ -13,6 +14,8 @@ // [itest->dsn~openfasttrace-run-configuration~1] public class OftRunConfigurationTest extends AbstractOftPlatformTestCase { + // [itest->dsn~select-test-runner-trace-result-view~1] + // [itest->dsn~trace-configuration-integration~1] public void testGivenRunConfigurationWhenUpdatingFromSnapshotThenItStoresTheSettings() { final OftRunConfiguration configuration = createConfiguration("Test"); final OftTraceSettingsSnapshot snapshot = new OftTraceSettingsSnapshot( @@ -21,7 +24,8 @@ public void testGivenRunConfigurationWhenUpdatingFromSnapshotThenItStoresTheSett true, "additional", "dsn", - "mvp" + "mvp", + OftTraceResultView.TEST_RUNNER ); configuration.updateFrom(snapshot); @@ -33,10 +37,13 @@ public void testGivenRunConfigurationWhenUpdatingFromSnapshotThenItStoresTheSett () -> assertThat(stored.includeTestRoots(), is(snapshot.includeTestRoots())), () -> assertThat(stored.additionalPathsText(), is(snapshot.additionalPathsText())), () -> assertThat(stored.artifactTypesText(), is(snapshot.artifactTypesText())), - () -> assertThat(stored.tagsText(), is(snapshot.tagsText())) + () -> assertThat(stored.tagsText(), is(snapshot.tagsText())), + () -> assertThat(stored.resultView(), is(snapshot.resultView())) ); } + // [itest->dsn~select-test-runner-trace-result-view~1] + // [itest->dsn~trace-configuration-integration~1] public void testGivenRunConfigurationWithSettingsWhenWritingAndReadingExternalThenItPreservesSettings() throws WriteExternalException, InvalidDataException { final OftRunConfiguration configuration = createConfiguration("Test"); @@ -46,7 +53,8 @@ public void testGivenRunConfigurationWithSettingsWhenWritingAndReadingExternalTh true, "additional", "dsn", - "mvp" + "mvp", + OftTraceResultView.TEST_RUNNER ); configuration.updateFrom(snapshot); @@ -63,10 +71,37 @@ public void testGivenRunConfigurationWithSettingsWhenWritingAndReadingExternalTh () -> assertThat(stored.includeTestRoots(), is(snapshot.includeTestRoots())), () -> assertThat(stored.additionalPathsText(), is(snapshot.additionalPathsText())), () -> assertThat(stored.artifactTypesText(), is(snapshot.artifactTypesText())), - () -> assertThat(stored.tagsText(), is(snapshot.tagsText())) + () -> assertThat(stored.tagsText(), is(snapshot.tagsText())), + () -> assertThat(stored.resultView(), is(snapshot.resultView())) ); } + // [itest->dsn~plain-text-as-default-run-configuration-result-view~1] + // [itest->dsn~trace-configuration-integration~1] + public void testGivenRunConfigurationWithNoStoredResultViewWhenReadingExternalThenItDefaultsToPlainText() + throws InvalidDataException { + final OftRunConfiguration configuration = createConfiguration("Test"); + + configuration.readExternal(new Element("configuration")); + + assertThat(configuration.snapshot().resultView(), is(OftTraceResultView.PLAIN_TEXT)); + } + + // [itest->dsn~plain-text-as-default-run-configuration-result-view~1] + // [itest->dsn~trace-configuration-integration~1] + public void testGivenRunConfigurationWithInvalidStoredResultViewWhenReadingExternalThenItDefaultsToPlainText() + throws InvalidDataException { + final Element element = new Element("configuration"); + element.addContent(new Element("option") + .setAttribute("name", "resultView") + .setAttribute("value", "UNKNOWN_RESULT_VIEW")); + final OftRunConfiguration configuration = createConfiguration("Test"); + + configuration.readExternal(element); + + assertThat(configuration.snapshot().resultView(), is(OftTraceResultView.PLAIN_TEXT)); + } + private OftRunConfiguration createConfiguration(final String name) { final OftRunConfigurationType type = new OftRunConfigurationType(); final OftRunConfigurationFactory factory = (OftRunConfigurationFactory) type.getConfigurationFactories()[0]; diff --git a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunProfileStateTest.java b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunProfileStateTest.java index cfd1b62..e863aca 100644 --- a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunProfileStateTest.java +++ b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunProfileStateTest.java @@ -4,28 +4,83 @@ import com.intellij.execution.executors.DefaultRunExecutor; import com.intellij.execution.runners.ExecutionEnvironment; import com.intellij.execution.runners.ExecutionEnvironmentBuilder; +import com.intellij.execution.testframework.sm.runner.ui.SMTRunnerConsoleView; import com.intellij.openapi.util.Disposer; import org.itsallcode.openfasttrace.intellijplugin.AbstractOftPlatformTestCase; +import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceResultView; +import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceScopeMode; import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceSettingsSnapshot; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; + public class OftRunProfileStateTest extends AbstractOftPlatformTestCase { public void testExecuteReturnsNotNull() throws Exception { - OftRunConfiguration configuration = new OftRunConfiguration( + final ExecutionEnvironment environment = createEnvironment(); + final OftRunProfileState state = new OftRunProfileState( + environment, + invalidInputsSettings(OftTraceResultView.PLAIN_TEXT) + ); + + final ExecutionResult result = state.execute(environment.getExecutor(), environment.getRunner()); + + assertNotNull("ExecutionResult should not be null", result); + if (result != null && result.getExecutionConsole() != null) { + Disposer.register(getTestRootDisposable(), result.getExecutionConsole()); + } + } + + // [itest->dsn~plain-text-as-default-run-configuration-result-view~1] + public void testGivenPlainTextResultViewWhenExecutingThenItUsesPlainTextConsole() throws Exception { + final ExecutionEnvironment environment = createEnvironment(); + final OftRunProfileState state = new OftRunProfileState( + environment, + invalidInputsSettings(OftTraceResultView.PLAIN_TEXT) + ); + + final ExecutionResult result = state.execute(environment.getExecutor(), environment.getRunner()); + + Disposer.register(getTestRootDisposable(), result.getExecutionConsole()); + assertThat(result.getExecutionConsole(), is(not(instanceOf(SMTRunnerConsoleView.class)))); + } + + // [itest->dsn~select-test-runner-trace-result-view~1] + public void testGivenTestRunnerResultViewWhenExecutingThenItUsesTestRunnerConsole() throws Exception { + final ExecutionEnvironment environment = createEnvironment(); + final OftRunProfileState state = new OftRunProfileState( + environment, + invalidInputsSettings(OftTraceResultView.TEST_RUNNER) + ); + + final ExecutionResult result = state.execute(environment.getExecutor(), environment.getRunner()); + + Disposer.register(getTestRootDisposable(), result.getExecutionConsole()); + assertThat(result.getExecutionConsole(), is(instanceOf(SMTRunnerConsoleView.class))); + } + + private ExecutionEnvironment createEnvironment() { + final OftRunConfiguration configuration = new OftRunConfiguration( getProject(), new OftRunConfigurationFactory(new OftRunConfigurationType()), "Test" ); - ExecutionEnvironment environment = ExecutionEnvironmentBuilder.create( + return ExecutionEnvironmentBuilder.create( DefaultRunExecutor.getRunExecutorInstance(), configuration ).build(); - OftRunProfileState state = new OftRunProfileState(environment, OftTraceSettingsSnapshot.DEFAULT); - - ExecutionResult result = state.execute(environment.getExecutor(), environment.getRunner()); + } - assertNotNull("ExecutionResult should not be null", result); - if (result != null && result.getExecutionConsole() != null) { - Disposer.register(getTestRootDisposable(), result.getExecutionConsole()); - } + private static OftTraceSettingsSnapshot invalidInputsSettings(final OftTraceResultView resultView) { + return new OftTraceSettingsSnapshot( + OftTraceScopeMode.SELECTED_RESOURCES, + false, + false, + "", + "", + "", + resultView + ); } } From 20c585279b1c1813c97c1a08281a378e527fd324 Mon Sep 17 00:00:00 2001 From: redcatbear Date: Tue, 9 Jun 2026 13:11:21 +0200 Subject: [PATCH 02/10] #40: Project-local filenames in test result UI. --- README.md | 2 +- doc/changes/changes_0.8.0.md | 9 + ...integrate-oft-trace-into-test-runner-ui.md | 10 + doc/design/building_block_view.md | 9 +- doc/design/runtime_view.md | 86 +++++- doc/design/solution_strategy.md | 4 +- doc/system_requirements.md | 158 ++++++++++- .../trace/OftTraceResultView.java | 6 + .../trace/OftTraceTestNodeDetails.java | 185 +++++++++++++ .../trace/OftTraceTestProxy.java | 48 ++++ .../OftTraceTestRunnerOutputPresenter.java | 176 ++++++++++++ .../trace/OftTraceTestTree.java | 50 ++++ .../trace/OftTraceTestTreeMapper.java | 189 +++++++++++++ .../trace/OftTraceTestProxyTest.java | 102 +++++++ ...OftTraceTestRunnerOutputPresenterTest.java | 202 ++++++++++++++ .../trace/OftTraceTestTreeMapperTest.java | 253 ++++++++++++++++++ 16 files changed, 1483 insertions(+), 6 deletions(-) create mode 100644 doc/changes/changes_0.8.0.md create mode 100644 src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceResultView.java create mode 100644 src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestNodeDetails.java create mode 100644 src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestProxy.java create mode 100644 src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerOutputPresenter.java create mode 100644 src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTree.java create mode 100644 src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTreeMapper.java create mode 100644 src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestProxyTest.java create mode 100644 src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerOutputPresenterTest.java create mode 100644 src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTreeMapperTest.java diff --git a/README.md b/README.md index 528723d..ab32de3 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ After the sandbox IDE opens a project, configure trace scope under `Settings | T Run an OFT trace from `Tools | OpenFastTrace | Trace Project`, use the default shortcut `Ctrl+Alt+Shift+O`, or create and run a dedicated `OpenFastTrace` run configuration from the IDE's run/debug toolbar. The plugin traces the configured inputs in the background and shows the plain text result in an IDE output tab with ANSI colors preserved. You can click specification item IDs in the report to jump to their declarations, including source-side items generated from coverage tags. -In an `OpenFastTrace` run configuration, keep `Plain text output` for the existing report view or select `IntelliJ Test Runner UI` to inspect the structured trace as source-file suites, specification-item tests, and trace-link sub-tests. Passed and failed nodes reflect the OpenFastTrace trace status, and source navigation from item and link nodes opens the corresponding specification declaration or source-side coverage tag. +In an `OpenFastTrace` run configuration, keep `Plain text output` for the existing report view or select `IntelliJ Test Runner UI` to inspect the structured trace as source-file suites, specification-item tests, and trace-link sub-tests. Specification items are sorted inside each source file by artifact type, ID name, and revision, and show their title before the ID when available. Passed and failed nodes reflect the OpenFastTrace trace status, and source navigation from item and link nodes opens the corresponding specification declaration or source-side coverage tag. The plugin also bundles an `OpenFastTrace` live-template group under `Settings | Editor | Live Templates`. Use abbreviations such as `feat`, `req`, `dsn`, and `scn` in a supported editing context, then press `Tab` to insert an OFT item skeleton. The `scn` template inserts a scenario stub with placeholders for `Given`, `When`, `Then`, and the covered requirement. While the caret is still in a template's covered-item field, use basic completion to select an existing specification item ID from the project index. diff --git a/doc/changes/changes_0.8.0.md b/doc/changes/changes_0.8.0.md new file mode 100644 index 0000000..6c70fb8 --- /dev/null +++ b/doc/changes/changes_0.8.0.md @@ -0,0 +1,9 @@ +# OpenFastTrace IntelliJ Plugin 0.8.0, released 2026-06-09 + +With this release of the plugin, OpenFastTrace run configurations can show trace results in IntelliJ's built-in Test Runner UI. + +The existing plain text output remains the default result view. Run configurations can opt into the Test Runner UI to inspect source-file suites, specification-item tests, trace-link sub-tests, pass/fail status, and navigation back to specification declarations or source-side coverage tags. + +## Features + +#40: Integrate OFT Trace into Test Runner UI diff --git a/doc/changesets/40-integrate-oft-trace-into-test-runner-ui.md b/doc/changesets/40-integrate-oft-trace-into-test-runner-ui.md index 4632f51..c858157 100644 --- a/doc/changesets/40-integrate-oft-trace-into-test-runner-ui.md +++ b/doc/changesets/40-integrate-oft-trace-into-test-runner-ui.md @@ -53,11 +53,15 @@ Keep presentation selection at the run-configuration layer. The global `Trace Pr - [x] Update `doc/system_requirements.md` with a user requirement for selecting the trace result view in an `OpenFastTrace` run configuration. - [x] Add system scenarios for showing trace results in the test runner by source file, specification item, and trace link. - [x] Add system scenarios for test-runner pass/fail mapping, bracketed status labels, incoming/outgoing link markers, and navigation from result nodes to source. +- [x] Add system requirements and scenarios for source-file suite roll-up, top-level trace roll-up, Unicode direction arrows, and node defect details. +- [x] Add system requirements and scenarios for title-prefixed specification item labels and deterministic per-file item ordering. - [x] Keep the existing plain text trace-output requirements and scenarios valid for the global `Trace Project` action and default run-configuration behavior. - [x] Stop and ask user for a review of the system requirements. - [x] Update `doc/design/solution_strategy.md` to document reuse of IntelliJ's SM test runner infrastructure for structured trace result presentation. - [x] Update `doc/design/building_block_view.md` with a test-runner trace presentation building block and the run-configuration result-view option. - [x] Update `doc/design/runtime_view.md` with design items for presenter selection, test-runner hierarchy construction, status mapping, and navigation. +- [x] Update the design with suite roll-up behavior, Unicode direction markers, and static trace-link defect detail templates. +- [x] Update the design with title-aware item labels and source-file suite item ordering. - [x] Stop and ask user for a review of the design. ### Implementation @@ -72,6 +76,10 @@ Keep presentation selection at the run-configuration layer. The global `Trace Pr - [x] Implement status-label derivation for specification items and links from OpenFastTrace trace data. - [x] Implement pass/fail mapping for clean and defective specification items and links. - [x] Implement `OftTraceTestRunnerOutputPresenter` using `SMTRunnerConsoleView` and `SMTestProxy`. +- [x] Roll failed descendants up to source-file suites and the top-level trace suite. +- [x] Replace ASCII trace-link direction markers with Unicode arrows. +- [x] Add clear failed-node details for specification items and trace links, using static templates for link-status explanations. +- [x] Display specification item titles before IDs when available and sort item entries per source file by artifact type, ID name, and revision. - [x] Connect test-runner node navigation through `OftTraceNavigationResolver`. - [x] Present invalid input, cancellation, and unexpected errors coherently when the test-runner output mode is selected. - [x] Preserve existing ANSI-colored plain text output and console hyperlink behavior. @@ -84,6 +92,8 @@ Keep presentation selection at the run-configuration layer. The global `Trace Pr - [x] Add `OftTraceResult` and `OftTraceService` tests proving structured `Trace` data is retained for clean and defective runs and absent for invalid-input, cancelled, and unexpected-error results. - [x] Add focused mapper tests for source-file grouping, specification-item nodes, incoming/outgoing link nodes, bracketed status labels, and pass/fail status. - [x] Add presenter tests for `SMTRunnerConsoleView` creation and event emission for successful and defective traces. +- [x] Add mapper and presenter tests for suite roll-up, top-level roll-up, Unicode arrows, and failed-node details. +- [x] Add mapper and presenter tests for title-prefixed item labels and per-file item ordering. - [x] Add run-profile tests proving plain text output remains the default and test-runner output is selected only when configured. - [x] Add navigation tests proving test-runner nodes navigate to specification declarations and source-side coverage tags through `OftTraceNavigationResolver`. - [x] Keep existing trace action, trace service, console presenter, run-configuration, and navigation tests green. diff --git a/doc/design/building_block_view.md b/doc/design/building_block_view.md index af41da8..5343098 100644 --- a/doc/design/building_block_view.md +++ b/doc/design/building_block_view.md @@ -273,17 +273,24 @@ 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 source-file suites, specification-item tests, and incoming or outgoing trace-link sub-tests; derives labels and pass/fail status from the OpenFastTrace trace status; and connects 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 title-aware labels, Unicode direction markers, pass/fail status, status roll-up, and defect details from the OpenFastTrace trace status; and connects node navigation to the existing OpenFastTrace trace navigation support. Covers: - `scn~show-trace-source-files-as-test-runner-suites~1` - `scn~show-trace-specification-items-as-test-runner-tests~1` +- `scn~show-specification-item-title-in-test-runner-ui~1` +- `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~1` - `scn~show-trace-link-status-in-test-runner-ui~1` - `scn~show-trace-link-direction-in-test-runner-ui~1` +- `scn~show-unicode-trace-link-direction-in-test-runner-ui~1` - `scn~map-specification-item-trace-status-to-test-runner-status~1` - `scn~map-trace-link-status-to-test-runner-status~1` +- `scn~roll-up-source-file-suite-trace-status~1` +- `scn~roll-up-top-level-trace-status~1` +- `scn~show-specification-item-defect-details-in-test-runner-ui~1` +- `scn~show-trace-link-defect-details-in-test-runner-ui~1` - `scn~navigate-from-test-runner-specification-items~1` - `scn~navigate-from-test-runner-trace-links~1` diff --git a/doc/design/runtime_view.md b/doc/design/runtime_view.md index 50403b7..50deafb 100644 --- a/doc/design/runtime_view.md +++ b/doc/design/runtime_view.md @@ -524,7 +524,7 @@ Needs: impl, itest **Given** the trace test-runner presentation receives a structured OpenFastTrace trace result **When** it builds the SM test tree -**Then** it groups traced specification items by source file and creates one SM test suite node for each source file. +**Then** it groups traced specification items by source file and creates one SM test suite node for each source file, using a project-local suite label for source paths below the opened project directory. Covers: - `scn~show-trace-source-files-as-test-runner-suites~1` @@ -543,6 +543,30 @@ Covers: Needs: impl, itest +### Show Specification Item Title in Test Runner UI +`dsn~show-specification-item-title-in-test-runner-ui~1` + +**Given** the trace test-runner presentation creates a node whose visible name contains a specification item ID +**When** the OpenFastTrace item for that visible ID has a non-blank title +**Then** it prefixes the visible node name with the title, followed by ` — ` and the full specification item ID. + +Covers: +- `scn~show-specification-item-title-in-test-runner-ui~1` + +Needs: impl, itest + +### Sort Specification Items in Test Runner UI +`dsn~sort-specification-items-in-test-runner-ui~1` + +**Given** the trace test-runner presentation maps traced specification items for one source-file suite +**When** it creates the specification-item test nodes +**Then** it orders them by artifact type, the specification item ID name part, and revision number. + +Covers: +- `scn~sort-specification-items-in-test-runner-ui~1` + +Needs: impl, itest + ### Show Trace Links as Test Runner Sub-Tests `dsn~show-trace-links-as-test-runner-sub-tests~1` @@ -591,6 +615,18 @@ Covers: Needs: impl, itest +### Show Unicode Trace Link Direction in Test Runner UI +`dsn~show-unicode-trace-link-direction-in-test-runner-ui~1` + +**Given** the trace test-runner presentation creates a trace-link sub-test node +**When** it derives the direction marker from the OpenFastTrace link status +**Then** it uses `←` for incoming links, `→` for outgoing links, and `↔` for links without a single incoming or outgoing direction. + +Covers: +- `scn~show-unicode-trace-link-direction-in-test-runner-ui~1` + +Needs: impl, itest + ### Map Specification Item Trace Status to Test Runner Status `dsn~map-specification-item-trace-status-to-test-runner-status~1` @@ -615,6 +651,54 @@ Covers: Needs: impl, itest +### Roll Up Source File Suite Trace Status +`dsn~roll-up-source-file-suite-trace-status~1` + +**Given** the trace test-runner presentation has created a source-file suite +**When** at least one specification-item test or trace-link sub-test below that suite is failed +**Then** it marks the source-file suite as failed before finishing the suite node. + +Covers: +- `scn~roll-up-source-file-suite-trace-status~1` + +Needs: impl, itest + +### Roll Up Top-Level Trace Status +`dsn~roll-up-top-level-trace-status~1` + +**Given** the trace test-runner presentation has created the top-level trace suite +**When** at least one source-file suite contains a failed descendant +**Then** it marks the top-level trace suite as failed before finishing the trace result. + +Covers: +- `scn~roll-up-top-level-trace-status~1` + +Needs: impl, itest + +### Show Specification Item Defect Details in Test Runner UI +`dsn~show-specification-item-defect-details-in-test-runner-ui~1` + +**Given** the trace test-runner presentation creates a failed specification-item test node +**When** it maps the OpenFastTrace item status to the SM test node +**Then** it sets a concise failure message and detail text that identify the item and explain the defective status. + +Covers: +- `scn~show-specification-item-defect-details-in-test-runner-ui~1` + +Needs: impl, itest + +### Show Trace Link Defect Details in Test Runner UI +`dsn~show-trace-link-defect-details-in-test-runner-ui~1` + +**Given** the trace test-runner presentation creates a failed trace-link sub-test node +**When** it maps the OpenFastTrace link status to the SM test node +**Then** it combines node-specific IDs and direction with a static shared detail template keyed by the OpenFastTrace link status. + +Covers: +- `scn~show-trace-link-defect-details-in-test-runner-ui~1` + +Needs: impl, itest + ### Navigate from Test Runner Specification Items `dsn~navigate-from-test-runner-specification-items~1` diff --git a/doc/design/solution_strategy.md b/doc/design/solution_strategy.md index e0d1889..23fa432 100644 --- a/doc/design/solution_strategy.md +++ b/doc/design/solution_strategy.md @@ -75,8 +75,10 @@ Failure presentation is likewise kept narrow. The plugin may emphasize the short The test-runner trace-result increment keeps the existing plain text trace output as the default result view and adds structured presentation as a per-run-configuration choice. -The plugin uses IntelliJ's SM test runner infrastructure for this structured presentation. Source files become test suites, specification items become tests below their source-file suite, and incoming or outgoing trace links become sub-tests below their owning specification item. This gives users the native test runner tree, progress, status, and navigation behavior without introducing a parallel result-tree widget. +The plugin uses IntelliJ's SM test runner infrastructure for this structured presentation. Source files become test suites labeled with project-local paths when they are below the opened project directory, specification items become tests below their source-file suite, and incoming or outgoing trace links become sub-tests below their owning specification item. This gives users the native test runner tree, progress, status, and navigation behavior without introducing a parallel result-tree widget. The structured presentation is derived from the OpenFastTrace `Trace` model produced during trace execution, not from the rendered plain text report. OpenFastTrace remains responsible for import, linking, tracing, defect calculation, and text report rendering. The plugin maps the resulting trace model to IntelliJ test-runner nodes only at the presentation boundary. +Suite status and detail text are part of the structured presentation rather than the tracing core. The presenter rolls failed descendants up to source-file suites and the top-level trace suite, uses Unicode direction arrows for trace-link labels, and attaches concise defect details to failed nodes. Link-detail explanations reuse static templates keyed by OpenFastTrace link status so the constant explanation text is shared while node-specific IDs and directions stay dynamic. + Navigation from structured result nodes reuses the existing OpenFastTrace trace navigation support. Specification item nodes resolve to specification declarations, while link nodes resolve to the corresponding declaration or source-side coverage tag where possible. diff --git a/doc/system_requirements.md b/doc/system_requirements.md index d83f5a6..98bc046 100644 --- a/doc/system_requirements.md +++ b/doc/system_requirements.md @@ -496,7 +496,7 @@ Needs: scn ### Show Trace Source Files as Test Runner Suites `req~show-trace-source-files-as-test-runner-suites~1` -The IntelliJ Test Runner UI result view shows each traced source file as a test runner suite. +The IntelliJ Test Runner UI result view shows each traced source file as a test runner suite. For traced source files below the opened project directory, the suite label uses the project-local path, for example `doc/system_requirements.md`. Covers: - `feat~oft-test-runner-trace-results~1` @@ -513,6 +513,26 @@ Covers: Needs: scn +### Show Specification Item Title in Test Runner UI +`req~show-specification-item-title-in-test-runner-ui~1` + +The IntelliJ Test Runner UI result view prefixes a visible specification item ID with the item's title when the imported OpenFastTrace item provides a non-blank title. The title is followed by ` — ` and the full specification item ID. Items without a title show the full specification item ID without a title prefix. + +Covers: +- `feat~oft-test-runner-trace-results~1` + +Needs: scn + +### Sort Specification Items in Test Runner UI +`req~sort-specification-items-in-test-runner-ui~1` + +Within each source-file suite, the IntelliJ Test Runner UI result view sorts specification item entries by artifact type, then by the name part of the specification item ID, then by revision number. + +Covers: +- `feat~oft-test-runner-trace-results~1` + +Needs: scn + ### Show Trace Links as Test Runner Sub-Tests `req~show-trace-links-as-test-runner-sub-tests~1` @@ -553,6 +573,16 @@ Covers: Needs: scn +### Show Unicode Trace Link Direction in Test Runner UI +`req~show-unicode-trace-link-direction-in-test-runner-ui~1` + +The IntelliJ Test Runner UI result view uses Unicode arrows in trace-link entries. Incoming links use `←`, outgoing links use `→`, and links without a single incoming or outgoing direction use `↔`. + +Covers: +- `feat~oft-test-runner-trace-results~1` + +Needs: scn + ### Map Specification Item Trace Status to Test Runner Status `req~map-specification-item-trace-status-to-test-runner-status~1` @@ -573,6 +603,46 @@ Covers: Needs: scn +### Roll Up Source File Suite Trace Status +`req~roll-up-source-file-suite-trace-status~1` + +The IntelliJ Test Runner UI result view marks a source-file suite as failed if any specification item or trace-link sub-test below that source file is failed. It marks the source-file suite as passed only if all descendants are clean. + +Covers: +- `feat~oft-test-runner-trace-results~1` + +Needs: scn + +### Roll Up Top-Level Trace Status +`req~roll-up-top-level-trace-status~1` + +The IntelliJ Test Runner UI result view marks the top-level trace suite as failed if any source-file suite contains a failed descendant. It marks the top-level trace suite as passed only if all source-file suites are clean. + +Covers: +- `feat~oft-test-runner-trace-results~1` + +Needs: scn + +### Show Specification Item Defect Details in Test Runner UI +`req~show-specification-item-defect-details-in-test-runner-ui~1` + +The IntelliJ Test Runner UI result view shows a clear details text for failed specification-item entries. The details identify the specification item, its trace status, and the reason the item is defective. + +Covers: +- `feat~oft-test-runner-trace-results~1` + +Needs: scn + +### Show Trace Link Defect Details in Test Runner UI +`req~show-trace-link-defect-details-in-test-runner-ui~1` + +The IntelliJ Test Runner UI result view shows a clear details text for failed trace-link entries. The details identify the owning specification item, the linked item, the link direction, the trace-link status, and the reason that status is defective. + +Covers: +- `feat~oft-test-runner-trace-results~1` + +Needs: scn + ### Navigate from Test Runner Specification Items `req~navigate-from-test-runner-specification-items~1` @@ -1266,7 +1336,7 @@ Needs: dsn **Given** an `OpenFastTrace` run configuration uses the IntelliJ Test Runner UI result view and the trace result contains specification items from supported source files **When** the trace completes -**Then** the test runner tree contains one suite per source file +**Then** the test runner tree contains one suite per source file and labels source files below the opened project directory with project-local paths Covers: - `req~show-trace-source-files-as-test-runner-suites~1` @@ -1285,6 +1355,30 @@ Covers: Needs: dsn +### Show Specification Item Title in Test Runner UI +`scn~show-specification-item-title-in-test-runner-ui~1` + +**Given** an `OpenFastTrace` run configuration uses the IntelliJ Test Runner UI result view and the trace result contains a visible specification item ID for an item with a title +**When** the trace completes +**Then** the test runner tree shows the title before the full specification item ID + +Covers: +- `req~show-specification-item-title-in-test-runner-ui~1` + +Needs: dsn + +### Sort Specification Items in Test Runner UI +`scn~sort-specification-items-in-test-runner-ui~1` + +**Given** an `OpenFastTrace` run configuration uses the IntelliJ Test Runner UI result view and one source-file suite contains multiple specification items +**When** the trace completes +**Then** the specification item entries in that source-file suite are ordered by artifact type, ID name part, and revision number + +Covers: +- `req~sort-specification-items-in-test-runner-ui~1` + +Needs: dsn + ### Show Trace Links as Test Runner Sub-Tests `scn~show-trace-links-as-test-runner-sub-tests~1` @@ -1333,6 +1427,18 @@ Covers: Needs: dsn +### Show Unicode Trace Link Direction in Test Runner UI +`scn~show-unicode-trace-link-direction-in-test-runner-ui~1` + +**Given** an `OpenFastTrace` run configuration uses the IntelliJ Test Runner UI result view and the trace result contains incoming and outgoing trace links +**When** the trace completes +**Then** the trace-link sub-tests use Unicode arrows for the visible link direction + +Covers: +- `req~show-unicode-trace-link-direction-in-test-runner-ui~1` + +Needs: dsn + ### Map Specification Item Trace Status to Test Runner Status `scn~map-specification-item-trace-status-to-test-runner-status~1` @@ -1357,6 +1463,54 @@ Covers: Needs: dsn +### Roll Up Source File Suite Trace Status +`scn~roll-up-source-file-suite-trace-status~1` + +**Given** an `OpenFastTrace` run configuration uses the IntelliJ Test Runner UI result view and a source-file suite contains at least one failed specification item or trace-link sub-test +**When** the trace completes +**Then** the source-file suite is shown as failed + +Covers: +- `req~roll-up-source-file-suite-trace-status~1` + +Needs: dsn + +### Roll Up Top-Level Trace Status +`scn~roll-up-top-level-trace-status~1` + +**Given** an `OpenFastTrace` run configuration uses the IntelliJ Test Runner UI result view and at least one source-file suite contains a failed descendant +**When** the trace completes +**Then** the top-level trace suite is shown as failed + +Covers: +- `req~roll-up-top-level-trace-status~1` + +Needs: dsn + +### Show Specification Item Defect Details in Test Runner UI +`scn~show-specification-item-defect-details-in-test-runner-ui~1` + +**Given** an `OpenFastTrace` run configuration uses the IntelliJ Test Runner UI result view and the trace result contains a defective specification item +**When** the user selects that specification item in the test runner tree +**Then** the detail view explains why the specification item is defective + +Covers: +- `req~show-specification-item-defect-details-in-test-runner-ui~1` + +Needs: dsn + +### Show Trace Link Defect Details in Test Runner UI +`scn~show-trace-link-defect-details-in-test-runner-ui~1` + +**Given** an `OpenFastTrace` run configuration uses the IntelliJ Test Runner UI result view and the trace result contains a defective trace link +**When** the user selects that trace link in the test runner tree +**Then** the detail view explains why the trace link is defective + +Covers: +- `req~show-trace-link-defect-details-in-test-runner-ui~1` + +Needs: dsn + ### Navigate from Test Runner Specification Items `scn~navigate-from-test-runner-specification-items~1` diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceResultView.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceResultView.java new file mode 100644 index 0000000..eca17f3 --- /dev/null +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceResultView.java @@ -0,0 +1,6 @@ +package org.itsallcode.openfasttrace.intellijplugin.trace; + +public enum OftTraceResultView { + PLAIN_TEXT, + TEST_RUNNER +} diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestNodeDetails.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestNodeDetails.java new file mode 100644 index 0000000..02bd39b --- /dev/null +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestNodeDetails.java @@ -0,0 +1,185 @@ +package org.itsallcode.openfasttrace.intellijplugin.trace; + +import org.itsallcode.openfasttrace.api.core.DeepCoverageStatus; +import org.itsallcode.openfasttrace.api.core.LinkedSpecificationItem; +import org.itsallcode.openfasttrace.api.core.LinkStatus; +import org.itsallcode.openfasttrace.api.core.TracedLink; + +import java.util.EnumMap; +import java.util.List; + +record OftTraceTestNodeDetails(String failureMessage, String detailText) { + private static final String LINE_SEPARATOR = System.lineSeparator(); + private static final OftTraceTestNodeDetails NONE = new OftTraceTestNodeDetails("", ""); + private static final OftTraceTestNodeDetails TOP_LEVEL_FAILURE = new OftTraceTestNodeDetails( + "OpenFastTrace trace contains defects.", + "At least one source-file suite contains a failed specification item or trace link." + ); + private static final EnumMap LINK_DETAIL_TEMPLATES = + createLinkDetailTemplates(); + + static OftTraceTestNodeDetails none() { + return NONE; + } + + static OftTraceTestNodeDetails topLevelFailure() { + return TOP_LEVEL_FAILURE; + } + + static OftTraceTestNodeDetails sourceSuiteFailure(final String sourceName) { + return new OftTraceTestNodeDetails( + "OpenFastTrace defects in " + sourceName + ".", + "At least one specification item or trace link from this source file is failed." + ); + } + + // [impl->dsn~show-specification-item-defect-details-in-test-runner-ui~1] + static OftTraceTestNodeDetails specificationItemFailure( + final LinkedSpecificationItem item, + final String visibleStatus + ) { + return new OftTraceTestNodeDetails( + itemFailureMessage(item, visibleStatus), + "Specification item: " + item.getId() + + LINE_SEPARATOR + + "Trace status: " + visibleStatus + + LINE_SEPARATOR + + itemFailureExplanation(item, visibleStatus) + ); + } + + // [impl->dsn~show-trace-link-defect-details-in-test-runner-ui~1] + static OftTraceTestNodeDetails traceLinkFailure( + final String owningItemId, + final String linkedItemId, + final String directionLabel, + final LinkStatus linkStatus, + final String visibleStatus + ) { + return LINK_DETAIL_TEMPLATES.get(linkStatus) + .create(owningItemId, linkedItemId, directionLabel, visibleStatus); + } + + static OftTraceTestNodeDetails resultWithoutTrace(final OftTraceResult result) { + return new OftTraceTestNodeDetails( + result.statusMessage(), + result.output() + ); + } + + private static String itemFailureMessage(final LinkedSpecificationItem item, final String visibleStatus) { + return switch (visibleStatus) { + case "duplicate" -> "Duplicate OpenFastTrace specification item."; + case "cycle" -> "OpenFastTrace coverage cycle."; + case "uncovered" -> "Uncovered OpenFastTrace specification item."; + default -> "Defective OpenFastTrace specification item " + item.getId() + "."; + }; + } + + private static String itemFailureExplanation(final LinkedSpecificationItem item, final String visibleStatus) { + 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."; + case "uncovered" -> uncoveredExplanation(item); + default -> defectiveLinksExplanation(item); + }; + } + + private static String defectiveLinksExplanation(final LinkedSpecificationItem item) { + final List defectiveLinks = item.getTracedLinks().stream() + .filter(link -> link.getStatus().isBad()) + .map(OftTraceTestNodeDetails::defectiveLinkDescription) + .toList(); + if (defectiveLinks.isEmpty()) { + return "OpenFastTrace reported one or more trace defects on this item."; + } + return "OpenFastTrace reported defective trace link(s): " + + String.join("; ", defectiveLinks) + + "."; + } + + private static String defectiveLinkDescription(final TracedLink link) { + return link.getStatus() + " link to " + link.getOtherLinkEnd().getId(); + } + + private static String uncoveredExplanation(final LinkedSpecificationItem item) { + final List uncoveredArtifactTypes = item.getUncoveredArtifactTypes(); + if (uncoveredArtifactTypes.isEmpty() || item.getDeepCoverageStatus() != DeepCoverageStatus.UNCOVERED) { + return "Required coverage is missing for this specification item."; + } + return "Required coverage is missing for artifact type(s): " + + String.join(", ", uncoveredArtifactTypes) + + "."; + } + + private static EnumMap createLinkDetailTemplates() { + final EnumMap templates = new EnumMap<>(LinkStatus.class); + templates.put(LinkStatus.COVERS, new LinkDetailTemplate( + "Valid outgoing trace link.", + "The owning specification item covers the linked item." + )); + templates.put(LinkStatus.COVERED_SHALLOW, new LinkDetailTemplate( + "Valid incoming trace link.", + "The linked item covers the owning specification item." + )); + templates.put(LinkStatus.PREDATED, new LinkDetailTemplate( + "Predated outgoing trace link.", + "The owning item covers a newer revision than the linked item in the trace." + )); + templates.put(LinkStatus.OUTDATED, new LinkDetailTemplate( + "Outdated outgoing trace link.", + "The owning item covers an older revision than the linked item in the trace." + )); + templates.put(LinkStatus.AMBIGUOUS, new LinkDetailTemplate( + "Ambiguous outgoing trace link.", + "The target item ID is ambiguous because the trace contains more than one matching item." + )); + templates.put(LinkStatus.UNWANTED, new LinkDetailTemplate( + "Unwanted outgoing trace link.", + "The owning item covers an item that does not require coverage." + )); + templates.put(LinkStatus.ORPHANED, new LinkDetailTemplate( + "Orphaned outgoing trace link.", + "The owning item covers an item that OpenFastTrace could not find." + )); + templates.put(LinkStatus.COVERED_UNWANTED, new LinkDetailTemplate( + "Unwanted incoming trace link.", + "The owning item is covered although it does not require coverage." + )); + templates.put(LinkStatus.COVERED_PREDATED, new LinkDetailTemplate( + "Predated incoming trace link.", + "The linked item covers a newer revision than the owning item in the trace." + )); + templates.put(LinkStatus.COVERED_OUTDATED, new LinkDetailTemplate( + "Outdated incoming trace link.", + "The linked item covers an older revision than the owning item in the trace." + )); + templates.put(LinkStatus.DUPLICATE, new LinkDetailTemplate( + "Duplicate trace link.", + "The trace contains duplicate specification items with the same ID." + )); + return templates; + } + + private record LinkDetailTemplate(String failureMessage, String explanation) { + private OftTraceTestNodeDetails create( + final String owningItemId, + final String linkedItemId, + final String directionLabel, + final String visibleStatus + ) { + return new OftTraceTestNodeDetails( + failureMessage, + "Owning item: " + owningItemId + + LINE_SEPARATOR + + "Linked item: " + linkedItemId + + LINE_SEPARATOR + + "Direction: " + directionLabel + + LINE_SEPARATOR + + "Trace-link status: " + visibleStatus + + LINE_SEPARATOR + + explanation + ); + } + } +} diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestProxy.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestProxy.java new file mode 100644 index 0000000..ced8c34 --- /dev/null +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestProxy.java @@ -0,0 +1,48 @@ +package org.itsallcode.openfasttrace.intellijplugin.trace; + +import com.intellij.execution.testframework.sm.runner.SMTestProxy; +import com.intellij.openapi.fileEditor.OpenFileDescriptor; +import com.intellij.openapi.project.Project; + +import java.io.Serial; +import java.util.Optional; + +final class OftTraceTestProxy extends SMTestProxy { + @Serial + private static final long serialVersionUID = 1L; + + private final transient Project project; + private final String navigationId; + private final transient OftTraceNavigationResolver navigationResolver; + + OftTraceTestProxy(final Project project, final String name, final boolean suite, final String navigationId) { + super(name, suite, null, true); + this.project = project; + this.navigationId = navigationId; + this.navigationResolver = new OftTraceNavigationResolver(project); + } + + @Override + public void navigate(final boolean requestFocus) { + resolveNavigationTarget() + .ifPresent(target -> new OpenFileDescriptor(project, target.file(), target.offset()) + .navigate(requestFocus)); + } + + @Override + public boolean canNavigate() { + return resolveNavigationTarget().isPresent(); + } + + @Override + public boolean canNavigateToSource() { + return canNavigate(); + } + + private Optional resolveNavigationTarget() { + if (navigationId == null || navigationResolver == null || project == null) { + return Optional.empty(); + } + return navigationResolver.resolve(navigationId); + } +} diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerOutputPresenter.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerOutputPresenter.java new file mode 100644 index 0000000..41c5f17 --- /dev/null +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerOutputPresenter.java @@ -0,0 +1,176 @@ +package org.itsallcode.openfasttrace.intellijplugin.trace; + +import com.intellij.execution.testframework.sm.runner.SMTestProxy; +import com.intellij.execution.testframework.sm.runner.ui.SMTRunnerConsoleView; +import com.intellij.execution.testframework.sm.runner.ui.SMTestRunnerResultsForm; +import com.intellij.execution.ui.ConsoleViewContentType; +import com.intellij.openapi.project.Project; + +import java.util.function.Function; + +public final class OftTraceTestRunnerOutputPresenter implements OftTraceOutputPresenter { + private final Function consoleFactory; + private final OftTraceTestTreeMapper testTreeMapper; + + public OftTraceTestRunnerOutputPresenter(final Function consoleFactory) { + this(consoleFactory, new OftTraceTestTreeMapper()); + } + + OftTraceTestRunnerOutputPresenter( + final Function consoleFactory, + final OftTraceTestTreeMapper testTreeMapper + ) { + this.consoleFactory = consoleFactory; + this.testTreeMapper = testTreeMapper; + } + + // [impl->dsn~trace-test-runner-presentation~1] + @Override + public void show(final Project project, final String contentTitle, final OftTraceResult result) { + final SMTRunnerConsoleView console = consoleFactory.apply(project); + final SMTestRunnerResultsForm resultsViewer = console.getResultsViewer(); + final SMTestProxy.SMRootTestProxy root = resultsViewer.getTestsRootNode(); + root.setPresentation(contentTitle); + resultsViewer.onTestingStarted(root); + final PresentationOutcome outcome = result.trace() + .map(trace -> showTrace(project, resultsViewer, root, testTreeMapper.map(trace, project.getBasePath()))) + .orElseGet(() -> showResultWithoutTrace(console, resultsViewer, root, result)); + if (outcome.failed()) { + markFailed(root, outcome.details()); + } + root.setFinished(); + resultsViewer.onTestingFinished(root); + } + + private static PresentationOutcome showTrace( + final Project project, + final SMTestRunnerResultsForm resultsViewer, + final SMTestProxy.SMRootTestProxy root, + final OftTraceTestTree tree + ) { + resultsViewer.onTestsCountInSuite(tree.testCount()); + for (final OftTraceSuiteNode suite : tree.suites()) { + showSuite(project, resultsViewer, root, suite); + } + return new PresentationOutcome( + tree.failed(), + tree.failed() ? OftTraceTestNodeDetails.topLevelFailure() : OftTraceTestNodeDetails.none() + ); + } + + private static void showSuite( + final Project project, + final SMTestRunnerResultsForm resultsViewer, + final SMTestProxy parent, + final OftTraceSuiteNode suite + ) { + final SMTestProxy suiteProxy = new OftTraceTestProxy(project, suite.name(), true, null); + parent.addChild(suiteProxy); + suiteProxy.setSuiteStarted(); + resultsViewer.onSuiteStarted(suiteProxy); + for (final OftTraceItemNode item : suite.items()) { + showItem(project, resultsViewer, suiteProxy, item); + } + if (suite.failed()) { + markFailed(suiteProxy, suite.failureDetails()); + } + suiteProxy.setFinished(); + resultsViewer.onSuiteFinished(suiteProxy); + } + + private static void showItem( + final Project project, + final SMTestRunnerResultsForm resultsViewer, + final SMTestProxy parent, + final OftTraceItemNode item + ) { + final boolean expandable = itemHasLinks(item); + final SMTestProxy itemProxy = new OftTraceTestProxy(project, item.name(), expandable, item.navigationId()); + parent.addChild(itemProxy); + if (expandable) { + itemProxy.setSuiteStarted(); + resultsViewer.onSuiteStarted(itemProxy); + } else { + itemProxy.setStarted(); + resultsViewer.onTestStarted(itemProxy); + } + if (item.failed()) { + markFailed(itemProxy, item.details()); + // Expandable item nodes are suite-shaped, but the item status itself still counts as a test. + resultsViewer.onTestFailed(itemProxy); + } + for (final OftTraceLinkNode link : item.links()) { + showLink(project, resultsViewer, itemProxy, link); + } + itemProxy.setFinished(); + if (expandable) { + resultsViewer.onSuiteFinished(itemProxy); + } else { + resultsViewer.onTestFinished(itemProxy); + } + } + + private static boolean itemHasLinks(final OftTraceItemNode item) { + return !item.links().isEmpty(); + } + + private static void showLink( + final Project project, + final SMTestRunnerResultsForm resultsViewer, + final SMTestProxy parent, + final OftTraceLinkNode link + ) { + final SMTestProxy linkProxy = new OftTraceTestProxy(project, link.name(), false, link.navigationId()); + parent.addChild(linkProxy); + showTestNode(resultsViewer, linkProxy, link.failed(), link.details()); + } + + private static void showTestNode( + final SMTestRunnerResultsForm resultsViewer, + final SMTestProxy testProxy, + final boolean failed, + final OftTraceTestNodeDetails details + ) { + testProxy.setStarted(); + resultsViewer.onTestStarted(testProxy); + if (failed) { + markFailed(testProxy, details); + resultsViewer.onTestFailed(testProxy); + } + testProxy.setFinished(); + resultsViewer.onTestFinished(testProxy); + } + + private static PresentationOutcome showResultWithoutTrace( + final SMTRunnerConsoleView console, + final SMTestRunnerResultsForm resultsViewer, + final SMTestProxy.SMRootTestProxy root, + final OftTraceResult result + ) { + console.print(result.statusMessage() + System.lineSeparator(), ConsoleViewContentType.ERROR_OUTPUT); + console.print(System.lineSeparator(), ConsoleViewContentType.NORMAL_OUTPUT); + console.print(result.output(), ConsoleViewContentType.ERROR_OUTPUT); + final SMTestProxy failureProxy = + new SMTestProxy(result.statusMessage(), false, null, true); + root.addChild(failureProxy); + resultsViewer.onTestsCountInSuite(1); + showTestNode(resultsViewer, failureProxy, result.requiresAttention(), OftTraceTestNodeDetails.resultWithoutTrace(result)); + return new PresentationOutcome( + result.requiresAttention(), + result.requiresAttention() + ? OftTraceTestNodeDetails.resultWithoutTrace(result) + : OftTraceTestNodeDetails.none() + ); + } + + // [impl->dsn~roll-up-source-file-suite-trace-status~1] + // [impl->dsn~roll-up-top-level-trace-status~1] + // [impl->dsn~show-specification-item-defect-details-in-test-runner-ui~1] + // [impl->dsn~show-trace-link-defect-details-in-test-runner-ui~1] + private static void markFailed(final SMTestProxy testProxy, final OftTraceTestNodeDetails details) { + testProxy.setTestFailed(details.failureMessage(), details.detailText(), false); + } + + private record PresentationOutcome(boolean failed, OftTraceTestNodeDetails details) { + } +} diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTree.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTree.java new file mode 100644 index 0000000..9ef1b99 --- /dev/null +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTree.java @@ -0,0 +1,50 @@ +package org.itsallcode.openfasttrace.intellijplugin.trace; + +import java.util.List; + +record OftTraceTestTree(List suites) { + int testCount() { + return suites.stream() + .mapToInt(OftTraceSuiteNode::testCount) + .sum(); + } + + boolean failed() { + return suites.stream().anyMatch(OftTraceSuiteNode::failed); + } +} + +record OftTraceSuiteNode(String name, List items) { + int testCount() { + return items.stream() + .mapToInt(OftTraceItemNode::testCount) + .sum(); + } + + boolean failed() { + return items.stream().anyMatch(OftTraceItemNode::failed); + } + + OftTraceTestNodeDetails failureDetails() { + return OftTraceTestNodeDetails.sourceSuiteFailure(name); + } +} + +record OftTraceItemNode( + String name, + String navigationId, + boolean defective, + OftTraceTestNodeDetails details, + List links +) { + int testCount() { + return 1 + links.size(); + } + + boolean failed() { + return defective || links.stream().anyMatch(OftTraceLinkNode::failed); + } +} + +record OftTraceLinkNode(String name, String navigationId, boolean failed, OftTraceTestNodeDetails details) { +} diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTreeMapper.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTreeMapper.java new file mode 100644 index 0000000..a026767 --- /dev/null +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTreeMapper.java @@ -0,0 +1,189 @@ +package org.itsallcode.openfasttrace.intellijplugin.trace; + +import org.itsallcode.openfasttrace.api.core.DeepCoverageStatus; +import org.itsallcode.openfasttrace.api.core.LinkedSpecificationItem; +import org.itsallcode.openfasttrace.api.core.LinkStatus; +import org.itsallcode.openfasttrace.api.core.Location; +import org.itsallcode.openfasttrace.api.core.Trace; +import org.itsallcode.openfasttrace.api.core.TracedLink; + +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +final class OftTraceTestTreeMapper { + private static final String UNKNOWN_SOURCE = "Unknown source"; + private static final Comparator ITEM_ORDER = Comparator + .comparing(LinkedSpecificationItem::getArtifactType) + .thenComparing(LinkedSpecificationItem::getName) + .thenComparingInt(LinkedSpecificationItem::getRevision); + + // [impl->dsn~trace-test-runner-presentation~1] + // [impl->dsn~show-trace-source-files-as-test-runner-suites~1] + // [impl->dsn~show-trace-specification-items-as-test-runner-tests~1] + // [impl->dsn~show-specification-item-title-in-test-runner-ui~1] + // [impl->dsn~sort-specification-items-in-test-runner-ui~1] + // [impl->dsn~show-trace-links-as-test-runner-sub-tests~1] + // [impl->dsn~show-specification-item-status-in-test-runner-ui~1] + // [impl->dsn~show-trace-link-status-in-test-runner-ui~1] + // [impl->dsn~show-trace-link-direction-in-test-runner-ui~1] + // [impl->dsn~show-unicode-trace-link-direction-in-test-runner-ui~1] + // [impl->dsn~map-specification-item-trace-status-to-test-runner-status~1] + // [impl->dsn~map-trace-link-status-to-test-runner-status~1] + // [impl->dsn~show-specification-item-defect-details-in-test-runner-ui~1] + // [impl->dsn~show-trace-link-defect-details-in-test-runner-ui~1] + // [impl->dsn~navigate-from-test-runner-specification-items~1] + // [impl->dsn~navigate-from-test-runner-trace-links~1] + OftTraceTestTree map(final Trace trace) { + return map(trace, null); + } + + OftTraceTestTree map(final Trace trace, final String projectBasePath) { + final Map> itemsBySource = new LinkedHashMap<>(); + for (final LinkedSpecificationItem item : trace.getItems()) { + itemsBySource.computeIfAbsent(sourceName(item, projectBasePath), ignored -> new ArrayList<>()) + .add(item); + } + final List suites = itemsBySource.entrySet().stream() + .map(entry -> new OftTraceSuiteNode(entry.getKey(), mapItems(entry.getValue()))) + .toList(); + return new OftTraceTestTree(suites); + } + + private static List mapItems(final List items) { + return items.stream() + .sorted(ITEM_ORDER) + .map(OftTraceTestTreeMapper::mapItem) + .toList(); + } + + private static OftTraceItemNode mapItem(final LinkedSpecificationItem item) { + final String itemStatus = itemStatus(item); + final String itemId = item.getId().toString(); + return new OftTraceItemNode( + itemLabel(item) + " (" + itemStatus + ")", + itemId, + item.isDefect(), + item.isDefect() + ? OftTraceTestNodeDetails.specificationItemFailure(item, itemStatus) + : OftTraceTestNodeDetails.none(), + item.getTracedLinks().stream() + .map(link -> mapLink(item, link)) + .toList() + ); + } + + private static OftTraceLinkNode mapLink(final LinkedSpecificationItem owner, final TracedLink link) { + final LinkedSpecificationItem otherItem = link.getOtherLinkEnd(); + final String otherItemId = otherItem.getId().toString(); + final LinkDirection direction = direction(link); + final String linkStatus = linkStatus(link.getStatus()); + return new OftTraceLinkNode( + direction.marker() + " " + itemLabel(otherItem) + " (" + linkStatus + ")", + otherItemId, + link.getStatus().isBad(), + link.getStatus().isBad() + ? OftTraceTestNodeDetails.traceLinkFailure( + owner.getId().toString(), + otherItemId, + direction.label(), + link.getStatus(), + linkStatus + ) + : OftTraceTestNodeDetails.none() + ); + } + + private static String itemLabel(final LinkedSpecificationItem item) { + final String itemId = item.getId().toString(); + final String title = item.getTitle(); + return title == null || title.isBlank() ? itemId : title + " \u2014 " + itemId; + } + + private static String sourceName(final LinkedSpecificationItem item, final String projectBasePath) { + final Location location = item.getLocation(); + if (location == null || location.getPath() == null || location.getPath().isBlank()) { + return UNKNOWN_SOURCE; + } + return sourcePathLabel(location.getPath(), projectBasePath); + } + + private static String sourcePathLabel(final String sourcePath, final String projectBasePath) { + final Path source = pathOrNull(sourcePath); + if (source == null) { + return displayPath(sourcePath); + } + final Path labelPath = projectLocalPath(source, projectBasePath); + return displayPath(labelPath.toString()); + } + + private static Path projectLocalPath(final Path source, final String projectBasePath) { + final Path normalizedSource = source.normalize(); + if (!normalizedSource.isAbsolute() || projectBasePath == null || projectBasePath.isBlank()) { + return normalizedSource; + } + final Path projectBase = pathOrNull(projectBasePath); + if (projectBase == null) { + return normalizedSource; + } + final Path normalizedProjectBase = projectBase.normalize(); + if (normalizedProjectBase.isAbsolute() && normalizedSource.startsWith(normalizedProjectBase)) { + return normalizedProjectBase.relativize(normalizedSource); + } + return normalizedSource; + } + + private static Path pathOrNull(final String path) { + try { + return Path.of(path); + } catch (final InvalidPathException exception) { + return null; + } + } + + private static String displayPath(final String path) { + return path.replace('\\', '/'); + } + + private static String itemStatus(final LinkedSpecificationItem item) { + if (!item.isDefect()) { + return "covered"; + } + if (item.hasDuplicates()) { + return "duplicate"; + } + final DeepCoverageStatus deepCoverageStatus = item.getDeepCoverageStatus(); + if (deepCoverageStatus == DeepCoverageStatus.CYCLE) { + return "cycle"; + } + if (deepCoverageStatus == DeepCoverageStatus.UNCOVERED) { + return "uncovered"; + } + return "defective"; + } + + private static String linkStatus(final LinkStatus status) { + if (status == LinkStatus.COVERED_SHALLOW) { + return "covered"; + } + return status.toString().toLowerCase(Locale.ROOT); + } + + private static LinkDirection direction(final TracedLink link) { + if (link.isIncoming()) { + return new LinkDirection("\u2190", "incoming"); + } + if (link.isOutgoing()) { + return new LinkDirection("\u2192", "outgoing"); + } + return new LinkDirection("\u2194", "non-directional"); + } + + private record LinkDirection(String marker, String label) { + } +} diff --git a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestProxyTest.java b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestProxyTest.java new file mode 100644 index 0000000..82f5aac --- /dev/null +++ b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestProxyTest.java @@ -0,0 +1,102 @@ +package org.itsallcode.openfasttrace.intellijplugin.trace; + +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.fileEditor.TextEditor; +import com.intellij.testFramework.EdtTestUtil; +import org.hamcrest.Matchers; +import org.itsallcode.openfasttrace.intellijplugin.AbstractOftPlatformTestCase; + +import java.nio.charset.StandardCharsets; +import java.util.zip.CRC32; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +public class OftTraceTestProxyTest extends AbstractOftPlatformTestCase { + // [itest->dsn~navigate-from-test-runner-specification-items~1] + public void testGivenSpecificationItemNodeWhenNavigatingThenItOpensTheSpecificationDeclaration() { + myFixture.addFileToProject("doc/spec.md", """ + req~runner_navigation_target~1 + Needs: impl + """); + final OftTraceTestProxy proxy = new OftTraceTestProxy( + getProject(), + "req~runner_navigation_target~1 (uncovered)", + false, + "req~runner_navigation_target~1" + ); + + assertThat(proxy.canNavigate(), is(true)); + EdtTestUtil.runInEdtAndWait(() -> proxy.navigate(false)); + + assertThat(selectedEditorFileName(), is("spec.md")); + } + + // [itest->dsn~navigate-from-test-runner-trace-links~1] + public void testGivenTraceLinkNodeForGeneratedCoverageTagItemWhenNavigatingThenItOpensTheCoverageTag() { + myFixture.addFileToProject("doc/design.md", """ + dsn~runner_navigation_target~1 + Needs: impl + """); + final var coverageTagFile = myFixture.addFileToProject( + "src/Main.java", + """ + // [impl""" + "->dsn~runner_navigation_target~1]\n" + """ + class Main { + } + """ + ); + final String generatedCoverageItemId = generatedCoverageItemId( + coverageTagFile.getVirtualFile().getPath(), + 1, + 0, + "impl", + "dsn~runner_navigation_target~1" + ); + final OftTraceTestProxy proxy = new OftTraceTestProxy( + getProject(), + "<- " + generatedCoverageItemId + " (covered)", + false, + generatedCoverageItemId + ); + + assertThat(proxy.canNavigate(), is(true)); + EdtTestUtil.runInEdtAndWait(() -> proxy.navigate(false)); + + assertThat(selectedEditorFileName(), is("Main.java")); + } + + public void testGivenNodeWithoutNavigationTargetWhenCheckingNavigationThenItCannotNavigate() { + final OftTraceTestProxy proxy = new OftTraceTestProxy(getProject(), "doc/spec.md", true, null); + + assertThat(proxy.canNavigate(), is(false)); + assertThat(proxy.canNavigateToSource(), is(false)); + EdtTestUtil.runInEdtAndWait(() -> proxy.navigate(false)); + } + + private String generatedCoverageItemId( + final String filePath, + final int lineNumber, + final int lineMatchCount, + final String sourceArtifactType, + final String coveredId + ) { + return sourceArtifactType + "~" + coveredId.split("~")[1] + "-" + + crc32(filePath + lineNumber + lineMatchCount + coveredId) + "~0"; + } + + private long crc32(final String value) { + final CRC32 checksum = new CRC32(); + checksum.update(value.getBytes(StandardCharsets.UTF_8)); + return checksum.getValue(); + } + + private String selectedEditorFileName() { + final TextEditor selectedEditor = (TextEditor) FileEditorManager.getInstance(getProject()).getSelectedEditor(); + assertThat(selectedEditor, notNullValue()); + assertThat(selectedEditor.getFile(), Matchers.not(nullValue())); + return selectedEditor.getFile().getName(); + } +} diff --git a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerOutputPresenterTest.java b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerOutputPresenterTest.java new file mode 100644 index 0000000..0728024 --- /dev/null +++ b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerOutputPresenterTest.java @@ -0,0 +1,202 @@ +package org.itsallcode.openfasttrace.intellijplugin.trace; + +import com.intellij.execution.executors.DefaultRunExecutor; +import com.intellij.execution.testframework.sm.runner.SMTRunnerConsoleProperties; +import com.intellij.execution.testframework.sm.runner.SMTestProxy; +import com.intellij.execution.testframework.sm.runner.ui.SMTRunnerConsoleView; +import com.intellij.execution.testframework.sm.runner.ui.SMTestRunnerResultsForm; +import com.intellij.openapi.util.Disposer; +import com.intellij.testFramework.EdtTestUtil; +import org.itsallcode.openfasttrace.api.core.ItemStatus; +import org.itsallcode.openfasttrace.api.core.LinkedSpecificationItem; +import org.itsallcode.openfasttrace.api.core.LinkStatus; +import org.itsallcode.openfasttrace.api.core.SpecificationItem; +import org.itsallcode.openfasttrace.api.core.SpecificationItemId; +import org.itsallcode.openfasttrace.api.core.Trace; +import org.itsallcode.openfasttrace.intellijplugin.AbstractOftPlatformTestCase; +import org.itsallcode.openfasttrace.intellijplugin.trace.runconfig.OftRunConfiguration; +import org.itsallcode.openfasttrace.intellijplugin.trace.runconfig.OftRunConfigurationFactory; +import org.itsallcode.openfasttrace.intellijplugin.trace.runconfig.OftRunConfigurationType; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +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; + +// [itest->dsn~trace-test-runner-presentation~1] +public class OftTraceTestRunnerOutputPresenterTest extends AbstractOftPlatformTestCase { + // [itest->dsn~show-trace-source-files-as-test-runner-suites~1] + // [itest->dsn~show-trace-specification-items-as-test-runner-tests~1] + // [itest->dsn~show-specification-item-title-in-test-runner-ui~1] + // [itest->dsn~map-specification-item-trace-status-to-test-runner-status~1] + // [itest->dsn~roll-up-source-file-suite-trace-status~1] + // [itest->dsn~roll-up-top-level-trace-status~1] + public void testGivenSuccessfulTraceResultWhenPresentedThenItCreatesPassedTestRunnerNodes() { + final LinkedSpecificationItem requirement = titledItem( + "req~clean_requirement~1", + projectLocalPath("doc/requirements.md"), + "Clean requirement" + ); + final SMTRunnerConsoleView console = present(OftTraceResult.success("ok", trace(requirement))); + final SMTestRunnerResultsForm resultsViewer = console.getResultsViewer(); + + final SMTestProxy suite = childNamed(resultsViewer.getTestsRootNode(), "doc/requirements.md"); + final SMTestProxy item = childNamed(suite, "Clean requirement \u2014 req~clean_requirement~1 (covered)"); + + assertThat(suite.isSuite(), is(true)); + assertThat(item.isSuite(), is(false)); + assertThat(item.getPresentableName(), is("Clean requirement \u2014 req~clean_requirement~1 (covered)")); + assertThat(item.isPassed(), is(true)); + assertThat(suite.isDefect(), is(false)); + assertThat(resultsViewer.getTestsRootNode().isDefect(), is(false)); + assertThat(resultsViewer.getTotalTestCount(), is(1)); + assertThat(resultsViewer.getFailedTestCount(), is(0)); + } + + // [itest->dsn~show-trace-source-files-as-test-runner-suites~1] + // [itest->dsn~show-trace-specification-items-as-test-runner-tests~1] + // [itest->dsn~show-trace-links-as-test-runner-sub-tests~1] + // [itest->dsn~map-specification-item-trace-status-to-test-runner-status~1] + // [itest->dsn~map-trace-link-status-to-test-runner-status~1] + // [itest->dsn~roll-up-source-file-suite-trace-status~1] + // [itest->dsn~roll-up-top-level-trace-status~1] + // [itest->dsn~show-specification-item-defect-details-in-test-runner-ui~1] + // [itest->dsn~show-trace-link-defect-details-in-test-runner-ui~1] + public void testGivenDefectiveTraceResultWhenPresentedThenItCreatesFailedItemAndLinkNodes() { + final LinkedSpecificationItem implementation = titledItem( + "impl~missing_requirement~1", + projectLocalPath("src/Main.java"), + "Missing requirement implementation" + ); + implementation.addLinkToItemWithStatus( + titledItem("req~missing_requirement~1", projectLocalPath("doc/requirements.md"), "Missing requirement"), + LinkStatus.ORPHANED + ); + + final SMTRunnerConsoleView console = present(OftTraceResult.failure("not ok", trace(implementation))); + final SMTestRunnerResultsForm resultsViewer = console.getResultsViewer(); + final SMTestProxy suite = childNamed(resultsViewer.getTestsRootNode(), "src/Main.java"); + final SMTestProxy item = childNamed( + suite, + "Missing requirement implementation \u2014 impl~missing_requirement~1 (defective)" + ); + final SMTestProxy link = childNamed( + item, + "\u2192 Missing requirement \u2014 req~missing_requirement~1 (orphaned)" + ); + + assertThat(resultsViewer.getTestsRootNode().isDefect(), is(true)); + assertThat(suite.isDefect(), is(true)); + assertThat(item.isSuite(), is(true)); + assertThat(item.isDefect(), is(true)); + assertThat(link.isDefect(), is(true)); + assertThat(suite.getErrorMessage(), is("OpenFastTrace defects in src/Main.java.")); + assertThat(resultsViewer.getTestsRootNode().getErrorMessage(), is("OpenFastTrace trace contains defects.")); + assertThat(item.getErrorMessage(), is("Defective OpenFastTrace specification item impl~missing_requirement~1.")); + assertThat(item.getStacktrace(), containsString("Trace status: defective")); + assertThat(item.getStacktrace(), containsString("orphaned link to req~missing_requirement~1")); + assertThat(link.getPresentableName(), + is("\u2192 Missing requirement \u2014 req~missing_requirement~1 (orphaned)")); + assertThat(link.getErrorMessage(), is("Orphaned outgoing trace link.")); + assertThat(link.getStacktrace(), containsString("Owning item: impl~missing_requirement~1")); + assertThat(link.getStacktrace(), containsString("Linked item: req~missing_requirement~1")); + assertThat(link.getStacktrace(), containsString("OpenFastTrace could not find")); + assertThat(resultsViewer.getTotalTestCount(), is(2)); + assertThat(resultsViewer.getFailedTestCount(), is(2)); + } + + public void testGivenResultWithoutStructuredTraceWhenPresentedThenItCreatesFailedFallbackNode() { + final SMTRunnerConsoleView console = present(OftTraceResult.invalidInput("invalid configuration")); + final SMTestRunnerResultsForm resultsViewer = console.getResultsViewer(); + + final SMTestProxy fallbackNode = childNamed( + resultsViewer.getTestsRootNode(), + "OpenFastTrace trace could not start." + ); + + assertThat(fallbackNode.isDefect(), is(true)); + assertThat(fallbackNode.getPresentableName(), is("OpenFastTrace trace could not start.")); + assertThat(resultsViewer.getTestsRootNode().isDefect(), is(true)); + assertThat(fallbackNode.getErrorMessage(), is("OpenFastTrace trace could not start.")); + assertThat(fallbackNode.getStacktrace(), is("invalid configuration")); + assertThat(resultsViewer.getTestsRootNode().getErrorMessage(), is("OpenFastTrace trace could not start.")); + assertThat(resultsViewer.getTestsRootNode().getStacktrace(), is("invalid configuration")); + assertThat(resultsViewer.getTotalTestCount(), is(1)); + assertThat(resultsViewer.getFailedTestCount(), is(1)); + } + + private SMTRunnerConsoleView present(final OftTraceResult result) { + final AtomicReference consoleRef = new AtomicReference<>(); + final OftTraceTestRunnerOutputPresenter presenter = new OftTraceTestRunnerOutputPresenter(project -> { + final SMTRunnerConsoleView console = createConsole(); + consoleRef.set(console); + return console; + }); + + EdtTestUtil.runInEdtAndWait(() -> presenter.show( + getProject(), + "OpenFastTrace Trace: test-runner", + result + )); + + assertThat(consoleRef.get(), notNullValue()); + return consoleRef.get(); + } + + private SMTRunnerConsoleView createConsole() { + final SMTRunnerConsoleProperties properties = new SMTRunnerConsoleProperties( + createConfiguration(), + "OpenFastTrace", + DefaultRunExecutor.getRunExecutorInstance() + ); + final SMTRunnerConsoleView console = new SMTRunnerConsoleView(properties); + console.initUI(); + Disposer.register(getTestRootDisposable(), console); + return console; + } + + private OftRunConfiguration createConfiguration() { + return new OftRunConfiguration( + getProject(), + new OftRunConfigurationFactory(new OftRunConfigurationType()), + "OpenFastTrace" + ); + } + + private String projectLocalPath(final String relativePath) { + return Objects.requireNonNull(getProject().getBasePath()) + "/" + relativePath; + } + + private static SMTestProxy childNamed(final SMTestProxy parent, final String name) { + final List matchingChildren = parent.getChildren().stream() + .filter(child -> child.getName().equals(name)) + .toList(); + assertThat(matchingChildren, hasSize(1)); + return matchingChildren.getFirst(); + } + + private static Trace trace(final LinkedSpecificationItem... items) { + final List traceItems = Arrays.asList(items); + return Trace.builder() + .items(traceItems) + .defectItems(traceItems.stream() + .filter(LinkedSpecificationItem::isDefect) + .toList()) + .build(); + } + + private static LinkedSpecificationItem titledItem(final String id, final String locationPath, final String title) { + return new LinkedSpecificationItem(SpecificationItem.builder() + .id(SpecificationItemId.parseId(id)) + .title(title) + .status(ItemStatus.APPROVED) + .location(locationPath, 1) + .build()); + } +} diff --git a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTreeMapperTest.java b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTreeMapperTest.java new file mode 100644 index 0000000..7edd498 --- /dev/null +++ b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTreeMapperTest.java @@ -0,0 +1,253 @@ +package org.itsallcode.openfasttrace.intellijplugin.trace; + +import org.itsallcode.openfasttrace.api.core.ItemStatus; +import org.itsallcode.openfasttrace.api.core.LinkedSpecificationItem; +import org.itsallcode.openfasttrace.api.core.LinkStatus; +import org.itsallcode.openfasttrace.api.core.SpecificationItem; +import org.itsallcode.openfasttrace.api.core.SpecificationItemId; +import org.itsallcode.openfasttrace.api.core.Trace; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; + +class OftTraceTestTreeMapperTest { + private static final String PROJECT_BASE = "/workspace/openfasttrace-intellij-plugin"; + + private final OftTraceTestTreeMapper mapper = new OftTraceTestTreeMapper(); + + // [itest->dsn~trace-test-runner-presentation~1] + // [itest->dsn~show-trace-source-files-as-test-runner-suites~1] + // [itest->dsn~roll-up-source-file-suite-trace-status~1] + // [itest->dsn~roll-up-top-level-trace-status~1] + @Test + void testGivenTraceItemsFromDifferentSourceFilesWhenMappingThenItCreatesSuitesBySourceFile() { + final LinkedSpecificationItem firstRequirement = item("req~first_requirement~1", "doc/requirements.md"); + final LinkedSpecificationItem secondRequirement = item("req~second_requirement~1", "doc/requirements.md"); + final LinkedSpecificationItem implementation = item("impl~first_requirement~1", "src/Main.java"); + + final OftTraceTestTree tree = mapper.map(trace(firstRequirement, secondRequirement, implementation)); + + Assertions.assertAll( + () -> assertThat(tree.suites().stream().map(OftTraceSuiteNode::name).toList(), + contains("doc/requirements.md", "src/Main.java")), + () -> assertThat(suiteNamed(tree, "doc/requirements.md").items(), hasSize(2)), + () -> assertThat(suiteNamed(tree, "src/Main.java").items(), hasSize(1)), + () -> assertThat(tree.testCount(), is(3)), + () -> assertThat(suiteNamed(tree, "doc/requirements.md").failed(), is(false)), + () -> assertThat(tree.failed(), is(false)) + ); + } + + // [itest->dsn~trace-test-runner-presentation~1] + // [itest->dsn~show-trace-source-files-as-test-runner-suites~1] + @Test + void testGivenAbsoluteTraceItemPathsBelowProjectWhenMappingThenItCreatesSuitesWithProjectLocalPaths() { + final LinkedSpecificationItem requirement = item( + "req~project_local_requirement~1", + PROJECT_BASE + "/doc/system_requirements.md" + ); + final LinkedSpecificationItem implementation = item( + "impl~project_local_requirement~1", + PROJECT_BASE + "/src/main/java/Main.java" + ); + + final OftTraceTestTree tree = mapper.map(trace(requirement, implementation), PROJECT_BASE); + + assertThat( + tree.suites().stream() + .map(OftTraceSuiteNode::name) + .toList(), + contains("doc/system_requirements.md", "src/main/java/Main.java") + ); + } + + // [itest->dsn~trace-test-runner-presentation~1] + // [itest->dsn~show-trace-specification-items-as-test-runner-tests~1] + // [itest->dsn~show-trace-links-as-test-runner-sub-tests~1] + // [itest->dsn~show-specification-item-title-in-test-runner-ui~1] + // [itest->dsn~show-specification-item-status-in-test-runner-ui~1] + // [itest->dsn~show-trace-link-status-in-test-runner-ui~1] + // [itest->dsn~show-trace-link-direction-in-test-runner-ui~1] + // [itest->dsn~show-unicode-trace-link-direction-in-test-runner-ui~1] + // [itest->dsn~map-specification-item-trace-status-to-test-runner-status~1] + // [itest->dsn~map-trace-link-status-to-test-runner-status~1] + @Test + void testGivenCoveredRequirementWithIncomingTraceLinkWhenMappingThenItCreatesPassedItemAndLinkNodes() { + final LinkedSpecificationItem requirement = titledItem( + "req~covered_requirement~1", + "doc/requirements.md", + "Covered requirement", + "tst" + ); + final LinkedSpecificationItem test = titledItem( + "tst~covered_requirement~1", + "src/CoveredRequirementTest.java", + "Covered requirement test" + ); + requirement.addLinkToItemWithStatus(test, LinkStatus.COVERED_SHALLOW); + + final OftTraceItemNode requirementNode = onlyItem( + mapper.map(trace(requirement, test)), + "doc/requirements.md" + ); + + Assertions.assertAll( + () -> assertThat(requirementNode.name(), + is("Covered requirement \u2014 req~covered_requirement~1 (covered)")), + () -> assertThat(requirementNode.navigationId(), is("req~covered_requirement~1")), + () -> assertThat(requirementNode.failed(), is(false)), + () -> assertThat(requirementNode.testCount(), is(2)), + () -> assertThat(requirementNode.links(), hasSize(1)), + () -> assertThat(requirementNode.links().getFirst().name(), + is("\u2190 Covered requirement test \u2014 tst~covered_requirement~1 (covered)")), + () -> assertThat(requirementNode.links().getFirst().navigationId(), + is("tst~covered_requirement~1")), + () -> assertThat(requirementNode.links().getFirst().failed(), is(false)) + ); + } + + // [itest->dsn~trace-test-runner-presentation~1] + // [itest->dsn~sort-specification-items-in-test-runner-ui~1] + @Test + void testGivenUnsortedSpecificationItemsInOneSourceFileWhenMappingThenItSortsItemsByIdParts() { + final OftTraceTestTree tree = mapper.map(trace( + item("req~zeta_requirement~1", "doc/requirements.md"), + item("impl~zeta_requirement~1", "doc/requirements.md"), + item("req~alpha_requirement~2", "doc/requirements.md"), + item("feat~trace_results~1", "doc/requirements.md"), + item("req~alpha_requirement~1", "doc/requirements.md") + )); + + assertThat( + suiteNamed(tree, "doc/requirements.md").items().stream() + .map(OftTraceItemNode::name) + .toList(), + contains( + "feat~trace_results~1 (covered)", + "impl~zeta_requirement~1 (covered)", + "req~alpha_requirement~1 (covered)", + "req~alpha_requirement~2 (covered)", + "req~zeta_requirement~1 (covered)" + ) + ); + } + + // [itest->dsn~trace-test-runner-presentation~1] + // [itest->dsn~show-trace-links-as-test-runner-sub-tests~1] + // [itest->dsn~show-trace-link-status-in-test-runner-ui~1] + // [itest->dsn~show-trace-link-direction-in-test-runner-ui~1] + // [itest->dsn~show-unicode-trace-link-direction-in-test-runner-ui~1] + // [itest->dsn~map-specification-item-trace-status-to-test-runner-status~1] + // [itest->dsn~map-trace-link-status-to-test-runner-status~1] + // [itest->dsn~roll-up-source-file-suite-trace-status~1] + // [itest->dsn~roll-up-top-level-trace-status~1] + // [itest->dsn~show-trace-link-defect-details-in-test-runner-ui~1] + @Test + void testGivenOutgoingOrphanedTraceLinkWhenMappingThenItCreatesFailedItemAndLinkNodes() { + final LinkedSpecificationItem implementation = item("impl~missing_requirement~1", "src/Main.java"); + final LinkedSpecificationItem missingRequirement = item("req~missing_requirement~1", "doc/requirements.md"); + implementation.addLinkToItemWithStatus(missingRequirement, LinkStatus.ORPHANED); + + final OftTraceTestTree tree = mapper.map(trace(implementation)); + final OftTraceItemNode implementationNode = onlyItem( + tree, + "src/Main.java" + ); + final OftTraceLinkNode link = implementationNode.links().getFirst(); + + Assertions.assertAll( + () -> assertThat(tree.failed(), is(true)), + () -> assertThat(suiteNamed(tree, "src/Main.java").failed(), is(true)), + () -> assertThat(implementationNode.failed(), is(true)), + () -> assertThat(implementationNode.links(), hasSize(1)), + () -> assertThat(link.name(), is("\u2192 req~missing_requirement~1 (orphaned)")), + () -> assertThat(link.failed(), is(true)), + () -> assertThat(link.details().failureMessage(), is("Orphaned outgoing trace link.")), + () -> assertThat(link.details().detailText(), containsString("Owning item: impl~missing_requirement~1")), + () -> assertThat(link.details().detailText(), containsString("Linked item: req~missing_requirement~1")), + () -> assertThat(link.details().detailText(), containsString("Direction: outgoing")), + () -> assertThat(link.details().detailText(), containsString("OpenFastTrace could not find")) + ); + } + + // [itest->dsn~trace-test-runner-presentation~1] + // [itest->dsn~show-specification-item-status-in-test-runner-ui~1] + // [itest->dsn~map-specification-item-trace-status-to-test-runner-status~1] + // [itest->dsn~show-specification-item-defect-details-in-test-runner-ui~1] + @Test + void testGivenUncoveredRequirementWhenMappingThenItCreatesFailedItemNodeWithUncoveredStatus() { + final LinkedSpecificationItem requirement = item("req~uncovered_requirement~1", "doc/requirements.md", "dsn"); + + final OftTraceItemNode requirementNode = onlyItem( + mapper.map(trace(requirement)), + "doc/requirements.md" + ); + + Assertions.assertAll( + () -> assertThat(requirementNode.name(), is("req~uncovered_requirement~1 (uncovered)")), + () -> assertThat(requirementNode.failed(), is(true)), + () -> assertThat(requirementNode.details().failureMessage(), + is("Uncovered OpenFastTrace specification item.")), + () -> assertThat(requirementNode.details().detailText(), + containsString("Specification item: req~uncovered_requirement~1")), + () -> assertThat(requirementNode.details().detailText(), + containsString("Trace status: uncovered")), + () -> assertThat(requirementNode.details().detailText(), + containsString("Required coverage is missing")) + ); + } + + private static OftTraceSuiteNode suiteNamed(final OftTraceTestTree tree, final String name) { + return tree.suites().stream() + .filter(suite -> suite.name().equals(name)) + .findFirst() + .orElseThrow(); + } + + private static OftTraceItemNode onlyItem(final OftTraceTestTree tree, final String sourceName) { + final List items = suiteNamed(tree, sourceName).items(); + assertThat(items, hasSize(1)); + return items.getFirst(); + } + + private static Trace trace(final LinkedSpecificationItem... items) { + final List traceItems = Arrays.asList(items); + return Trace.builder() + .items(traceItems) + .defectItems(traceItems.stream() + .filter(LinkedSpecificationItem::isDefect) + .toList()) + .build(); + } + + private static LinkedSpecificationItem item( + final String id, + final String locationPath, + final String... needsArtifactTypes + ) { + return titledItem(id, locationPath, "", needsArtifactTypes); + } + + 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); + Arrays.stream(needsArtifactTypes).forEach(builder::addNeedsArtifactType); + return new LinkedSpecificationItem(builder.build()); + } +} From 7be6e40e13bfdc0e1d5d01ecf3f30b030f23eab7 Mon Sep 17 00:00:00 2001 From: redcatbear Date: Tue, 9 Jun 2026 14:12:51 +0200 Subject: [PATCH 03/10] #40: Fixed navigation. --- README.md | 2 +- doc/changes/changes_0.8.0.md | 2 +- ...integrate-oft-trace-into-test-runner-ui.md | 22 ++- doc/design/building_block_view.md | 11 +- doc/design/runtime_view.md | 60 ++++-- doc/design/solution_strategy.md | 6 +- doc/system_requirements.md | 100 ++++++++-- .../trace/OftTraceNavigationResolver.java | 159 ++++++++++++++++ .../trace/OftTraceTestNodeDetails.java | 75 +++++++- .../trace/OftTraceTestProxy.java | 31 ++- .../OftTraceTestRunnerOutputPresenter.java | 16 +- .../trace/OftTraceTestTree.java | 8 +- .../trace/OftTraceTestTreeMapper.java | 180 ++++++++++++++---- .../trace/OftTraceTestProxyTest.java | 37 +++- ...OftTraceTestRunnerOutputPresenterTest.java | 56 +++++- .../trace/OftTraceTestTreeMapperTest.java | 93 +++++++-- 16 files changed, 729 insertions(+), 129 deletions(-) diff --git a/README.md b/README.md index ab32de3..85346ad 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ After the sandbox IDE opens a project, configure trace scope under `Settings | T Run an OFT trace from `Tools | OpenFastTrace | Trace Project`, use the default shortcut `Ctrl+Alt+Shift+O`, or create and run a dedicated `OpenFastTrace` run configuration from the IDE's run/debug toolbar. The plugin traces the configured inputs in the background and shows the plain text result in an IDE output tab with ANSI colors preserved. You can click specification item IDs in the report to jump to their declarations, including source-side items generated from coverage tags. -In an `OpenFastTrace` run configuration, keep `Plain text output` for the existing report view or select `IntelliJ Test Runner UI` to inspect the structured trace as source-file suites, specification-item tests, and trace-link sub-tests. Specification items are sorted inside each source file by artifact type, ID name, and revision, and show their title before the ID when available. Passed and failed nodes reflect the OpenFastTrace trace status, and source navigation from item and link nodes opens the corresponding specification declaration or source-side coverage tag. +In an `OpenFastTrace` run configuration, keep `Plain text output` for the existing report view or select `IntelliJ Test Runner UI` to inspect the structured trace as source-file suites, specification-item tests, and trace-link sub-tests. Specification items are sorted inside each source file by artifact type, ID name, and revision, use their title as the tree label when available, and show status labels only for non-clean statuses. Passed and failed nodes reflect the OpenFastTrace trace status and show IDs in their details, and source navigation from suite, item, and link nodes opens the corresponding file, specification declaration, or source-side coverage tag. The plugin also bundles an `OpenFastTrace` live-template group under `Settings | Editor | Live Templates`. Use abbreviations such as `feat`, `req`, `dsn`, and `scn` in a supported editing context, then press `Tab` to insert an OFT item skeleton. The `scn` template inserts a scenario stub with placeholders for `Given`, `When`, `Then`, and the covered requirement. While the caret is still in a template's covered-item field, use basic completion to select an existing specification item ID from the project index. diff --git a/doc/changes/changes_0.8.0.md b/doc/changes/changes_0.8.0.md index 6c70fb8..3d0399b 100644 --- a/doc/changes/changes_0.8.0.md +++ b/doc/changes/changes_0.8.0.md @@ -2,7 +2,7 @@ With this release of the plugin, OpenFastTrace run configurations can show trace results in IntelliJ's built-in Test Runner UI. -The existing plain text output remains the default result view. Run configurations can opt into the Test Runner UI to inspect source-file suites, specification-item tests, trace-link sub-tests, pass/fail status, and navigation back to specification declarations or source-side coverage tags. +The existing plain text output remains the default result view. Run configurations can opt into the Test Runner UI to inspect source-file suites, specification-item tests, trace-link sub-tests, pass/fail status, and navigation back to source files, specification declarations, or source-side coverage tags. ## Features diff --git a/doc/changesets/40-integrate-oft-trace-into-test-runner-ui.md b/doc/changesets/40-integrate-oft-trace-into-test-runner-ui.md index c858157..ecdb550 100644 --- a/doc/changesets/40-integrate-oft-trace-into-test-runner-ui.md +++ b/doc/changesets/40-integrate-oft-trace-into-test-runner-ui.md @@ -16,9 +16,10 @@ In scope: * Implement an `OftTraceOutputPresenter` that feeds trace results into `SMTRunnerConsoleView`. * Build the test runner hierarchy by source file, specification item, and incoming or outgoing trace link. * Map clean specification items and links to passed tests and defective items or links to failed tests. -* Show item and link status labels in brackets, for example `(covered)` and `(orphaned)`. +* Show item and link status labels in brackets only for non-clean statuses, for example `(uncovered)` and `(orphaned)`. +* Keep tree labels compact by showing item titles when available and moving IDs to node details. * Mark links as incoming or outgoing as required by GH-40. -* Reuse `OftTraceNavigationResolver` so test runner nodes can navigate to specification declarations and source-side coverage tags. +* Reuse `OftTraceNavigationResolver` so test runner nodes can navigate to source files, specification declarations, and source-side coverage tags. Out of scope: @@ -52,16 +53,16 @@ Keep presentation selection at the run-configuration layer. The global `Trace Pr - [x] Update `doc/system_requirements.md` with a user requirement for selecting the trace result view in an `OpenFastTrace` run configuration. - [x] Add system scenarios for showing trace results in the test runner by source file, specification item, and trace link. -- [x] Add system scenarios for test-runner pass/fail mapping, bracketed status labels, incoming/outgoing link markers, and navigation from result nodes to source. +- [x] Add system scenarios for test-runner pass/fail mapping, non-clean status labels, incoming/outgoing link markers, and navigation from result nodes to source. - [x] Add system requirements and scenarios for source-file suite roll-up, top-level trace roll-up, Unicode direction arrows, and node defect details. -- [x] Add system requirements and scenarios for title-prefixed specification item labels and deterministic per-file item ordering. +- [x] Add system requirements and scenarios for title-aware compact specification item labels, ID details, and deterministic per-file item ordering. - [x] Keep the existing plain text trace-output requirements and scenarios valid for the global `Trace Project` action and default run-configuration behavior. - [x] Stop and ask user for a review of the system requirements. - [x] Update `doc/design/solution_strategy.md` to document reuse of IntelliJ's SM test runner infrastructure for structured trace result presentation. - [x] Update `doc/design/building_block_view.md` with a test-runner trace presentation building block and the run-configuration result-view option. - [x] Update `doc/design/runtime_view.md` with design items for presenter selection, test-runner hierarchy construction, status mapping, and navigation. - [x] Update the design with suite roll-up behavior, Unicode direction markers, and static trace-link defect detail templates. -- [x] Update the design with title-aware item labels and source-file suite item ordering. +- [x] Update the design with title-aware compact item labels, ID details, and source-file suite item ordering. - [x] Stop and ask user for a review of the design. ### Implementation @@ -73,14 +74,15 @@ Keep presentation selection at the run-configuration layer. The global `Trace Pr - [x] Extend `OftTraceResult` so successful and defective trace runs retain the structured `Trace` in addition to the rendered text report. - [x] Update `OftTraceService` to return structured trace data without changing importer, linker, tracer, class-loader, filter, or text-rendering behavior. - [x] Implement a trace-to-test-tree mapper that groups results by source file, creates specification-item test nodes, and creates incoming and outgoing link sub-test nodes. -- [x] Implement status-label derivation for specification items and links from OpenFastTrace trace data. +- [x] Implement non-clean status-label derivation for specification items and links from OpenFastTrace trace data. - [x] Implement pass/fail mapping for clean and defective specification items and links. - [x] Implement `OftTraceTestRunnerOutputPresenter` using `SMTRunnerConsoleView` and `SMTestProxy`. - [x] Roll failed descendants up to source-file suites and the top-level trace suite. - [x] Replace ASCII trace-link direction markers with Unicode arrows. -- [x] Add clear failed-node details for specification items and trace links, using static templates for link-status explanations. -- [x] Display specification item titles before IDs when available and sort item entries per source file by artifact type, ID name, and revision. +- [x] Add clear node details for specification items and trace links, using static templates for failed link-status explanations. +- [x] Display compact specification item titles when available, keep IDs in details, and sort item entries per source file by artifact type, ID name, and revision. - [x] Connect test-runner node navigation through `OftTraceNavigationResolver`. +- [x] Let source-file suite nodes navigate to their corresponding file. - [x] Present invalid input, cancellation, and unexpected errors coherently when the test-runner output mode is selected. - [x] Preserve existing ANSI-colored plain text output and console hyperlink behavior. - [x] Avoid adding third-party dependencies unless a separate design decision is approved. @@ -90,10 +92,10 @@ Keep presentation selection at the run-configuration layer. The global `Trace Pr - [x] Add tests for run-configuration presentation-mode defaults, persistence, and backward-compatible XML reading. - [x] Add settings-editor tests for selecting and applying the result-view option. - [x] Add `OftTraceResult` and `OftTraceService` tests proving structured `Trace` data is retained for clean and defective runs and absent for invalid-input, cancelled, and unexpected-error results. -- [x] Add focused mapper tests for source-file grouping, specification-item nodes, incoming/outgoing link nodes, bracketed status labels, and pass/fail status. +- [x] Add focused mapper tests for source-file grouping, specification-item nodes, incoming/outgoing link nodes, non-clean status labels, and pass/fail status. - [x] Add presenter tests for `SMTRunnerConsoleView` creation and event emission for successful and defective traces. - [x] Add mapper and presenter tests for suite roll-up, top-level roll-up, Unicode arrows, and failed-node details. -- [x] Add mapper and presenter tests for title-prefixed item labels and per-file item ordering. +- [x] Add mapper and presenter tests for compact item labels, ID details, and per-file item ordering. - [x] Add run-profile tests proving plain text output remains the default and test-runner output is selected only when configured. - [x] Add navigation tests proving test-runner nodes navigate to specification declarations and source-side coverage tags through `OftTraceNavigationResolver`. - [x] Keep existing trace action, trace service, console presenter, run-configuration, and navigation tests green. diff --git a/doc/design/building_block_view.md b/doc/design/building_block_view.md index 5343098..84bc272 100644 --- a/doc/design/building_block_view.md +++ b/doc/design/building_block_view.md @@ -273,16 +273,17 @@ 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 title-aware labels, Unicode direction markers, pass/fail status, status roll-up, and defect details from the OpenFastTrace trace status; and connects 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, 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` - `scn~show-trace-specification-items-as-test-runner-tests~1` -- `scn~show-specification-item-title-in-test-runner-ui~1` +- `scn~show-specification-item-title-in-test-runner-ui~2` +- `scn~show-specification-item-id-in-test-runner-details~1` - `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~1` -- `scn~show-trace-link-status-in-test-runner-ui~1` +- `scn~show-specification-item-status-in-test-runner-ui~2` +- `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` - `scn~map-specification-item-trace-status-to-test-runner-status~1` @@ -291,8 +292,10 @@ Covers: - `scn~roll-up-top-level-trace-status~1` - `scn~show-specification-item-defect-details-in-test-runner-ui~1` - `scn~show-trace-link-defect-details-in-test-runner-ui~1` +- `scn~show-trace-link-id-details-in-test-runner-ui~1` - `scn~navigate-from-test-runner-specification-items~1` - `scn~navigate-from-test-runner-trace-links~1` +- `scn~navigate-from-test-runner-source-files~1` Needs: impl, itest diff --git a/doc/design/runtime_view.md b/doc/design/runtime_view.md index 50deafb..1aa046d 100644 --- a/doc/design/runtime_view.md +++ b/doc/design/runtime_view.md @@ -544,14 +544,26 @@ Covers: Needs: impl, itest ### Show Specification Item Title in Test Runner UI -`dsn~show-specification-item-title-in-test-runner-ui~1` +`dsn~show-specification-item-title-in-test-runner-ui~2` -**Given** the trace test-runner presentation creates a node whose visible name contains a specification item ID -**When** the OpenFastTrace item for that visible ID has a non-blank title -**Then** it prefixes the visible node name with the title, followed by ` — ` and the full specification item ID. +**Given** the trace test-runner presentation creates a node for an OpenFastTrace specification item +**When** the OpenFastTrace item has a non-blank title +**Then** it uses the title as the visible node name and keeps the full specification item ID out of the tree label. Covers: -- `scn~show-specification-item-title-in-test-runner-ui~1` +- `scn~show-specification-item-title-in-test-runner-ui~2` + +Needs: impl, itest + +### Show Specification Item ID in Test Runner Details +`dsn~show-specification-item-id-in-test-runner-details~1` + +**Given** the trace test-runner presentation creates a specification-item test node +**When** it maps the OpenFastTrace item status to the SM test node +**Then** it attaches details that identify the full specification item ID and trace status for passed and failed item nodes. + +Covers: +- `scn~show-specification-item-id-in-test-runner-details~1` Needs: impl, itest @@ -571,7 +583,7 @@ Needs: impl, itest `dsn~show-trace-links-as-test-runner-sub-tests~1` **Given** the trace test-runner presentation has created a specification-item test node -**When** it maps that item's incoming and outgoing trace links +**When** it maps incoming and outgoing trace links connected to that item from the structured trace **Then** it creates one SM sub-test node for each trace link below the specification-item test node. Covers: @@ -580,26 +592,26 @@ Covers: Needs: impl, itest ### Show Specification Item Status in Test Runner UI -`dsn~show-specification-item-status-in-test-runner-ui~1` +`dsn~show-specification-item-status-in-test-runner-ui~2` **Given** the trace test-runner presentation creates a specification-item test node **When** it derives the node name from the OpenFastTrace trace result -**Then** it appends the specification item's trace status in brackets to the visible node name. +**Then** it appends the specification item's trace status in brackets to the visible node name only if the item status is not clean. Covers: -- `scn~show-specification-item-status-in-test-runner-ui~1` +- `scn~show-specification-item-status-in-test-runner-ui~2` Needs: impl, itest ### Show Trace Link Status in Test Runner UI -`dsn~show-trace-link-status-in-test-runner-ui~1` +`dsn~show-trace-link-status-in-test-runner-ui~2` **Given** the trace test-runner presentation creates a trace-link sub-test node **When** it derives the node name from the OpenFastTrace trace result -**Then** it appends the trace link's status in brackets to the visible node name. +**Then** it appends the trace link's status in brackets to the visible node name only if the link status is not clean. Covers: -- `scn~show-trace-link-status-in-test-runner-ui~1` +- `scn~show-trace-link-status-in-test-runner-ui~2` Needs: impl, itest @@ -699,6 +711,18 @@ Covers: Needs: impl, itest +### Show Trace Link ID Details in Test Runner UI +`dsn~show-trace-link-id-details-in-test-runner-ui~1` + +**Given** the trace test-runner presentation creates a trace-link sub-test node +**When** it maps the OpenFastTrace link status to the SM test node +**Then** it attaches details that identify the owning specification item ID, linked specification item ID, link direction, and trace-link status for passed and failed link nodes. + +Covers: +- `scn~show-trace-link-id-details-in-test-runner-ui~1` + +Needs: impl, itest + ### Navigate from Test Runner Specification Items `dsn~navigate-from-test-runner-specification-items~1` @@ -723,6 +747,18 @@ Covers: Needs: impl, itest +### Navigate from Test Runner Source Files +`dsn~navigate-from-test-runner-source-files~1` + +**Given** the trace test-runner presentation creates a source-file suite node +**When** it attaches source navigation to that node +**Then** it opens the corresponding source file in the editor. + +Covers: +- `scn~navigate-from-test-runner-source-files~1` + +Needs: impl, itest + ### Show Successful Trace Output in IDE Output Window `dsn~show-successful-trace-output-in-ide-output-window~1` diff --git a/doc/design/solution_strategy.md b/doc/design/solution_strategy.md index 23fa432..4679078 100644 --- a/doc/design/solution_strategy.md +++ b/doc/design/solution_strategy.md @@ -75,10 +75,10 @@ Failure presentation is likewise kept narrow. The plugin may emphasize the short The test-runner trace-result increment keeps the existing plain text trace output as the default result view and adds structured presentation as a per-run-configuration choice. -The plugin uses IntelliJ's SM test runner infrastructure for this structured presentation. Source files become test suites labeled with project-local paths when they are below the opened project directory, specification items become tests below their source-file suite, and incoming or outgoing trace links become sub-tests below their owning specification item. This gives users the native test runner tree, progress, status, and navigation behavior without introducing a parallel result-tree widget. +The plugin uses IntelliJ's SM test runner infrastructure for this structured presentation. Source files become test suites labeled with project-local paths when they are below the opened project directory, specification items become tests below their source-file suite, and incoming or outgoing trace links become sub-tests below connected specification items. This gives users the native test runner tree, progress, status, and navigation behavior without introducing a parallel result-tree widget. The structured presentation is derived from the OpenFastTrace `Trace` model produced during trace execution, not from the rendered plain text report. OpenFastTrace remains responsible for import, linking, tracing, defect calculation, and text report rendering. The plugin maps the resulting trace model to IntelliJ test-runner nodes only at the presentation boundary. -Suite status and detail text are part of the structured presentation rather than the tracing core. The presenter rolls failed descendants up to source-file suites and the top-level trace suite, uses Unicode direction arrows for trace-link labels, and attaches concise defect details to failed nodes. Link-detail explanations reuse static templates keyed by OpenFastTrace link status so the constant explanation text is shared while node-specific IDs and directions stay dynamic. +Suite status and detail text are part of the structured presentation rather than the tracing core. The presenter rolls failed descendants up to source-file suites and the top-level trace suite, uses compact item titles or ID fallback names with Unicode direction arrows for trace-link labels, appends status labels only for non-clean statuses, and attaches concise ID and status details to item and link nodes. Link-detail explanations reuse static templates keyed by OpenFastTrace link status so the constant explanation text is shared while node-specific IDs and directions stay dynamic. -Navigation from structured result nodes reuses the existing OpenFastTrace trace navigation support. Specification item nodes resolve to specification declarations, while link nodes resolve to the corresponding declaration or source-side coverage tag where possible. +Navigation from structured result nodes reuses the existing OpenFastTrace trace navigation support. Source-file suite nodes open their corresponding file, specification item nodes resolve to specification declarations, and link nodes resolve to the corresponding declaration or source-side coverage tag where possible. diff --git a/doc/system_requirements.md b/doc/system_requirements.md index 98bc046..4193cac 100644 --- a/doc/system_requirements.md +++ b/doc/system_requirements.md @@ -514,9 +514,19 @@ Covers: Needs: scn ### Show Specification Item Title in Test Runner UI -`req~show-specification-item-title-in-test-runner-ui~1` +`req~show-specification-item-title-in-test-runner-ui~2` -The IntelliJ Test Runner UI result view prefixes a visible specification item ID with the item's title when the imported OpenFastTrace item provides a non-blank title. The title is followed by ` — ` and the full specification item ID. Items without a title show the full specification item ID without a title prefix. +The IntelliJ Test Runner UI result view uses the item's title as the visible specification-item name when the imported OpenFastTrace item provides a non-blank title. Items without a title show the full specification item ID as the visible name. + +Covers: +- `feat~oft-test-runner-trace-results~1` + +Needs: scn + +### Show Specification Item ID in Test Runner Details +`req~show-specification-item-id-in-test-runner-details~1` + +The IntelliJ Test Runner UI result view shows the full specification item ID and trace status in the details for specification-item entries, even when the entry is passed. Covers: - `feat~oft-test-runner-trace-results~1` @@ -536,7 +546,7 @@ Needs: scn ### Show Trace Links as Test Runner Sub-Tests `req~show-trace-links-as-test-runner-sub-tests~1` -The IntelliJ Test Runner UI result view shows each incoming or outgoing trace link as a sub-test below the specification item that owns that link. +The IntelliJ Test Runner UI result view shows each incoming or outgoing trace link as a sub-test below the connected specification item for which the link is shown. Covers: - `feat~oft-test-runner-trace-results~1` @@ -544,9 +554,9 @@ Covers: Needs: scn ### Show Specification Item Status in Test Runner UI -`req~show-specification-item-status-in-test-runner-ui~1` +`req~show-specification-item-status-in-test-runner-ui~2` -The IntelliJ Test Runner UI result view includes each specification item's trace status in brackets in the item entry, for example `(covered)`. +The IntelliJ Test Runner UI result view includes a specification item's trace status in brackets in the item entry only when the item status is not clean, for example `(uncovered)` or `(defective)`. Covers: - `feat~oft-test-runner-trace-results~1` @@ -554,9 +564,9 @@ Covers: Needs: scn ### Show Trace Link Status in Test Runner UI -`req~show-trace-link-status-in-test-runner-ui~1` +`req~show-trace-link-status-in-test-runner-ui~2` -The IntelliJ Test Runner UI result view includes each trace link's status in brackets in the link entry, for example `(orphaned)`. +The IntelliJ Test Runner UI result view includes a trace link's status in brackets in the link entry only when the link status is not clean, for example `(orphaned)`. Covers: - `feat~oft-test-runner-trace-results~1` @@ -643,6 +653,16 @@ Covers: Needs: scn +### Show Trace Link ID Details in Test Runner UI +`req~show-trace-link-id-details-in-test-runner-ui~1` + +The IntelliJ Test Runner UI result view shows the owning specification item ID, linked specification item ID, link direction, and trace-link status in the details for trace-link entries, even when the entry is passed. + +Covers: +- `feat~oft-test-runner-trace-results~1` + +Needs: scn + ### Navigate from Test Runner Specification Items `req~navigate-from-test-runner-specification-items~1` @@ -663,6 +683,16 @@ Covers: Needs: scn +### Navigate from Test Runner Source Files +`req~navigate-from-test-runner-source-files~1` + +Users can navigate from a source-file suite entry in the IntelliJ Test Runner UI result view to the corresponding file in the opened project. + +Covers: +- `feat~oft-test-runner-trace-results~1` + +Needs: scn + ### Show Scanned Base Directory in Trace Output Window `req~show-scanned-base-directory-in-trace-output-window~1` @@ -1356,14 +1386,26 @@ Covers: Needs: dsn ### Show Specification Item Title in Test Runner UI -`scn~show-specification-item-title-in-test-runner-ui~1` +`scn~show-specification-item-title-in-test-runner-ui~2` -**Given** an `OpenFastTrace` run configuration uses the IntelliJ Test Runner UI result view and the trace result contains a visible specification item ID for an item with a title +**Given** an `OpenFastTrace` run configuration uses the IntelliJ Test Runner UI result view and the trace result contains a specification item with a title **When** the trace completes -**Then** the test runner tree shows the title before the full specification item ID +**Then** the test runner tree shows the title as the visible specification-item name and omits the full specification item ID from the tree label Covers: -- `req~show-specification-item-title-in-test-runner-ui~1` +- `req~show-specification-item-title-in-test-runner-ui~2` + +Needs: dsn + +### Show Specification Item ID in Test Runner Details +`scn~show-specification-item-id-in-test-runner-details~1` + +**Given** an `OpenFastTrace` run configuration uses the IntelliJ Test Runner UI result view and the trace result contains a specification item +**When** the user selects that specification item in the test runner tree +**Then** the detail view shows the full specification item ID and trace status + +Covers: +- `req~show-specification-item-id-in-test-runner-details~1` Needs: dsn @@ -1392,26 +1434,26 @@ Covers: Needs: dsn ### Show Specification Item Status in Test Runner UI -`scn~show-specification-item-status-in-test-runner-ui~1` +`scn~show-specification-item-status-in-test-runner-ui~2` **Given** an `OpenFastTrace` run configuration uses the IntelliJ Test Runner UI result view and the trace result contains a specification item **When** the trace completes -**Then** the specification item entry shows the item's trace status in brackets +**Then** the specification item entry shows the item's trace status in brackets only when the item status is not clean Covers: -- `req~show-specification-item-status-in-test-runner-ui~1` +- `req~show-specification-item-status-in-test-runner-ui~2` Needs: dsn ### Show Trace Link Status in Test Runner UI -`scn~show-trace-link-status-in-test-runner-ui~1` +`scn~show-trace-link-status-in-test-runner-ui~2` **Given** an `OpenFastTrace` run configuration uses the IntelliJ Test Runner UI result view and the trace result contains an incoming or outgoing trace link **When** the trace completes -**Then** the trace-link sub-test shows the link status in brackets +**Then** the trace-link sub-test shows the link status in brackets only when the link status is not clean Covers: -- `req~show-trace-link-status-in-test-runner-ui~1` +- `req~show-trace-link-status-in-test-runner-ui~2` Needs: dsn @@ -1511,6 +1553,18 @@ Covers: Needs: dsn +### Show Trace Link ID Details in Test Runner UI +`scn~show-trace-link-id-details-in-test-runner-ui~1` + +**Given** an `OpenFastTrace` run configuration uses the IntelliJ Test Runner UI result view and the trace result contains a trace link +**When** the user selects that trace link in the test runner tree +**Then** the detail view shows the owning specification item ID, linked specification item ID, link direction, and trace-link status + +Covers: +- `req~show-trace-link-id-details-in-test-runner-ui~1` + +Needs: dsn + ### Navigate from Test Runner Specification Items `scn~navigate-from-test-runner-specification-items~1` @@ -1534,3 +1588,15 @@ Covers: - `req~navigate-from-test-runner-trace-links~1` Needs: dsn + +### Navigate from Test Runner Source Files +`scn~navigate-from-test-runner-source-files~1` + +**Given** an IntelliJ project is open and the test runner tree shows an OpenFastTrace source-file suite entry from that project +**When** a user activates source navigation for that source-file suite entry +**Then** the IDE opens the corresponding source file in the editor + +Covers: +- `req~navigate-from-test-runner-source-files~1` + +Needs: dsn diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceNavigationResolver.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceNavigationResolver.java index 963552a..8f6941f 100644 --- a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceNavigationResolver.java +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceNavigationResolver.java @@ -5,8 +5,10 @@ import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.ProjectFileIndex; +import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.search.FilenameIndex; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.util.indexing.FileBasedIndex; import org.itsallcode.openfasttrace.intellijplugin.OftSupportedFiles; @@ -15,6 +17,8 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; import java.util.Optional; import java.util.zip.CRC32; @@ -32,6 +36,161 @@ Optional resolve(final String specificationId) { .or(() -> findCoverageTagSourceTarget(specificationId)); } + // [impl->dsn~navigate-from-test-runner-source-files~1] + Optional resolveSourceFile(final String sourcePath) { + if (sourcePath == null || sourcePath.isBlank()) { + return Optional.empty(); + } + final Path path = sourcePath(sourcePath); + if (path != null) { + final VirtualFile file = LocalFileSystem.getInstance().findFileByNioFile(path); + if (file != null) { + return Optional.of(new OftTraceNavigationTarget(file, 0)); + } + } + return findIndexedSourceFile(sourcePath, path) + .or(() -> findProjectSourceFile(sourcePath, path)); + } + + private Path sourcePath(final String sourcePath) { + if (sourcePath == null || sourcePath.isBlank()) { + return null; + } + try { + final Path path = Path.of(sourcePath); + if (path.isAbsolute()) { + return path.normalize(); + } + final String projectBasePath = project.getBasePath(); + return projectBasePath == null || projectBasePath.isBlank() + ? null + : Path.of(projectBasePath).resolve(path).normalize(); + } catch (final InvalidPathException exception) { + return null; + } + } + + private Optional findIndexedSourceFile( + final String sourcePath, + final @Nullable Path path + ) { + final String fileName = sourceFileName(sourcePath, path); + if (fileName == null || fileName.isBlank()) { + return Optional.empty(); + } + final String requestedPath = displayPath(sourcePath); + final String resolvedPath = path == null ? null : displayPath(path.toString()); + final String projectRelativeSourcePath = projectRelativeSourcePath(path).orElse(requestedPath); + return FilenameIndex.getVirtualFilesByName(fileName, GlobalSearchScope.projectScope(project)) + .stream() + .filter(file -> matchesSourcePath(file, requestedPath, resolvedPath, projectRelativeSourcePath)) + .findFirst() + .map(file -> new OftTraceNavigationTarget(file, 0)); + } + + private @Nullable String sourceFileName(final String sourcePath, final @Nullable Path path) { + if (path != null) { + final Path fileName = path.getFileName(); + if (fileName != null) { + return fileName.toString(); + } + } + if (sourcePath == null || sourcePath.isBlank()) { + return null; + } + final String displayPath = displayPath(sourcePath); + final int lastSeparator = displayPath.lastIndexOf('/'); + if (lastSeparator < 0) { + return displayPath; + } + return displayPath.substring(lastSeparator + 1); + } + + private Optional projectRelativeSourcePath(final @Nullable Path path) { + if (path == null || !path.isAbsolute()) { + return Optional.empty(); + } + final String projectBasePath = project.getBasePath(); + if (projectBasePath == null || projectBasePath.isBlank()) { + return Optional.empty(); + } + final Path basePath = Path.of(projectBasePath).normalize(); + if (!path.startsWith(basePath)) { + return Optional.empty(); + } + return Optional.of(displayPath(basePath.relativize(path).toString())); + } + + private Optional findProjectSourceFile( + final String sourcePath, + final @Nullable Path path + ) { + final String requestedPath = displayPath(sourcePath); + final String resolvedPath = path == null ? null : displayPath(path.toString()); + final String projectRelativeSourcePath = projectRelativeSourcePath(path).orElse(requestedPath); + final OftTraceNavigationTarget[] target = new OftTraceNavigationTarget[1]; + ProjectFileIndex.getInstance(project) + .iterateContent(file -> processProjectSourceFile( + requestedPath, + resolvedPath, + projectRelativeSourcePath, + target, + file + )); + return Optional.ofNullable(target[0]); + } + + private boolean processProjectSourceFile( + final String requestedPath, + final @Nullable String resolvedPath, + final String projectRelativeSourcePath, + final OftTraceNavigationTarget[] target, + final VirtualFile file + ) { + if (matchesSourcePath(file, requestedPath, resolvedPath, projectRelativeSourcePath)) { + target[0] = new OftTraceNavigationTarget(file, 0); + return false; + } + return true; + } + + private boolean matchesSourcePath( + final VirtualFile file, + final String requestedPath, + final @Nullable String resolvedPath, + final String projectRelativeSourcePath + ) { + final String filePath = displayPath(file.getPath()); + return filePath.equals(requestedPath) + || filePath.equals(resolvedPath) + || projectRelativePath(file).filter(path -> requestedPath.equals(path) + || projectRelativeSourcePath.equals(path)).isPresent() + || matchesRelativeSourcePath(filePath, projectRelativeSourcePath); + } + + private boolean matchesRelativeSourcePath(final String filePath, final String requestedPath) { + return !requestedPath.startsWith("/") + && requestedPath.contains("/") + && filePath.endsWith("/" + requestedPath); + } + + private Optional projectRelativePath(final VirtualFile file) { + final String projectBasePath = project.getBasePath(); + if (projectBasePath == null || projectBasePath.isBlank()) { + return Optional.empty(); + } + final String basePath = displayPath(projectBasePath); + final String filePath = displayPath(file.getPath()); + if (!filePath.startsWith(basePath + "/")) { + return Optional.empty(); + } + return Optional.of(filePath.substring(basePath.length() + 1)); + } + + private static String displayPath(final String path) { + return path.replace('\\', '/'); + } + private Optional findDeclaredSpecificationTarget(final String specificationId) { final OftTraceNavigationTarget[] target = new OftTraceNavigationTarget[1]; FileBasedIndex.getInstance().processValues( diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestNodeDetails.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestNodeDetails.java index 02bd39b..aafa029 100644 --- a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestNodeDetails.java +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestNodeDetails.java @@ -33,22 +33,55 @@ static OftTraceTestNodeDetails sourceSuiteFailure(final String sourceName) { ); } + // [impl->dsn~show-specification-item-id-in-test-runner-details~1] + static OftTraceTestNodeDetails specificationItem( + final LinkedSpecificationItem item, + final String visibleStatus + ) { + return new OftTraceTestNodeDetails( + "", + specificationItemDetailText(item, visibleStatus) + ); + } + // [impl->dsn~show-specification-item-defect-details-in-test-runner-ui~1] + // [impl->dsn~show-specification-item-id-in-test-runner-details~1] static OftTraceTestNodeDetails specificationItemFailure( final LinkedSpecificationItem item, final String visibleStatus ) { return new OftTraceTestNodeDetails( itemFailureMessage(item, visibleStatus), - "Specification item: " + item.getId() - + LINE_SEPARATOR - + "Trace status: " + visibleStatus + specificationItemDetailText(item, visibleStatus) + LINE_SEPARATOR + itemFailureExplanation(item, visibleStatus) ); } + static OftTraceTestNodeDetails specificationItemLinkFailure(final String specificationItemId) { + return new OftTraceTestNodeDetails( + "OpenFastTrace trace-link defect for specification item " + specificationItemId + ".", + "Specification item ID: " + specificationItemId + + LINE_SEPARATOR + + "At least one trace link below this specification item is failed." + ); + } + + // [impl->dsn~show-trace-link-id-details-in-test-runner-ui~1] + static OftTraceTestNodeDetails traceLink( + final String owningItemId, + final String linkedItemId, + final String directionLabel, + final String visibleStatus + ) { + return new OftTraceTestNodeDetails( + "", + traceLinkDetailText(owningItemId, linkedItemId, directionLabel, visibleStatus) + ); + } + // [impl->dsn~show-trace-link-defect-details-in-test-runner-ui~1] + // [impl->dsn~show-trace-link-id-details-in-test-runner-ui~1] static OftTraceTestNodeDetails traceLinkFailure( final String owningItemId, final String linkedItemId, @@ -67,6 +100,34 @@ static OftTraceTestNodeDetails resultWithoutTrace(final OftTraceResult result) { ); } + boolean hasDetailText() { + return !detailText.isBlank(); + } + + private static String specificationItemDetailText( + final LinkedSpecificationItem item, + final String visibleStatus + ) { + return "Specification item ID: " + item.getId() + + LINE_SEPARATOR + + "Trace status: " + visibleStatus; + } + + private static String traceLinkDetailText( + final String owningItemId, + final String linkedItemId, + final String directionLabel, + final String visibleStatus + ) { + return "Owning item ID: " + owningItemId + + LINE_SEPARATOR + + "Linked item ID: " + linkedItemId + + LINE_SEPARATOR + + "Direction: " + directionLabel + + LINE_SEPARATOR + + "Trace-link status: " + visibleStatus; + } + private static String itemFailureMessage(final LinkedSpecificationItem item, final String visibleStatus) { return switch (visibleStatus) { case "duplicate" -> "Duplicate OpenFastTrace specification item."; @@ -170,13 +231,7 @@ private OftTraceTestNodeDetails create( ) { return new OftTraceTestNodeDetails( failureMessage, - "Owning item: " + owningItemId - + LINE_SEPARATOR - + "Linked item: " + linkedItemId - + LINE_SEPARATOR - + "Direction: " + directionLabel - + LINE_SEPARATOR - + "Trace-link status: " + visibleStatus + traceLinkDetailText(owningItemId, linkedItemId, directionLabel, visibleStatus) + LINE_SEPARATOR + explanation ); diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestProxy.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestProxy.java index ced8c34..44a73ac 100644 --- a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestProxy.java +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestProxy.java @@ -12,13 +12,30 @@ final class OftTraceTestProxy extends SMTestProxy { private static final long serialVersionUID = 1L; private final transient Project project; - private final String navigationId; + private final String specificationId; + private final String sourcePath; private final transient OftTraceNavigationResolver navigationResolver; OftTraceTestProxy(final Project project, final String name, final boolean suite, final String navigationId) { + this(project, name, suite, navigationId, null); + } + + // [impl->dsn~navigate-from-test-runner-source-files~1] + static OftTraceTestProxy sourceFileSuite(final Project project, final String name, final String sourcePath) { + return new OftTraceTestProxy(project, name, true, null, sourcePath); + } + + private OftTraceTestProxy( + final Project project, + final String name, + final boolean suite, + final String specificationId, + final String sourcePath + ) { super(name, suite, null, true); this.project = project; - this.navigationId = navigationId; + this.specificationId = specificationId; + this.sourcePath = sourcePath; this.navigationResolver = new OftTraceNavigationResolver(project); } @@ -40,9 +57,15 @@ public boolean canNavigateToSource() { } private Optional resolveNavigationTarget() { - if (navigationId == null || navigationResolver == null || project == null) { + if (navigationResolver == null || project == null) { return Optional.empty(); } - return navigationResolver.resolve(navigationId); + if (specificationId != null) { + return navigationResolver.resolve(specificationId); + } + if (sourcePath != null) { + return navigationResolver.resolveSourceFile(sourcePath); + } + return Optional.empty(); } } diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerOutputPresenter.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerOutputPresenter.java index 41c5f17..245037e 100644 --- a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerOutputPresenter.java +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerOutputPresenter.java @@ -64,7 +64,7 @@ private static void showSuite( final SMTestProxy parent, final OftTraceSuiteNode suite ) { - final SMTestProxy suiteProxy = new OftTraceTestProxy(project, suite.name(), true, null); + final SMTestProxy suiteProxy = OftTraceTestProxy.sourceFileSuite(project, suite.name(), suite.sourcePath()); parent.addChild(suiteProxy); suiteProxy.setSuiteStarted(); resultsViewer.onSuiteStarted(suiteProxy); @@ -95,9 +95,11 @@ private static void showItem( resultsViewer.onTestStarted(itemProxy); } if (item.failed()) { - markFailed(itemProxy, item.details()); + markFailed(itemProxy, item.failureDetails()); // Expandable item nodes are suite-shaped, but the item status itself still counts as a test. resultsViewer.onTestFailed(itemProxy); + } else { + addPassedDetails(itemProxy, item.details()); } for (final OftTraceLinkNode link : item.links()) { showLink(project, resultsViewer, itemProxy, link); @@ -136,6 +138,8 @@ private static void showTestNode( if (failed) { markFailed(testProxy, details); resultsViewer.onTestFailed(testProxy); + } else { + addPassedDetails(testProxy, details); } testProxy.setFinished(); resultsViewer.onTestFinished(testProxy); @@ -166,11 +170,19 @@ private static PresentationOutcome showResultWithoutTrace( // [impl->dsn~roll-up-source-file-suite-trace-status~1] // [impl->dsn~roll-up-top-level-trace-status~1] // [impl->dsn~show-specification-item-defect-details-in-test-runner-ui~1] + // [impl->dsn~show-specification-item-id-in-test-runner-details~1] // [impl->dsn~show-trace-link-defect-details-in-test-runner-ui~1] + // [impl->dsn~show-trace-link-id-details-in-test-runner-ui~1] private static void markFailed(final SMTestProxy testProxy, final OftTraceTestNodeDetails details) { testProxy.setTestFailed(details.failureMessage(), details.detailText(), false); } + private static void addPassedDetails(final SMTestProxy testProxy, final OftTraceTestNodeDetails details) { + if (details.hasDetailText()) { + testProxy.addStdOutput(details.detailText() + System.lineSeparator()); + } + } + private record PresentationOutcome(boolean failed, OftTraceTestNodeDetails details) { } } diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTree.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTree.java index 9ef1b99..b400e33 100644 --- a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTree.java +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTree.java @@ -14,7 +14,7 @@ boolean failed() { } } -record OftTraceSuiteNode(String name, List items) { +record OftTraceSuiteNode(String name, String sourcePath, List items) { int testCount() { return items.stream() .mapToInt(OftTraceItemNode::testCount) @@ -44,6 +44,12 @@ int testCount() { boolean failed() { return defective || links.stream().anyMatch(OftTraceLinkNode::failed); } + + OftTraceTestNodeDetails failureDetails() { + return defective + ? details + : OftTraceTestNodeDetails.specificationItemLinkFailure(navigationId); + } } record OftTraceLinkNode(String name, String navigationId, boolean failed, OftTraceTestNodeDetails details) { diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTreeMapper.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTreeMapper.java index a026767..04c318b 100644 --- a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTreeMapper.java +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTreeMapper.java @@ -6,15 +6,21 @@ import org.itsallcode.openfasttrace.api.core.Location; import org.itsallcode.openfasttrace.api.core.Trace; import org.itsallcode.openfasttrace.api.core.TracedLink; +import org.jetbrains.annotations.Nullable; import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; +import java.util.IdentityHashMap; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Optional; +import java.util.Set; final class OftTraceTestTreeMapper { private static final String UNKNOWN_SOURCE = "Unknown source"; @@ -26,17 +32,20 @@ final class OftTraceTestTreeMapper { // [impl->dsn~trace-test-runner-presentation~1] // [impl->dsn~show-trace-source-files-as-test-runner-suites~1] // [impl->dsn~show-trace-specification-items-as-test-runner-tests~1] - // [impl->dsn~show-specification-item-title-in-test-runner-ui~1] + // [impl->dsn~show-specification-item-title-in-test-runner-ui~2] + // [impl->dsn~show-specification-item-id-in-test-runner-details~1] // [impl->dsn~sort-specification-items-in-test-runner-ui~1] // [impl->dsn~show-trace-links-as-test-runner-sub-tests~1] - // [impl->dsn~show-specification-item-status-in-test-runner-ui~1] - // [impl->dsn~show-trace-link-status-in-test-runner-ui~1] + // [impl->dsn~show-specification-item-status-in-test-runner-ui~2] + // [impl->dsn~show-trace-link-status-in-test-runner-ui~2] // [impl->dsn~show-trace-link-direction-in-test-runner-ui~1] // [impl->dsn~show-unicode-trace-link-direction-in-test-runner-ui~1] // [impl->dsn~map-specification-item-trace-status-to-test-runner-status~1] // [impl->dsn~map-trace-link-status-to-test-runner-status~1] // [impl->dsn~show-specification-item-defect-details-in-test-runner-ui~1] // [impl->dsn~show-trace-link-defect-details-in-test-runner-ui~1] + // [impl->dsn~show-trace-link-id-details-in-test-runner-ui~1] + // [impl->dsn~navigate-from-test-runner-source-files~1] // [impl->dsn~navigate-from-test-runner-specification-items~1] // [impl->dsn~navigate-from-test-runner-trace-links~1] OftTraceTestTree map(final Trace trace) { @@ -44,82 +53,152 @@ OftTraceTestTree map(final Trace trace) { } OftTraceTestTree map(final Trace trace, final String projectBasePath) { - final Map> itemsBySource = new LinkedHashMap<>(); + final Map> visibleLinksByItem = + visibleLinksByItem(trace.getItems()); + final Map itemsBySource = new LinkedHashMap<>(); for (final LinkedSpecificationItem item : trace.getItems()) { - itemsBySource.computeIfAbsent(sourceName(item, projectBasePath), ignored -> new ArrayList<>()) - .add(item); + final SourceFileSuite sourceFileSuite = sourceFileSuite(item, projectBasePath); + itemsBySource.computeIfAbsent( + sourceFileSuite.name(), + ignored -> new SourceFileItems(sourceFileSuite.name(), sourceFileSuite.sourcePath()) + ).add(item); } - final List suites = itemsBySource.entrySet().stream() - .map(entry -> new OftTraceSuiteNode(entry.getKey(), mapItems(entry.getValue()))) + final List suites = itemsBySource.values().stream() + .map(source -> new OftTraceSuiteNode( + source.name(), + source.sourcePath(), + mapItems(source.items(), visibleLinksByItem) + )) .toList(); return new OftTraceTestTree(suites); } - private static List mapItems(final List items) { + private static List mapItems( + final List items, + final Map> visibleLinksByItem + ) { return items.stream() .sorted(ITEM_ORDER) - .map(OftTraceTestTreeMapper::mapItem) + .map(item -> mapItem(item, visibleLinksByItem.get(item))) .toList(); } - private static OftTraceItemNode mapItem(final LinkedSpecificationItem item) { + private static OftTraceItemNode mapItem( + final LinkedSpecificationItem item, + final List visibleLinks + ) { final String itemStatus = itemStatus(item); final String itemId = item.getId().toString(); return new OftTraceItemNode( - itemLabel(item) + " (" + itemStatus + ")", + nodeName(itemName(item), itemStatus, !item.isDefect()), itemId, item.isDefect(), item.isDefect() ? OftTraceTestNodeDetails.specificationItemFailure(item, itemStatus) - : OftTraceTestNodeDetails.none(), - item.getTracedLinks().stream() + : OftTraceTestNodeDetails.specificationItem(item, itemStatus), + visibleLinks.stream() .map(link -> mapLink(item, link)) .toList() ); } - private static OftTraceLinkNode mapLink(final LinkedSpecificationItem owner, final TracedLink link) { - final LinkedSpecificationItem otherItem = link.getOtherLinkEnd(); + private static Map> visibleLinksByItem( + final List items + ) { + final Set traceItems = Collections.newSetFromMap(new IdentityHashMap<>()); + final Map> linkSetsByItem = new LinkedHashMap<>(); + for (final LinkedSpecificationItem item : items) { + traceItems.add(item); + linkSetsByItem.put(item, new LinkedHashSet<>()); + } + for (final LinkedSpecificationItem owner : items) { + for (final TracedLink link : owner.getTracedLinks()) { + addVisibleLink(linkSetsByItem, owner, link.getOtherLinkEnd(), link.getStatus()); + if (traceItems.contains(link.getOtherLinkEnd())) { + reverseStatus(link.getStatus()) + .ifPresent(status -> addVisibleLink(linkSetsByItem, link.getOtherLinkEnd(), owner, status)); + } + } + } + final Map> linksByItem = new LinkedHashMap<>(); + linkSetsByItem.forEach((item, links) -> linksByItem.put(item, List.copyOf(links))); + return linksByItem; + } + + private static void addVisibleLink( + final Map> linkSetsByItem, + final LinkedSpecificationItem owner, + final LinkedSpecificationItem otherItem, + final LinkStatus status + ) { + linkSetsByItem.get(owner).add(new VisibleTraceLink(otherItem, status)); + } + + private static Optional reverseStatus(final LinkStatus status) { + return switch (status) { + case COVERS -> Optional.of(LinkStatus.COVERED_SHALLOW); + case COVERED_SHALLOW -> Optional.of(LinkStatus.COVERS); + case PREDATED -> Optional.of(LinkStatus.COVERED_PREDATED); + case COVERED_PREDATED -> Optional.of(LinkStatus.PREDATED); + case OUTDATED -> Optional.of(LinkStatus.COVERED_OUTDATED); + case COVERED_OUTDATED -> Optional.of(LinkStatus.OUTDATED); + case UNWANTED -> Optional.of(LinkStatus.COVERED_UNWANTED); + case COVERED_UNWANTED -> Optional.of(LinkStatus.UNWANTED); + case DUPLICATE -> Optional.of(LinkStatus.DUPLICATE); + case AMBIGUOUS, ORPHANED -> Optional.empty(); + }; + } + + private static OftTraceLinkNode mapLink(final LinkedSpecificationItem owner, final VisibleTraceLink link) { + final LinkedSpecificationItem otherItem = link.otherItem(); final String otherItemId = otherItem.getId().toString(); - final LinkDirection direction = direction(link); - final String linkStatus = linkStatus(link.getStatus()); + final LinkDirection direction = direction(link.status()); + final String linkStatus = linkStatus(link.status()); return new OftTraceLinkNode( - direction.marker() + " " + itemLabel(otherItem) + " (" + linkStatus + ")", + nodeName(direction.marker() + " " + itemName(otherItem), linkStatus, !link.status().isBad()), otherItemId, - link.getStatus().isBad(), - link.getStatus().isBad() + link.status().isBad(), + link.status().isBad() ? OftTraceTestNodeDetails.traceLinkFailure( owner.getId().toString(), otherItemId, direction.label(), - link.getStatus(), + link.status(), + linkStatus + ) + : OftTraceTestNodeDetails.traceLink( + owner.getId().toString(), + otherItemId, + direction.label(), linkStatus ) - : OftTraceTestNodeDetails.none() ); } - private static String itemLabel(final LinkedSpecificationItem item) { - final String itemId = item.getId().toString(); + private static String itemName(final LinkedSpecificationItem item) { final String title = item.getTitle(); - return title == null || title.isBlank() ? itemId : title + " \u2014 " + itemId; + return title == null || title.isBlank() ? item.getId().toString() : title; + } + + private static String nodeName(final String baseName, final String status, final boolean clean) { + return clean ? baseName : baseName + " (" + status + ")"; } - private static String sourceName(final LinkedSpecificationItem item, final String projectBasePath) { + private static SourceFileSuite sourceFileSuite(final LinkedSpecificationItem item, final String projectBasePath) { final Location location = item.getLocation(); if (location == null || location.getPath() == null || location.getPath().isBlank()) { - return UNKNOWN_SOURCE; + return new SourceFileSuite(UNKNOWN_SOURCE, null); } return sourcePathLabel(location.getPath(), projectBasePath); } - private static String sourcePathLabel(final String sourcePath, final String projectBasePath) { + private static SourceFileSuite sourcePathLabel(final String sourcePath, final String projectBasePath) { final Path source = pathOrNull(sourcePath); if (source == null) { - return displayPath(sourcePath); + return new SourceFileSuite(displayPath(sourcePath), null); } final Path labelPath = projectLocalPath(source, projectBasePath); - return displayPath(labelPath.toString()); + return new SourceFileSuite(displayPath(labelPath.toString()), displayPath(source.normalize().toString())); } private static Path projectLocalPath(final Path source, final String projectBasePath) { @@ -174,16 +253,49 @@ private static String linkStatus(final LinkStatus status) { return status.toString().toLowerCase(Locale.ROOT); } - private static LinkDirection direction(final TracedLink link) { - if (link.isIncoming()) { + private static LinkDirection direction(final LinkStatus status) { + if (status.isIncoming()) { return new LinkDirection("\u2190", "incoming"); } - if (link.isOutgoing()) { + if (status.isOutgoing()) { return new LinkDirection("\u2192", "outgoing"); } return new LinkDirection("\u2194", "non-directional"); } + private record VisibleTraceLink(LinkedSpecificationItem otherItem, LinkStatus status) { + } + + private record SourceFileSuite(String name, @Nullable String sourcePath) { + } + + private static final class SourceFileItems { + private final String name; + private final String sourcePath; + private final List items = new ArrayList<>(); + + private SourceFileItems(final String name, final String sourcePath) { + this.name = name; + this.sourcePath = sourcePath; + } + + private void add(final LinkedSpecificationItem item) { + items.add(item); + } + + private String name() { + return name; + } + + private String sourcePath() { + return sourcePath; + } + + private List items() { + return items; + } + } + private record LinkDirection(String marker, String label) { } } diff --git a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestProxyTest.java b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestProxyTest.java index 82f5aac..abe6a99 100644 --- a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestProxyTest.java +++ b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestProxyTest.java @@ -7,6 +7,7 @@ import org.itsallcode.openfasttrace.intellijplugin.AbstractOftPlatformTestCase; import java.nio.charset.StandardCharsets; +import java.util.Objects; import java.util.zip.CRC32; import static org.hamcrest.MatcherAssert.assertThat; @@ -57,7 +58,7 @@ class Main { ); final OftTraceTestProxy proxy = new OftTraceTestProxy( getProject(), - "<- " + generatedCoverageItemId + " (covered)", + "<- " + generatedCoverageItemId, false, generatedCoverageItemId ); @@ -68,6 +69,40 @@ class Main { assertThat(selectedEditorFileName(), is("Main.java")); } + // [itest->dsn~navigate-from-test-runner-source-files~1] + public void testGivenSourceFileSuiteNodeWhenNavigatingThenItOpensTheSourceFile() { + myFixture.addFileToProject("doc/source.md", """ + req~runner_source_navigation~1 + """); + final OftTraceTestProxy proxy = OftTraceTestProxy.sourceFileSuite( + getProject(), + "doc/source.md", + "doc/source.md" + ); + + assertThat(proxy.canNavigate(), is(true)); + EdtTestUtil.runInEdtAndWait(() -> proxy.navigate(false)); + + assertThat(selectedEditorFileName(), is("source.md")); + } + + // [itest->dsn~navigate-from-test-runner-source-files~1] + public void testGivenSourceFileSuiteNodeWithAbsoluteProjectPathWhenNavigatingThenItOpensTheSourceFile() { + myFixture.addFileToProject("doc/absolute-source.md", """ + req~runner_absolute_source_navigation~1 + """); + final OftTraceTestProxy proxy = OftTraceTestProxy.sourceFileSuite( + getProject(), + "doc/absolute-source.md", + Objects.requireNonNull(getProject().getBasePath()) + "/doc/absolute-source.md" + ); + + assertThat(proxy.canNavigate(), is(true)); + EdtTestUtil.runInEdtAndWait(() -> proxy.navigate(false)); + + assertThat(selectedEditorFileName(), is("absolute-source.md")); + } + public void testGivenNodeWithoutNavigationTargetWhenCheckingNavigationThenItCannotNavigate() { final OftTraceTestProxy proxy = new OftTraceTestProxy(getProject(), "doc/spec.md", true, null); diff --git a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerOutputPresenterTest.java b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerOutputPresenterTest.java index 0728024..e1ccd07 100644 --- a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerOutputPresenterTest.java +++ b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerOutputPresenterTest.java @@ -1,10 +1,14 @@ package org.itsallcode.openfasttrace.intellijplugin.trace; import com.intellij.execution.executors.DefaultRunExecutor; +import com.intellij.execution.filters.HyperlinkInfo; +import com.intellij.execution.testframework.Printable; +import com.intellij.execution.testframework.Printer; import com.intellij.execution.testframework.sm.runner.SMTRunnerConsoleProperties; import com.intellij.execution.testframework.sm.runner.SMTestProxy; import com.intellij.execution.testframework.sm.runner.ui.SMTRunnerConsoleView; import com.intellij.execution.testframework.sm.runner.ui.SMTestRunnerResultsForm; +import com.intellij.execution.ui.ConsoleViewContentType; import com.intellij.openapi.util.Disposer; import com.intellij.testFramework.EdtTestUtil; import org.itsallcode.openfasttrace.api.core.ItemStatus; @@ -33,11 +37,17 @@ public class OftTraceTestRunnerOutputPresenterTest extends AbstractOftPlatformTestCase { // [itest->dsn~show-trace-source-files-as-test-runner-suites~1] // [itest->dsn~show-trace-specification-items-as-test-runner-tests~1] - // [itest->dsn~show-specification-item-title-in-test-runner-ui~1] + // [itest->dsn~show-specification-item-title-in-test-runner-ui~2] + // [itest->dsn~show-specification-item-id-in-test-runner-details~1] + // [itest->dsn~show-specification-item-status-in-test-runner-ui~2] + // [itest->dsn~navigate-from-test-runner-source-files~1] // [itest->dsn~map-specification-item-trace-status-to-test-runner-status~1] // [itest->dsn~roll-up-source-file-suite-trace-status~1] // [itest->dsn~roll-up-top-level-trace-status~1] public void testGivenSuccessfulTraceResultWhenPresentedThenItCreatesPassedTestRunnerNodes() { + myFixture.addFileToProject("doc/requirements.md", """ + req~clean_requirement~1 + """); final LinkedSpecificationItem requirement = titledItem( "req~clean_requirement~1", projectLocalPath("doc/requirements.md"), @@ -47,12 +57,15 @@ public void testGivenSuccessfulTraceResultWhenPresentedThenItCreatesPassedTestRu final SMTestRunnerResultsForm resultsViewer = console.getResultsViewer(); final SMTestProxy suite = childNamed(resultsViewer.getTestsRootNode(), "doc/requirements.md"); - final SMTestProxy item = childNamed(suite, "Clean requirement \u2014 req~clean_requirement~1 (covered)"); + final SMTestProxy item = childNamed(suite, "Clean requirement"); assertThat(suite.isSuite(), is(true)); + assertThat(suite.canNavigate(), is(true)); assertThat(item.isSuite(), is(false)); - assertThat(item.getPresentableName(), is("Clean requirement \u2014 req~clean_requirement~1 (covered)")); + assertThat(item.getPresentableName(), is("Clean requirement")); assertThat(item.isPassed(), is(true)); + assertThat(ownOutput(item), containsString("Specification item ID: req~clean_requirement~1")); + assertThat(ownOutput(item), containsString("Trace status: covered")); assertThat(suite.isDefect(), is(false)); assertThat(resultsViewer.getTestsRootNode().isDefect(), is(false)); assertThat(resultsViewer.getTotalTestCount(), is(1)); @@ -62,6 +75,8 @@ public void testGivenSuccessfulTraceResultWhenPresentedThenItCreatesPassedTestRu // [itest->dsn~show-trace-source-files-as-test-runner-suites~1] // [itest->dsn~show-trace-specification-items-as-test-runner-tests~1] // [itest->dsn~show-trace-links-as-test-runner-sub-tests~1] + // [itest->dsn~show-specification-item-status-in-test-runner-ui~2] + // [itest->dsn~show-trace-link-status-in-test-runner-ui~2] // [itest->dsn~map-specification-item-trace-status-to-test-runner-status~1] // [itest->dsn~map-trace-link-status-to-test-runner-status~1] // [itest->dsn~roll-up-source-file-suite-trace-status~1] @@ -84,11 +99,11 @@ public void testGivenDefectiveTraceResultWhenPresentedThenItCreatesFailedItemAnd final SMTestProxy suite = childNamed(resultsViewer.getTestsRootNode(), "src/Main.java"); final SMTestProxy item = childNamed( suite, - "Missing requirement implementation \u2014 impl~missing_requirement~1 (defective)" + "Missing requirement implementation (defective)" ); final SMTestProxy link = childNamed( item, - "\u2192 Missing requirement \u2014 req~missing_requirement~1 (orphaned)" + "\u2192 Missing requirement (orphaned)" ); assertThat(resultsViewer.getTestsRootNode().isDefect(), is(true)); @@ -99,13 +114,14 @@ public void testGivenDefectiveTraceResultWhenPresentedThenItCreatesFailedItemAnd assertThat(suite.getErrorMessage(), is("OpenFastTrace defects in src/Main.java.")); assertThat(resultsViewer.getTestsRootNode().getErrorMessage(), is("OpenFastTrace trace contains defects.")); assertThat(item.getErrorMessage(), is("Defective OpenFastTrace specification item impl~missing_requirement~1.")); + assertThat(item.getStacktrace(), containsString("Specification item ID: impl~missing_requirement~1")); assertThat(item.getStacktrace(), containsString("Trace status: defective")); assertThat(item.getStacktrace(), containsString("orphaned link to req~missing_requirement~1")); assertThat(link.getPresentableName(), - is("\u2192 Missing requirement \u2014 req~missing_requirement~1 (orphaned)")); + is("\u2192 Missing requirement (orphaned)")); assertThat(link.getErrorMessage(), is("Orphaned outgoing trace link.")); - assertThat(link.getStacktrace(), containsString("Owning item: impl~missing_requirement~1")); - assertThat(link.getStacktrace(), containsString("Linked item: req~missing_requirement~1")); + assertThat(link.getStacktrace(), containsString("Owning item ID: impl~missing_requirement~1")); + assertThat(link.getStacktrace(), containsString("Linked item ID: req~missing_requirement~1")); assertThat(link.getStacktrace(), containsString("OpenFastTrace could not find")); assertThat(resultsViewer.getTotalTestCount(), is(2)); assertThat(resultsViewer.getFailedTestCount(), is(2)); @@ -181,6 +197,30 @@ private static SMTestProxy childNamed(final SMTestProxy parent, final String nam return matchingChildren.getFirst(); } + private static String ownOutput(final SMTestProxy proxy) { + final StringBuilder output = new StringBuilder(); + proxy.printOwnPrintablesOn(new Printer() { + @Override + public void print(final String text, final ConsoleViewContentType contentType) { + output.append(text); + } + + @Override + public void onNewAvailable(final Printable printable) { + } + + @Override + public void printHyperlink(final String text, final HyperlinkInfo info) { + output.append(text); + } + + @Override + public void mark() { + } + }); + return output.toString(); + } + private static Trace trace(final LinkedSpecificationItem... items) { final List traceItems = Arrays.asList(items); return Trace.builder() diff --git a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTreeMapperTest.java b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTreeMapperTest.java index 7edd498..a378af9 100644 --- a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTreeMapperTest.java +++ b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTreeMapperTest.java @@ -48,6 +48,7 @@ void testGivenTraceItemsFromDifferentSourceFilesWhenMappingThenItCreatesSuitesBy // [itest->dsn~trace-test-runner-presentation~1] // [itest->dsn~show-trace-source-files-as-test-runner-suites~1] + // [itest->dsn~navigate-from-test-runner-source-files~1] @Test void testGivenAbsoluteTraceItemPathsBelowProjectWhenMappingThenItCreatesSuitesWithProjectLocalPaths() { final LinkedSpecificationItem requirement = item( @@ -61,24 +62,32 @@ void testGivenAbsoluteTraceItemPathsBelowProjectWhenMappingThenItCreatesSuitesWi final OftTraceTestTree tree = mapper.map(trace(requirement, implementation), PROJECT_BASE); - assertThat( - tree.suites().stream() - .map(OftTraceSuiteNode::name) - .toList(), - contains("doc/system_requirements.md", "src/main/java/Main.java") + Assertions.assertAll( + () -> assertThat( + tree.suites().stream() + .map(OftTraceSuiteNode::name) + .toList(), + contains("doc/system_requirements.md", "src/main/java/Main.java") + ), + () -> assertThat( + suiteNamed(tree, "doc/system_requirements.md").sourcePath(), + is(PROJECT_BASE + "/doc/system_requirements.md") + ) ); } // [itest->dsn~trace-test-runner-presentation~1] // [itest->dsn~show-trace-specification-items-as-test-runner-tests~1] // [itest->dsn~show-trace-links-as-test-runner-sub-tests~1] - // [itest->dsn~show-specification-item-title-in-test-runner-ui~1] - // [itest->dsn~show-specification-item-status-in-test-runner-ui~1] - // [itest->dsn~show-trace-link-status-in-test-runner-ui~1] + // [itest->dsn~show-specification-item-title-in-test-runner-ui~2] + // [itest->dsn~show-specification-item-id-in-test-runner-details~1] + // [itest->dsn~show-specification-item-status-in-test-runner-ui~2] + // [itest->dsn~show-trace-link-status-in-test-runner-ui~2] // [itest->dsn~show-trace-link-direction-in-test-runner-ui~1] // [itest->dsn~show-unicode-trace-link-direction-in-test-runner-ui~1] // [itest->dsn~map-specification-item-trace-status-to-test-runner-status~1] // [itest->dsn~map-trace-link-status-to-test-runner-status~1] + // [itest->dsn~show-trace-link-id-details-in-test-runner-ui~1] @Test void testGivenCoveredRequirementWithIncomingTraceLinkWhenMappingThenItCreatesPassedItemAndLinkNodes() { final LinkedSpecificationItem requirement = titledItem( @@ -101,16 +110,58 @@ void testGivenCoveredRequirementWithIncomingTraceLinkWhenMappingThenItCreatesPas Assertions.assertAll( () -> assertThat(requirementNode.name(), - is("Covered requirement \u2014 req~covered_requirement~1 (covered)")), + is("Covered requirement")), () -> assertThat(requirementNode.navigationId(), is("req~covered_requirement~1")), () -> assertThat(requirementNode.failed(), is(false)), + () -> assertThat(requirementNode.details().detailText(), + containsString("Specification item ID: req~covered_requirement~1")), + () -> assertThat(requirementNode.details().detailText(), containsString("Trace status: covered")), () -> assertThat(requirementNode.testCount(), is(2)), () -> assertThat(requirementNode.links(), hasSize(1)), () -> assertThat(requirementNode.links().getFirst().name(), - is("\u2190 Covered requirement test \u2014 tst~covered_requirement~1 (covered)")), + is("\u2190 Covered requirement test")), () -> assertThat(requirementNode.links().getFirst().navigationId(), is("tst~covered_requirement~1")), - () -> assertThat(requirementNode.links().getFirst().failed(), is(false)) + () -> assertThat(requirementNode.links().getFirst().failed(), is(false)), + () -> assertThat(requirementNode.links().getFirst().details().detailText(), + containsString("Owning item ID: req~covered_requirement~1")), + () -> assertThat(requirementNode.links().getFirst().details().detailText(), + containsString("Linked item ID: tst~covered_requirement~1")), + () -> assertThat(requirementNode.links().getFirst().details().detailText(), + containsString("Direction: incoming")), + () -> assertThat(requirementNode.links().getFirst().details().detailText(), + containsString("Trace-link status: covered")) + ); + } + + // [itest->dsn~trace-test-runner-presentation~1] + // [itest->dsn~show-trace-links-as-test-runner-sub-tests~1] + // [itest->dsn~show-trace-link-direction-in-test-runner-ui~1] + // [itest->dsn~show-unicode-trace-link-direction-in-test-runner-ui~1] + @Test + void testGivenOnlyOutgoingTraceLinkWhenMappingThenItCreatesIncomingLinkOnLinkedItem() { + final LinkedSpecificationItem implementation = titledItem( + "impl~covered_requirement~1", + "src/CoveredRequirement.java", + "Covered requirement implementation" + ); + final LinkedSpecificationItem requirement = titledItem( + "req~covered_requirement~1", + "doc/requirements.md", + "Covered requirement" + ); + implementation.addLinkToItemWithStatus(requirement, LinkStatus.COVERS); + + final OftTraceItemNode requirementNode = onlyItem( + mapper.map(trace(implementation, requirement)), + "doc/requirements.md" + ); + + assertThat( + requirementNode.links().stream() + .map(OftTraceLinkNode::name) + .toList(), + contains("\u2190 Covered requirement implementation") ); } @@ -131,18 +182,18 @@ void testGivenUnsortedSpecificationItemsInOneSourceFileWhenMappingThenItSortsIte .map(OftTraceItemNode::name) .toList(), contains( - "feat~trace_results~1 (covered)", - "impl~zeta_requirement~1 (covered)", - "req~alpha_requirement~1 (covered)", - "req~alpha_requirement~2 (covered)", - "req~zeta_requirement~1 (covered)" + "feat~trace_results~1", + "impl~zeta_requirement~1", + "req~alpha_requirement~1", + "req~alpha_requirement~2", + "req~zeta_requirement~1" ) ); } // [itest->dsn~trace-test-runner-presentation~1] // [itest->dsn~show-trace-links-as-test-runner-sub-tests~1] - // [itest->dsn~show-trace-link-status-in-test-runner-ui~1] + // [itest->dsn~show-trace-link-status-in-test-runner-ui~2] // [itest->dsn~show-trace-link-direction-in-test-runner-ui~1] // [itest->dsn~show-unicode-trace-link-direction-in-test-runner-ui~1] // [itest->dsn~map-specification-item-trace-status-to-test-runner-status~1] @@ -171,15 +222,15 @@ void testGivenOutgoingOrphanedTraceLinkWhenMappingThenItCreatesFailedItemAndLink () -> assertThat(link.name(), is("\u2192 req~missing_requirement~1 (orphaned)")), () -> assertThat(link.failed(), is(true)), () -> assertThat(link.details().failureMessage(), is("Orphaned outgoing trace link.")), - () -> assertThat(link.details().detailText(), containsString("Owning item: impl~missing_requirement~1")), - () -> assertThat(link.details().detailText(), containsString("Linked item: req~missing_requirement~1")), + () -> assertThat(link.details().detailText(), containsString("Owning item ID: impl~missing_requirement~1")), + () -> assertThat(link.details().detailText(), containsString("Linked item ID: req~missing_requirement~1")), () -> assertThat(link.details().detailText(), containsString("Direction: outgoing")), () -> assertThat(link.details().detailText(), containsString("OpenFastTrace could not find")) ); } // [itest->dsn~trace-test-runner-presentation~1] - // [itest->dsn~show-specification-item-status-in-test-runner-ui~1] + // [itest->dsn~show-specification-item-status-in-test-runner-ui~2] // [itest->dsn~map-specification-item-trace-status-to-test-runner-status~1] // [itest->dsn~show-specification-item-defect-details-in-test-runner-ui~1] @Test @@ -197,7 +248,7 @@ void testGivenUncoveredRequirementWhenMappingThenItCreatesFailedItemNodeWithUnco () -> assertThat(requirementNode.details().failureMessage(), is("Uncovered OpenFastTrace specification item.")), () -> assertThat(requirementNode.details().detailText(), - containsString("Specification item: req~uncovered_requirement~1")), + containsString("Specification item ID: req~uncovered_requirement~1")), () -> assertThat(requirementNode.details().detailText(), containsString("Trace status: uncovered")), () -> assertThat(requirementNode.details().detailText(), From 69036d899284213936659a566a3ab8cf7e009763 Mon Sep 17 00:00:00 2001 From: redcatbear Date: Tue, 9 Jun 2026 14:22:02 +0200 Subject: [PATCH 04/10] #40: Fixed missing comment for empty comment. --- .../trace/OftTraceTestRunnerOutputPresenterTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerOutputPresenterTest.java b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerOutputPresenterTest.java index e1ccd07..a52a04e 100644 --- a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerOutputPresenterTest.java +++ b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerOutputPresenterTest.java @@ -207,6 +207,7 @@ public void print(final String text, final ConsoleViewContentType contentType) { @Override public void onNewAvailable(final Printable printable) { + // Intentionally empty. } @Override @@ -216,6 +217,7 @@ public void printHyperlink(final String text, final HyperlinkInfo info) { @Override public void mark() { + // Intentionally empty. } }); return output.toString(); From 0270bd63d929f04c8c25c39c94a2a250c15d5028 Mon Sep 17 00:00:00 2001 From: redcatbear Date: Tue, 9 Jun 2026 14:34:50 +0200 Subject: [PATCH 05/10] #40: Link symbols are now more clear. --- doc/design/runtime_view.md | 2 +- .../intellijplugin/trace/OftTraceTestTreeMapper.java | 6 +++--- .../trace/OftTraceTestRunnerOutputPresenterTest.java | 11 ++++++----- .../trace/OftTraceTestTreeMapperTest.java | 6 +++--- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/doc/design/runtime_view.md b/doc/design/runtime_view.md index 1aa046d..028c610 100644 --- a/doc/design/runtime_view.md +++ b/doc/design/runtime_view.md @@ -632,7 +632,7 @@ Needs: impl, itest **Given** the trace test-runner presentation creates a trace-link sub-test node **When** it derives the direction marker from the OpenFastTrace link status -**Then** it uses `←` for incoming links, `→` for outgoing links, and `↔` for links without a single incoming or outgoing direction. +**Then** it uses `⊙←` for incoming links, `⊙→` for outgoing links, and `↔` for links without a single incoming or outgoing direction. Covers: - `scn~show-unicode-trace-link-direction-in-test-runner-ui~1` diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTreeMapper.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTreeMapper.java index 04c318b..bc272ad 100644 --- a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTreeMapper.java +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTreeMapper.java @@ -255,12 +255,12 @@ private static String linkStatus(final LinkStatus status) { private static LinkDirection direction(final LinkStatus status) { if (status.isIncoming()) { - return new LinkDirection("\u2190", "incoming"); + return new LinkDirection("⊙←", "incoming"); } if (status.isOutgoing()) { - return new LinkDirection("\u2192", "outgoing"); + return new LinkDirection("⊙→", "outgoing"); } - return new LinkDirection("\u2194", "non-directional"); + return new LinkDirection("↔", "non-directional"); } private record VisibleTraceLink(LinkedSpecificationItem otherItem, LinkStatus status) { diff --git a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerOutputPresenterTest.java b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerOutputPresenterTest.java index a52a04e..d038168 100644 --- a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerOutputPresenterTest.java +++ b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerOutputPresenterTest.java @@ -21,6 +21,7 @@ import org.itsallcode.openfasttrace.intellijplugin.trace.runconfig.OftRunConfiguration; import org.itsallcode.openfasttrace.intellijplugin.trace.runconfig.OftRunConfigurationFactory; import org.itsallcode.openfasttrace.intellijplugin.trace.runconfig.OftRunConfigurationType; +import org.jspecify.annotations.NonNull; import java.util.Arrays; import java.util.List; @@ -103,7 +104,7 @@ public void testGivenDefectiveTraceResultWhenPresentedThenItCreatesFailedItemAnd ); final SMTestProxy link = childNamed( item, - "\u2192 Missing requirement (orphaned)" + "⊙→ Missing requirement (orphaned)" ); assertThat(resultsViewer.getTestsRootNode().isDefect(), is(true)); @@ -118,7 +119,7 @@ public void testGivenDefectiveTraceResultWhenPresentedThenItCreatesFailedItemAnd assertThat(item.getStacktrace(), containsString("Trace status: defective")); assertThat(item.getStacktrace(), containsString("orphaned link to req~missing_requirement~1")); assertThat(link.getPresentableName(), - is("\u2192 Missing requirement (orphaned)")); + is("⊙→ Missing requirement (orphaned)")); assertThat(link.getErrorMessage(), is("Orphaned outgoing trace link.")); assertThat(link.getStacktrace(), containsString("Owning item ID: impl~missing_requirement~1")); assertThat(link.getStacktrace(), containsString("Linked item ID: req~missing_requirement~1")); @@ -201,17 +202,17 @@ private static String ownOutput(final SMTestProxy proxy) { final StringBuilder output = new StringBuilder(); proxy.printOwnPrintablesOn(new Printer() { @Override - public void print(final String text, final ConsoleViewContentType contentType) { + public void print(final @NonNull String text, final ConsoleViewContentType contentType) { output.append(text); } @Override - public void onNewAvailable(final Printable printable) { + public void onNewAvailable(final @NonNull Printable printable) { // Intentionally empty. } @Override - public void printHyperlink(final String text, final HyperlinkInfo info) { + public void printHyperlink(final @NonNull String text, final HyperlinkInfo info) { output.append(text); } diff --git a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTreeMapperTest.java b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTreeMapperTest.java index a378af9..9ae52d7 100644 --- a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTreeMapperTest.java +++ b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTreeMapperTest.java @@ -119,7 +119,7 @@ void testGivenCoveredRequirementWithIncomingTraceLinkWhenMappingThenItCreatesPas () -> assertThat(requirementNode.testCount(), is(2)), () -> assertThat(requirementNode.links(), hasSize(1)), () -> assertThat(requirementNode.links().getFirst().name(), - is("\u2190 Covered requirement test")), + is("⊙← Covered requirement test")), () -> assertThat(requirementNode.links().getFirst().navigationId(), is("tst~covered_requirement~1")), () -> assertThat(requirementNode.links().getFirst().failed(), is(false)), @@ -161,7 +161,7 @@ void testGivenOnlyOutgoingTraceLinkWhenMappingThenItCreatesIncomingLinkOnLinkedI requirementNode.links().stream() .map(OftTraceLinkNode::name) .toList(), - contains("\u2190 Covered requirement implementation") + contains("⊙← Covered requirement implementation") ); } @@ -219,7 +219,7 @@ void testGivenOutgoingOrphanedTraceLinkWhenMappingThenItCreatesFailedItemAndLink () -> assertThat(suiteNamed(tree, "src/Main.java").failed(), is(true)), () -> assertThat(implementationNode.failed(), is(true)), () -> assertThat(implementationNode.links(), hasSize(1)), - () -> assertThat(link.name(), is("\u2192 req~missing_requirement~1 (orphaned)")), + () -> assertThat(link.name(), is("⊙→ req~missing_requirement~1 (orphaned)")), () -> assertThat(link.failed(), is(true)), () -> assertThat(link.details().failureMessage(), is("Orphaned outgoing trace link.")), () -> assertThat(link.details().detailText(), containsString("Owning item ID: impl~missing_requirement~1")), From c9021ccbba5fe5259fb42794e52ce48582bf962c Mon Sep 17 00:00:00 2001 From: redcatbear Date: Tue, 9 Jun 2026 14:38:57 +0200 Subject: [PATCH 06/10] #40: Updated the Readme. --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 85346ad..a340e59 100644 --- a/README.md +++ b/README.md @@ -72,15 +72,17 @@ For manual IDE testing, launch a sandbox IDE with the plugin: ./gradlew manualTestIde ``` +## Using the Plugin + After the sandbox IDE opens a project, configure trace scope under `Settings | Tools | OpenFastTrace`. By default, `Trace Project` scans the whole opened project. If you switch to selected-resource tracing, the plugin includes IntelliJ source roots, IntelliJ test roots, and one default additional project-relative path entry, `doc/`, until you change it. Additional files or directories are entered one per line. -Run an OFT trace from `Tools | OpenFastTrace | Trace Project`, use the default shortcut `Ctrl+Alt+Shift+O`, or create and run a dedicated `OpenFastTrace` run configuration from the IDE's run/debug toolbar. The plugin traces the configured inputs in the background and shows the plain text result in an IDE output tab with ANSI colors preserved. You can click specification item IDs in the report to jump to their declarations, including source-side items generated from coverage tags. +## Run Configurations -In an `OpenFastTrace` run configuration, keep `Plain text output` for the existing report view or select `IntelliJ Test Runner UI` to inspect the structured trace as source-file suites, specification-item tests, and trace-link sub-tests. Specification items are sorted inside each source file by artifact type, ID name, and revision, use their title as the tree label when available, and show status labels only for non-clean statuses. Passed and failed nodes reflect the OpenFastTrace trace status and show IDs in their details, and source navigation from suite, item, and link nodes opens the corresponding file, specification declaration, or source-side coverage tag. +You can create and run a dedicated `OpenFastTrace` run configuration from the IDE's run/debug toolbar. The plugin traces the configured inputs in the background and shows the plain text result in an IDE output tab with ANSI colors preserved. You can click specification item IDs in the report to jump to their declarations, including source-side items generated from coverage tags. -The plugin also bundles an `OpenFastTrace` live-template group under `Settings | Editor | Live Templates`. Use abbreviations such as `feat`, `req`, `dsn`, and `scn` in a supported editing context, then press `Tab` to insert an OFT item skeleton. The `scn` template inserts a scenario stub with placeholders for `Given`, `When`, `Then`, and the covered requirement. While the caret is still in a template's covered-item field, use basic completion to select an existing specification item ID from the project index. +## Live-Templates Bundled With the Plugin -When editing a `Covers:` section in a supported specification document, invoke basic completion on a partial specification item ID to get existing declarations from the project index. The same completion is available on the target side of likely coverage tags in files supported by the OpenFastTrace Tag Importer after the left-hand artifact type and arrow, for example `// [impl->dsn~partial]`. Suggestions prefer full-ID prefix matches first, then name-prefix matches, then name-substring matches, and finally artifact-type prefix matches. +The plugin also bundles an `OpenFastTrace` live-template group under `Settings | Editor | Live Templates`. Use abbreviations such as `feat`, `req`, `dsn`, and `scn` in a supported editing context, then press `Tab` to insert an OFT item skeleton. The `scn` template inserts a scenario stub with placeholders for `Given`, `When`, `Then`, and the covered requirement. While the caret is still in a template's covered-item field, use basic completion to select an existing specification item ID from the project index. ## Installation ### Runtime Dependencies From dac418ebfa88e2406ffc9625b3d779f441a69320 Mon Sep 17 00:00:00 2001 From: redcatbear Date: Tue, 9 Jun 2026 15:03:39 +0200 Subject: [PATCH 07/10] #40: UI test result view is now the default. --- README.md | 6 +- doc/changes/changes_0.8.0.md | 4 +- ...ero-items-picked-up-by-the-trace-in-oft.md | 2 +- ...integrate-oft-trace-into-test-runner-ui.md | 6 +- doc/design/building_block_view.md | 18 +-- doc/design/runtime_view.md | 38 ++++-- doc/design/solution_strategy.md | 2 +- doc/system_requirements.md | 118 ++++++++++++------ .../trace/OftTraceProjectAction.java | 34 +++-- .../intellijplugin/trace/OftTraceService.java | 2 +- .../trace/OftTraceSettingsComponent.java | 5 +- .../trace/OftTraceSettingsSnapshot.java | 4 +- .../trace/runconfig/OftRunConfiguration.java | 1 + .../trace/runconfig/OftRunProfileState.java | 3 +- .../trace/OftTraceProjectActionTest.java | 10 +- .../trace/OftTraceProjectSettingsTest.java | 1 + .../trace/OftTraceServiceTest.java | 2 +- .../runconfig/OftRunConfigurationTest.java | 20 ++- .../runconfig/OftRunProfileStateTest.java | 16 ++- 19 files changed, 206 insertions(+), 86 deletions(-) diff --git a/README.md b/README.md index a340e59..df37863 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ The plugin currently focuses on: * bundled OFT live templates for common specification items, including a scenario template * project-wide or selected-resource OFT tracing from `Tools | OpenFastTrace | Trace Project` or through custom OpenFastTrace run configurations * clickable specification item IDs directly from the trace output window, including generated IDs created from coverage tags -* optional IntelliJ Test Runner UI output for OpenFastTrace run configurations, grouped by source file, specification item, and trace link +* IntelliJ Test Runner UI output for OpenFastTrace traces by default, grouped by source file, specification item, and trace link * quick access to the OpenFastTrace user guide from the IDE help menu For expected behavior and scope details, see: @@ -78,7 +78,9 @@ After the sandbox IDE opens a project, configure trace scope under `Settings | T ## Run Configurations -You can create and run a dedicated `OpenFastTrace` run configuration from the IDE's run/debug toolbar. The plugin traces the configured inputs in the background and shows the plain text result in an IDE output tab with ANSI colors preserved. You can click specification item IDs in the report to jump to their declarations, including source-side items generated from coverage tags. +You can create and run a dedicated `OpenFastTrace` run configuration from the IDE's run/debug toolbar. The plugin traces the configured inputs in the background and shows the result in IntelliJ's Test Runner UI by default. The result tree groups source files, specification items, and trace links with pass/fail status and navigation back to source. + +Run configurations can select plain text output instead. In that mode, the plugin shows the text report in an IDE output tab with ANSI colors preserved. You can click specification item IDs in the report to jump to their declarations, including source-side items generated from coverage tags. ## Live-Templates Bundled With the Plugin diff --git a/doc/changes/changes_0.8.0.md b/doc/changes/changes_0.8.0.md index 3d0399b..d08f373 100644 --- a/doc/changes/changes_0.8.0.md +++ b/doc/changes/changes_0.8.0.md @@ -1,8 +1,8 @@ # OpenFastTrace IntelliJ Plugin 0.8.0, released 2026-06-09 -With this release of the plugin, OpenFastTrace run configurations can show trace results in IntelliJ's built-in Test Runner UI. +With this release of the plugin, OpenFastTrace traces show results in IntelliJ's built-in Test Runner UI by default. -The existing plain text output remains the default result view. Run configurations can opt into the Test Runner UI to inspect source-file suites, specification-item tests, trace-link sub-tests, pass/fail status, and navigation back to source files, specification declarations, or source-side coverage tags. +The global `Tools | OpenFastTrace | Trace Project` action and new or previously unconfigured run configurations use the Test Runner UI to inspect source-file suites, specification-item tests, trace-link sub-tests, pass/fail status, and navigation back to source files, specification declarations, or source-side coverage tags. Run configurations can still opt into the existing plain text output when users prefer the text report. ## Features diff --git a/doc/changesets/17-zero-items-picked-up-by-the-trace-in-oft.md b/doc/changesets/17-zero-items-picked-up-by-the-trace-in-oft.md index fc9d4a5..8a198fd 100644 --- a/doc/changesets/17-zero-items-picked-up-by-the-trace-in-oft.md +++ b/doc/changesets/17-zero-items-picked-up-by-the-trace-in-oft.md @@ -40,7 +40,7 @@ The likely fault line is the OpenFastTrace library's use of `ServiceLoader` for ### Requirements And Design -- [x] Confirm that GH-17 is fully covered by the existing `feat~run-oft-trace~1`, `req~trace-open-project-from-project-root~1`, `req~show-trace-output-in-ide-output-window~1`, and related trace scenarios, so no user-facing requirement changes are needed +- [x] Confirm that GH-17 is fully covered by the existing `feat~run-oft-trace~2`, `req~trace-open-project-from-project-root~1`, `req~show-trace-output-in-ide-output-window~2`, and related trace scenarios, so no user-facing requirement changes are needed - [x] Update the design documentation to record the OFT importer/reporter discovery constraint in the plugin runtime, including that trace execution must use the plugin class loader for `ServiceLoader`-based OFT extension lookup - [x] Link the design clarification to the existing trace-execution design items instead of introducing redundant new runtime requirements diff --git a/doc/changesets/40-integrate-oft-trace-into-test-runner-ui.md b/doc/changesets/40-integrate-oft-trace-into-test-runner-ui.md index ecdb550..4e81159 100644 --- a/doc/changesets/40-integrate-oft-trace-into-test-runner-ui.md +++ b/doc/changesets/40-integrate-oft-trace-into-test-runner-ui.md @@ -4,14 +4,14 @@ Let users run an OpenFastTrace run configuration and inspect the structured trace result in IntelliJ's built-in test runner UI instead of only reading the plain text trace output. -The implementation should preserve the existing plain text output as the default result view while adding a run-configuration option that presents trace results as source-file suites, specification-item tests, and trace-link sub-tests with pass/fail status and navigation back to the source. +The implementation originally preserved the existing plain text output as the default result view while adding a run-configuration option that presents trace results as source-file suites, specification-item tests, and trace-link sub-tests with pass/fail status and navigation back to the source. The follow-up changeset [Default Test Runner Trace Result View](default-test-runner-trace-result-view.md) changes that default to the IntelliJ Test Runner UI. ## Scope In scope: * Add a result-view option to the `OpenFastTrace` run configuration editor for plain text output or IntelliJ test runner output. -* Preserve the existing plain text output behavior for `Tools | OpenFastTrace | Trace Project` and for run configurations unless the user selects the test runner view. +* Preserve the existing plain text output behavior for run configurations that select the plain text result view. * Keep the structured `Trace` object produced by `OftTraceService` available to output presenters. * Implement an `OftTraceOutputPresenter` that feeds trace results into `SMTRunnerConsoleView`. * Build the test runner hierarchy by source file, specification item, and incoming or outgoing trace link. @@ -96,7 +96,7 @@ Keep presentation selection at the run-configuration layer. The global `Trace Pr - [x] Add presenter tests for `SMTRunnerConsoleView` creation and event emission for successful and defective traces. - [x] Add mapper and presenter tests for suite roll-up, top-level roll-up, Unicode arrows, and failed-node details. - [x] Add mapper and presenter tests for compact item labels, ID details, and per-file item ordering. -- [x] Add run-profile tests proving plain text output remains the default and test-runner output is selected only when configured. +- [x] Add run-profile tests proving the configured result-view selection controls whether plain text or test-runner output is used. - [x] Add navigation tests proving test-runner nodes navigate to specification declarations and source-side coverage tags through `OftTraceNavigationResolver`. - [x] Keep existing trace action, trace service, console presenter, run-configuration, and navigation tests green. - [x] Keep the OpenFastTrace trace clean for `feat`, `req`, `scn`, `dsn`, `impl`, `utest`, and `itest` artifacts in scope. diff --git a/doc/design/building_block_view.md b/doc/design/building_block_view.md index 84bc272..b2d2ad4 100644 --- a/doc/design/building_block_view.md +++ b/doc/design/building_block_view.md @@ -220,23 +220,25 @@ Needs: impl ### Trace Configuration Integration `dsn~trace-configuration-integration~1` -The plugin provides a trace-configuration component that stores OpenFastTrace trace-scope settings per IntelliJ project and through dedicated run configurations. It exposes those settings through project configuration UI and the run configuration editor, resolves the selected-resource options and filters into a normalized OpenFastTrace input set and filter criteria, and stores the run-configuration result-view selection. +The plugin provides a trace-configuration component that stores OpenFastTrace trace-scope settings per IntelliJ project and through dedicated run configurations. It exposes those settings through project configuration UI and the run configuration editor, resolves the selected-resource options and filters into a normalized OpenFastTrace input set and filter criteria, stores the run-configuration result-view selection, and treats the IntelliJ Test Runner UI as the result-view default when no selection is stored. Covers: -- `scn~plain-text-as-default-run-configuration-result-view~1` +- `scn~test-runner-as-default-run-configuration-result-view~1` +- `scn~select-plain-text-trace-result-view~1` - `scn~select-test-runner-trace-result-view~1` Needs: impl, itest ### Trace Action Integration -`dsn~trace-action-integration~1` +`dsn~trace-action-integration~2` -The plugin provides a trace-action component that contributes an `OpenFastTrace` action group with a `Trace Project` action under the global `Tools` menu. This component is responsible for exposing the entry only in an opened project context and for handing the action invocation to trace-configuration resolution and trace execution. +The plugin provides a trace-action component that contributes an `OpenFastTrace` action group with a `Trace Project` action under the global `Tools` menu. This component is responsible for exposing the entry only in an opened project context and for handing the action invocation to trace-configuration resolution, trace execution, and the default IntelliJ Test Runner UI presentation. Covers: - `scn~show-trace-project-action-in-tools-menu~1` - `scn~disable-trace-project-action-without-open-project~1` - `scn~run-trace-project-in-background~1` +- `scn~show-trace-project-in-test-runner-ui-by-default~1` - `scn~reject-trace-project-without-valid-project-path~1` Needs: impl, itest @@ -252,7 +254,7 @@ Covers: - `scn~run-trace-project-in-background~1` - `scn~trace-selected-project-resources~1` - `scn~reject-trace-project-without-valid-project-path~1` -- `scn~show-successful-trace-output-in-ide-output-window~1` +- `scn~show-successful-trace-output-in-ide-output-window~2` - `scn~show-resolved-trace-inputs-in-trace-output-window~1` - `scn~show-failing-trace-output-in-ide-output-window~1` @@ -264,7 +266,7 @@ Needs: impl, itest The plugin provides a trace-output presentation component that opens an IDE output sub-window for each trace run, assigns a clear trace-specific content title, renders both successful and failing OpenFastTrace text output through the same IDE-visible flow, and adds declaration hyperlinks for OFT specification item IDs shown in that output when the corresponding items exist in the opened project. Covers: -- `scn~show-successful-trace-output-in-ide-output-window~1` +- `scn~show-successful-trace-output-in-ide-output-window~2` - `scn~show-failing-trace-output-in-ide-output-window~1` - `scn~open-specification-item-from-trace-output-window~1` @@ -348,8 +350,8 @@ scale 2 Tags: | "mvp " . | (comma-separated, empty = all) . | . - Result view: | (X) Plain text output - . | () IntelliJ Test Runner UI + Result view: | () Plain text output + . | (X) IntelliJ Test Runner UI . | . } diff --git a/doc/design/runtime_view.md b/doc/design/runtime_view.md index 028c610..0c46913 100644 --- a/doc/design/runtime_view.md +++ b/doc/design/runtime_view.md @@ -388,6 +388,18 @@ Covers: Needs: impl, itest +### Show Trace Project in Test Runner UI by Default +`dsn~show-trace-project-in-test-runner-ui-by-default~1` + +**Given** the global `Trace Project` action receives a valid OpenFastTrace trace result +**When** the trace-action flow presents that result +**Then** it creates an IntelliJ Test Runner UI console, maps the structured OpenFastTrace trace to SM test runner nodes, and shows that console as the default run content for the action. + +Covers: +- `scn~show-trace-project-in-test-runner-ui-by-default~1` + +Needs: impl, itest + ### Configure Trace Scope in Project Settings `dsn~configure-trace-scope-in-project-settings~1` @@ -495,15 +507,27 @@ Covers: Needs: impl, itest -### Plain Text as Default Run Configuration Result View -`dsn~plain-text-as-default-run-configuration-result-view~1` +### Test Runner as Default Run Configuration Result View +`dsn~test-runner-as-default-run-configuration-result-view~1` **Given** an OpenFastTrace run configuration has no persisted result-view selection **When** the run-profile state prepares the trace result presentation -**Then** it selects the existing plain text trace-output presentation. +**Then** it selects the trace test-runner presentation. + +Covers: +- `scn~test-runner-as-default-run-configuration-result-view~1` + +Needs: impl, itest + +### Select Plain Text Trace Result View +`dsn~select-plain-text-trace-result-view~1` + +**Given** an OpenFastTrace run configuration stores the plain text result-view selection +**When** the run-profile state prepares the trace result presentation +**Then** it selects the existing plain text trace-output presentation for that run configuration. Covers: -- `scn~plain-text-as-default-run-configuration-result-view~1` +- `scn~select-plain-text-trace-result-view~1` Needs: impl, itest @@ -760,14 +784,14 @@ Covers: Needs: impl, itest ### Show Successful Trace Output in IDE Output Window -`dsn~show-successful-trace-output-in-ide-output-window~1` +`dsn~show-successful-trace-output-in-ide-output-window~2` -**Given** a background OpenFastTrace trace run completes successfully for the opened IntelliJ project +**Given** a background OpenFastTrace trace run uses the plain text result view and completes successfully for the opened IntelliJ project **When** the trace-execution service temporarily switches the thread context class loader to the plugin class loader for OFT importer and reporter discovery, restores the previous context loader afterward, and hands the captured text report to trace-output presentation **Then** the plugin opens or updates an IDE output sub-window for that trace run and shows the OpenFastTrace text output under a clear trace-specific content title. Covers: -- `scn~show-successful-trace-output-in-ide-output-window~1` +- `scn~show-successful-trace-output-in-ide-output-window~2` Needs: impl, itest diff --git a/doc/design/solution_strategy.md b/doc/design/solution_strategy.md index 4679078..8ef903a 100644 --- a/doc/design/solution_strategy.md +++ b/doc/design/solution_strategy.md @@ -73,7 +73,7 @@ Failure presentation is likewise kept narrow. The plugin may emphasize the short ## Structured Test Runner Trace Results -The test-runner trace-result increment keeps the existing plain text trace output as the default result view and adds structured presentation as a per-run-configuration choice. +The test-runner trace-result increment makes structured presentation the default result view for both the global `Trace Project` action and OpenFastTrace run configurations. The existing plain text trace output remains available as a per-run-configuration choice. The plugin uses IntelliJ's SM test runner infrastructure for this structured presentation. Source files become test suites labeled with project-local paths when they are below the opened project directory, specification items become tests below their source-file suite, and incoming or outgoing trace links become sub-tests below connected specification items. This gives users the native test runner tree, progress, status, and navigation behavior without introducing a parallel result-tree widget. diff --git a/doc/system_requirements.md b/doc/system_requirements.md index 4193cac..f0edfcc 100644 --- a/doc/system_requirements.md +++ b/doc/system_requirements.md @@ -110,9 +110,9 @@ The plugin suggests existing OpenFastTrace specification item IDs while users fi Needs: req ### Run OFT Trace -`feat~run-oft-trace~1` +`feat~run-oft-trace~2` -The plugin lets users run an OpenFastTrace trace for the currently opened IntelliJ project and inspect the textual result inside the IDE using a global action for the default project trace. +The plugin lets users run an OpenFastTrace trace for the currently opened IntelliJ project and inspect the result inside the IDE using the IntelliJ Test Runner UI as the default view for the global project trace. Needs: req @@ -340,7 +340,7 @@ The following requirements refine the Run OFT Trace feature into user-visible ca The plugin adds an `OpenFastTrace` group with a `Trace Project` action to the global `Tools` menu. Users can find the trace entry in the established IDE location for project-level tooling actions. Covers: -- `feat~run-oft-trace~1` +- `feat~run-oft-trace~2` Needs: scn @@ -354,7 +354,7 @@ Rationale: Users cannot invoke the trace action when the IDE has no project context to trace. Covers: -- `feat~run-oft-trace~1` +- `feat~run-oft-trace~2` Needs: scn @@ -364,7 +364,7 @@ Needs: scn The plugin traces the currently opened IntelliJ project by using the opened project directory as the default OpenFastTrace input root. Users can run a whole-project trace without manually selecting files and directories first. Covers: -- `feat~run-oft-trace~1` +- `feat~run-oft-trace~2` Needs: scn @@ -374,7 +374,7 @@ Needs: scn The plugin integrates OpenFastTrace trace-scope settings into IntelliJ project configuration. Users can configure whether the `Trace Project` action traces the whole opened project or only selected resources and can edit the selected-resource paths directly in the IDE settings workflow. Covers: -- `feat~run-oft-trace~1` +- `feat~run-oft-trace~2` Needs: scn @@ -384,7 +384,7 @@ Needs: scn The plugin can trace selected project resources instead of the whole opened project directory. Users can restrict the OpenFastTrace scan to the resources they intend to include in the trace. Covers: -- `feat~run-oft-trace~1` +- `feat~run-oft-trace~2` Needs: scn @@ -394,7 +394,7 @@ Needs: scn When selected-resource tracing is active, the plugin can include source directories known to IntelliJ in the effective OpenFastTrace input set. Users do not need to discover and configure ordinary source roots by hand. Covers: -- `feat~run-oft-trace~1` +- `feat~run-oft-trace~2` Needs: scn @@ -404,7 +404,7 @@ Needs: scn When selected-resource tracing is active, the plugin can include test directories known to IntelliJ in the effective OpenFastTrace input set. Users do not need to discover and configure ordinary test roots by hand. Covers: -- `feat~run-oft-trace~1` +- `feat~run-oft-trace~2` Needs: scn @@ -414,7 +414,7 @@ Needs: scn When selected-resource tracing is active, the plugin lets users add additional trace inputs through a multi-line text field in the project settings. Each non-empty line specifies one file or directory path relative to the opened project directory that OpenFastTrace should scan. If the user has not changed that setting, the field contains exactly one default entry: `doc/`. Covers: -- `feat~run-oft-trace~1` +- `feat~run-oft-trace~2` Needs: scn @@ -424,17 +424,29 @@ Needs: scn The plugin runs the OpenFastTrace project trace in a background task with IDE progress reporting. Users can start a trace without blocking the editor UI while the trace is running. Covers: -- `feat~run-oft-trace~1` +- `feat~run-oft-trace~2` + +Needs: scn + +### Show Trace Project in Test Runner UI by Default +`req~show-trace-project-in-test-runner-ui-by-default~1` + +When a user starts the global `Trace Project` action from the `Tools` menu, the plugin shows the default trace result in IntelliJ's Test Runner UI. Users can inspect the default project trace as structured source-file suites, specification-item tests, and trace-link sub-tests without creating a run configuration first. + +Covers: +- `feat~run-oft-trace~2` +- `feat~oft-test-runner-trace-results~1` Needs: scn ### Show Trace Output in IDE Output Window -`req~show-trace-output-in-ide-output-window~1` +`req~show-trace-output-in-ide-output-window~2` -The plugin shows the OpenFastTrace text trace output in an IDE output sub-window and keeps that output available after the trace finishes. Users can inspect the plain text result inside the IDE without looking at log files or an external terminal. +The plugin can show the OpenFastTrace text trace output in an IDE output sub-window and keep that output available after the trace finishes. Users who select the plain text result view can inspect the plain text result inside the IDE without looking at log files or an external terminal. Covers: -- `feat~run-oft-trace~1` +- `feat~run-oft-trace~2` +- `feat~oft-run-configurations~1` Needs: scn @@ -444,7 +456,7 @@ Needs: scn The plugin makes OpenFastTrace specification item IDs in the trace output window navigable to their declarations in the opened project. Users can jump from a reported item in the trace output directly to the defining specification item without searching manually. Covers: -- `feat~run-oft-trace~1` +- `feat~run-oft-trace~2` Needs: scn @@ -479,13 +491,13 @@ Covers: Needs: scn ### Select Trace Result View in Run Configuration -`req~select-trace-result-view-in-run-configuration~1` +`req~select-trace-result-view-in-run-configuration~2` The OpenFastTrace run configuration includes a result-view option for choosing plain text output or the IntelliJ Test Runner UI. Rationale: -Users who prefer the existing text report can keep using it, while users who need structured trace inspection can opt into the native test runner presentation per run configuration. +Users get structured trace inspection by default, while users who prefer the existing text report can opt into plain text output per run configuration. Covers: - `feat~oft-run-configurations~1` @@ -696,20 +708,20 @@ Needs: scn ### Show Scanned Base Directory in Trace Output Window `req~show-scanned-base-directory-in-trace-output-window~1` -When a project trace starts, the plugin writes the resolved project base directory that OpenFastTrace scans into the IDE output sub-window. Users can confirm the actual trace input root directly from the trace output. +When a plain text project trace starts, the plugin writes the resolved project base directory that OpenFastTrace scans into the IDE output sub-window. Users can confirm the actual trace input root directly from the trace output. Covers: -- `feat~run-oft-trace~1` +- `feat~run-oft-trace~2` Needs: scn ### Show Resolved Trace Inputs in Trace Output Window `req~show-resolved-trace-inputs-in-trace-output-window~1` -When a project trace starts with selected-resource tracing, the plugin writes the resolved files and directories that it passes to OpenFastTrace into the IDE output sub-window. Users can confirm the actual configured trace scope directly from the trace output. +When a plain text project trace starts with selected-resource tracing, the plugin writes the resolved files and directories that it passes to OpenFastTrace into the IDE output sub-window. Users can confirm the actual configured trace scope directly from the trace output. Covers: -- `feat~run-oft-trace~1` +- `feat~run-oft-trace~2` Needs: scn @@ -719,17 +731,17 @@ Needs: scn The plugin reports when the opened project does not resolve to a valid local path before starting the trace. Users can see why the trace cannot start without inspecting plugin logs. Covers: -- `feat~run-oft-trace~1` +- `feat~run-oft-trace~2` Needs: scn ### Show Failing Trace Result in IDE Output Window `req~show-failing-trace-result-in-ide-output-window~1` -The plugin shows failing OpenFastTrace executions through the same IDE output flow as successful traces, including the text output and the failing result. Users can inspect trace failures without switching to plugin logs or an external terminal. +When plain text output is selected, the plugin shows failing OpenFastTrace executions through the same IDE output flow as successful traces, including the text output and the failing result. Users can inspect trace failures without switching to plugin logs or an external terminal. Covers: -- `feat~run-oft-trace~1` +- `feat~run-oft-trace~2` Needs: scn @@ -739,7 +751,7 @@ Needs: scn The plugin preserves the OpenFastTrace plain-text defect summary for an unclean trace chain in the IDE output window, including the reported total-item count and defect count. Users can verify how many trace issues OpenFastTrace found directly from the text result. Covers: -- `feat~run-oft-trace~1` +- `feat~run-oft-trace~2` Needs: scn @@ -1143,6 +1155,18 @@ Covers: Needs: dsn +### Show Trace Project in Test Runner UI by Default +`scn~show-trace-project-in-test-runner-ui-by-default~1` + +**Given** an IntelliJ project is open, its configured trace inputs are valid, and the OFT trace completes +**When** a user invokes `Tools | OpenFastTrace | Trace Project` +**Then** the IDE shows the trace result in the IntelliJ Test Runner UI + +Covers: +- `req~show-trace-project-in-test-runner-ui-by-default~1` + +Needs: dsn + ### Configure Trace Scope in Project Settings `scn~configure-trace-scope-in-project-settings~1` @@ -1229,14 +1253,14 @@ Covers: Needs: dsn ### Show Successful Trace Output in IDE Output Window -`scn~show-successful-trace-output-in-ide-output-window~1` +`scn~show-successful-trace-output-in-ide-output-window~2` -**Given** an IntelliJ project is open, its project directory is a valid OFT trace input, and the OFT trace completes successfully -**When** a user invokes `Tools | OpenFastTrace | Trace Project` +**Given** an IntelliJ project is open, its configured trace inputs are valid, an `OpenFastTrace` run configuration uses the plain text result view, and the OFT trace completes successfully +**When** a user runs that configuration **Then** the IDE shows the resulting OpenFastTrace text report in an output sub-window that remains available after the trace completes Covers: -- `req~show-trace-output-in-ide-output-window~1` +- `req~show-trace-output-in-ide-output-window~2` Needs: dsn @@ -1256,7 +1280,7 @@ Needs: dsn `scn~show-scanned-base-directory-in-trace-output-window~1` **Given** an IntelliJ project is open and its project directory resolves to a valid local OFT trace input -**When** a user invokes `Tools | OpenFastTrace | Trace Project` +**When** a user runs a trace using the plain text result view **Then** the IDE output for that trace starts with the resolved base directory that the plugin passes to OpenFastTrace Covers: @@ -1268,7 +1292,7 @@ Needs: dsn `scn~show-resolved-trace-inputs-in-trace-output-window~1` **Given** an IntelliJ project is open, selected-resource tracing is active, and the plugin resolved the configured trace files and directories successfully -**When** a user invokes `Tools | OpenFastTrace | Trace Project` +**When** a user runs a trace using the plain text result view **Then** the IDE output for that trace lists the resolved files and directories that the plugin passes to OpenFastTrace Covers: @@ -1279,8 +1303,8 @@ Needs: dsn ### Show Failing Trace Output in IDE Output Window `scn~show-failing-trace-output-in-ide-output-window~1` -**Given** an IntelliJ project is open, its project directory is a valid OFT trace input, and the OFT trace reports a failure -**When** a user invokes `Tools | OpenFastTrace | Trace Project` +**Given** an IntelliJ project is open, its configured trace inputs are valid, a trace uses the plain text result view, and the OFT trace reports a failure +**When** the trace completes **Then** the IDE shows the OpenFastTrace text output together with the failing result through the same trace output flow Covers: @@ -1291,8 +1315,8 @@ Needs: dsn ### Show Defect Count for Unclean Trace Chain in IDE Output Window `scn~show-defect-count-for-unclean-trace-chain-in-output-window~1` -**Given** an IntelliJ project is open, its project directory is a valid OFT trace input, and the OFT trace finds an unclean feature-to-requirement-to-design chain with missing implementation coverage -**When** a user invokes `Tools | OpenFastTrace | Trace Project` +**Given** an IntelliJ project is open, its configured trace inputs are valid, a trace uses the plain text result view, and the OFT trace finds an unclean feature-to-requirement-to-design chain with missing implementation coverage +**When** the trace completes **Then** the IDE output contains the OpenFastTrace plain-text summary line with the reported total-item count and defect count for that unclean chain Covers: @@ -1336,16 +1360,28 @@ Covers: Needs: dsn -### Plain Text as Default Run Configuration Result View -`scn~plain-text-as-default-run-configuration-result-view~1` +### Test Runner as Default Run Configuration Result View +`scn~test-runner-as-default-run-configuration-result-view~1` **Given** an IntelliJ project is open and an `OpenFastTrace` run configuration has no explicit result-view selection **When** a user runs that configuration -**Then** the plugin shows the trace result in the existing plain text output view +**Then** the plugin shows the trace result in the IntelliJ Test Runner UI + +Covers: +- `req~select-trace-result-view-in-run-configuration~2` + +Needs: dsn + +### Select Plain Text Trace Result View +`scn~select-plain-text-trace-result-view~1` + +**Given** an IntelliJ project is open and a user edits an `OpenFastTrace` run configuration +**When** the user selects plain text output as the result view and runs the configuration +**Then** the plugin saves that selection and shows the trace result in the existing plain text output view Covers: -- `req~select-trace-result-view-in-run-configuration~1` -- `req~show-trace-output-in-ide-output-window~1` +- `req~select-trace-result-view-in-run-configuration~2` +- `req~show-trace-output-in-ide-output-window~2` Needs: dsn @@ -1357,7 +1393,7 @@ Needs: dsn **Then** the plugin saves that selection and shows the trace result in the IntelliJ Test Runner UI Covers: -- `req~select-trace-result-view-in-run-configuration~1` +- `req~select-trace-result-view-in-run-configuration~2` Needs: dsn diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceProjectAction.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceProjectAction.java index 31a8cb9..e74f906 100644 --- a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceProjectAction.java +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceProjectAction.java @@ -7,9 +7,10 @@ import java.util.function.Function; -// [impl->dsn~trace-action-integration~1] +// [impl->dsn~trace-action-integration~2] // [impl->dsn~show-trace-project-action-in-tools-menu~1] // [impl->dsn~disable-trace-project-action-without-open-project~1] +// [impl->dsn~show-trace-project-in-test-runner-ui-by-default~1] public final class OftTraceProjectAction extends DumbAwareAction { public static final String ACTION_ID = "Oft.TraceProject.ToolsMenu"; public static final String TOOLS_GROUP_ID = "Oft.ToolsMenu"; @@ -19,14 +20,14 @@ public final class OftTraceProjectAction extends DumbAwareAction { private final OftTraceOutputPresenter outputPresenter; public OftTraceProjectAction() { + this(defaultDependencies()); + } + + private OftTraceProjectAction(final DefaultDependencies dependencies) { this( OftTraceInputResolver::resolve, - new OftTraceBackgroundRunner( - new OftTraceService(), - new OftTraceRunContentOutputPresenter(), - new NopProcessHandler() - ), - new OftTraceRunContentOutputPresenter() + dependencies.traceRunner(), + dependencies.outputPresenter() ); } @@ -64,4 +65,23 @@ public void update(final AnActionEvent event) { static String createContentTitle(final Project project) { return "OpenFastTrace Trace: " + project.getName(); } + + static OftTraceOutputPresenter createDefaultOutputPresenter() { + return new OftTraceTestRunnerRunContentOutputPresenter(); + } + + private static DefaultDependencies defaultDependencies() { + final OftTraceOutputPresenter presenter = createDefaultOutputPresenter(); + return new DefaultDependencies( + new OftTraceBackgroundRunner( + new OftTraceService(), + presenter, + new NopProcessHandler() + ), + presenter + ); + } + + private record DefaultDependencies(OftTraceRunner traceRunner, OftTraceOutputPresenter outputPresenter) { + } } diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceService.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceService.java index ae79e92..c8376e7 100644 --- a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceService.java +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceService.java @@ -39,7 +39,7 @@ public OftTraceService() { this.reportRenderer = reportRenderer; } - // [impl->dsn~show-successful-trace-output-in-ide-output-window~1] + // [impl->dsn~show-successful-trace-output-in-ide-output-window~2] // [impl->dsn~show-scanned-base-directory-in-trace-output-window~1] // [impl->dsn~show-failing-trace-output-in-ide-output-window~1] // [impl->dsn~preserve-defect-count-for-unclean-trace-chain-in-output-window~1] diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceSettingsComponent.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceSettingsComponent.java index 6fb88b2..d8c0074 100644 --- a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceSettingsComponent.java +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceSettingsComponent.java @@ -171,7 +171,10 @@ private void updateSelectedResourcesEnabledState() { } private OftTraceResultView selectedResultView() { - if (!showResultViewSelection || plainTextResultViewRadioButton.isSelected()) { + if (!showResultViewSelection) { + return OftTraceSettingsSnapshot.DEFAULT.resultView(); + } + if (plainTextResultViewRadioButton.isSelected()) { return OftTraceResultView.PLAIN_TEXT; } return OftTraceResultView.TEST_RUNNER; diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceSettingsSnapshot.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceSettingsSnapshot.java index feef386..dcac0ec 100644 --- a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceSettingsSnapshot.java +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceSettingsSnapshot.java @@ -19,7 +19,7 @@ public record OftTraceSettingsSnapshot( "doc/", "", "", - OftTraceResultView.PLAIN_TEXT + OftTraceResultView.TEST_RUNNER ); private static final Pattern LINE_SEPARATOR = Pattern.compile("\\R"); @@ -40,7 +40,7 @@ public OftTraceSettingsSnapshot( additionalPathsText, artifactTypesText, tagsText, - OftTraceResultView.PLAIN_TEXT + DEFAULT.resultView() ); } diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunConfiguration.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunConfiguration.java index bb3f13c..e61b57f 100644 --- a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunConfiguration.java +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunConfiguration.java @@ -85,6 +85,7 @@ private static OftTraceScopeMode parseScopeMode(final String value) { } } + // [impl->dsn~test-runner-as-default-run-configuration-result-view~1] private static OftTraceResultView parseResultView(final String value) { if (value == null || value.isBlank()) { return OftTraceSettingsSnapshot.DEFAULT.resultView(); diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunProfileState.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunProfileState.java index d278914..683f916 100644 --- a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunProfileState.java +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunProfileState.java @@ -70,7 +70,7 @@ private ExecutionPresentation createExecutionPresentation(final Project project, return createPlainTextExecutionPresentation(project); } - // [impl->dsn~plain-text-as-default-run-configuration-result-view~1] + // [impl->dsn~select-plain-text-trace-result-view~1] private static ExecutionPresentation createPlainTextExecutionPresentation(final Project project) { final ConsoleView console = OftTraceRunContentOutputPresenter.createTraceConsole(project); final OftTraceOutputPresenter outputPresenter = new OftTraceRunContentOutputPresenter( @@ -82,6 +82,7 @@ private static ExecutionPresentation createPlainTextExecutionPresentation(final return new ExecutionPresentation(console, outputPresenter); } + // [impl->dsn~test-runner-as-default-run-configuration-result-view~1] // [impl->dsn~select-test-runner-trace-result-view~1] private ExecutionPresentation createTestRunnerExecutionPresentation(final Project project, final Executor executor) { final SMTRunnerConsoleProperties properties = new SMTRunnerConsoleProperties( diff --git a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceProjectActionTest.java b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceProjectActionTest.java index 8d28377..718fd8b 100644 --- a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceProjectActionTest.java +++ b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceProjectActionTest.java @@ -33,7 +33,7 @@ import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.sameInstance; -// [itest->dsn~trace-action-integration~1] +// [itest->dsn~trace-action-integration~2] public class OftTraceProjectActionTest extends AbstractOftPlatformTestCase { // [itest->dsn~show-trace-project-action-in-tools-menu~1] public void testGivenPluginIsLoadedWhenToolsMenuActionIsQueriedThenTheTraceActionAndGroupAreRegistered() { @@ -76,6 +76,14 @@ public void testGivenProjectWhenActionUpdatesThenPresentationIsEnabled() { assertThat(presentation.isEnabled(), is(true)); } + // [itest->dsn~show-trace-project-in-test-runner-ui-by-default~1] + public void testGivenDefaultActionWhenCreatingDefaultPresenterThenItUsesTestRunnerRunContentPresenter() { + assertThat( + OftTraceProjectAction.createDefaultOutputPresenter(), + instanceOf(OftTraceTestRunnerRunContentOutputPresenter.class) + ); + } + // [itest->dsn~disable-trace-project-action-without-open-project~1] public void testGivenNoProjectWhenActionUpdatesThenPresentationIsDisabled() { final OftTraceProjectAction action = new OftTraceProjectAction(); diff --git a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceProjectSettingsTest.java b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceProjectSettingsTest.java index 2ddb23a..903378d 100644 --- a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceProjectSettingsTest.java +++ b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceProjectSettingsTest.java @@ -19,6 +19,7 @@ public void testGivenFreshProjectSettingsWhenReadingThenItReturnsTheDocumentedDe assertThat(snapshot.includeSourceRoots(), is(OftTraceSettingsSnapshot.DEFAULT.includeSourceRoots())); assertThat(snapshot.includeTestRoots(), is(OftTraceSettingsSnapshot.DEFAULT.includeTestRoots())); assertThat(snapshot.additionalPathsText(), is(OftTraceSettingsSnapshot.DEFAULT.additionalPathsText())); + assertThat(snapshot.resultView(), is(OftTraceSettingsSnapshot.DEFAULT.resultView())); } public void testGivenStoredStateWhenLoadingThenItNormalizesTheSnapshot() { diff --git a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceServiceTest.java b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceServiceTest.java index d5b69f3..9f2696e 100644 --- a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceServiceTest.java +++ b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceServiceTest.java @@ -22,7 +22,7 @@ class OftTraceServiceTest { private static final Pattern ANSI_ESCAPE_SEQUENCE = Pattern.compile("\u001B\\[[;\\d]*m"); - // [itest->dsn~show-successful-trace-output-in-ide-output-window~1] + // [itest->dsn~show-successful-trace-output-in-ide-output-window~2] @Test void testGivenCleanTraceInputWhenTracingThenItReturnsSuccessfulPlainTextOutput(@TempDir final Path temporaryDirectory) throws IOException { diff --git a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunConfigurationTest.java b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunConfigurationTest.java index 980a187..0fd9fc2 100644 --- a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunConfigurationTest.java +++ b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunConfigurationTest.java @@ -14,6 +14,14 @@ // [itest->dsn~openfasttrace-run-configuration~1] public class OftRunConfigurationTest extends AbstractOftPlatformTestCase { + // [itest->dsn~test-runner-as-default-run-configuration-result-view~1] + // [itest->dsn~trace-configuration-integration~1] + public void testGivenNewRunConfigurationWhenReadingSnapshotThenItDefaultsToTestRunner() { + final OftRunConfiguration configuration = createConfiguration("Test"); + + assertThat(configuration.snapshot().resultView(), is(OftTraceResultView.TEST_RUNNER)); + } + // [itest->dsn~select-test-runner-trace-result-view~1] // [itest->dsn~trace-configuration-integration~1] public void testGivenRunConfigurationWhenUpdatingFromSnapshotThenItStoresTheSettings() { @@ -76,20 +84,20 @@ public void testGivenRunConfigurationWithSettingsWhenWritingAndReadingExternalTh ); } - // [itest->dsn~plain-text-as-default-run-configuration-result-view~1] + // [itest->dsn~test-runner-as-default-run-configuration-result-view~1] // [itest->dsn~trace-configuration-integration~1] - public void testGivenRunConfigurationWithNoStoredResultViewWhenReadingExternalThenItDefaultsToPlainText() + public void testGivenRunConfigurationWithNoStoredResultViewWhenReadingExternalThenItDefaultsToTestRunner() throws InvalidDataException { final OftRunConfiguration configuration = createConfiguration("Test"); configuration.readExternal(new Element("configuration")); - assertThat(configuration.snapshot().resultView(), is(OftTraceResultView.PLAIN_TEXT)); + assertThat(configuration.snapshot().resultView(), is(OftTraceResultView.TEST_RUNNER)); } - // [itest->dsn~plain-text-as-default-run-configuration-result-view~1] + // [itest->dsn~test-runner-as-default-run-configuration-result-view~1] // [itest->dsn~trace-configuration-integration~1] - public void testGivenRunConfigurationWithInvalidStoredResultViewWhenReadingExternalThenItDefaultsToPlainText() + public void testGivenRunConfigurationWithInvalidStoredResultViewWhenReadingExternalThenItDefaultsToTestRunner() throws InvalidDataException { final Element element = new Element("configuration"); element.addContent(new Element("option") @@ -99,7 +107,7 @@ public void testGivenRunConfigurationWithInvalidStoredResultViewWhenReadingExter configuration.readExternal(element); - assertThat(configuration.snapshot().resultView(), is(OftTraceResultView.PLAIN_TEXT)); + assertThat(configuration.snapshot().resultView(), is(OftTraceResultView.TEST_RUNNER)); } private OftRunConfiguration createConfiguration(final String name) { diff --git a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunProfileStateTest.java b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunProfileStateTest.java index e863aca..0e70af3 100644 --- a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunProfileStateTest.java +++ b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunProfileStateTest.java @@ -32,7 +32,7 @@ public void testExecuteReturnsNotNull() throws Exception { } } - // [itest->dsn~plain-text-as-default-run-configuration-result-view~1] + // [itest->dsn~select-plain-text-trace-result-view~1] public void testGivenPlainTextResultViewWhenExecutingThenItUsesPlainTextConsole() throws Exception { final ExecutionEnvironment environment = createEnvironment(); final OftRunProfileState state = new OftRunProfileState( @@ -46,6 +46,20 @@ public void testGivenPlainTextResultViewWhenExecutingThenItUsesPlainTextConsole( assertThat(result.getExecutionConsole(), is(not(instanceOf(SMTRunnerConsoleView.class)))); } + // [itest->dsn~test-runner-as-default-run-configuration-result-view~1] + public void testGivenDefaultResultViewWhenExecutingThenItUsesTestRunnerConsole() throws Exception { + final ExecutionEnvironment environment = createEnvironment(); + final OftRunProfileState state = new OftRunProfileState( + environment, + invalidInputsSettings(OftTraceSettingsSnapshot.DEFAULT.resultView()) + ); + + final ExecutionResult result = state.execute(environment.getExecutor(), environment.getRunner()); + + Disposer.register(getTestRootDisposable(), result.getExecutionConsole()); + assertThat(result.getExecutionConsole(), is(instanceOf(SMTRunnerConsoleView.class))); + } + // [itest->dsn~select-test-runner-trace-result-view~1] public void testGivenTestRunnerResultViewWhenExecutingThenItUsesTestRunnerConsole() throws Exception { final ExecutionEnvironment environment = createEnvironment(); From 5a0b632611894395095e8e5e2ed9b99d09046385 Mon Sep 17 00:00:00 2001 From: redcatbear Date: Tue, 9 Jun 2026 15:03:55 +0200 Subject: [PATCH 08/10] #40: Added missing files. --- .../default-test-runner-trace-result-view.md | 76 ++++++++++++++++ ...ceTestRunnerRunContentOutputPresenter.java | 86 +++++++++++++++++++ ...stRunnerRunContentOutputPresenterTest.java | 77 +++++++++++++++++ 3 files changed, 239 insertions(+) create mode 100644 doc/changesets/default-test-runner-trace-result-view.md create mode 100644 src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerRunContentOutputPresenter.java create mode 100644 src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerRunContentOutputPresenterTest.java diff --git a/doc/changesets/default-test-runner-trace-result-view.md b/doc/changesets/default-test-runner-trace-result-view.md new file mode 100644 index 0000000..ca77855 --- /dev/null +++ b/doc/changesets/default-test-runner-trace-result-view.md @@ -0,0 +1,76 @@ +# Default Test Runner Trace Result View + +## Goal + +Make the IntelliJ Test Runner UI the default result view for OpenFastTrace traces started from `Tools | OpenFastTrace | Trace Project` and for OpenFastTrace run configurations that do not store an explicit result-view selection. + +## Scope + +In scope: + +* Change the global `Trace Project` action to present successful, defective, and startup-failure trace results in the IntelliJ Test Runner UI by default. +* Change new and previously unconfigured OpenFastTrace run configurations to default to the IntelliJ Test Runner UI. +* Keep plain text output available when a run configuration explicitly selects it. +* Update traced requirements, runtime design, README, and the current `0.8.0` changelog text. + +Out of scope: + +* Removing the plain text trace-output presenter. +* Changing OpenFastTrace trace execution, input resolution, filters, or trace-result mapping semantics. +* Adding Problems view integration, inspections, or automatic trace-on-save behavior. + +## Design References + +* [System Requirements](../system_requirements.md) +* [Quality Requirements](../design/quality_requirements.md) +* [Solution Strategy](../design/solution_strategy.md) +* [Building Block View](../design/building_block_view.md) +* [Runtime View](../design/runtime_view.md) +* [GH-40 Integrate OFT Trace into Test Runner UI](40-integrate-oft-trace-into-test-runner-ui.md) + +## Strategy + +Reuse the existing test-runner trace mapper and presenter. Add only the run-content wrapper needed for the global Tools-menu action, because run configurations already receive their console from IntelliJ's execution framework while the global action must explicitly show run content. + +## Task List + +- [x] No tracker issue was supplied; keep this descriptive changeset filename instead of creating an issue-numbered file. + +### Requirements And Design + +- [x] Revise the Run OFT Trace feature and output-window requirement so the Test Runner UI is the default and plain text is opt-in. +- [x] Add a global `Trace Project` default-test-runner scenario. +- [x] Replace the plain-text-default run-configuration scenario with a test-runner-default scenario and add explicit plain-text selection coverage. +- [x] Update building-block and runtime design items for the new defaults. + +### Implementation + +- [x] Default missing or invalid run-configuration result-view selections to `TEST_RUNNER`. +- [x] Change the global `Trace Project` action to use a test-runner run-content presenter by default. +- [x] Keep explicit plain text run-configuration execution wired to the existing plain text presenter. + +### Verification + +- [x] Update focused run-configuration, run-profile, and trace-action tests for the new defaults. +- [x] Add focused coverage for the global test-runner run-content presenter. +- [x] Keep the OpenFastTrace trace clean for `feat`, `req`, `scn`, `dsn`, `impl`, `utest`, and `itest` artifacts in scope. +- [x] Run `./gradlew test`. +- [x] Run `./gradlew check`. +- [x] Run `./gradlew verifyPlugin`. + +### Update user documentation + +- [x] Update README.md to describe Test Runner UI as the default trace result view and plain text as a run-configuration option. + +## Version and Changelog Update + +- [x] Keep version `0.8.0` because this refines the unreleased test-runner feature already documented for `0.8.0`. +- [x] Update the `0.8.0` changelog entry to describe the new default behavior. + +`./gradlew test` passes with one skipped `OftTraceServiceTest` parameterized case. + +`./gradlew traceRequirements` passes. + +`./gradlew check` passes. + +`./gradlew verifyPlugin` passes. The verifier still reports the existing two experimental API usages in `OftHighlightingPass`, and both configured IDE checks are compatible. diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerRunContentOutputPresenter.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerRunContentOutputPresenter.java new file mode 100644 index 0000000..13b4102 --- /dev/null +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerRunContentOutputPresenter.java @@ -0,0 +1,86 @@ +package org.itsallcode.openfasttrace.intellijplugin.trace; + +import com.intellij.execution.executors.DefaultRunExecutor; +import com.intellij.execution.testframework.sm.runner.SMTRunnerConsoleProperties; +import com.intellij.execution.testframework.sm.runner.ui.SMTRunnerConsoleView; +import com.intellij.execution.ui.RunContentDescriptor; +import com.intellij.execution.ui.RunContentManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Disposer; +import org.itsallcode.openfasttrace.intellijplugin.trace.runconfig.OftRunConfiguration; +import org.itsallcode.openfasttrace.intellijplugin.trace.runconfig.OftRunConfigurationFactory; +import org.itsallcode.openfasttrace.intellijplugin.trace.runconfig.OftRunConfigurationType; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; +import java.util.function.Function; + +public final class OftTraceTestRunnerRunContentOutputPresenter implements OftTraceOutputPresenter { + private static final AtomicInteger NEXT_DESCRIPTOR_ID = new AtomicInteger(1); + + private final Function consoleFactory; + private final BiConsumer runContentShower; + + public OftTraceTestRunnerRunContentOutputPresenter() { + this( + OftTraceTestRunnerRunContentOutputPresenter::createTraceConsole, + (project, descriptor) -> RunContentManager.getInstance(project) + .showRunContent(DefaultRunExecutor.getRunExecutorInstance(), descriptor) + ); + } + + OftTraceTestRunnerRunContentOutputPresenter( + final Function consoleFactory, + final BiConsumer runContentShower + ) { + this.consoleFactory = consoleFactory; + this.runContentShower = runContentShower; + } + + // [impl->dsn~show-trace-project-in-test-runner-ui-by-default~1] + @Override + public void show(final Project project, final String contentTitle, final OftTraceResult result) { + final AtomicReference consoleRef = new AtomicReference<>(); + final OftTraceTestRunnerOutputPresenter presenter = + new OftTraceTestRunnerOutputPresenter(p -> createConsole(p, consoleRef)); + + presenter.show(project, contentTitle, result); + + final SMTRunnerConsoleView console = consoleRef.get(); + final RunContentDescriptor descriptor = + new RunContentDescriptor(console, null, console.getComponent(), contentTitle); + descriptor.setExecutionId(NEXT_DESCRIPTOR_ID.getAndIncrement()); + Disposer.register(descriptor, console); + runContentShower.accept(project, descriptor); + } + + private SMTRunnerConsoleView createConsole( + final Project project, + final AtomicReference consoleRef + ) { + final SMTRunnerConsoleView console = consoleFactory.apply(project); + consoleRef.set(console); + return console; + } + + private static SMTRunnerConsoleView createTraceConsole(final Project project) { + final SMTRunnerConsoleProperties properties = new SMTRunnerConsoleProperties( + project, + createRunConfiguration(project), + "OpenFastTrace", + DefaultRunExecutor.getRunExecutorInstance() + ); + final SMTRunnerConsoleView console = new SMTRunnerConsoleView(properties); + console.initUI(); + return console; + } + + private static OftRunConfiguration createRunConfiguration(final Project project) { + return new OftRunConfiguration( + project, + new OftRunConfigurationFactory(new OftRunConfigurationType()), + "Trace Project" + ); + } +} diff --git a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerRunContentOutputPresenterTest.java b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerRunContentOutputPresenterTest.java new file mode 100644 index 0000000..42ad9f6 --- /dev/null +++ b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerRunContentOutputPresenterTest.java @@ -0,0 +1,77 @@ +package org.itsallcode.openfasttrace.intellijplugin.trace; + +import com.intellij.execution.executors.DefaultRunExecutor; +import com.intellij.execution.testframework.sm.runner.ui.SMTRunnerConsoleView; +import com.intellij.execution.testframework.sm.runner.SMTRunnerConsoleProperties; +import com.intellij.execution.ui.RunContentDescriptor; +import com.intellij.openapi.util.Disposer; +import com.intellij.testFramework.EdtTestUtil; +import org.hamcrest.Matchers; +import org.itsallcode.openfasttrace.intellijplugin.AbstractOftPlatformTestCase; +import org.itsallcode.openfasttrace.intellijplugin.trace.runconfig.OftRunConfiguration; +import org.itsallcode.openfasttrace.intellijplugin.trace.runconfig.OftRunConfigurationFactory; +import org.itsallcode.openfasttrace.intellijplugin.trace.runconfig.OftRunConfigurationType; + +import java.util.concurrent.atomic.AtomicReference; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.notNullValue; + +public class OftTraceTestRunnerRunContentOutputPresenterTest extends AbstractOftPlatformTestCase { + // [itest->dsn~show-trace-project-in-test-runner-ui-by-default~1] + public void testGivenTraceResultWhenPresentedThenItShowsTestRunnerRunContent() { + final AtomicReference consoleRef = new AtomicReference<>(); + final AtomicReference descriptorRef = new AtomicReference<>(); + final OftTraceTestRunnerRunContentOutputPresenter presenter = + new OftTraceTestRunnerRunContentOutputPresenter( + project -> { + final SMTRunnerConsoleView console = createConsole(); + consoleRef.set(console); + return console; + }, + (project, descriptor) -> descriptorRef.set(descriptor) + ); + + try { + EdtTestUtil.runInEdtAndWait(() -> presenter.show( + getProject(), + "OpenFastTrace Trace: default", + OftTraceResult.invalidInput("invalid configuration") + )); + + assertThat(consoleRef.get(), notNullValue()); + assertThat(descriptorRef.get(), notNullValue()); + assertThat(descriptorRef.get().getExecutionId(), Matchers.greaterThan(0L)); + assertThat( + consoleRef.get().getResultsViewer().getTestsRootNode().getChildren().getFirst().getName(), + containsString("could not start") + ); + } finally { + if (descriptorRef.get() != null) { + Disposer.dispose(descriptorRef.get()); + } else if (consoleRef.get() != null) { + Disposer.dispose(consoleRef.get()); + } + } + } + + private SMTRunnerConsoleView createConsole() { + final SMTRunnerConsoleProperties properties = new SMTRunnerConsoleProperties( + createConfiguration(), + "OpenFastTrace", + DefaultRunExecutor.getRunExecutorInstance() + ); + final SMTRunnerConsoleView console = new SMTRunnerConsoleView(properties); + console.initUI(); + return console; + } + + private OftRunConfiguration createConfiguration() { + return new OftRunConfiguration( + getProject(), + new OftRunConfigurationFactory(new OftRunConfigurationType()), + "OpenFastTrace" + ); + } +} From 97a56ab811b9fca96e3f073fff0431b7342e6dc7 Mon Sep 17 00:00:00 2001 From: redcatbear Date: Tue, 9 Jun 2026 15:31:52 +0200 Subject: [PATCH 09/10] #40: Fixed Sonar findings. --- .../OftCoverageTagTraceItemNavigator.java | 455 +++++++++++++++++ .../trace/OftTraceNavigationResolver.java | 468 +----------------- .../trace/OftTraceTestProxy.java | 14 +- .../OftTraceTestRunnerOutputPresenter.java | 24 +- .../trace/OftTraceTestTree.java | 66 +-- .../trace/OftTraceTestTreeMapper.java | 135 +++-- .../trace/runconfig/OftRunProfileState.java | 11 +- .../OftTraceNavigationResolverParserTest.java | 5 +- .../trace/OftTraceNavigationResolverTest.java | 17 +- .../trace/OftTraceTestTreeMapperTest.java | 72 ++- 10 files changed, 702 insertions(+), 565 deletions(-) create mode 100644 src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftCoverageTagTraceItemNavigator.java diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftCoverageTagTraceItemNavigator.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftCoverageTagTraceItemNavigator.java new file mode 100644 index 0000000..0fc0957 --- /dev/null +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftCoverageTagTraceItemNavigator.java @@ -0,0 +1,455 @@ +package org.itsallcode.openfasttrace.intellijplugin.trace; + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.roots.ProjectFileIndex; +import com.intellij.openapi.vfs.VfsUtilCore; +import com.intellij.openapi.vfs.VirtualFile; +import org.itsallcode.openfasttrace.intellijplugin.OftSupportedFiles; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.zip.CRC32; + +final class OftCoverageTagTraceItemNavigator { + private static final Logger LOG = Logger.getInstance(OftCoverageTagTraceItemNavigator.class); + + private final Project project; + + OftCoverageTagTraceItemNavigator(final Project project) { + this.project = project; + } + + Optional resolve(final String specificationId) { + final OftTraceNavigationTarget[] target = new OftTraceNavigationTarget[1]; + ProjectFileIndex.getInstance(project) + .iterateContent(file -> processCoverageTagFile(specificationId, target, file)); + return Optional.ofNullable(target[0]); + } + + private static boolean processCoverageTagFile( + final String specificationId, + final OftTraceNavigationTarget[] target, + final VirtualFile file + ) { + if (!OftSupportedFiles.isCoverageTagFile(file)) { + return true; + } + final OftTraceNavigationTarget match = findCoverageTagTargetInFile(file, specificationId); + if (match != null) { + target[0] = match; + return false; + } + return true; + } + + private static @Nullable OftTraceNavigationTarget findCoverageTagTargetInFile( + final VirtualFile file, + final String specificationId + ) { + final String text; + try { + text = currentFileText(file); + } catch (final IllegalStateException exception) { + LOG.debug("Failed to read OFT trace navigation source file: " + file.getPath(), exception); + return null; + } + int lineNumber = 1; + int lineStartOffset = 0; + int cursor = 0; + while (cursor < text.length()) { + final int lineEnd = findLineEnd(text, cursor); + final String line = text.substring(cursor, lineEnd); + final OftTraceNavigationTarget target = findCoverageTagTargetInLine( + file, + line, + lineNumber, + lineStartOffset, + specificationId + ); + if (target != null) { + return target; + } + final int nextCursor = skipLineSeparator(text, lineEnd); + lineStartOffset = nextCursor; + cursor = nextCursor; + lineNumber++; + } + return null; + } + + private static String currentFileText(final VirtualFile file) { + final Document document = FileDocumentManager.getInstance().getCachedDocument(file); + if (document != null) { + return document.getText(); + } + try { + return VfsUtilCore.loadText(file); + } catch (final IOException exception) { + throw new IllegalStateException( + "Failed to load OFT trace navigation source file " + file.getPath(), + exception + ); + } + } + + private static @Nullable OftTraceNavigationTarget findCoverageTagTargetInLine( + final VirtualFile file, + final String line, + final int lineNumber, + final int lineStartOffset, + final String specificationId + ) { + int searchStart = 0; + int lineMatchCount = 0; + while (searchStart < line.length()) { + final int openingBracket = line.indexOf('[', searchStart); + if (openingBracket < 0) { + return null; + } + final int closingBracket = line.indexOf(']', openingBracket + 1); + if (closingBracket < 0) { + return null; + } + final ParsedCoverageTag tag = parseCoverageTag(line, openingBracket, closingBracket); + if (tag != null) { + if (createCoverageTagSourceId(file, lineNumber, lineMatchCount, tag).equals(specificationId)) { + return new OftTraceNavigationTarget(file, lineStartOffset + tag.sourceIdStartOffset()); + } + lineMatchCount++; + } + searchStart = closingBracket + 1; + } + return null; + } + + private static String createCoverageTagSourceId( + final VirtualFile file, + final int lineNumber, + final int lineMatchCount, + final ParsedCoverageTag tag + ) { + final String sourceName = tag.sourceName(); + final int revision = tag.sourceRevision(); + final String name = sourceName != null + ? sourceName + : generateCoverageTagName(file, lineNumber, lineMatchCount, tag); + return tag.sourceArtifact() + "~" + name + "~" + revision; + } + + private static String generateCoverageTagName( + final VirtualFile file, + final int lineNumber, + final int lineMatchCount, + final ParsedCoverageTag tag + ) { + final String targetName = tag.targetName(); + if (tag.hasNeedsArtifactTypes()) { + return targetName; + } + final String uniqueName = file.getPath() + lineNumber + lineMatchCount + tag.targetId(); + return targetName + "-" + calculateCrc32(uniqueName); + } + + private static long calculateCrc32(final String value) { + final CRC32 checksum = new CRC32(); + checksum.update(value.getBytes(StandardCharsets.UTF_8)); + return checksum.getValue(); + } + + private static int findLineEnd(final String text, final int cursor) { + int index = cursor; + while (index < text.length()) { + final char current = text.charAt(index); + if (current == '\n' || current == '\r') { + return index; + } + index++; + } + return text.length(); + } + + private static int skipLineSeparator(final String text, final int cursor) { + if (cursor >= text.length()) { + return cursor; + } + if (text.charAt(cursor) == '\r' && cursor + 1 < text.length() && text.charAt(cursor + 1) == '\n') { + return cursor + 2; + } + return cursor + 1; + } + + private static @Nullable ParsedCoverageTag parseCoverageTag( + final String line, + final int openingBracket, + final int closingBracket + ) { + int cursor = skipWhitespace(line, openingBracket + 1, closingBracket); + final ParsedSource source = parseSource(line, cursor, closingBracket); + if (source == null) { + return null; + } + cursor = skipWhitespace(line, source.endOffset(), closingBracket); + if (!startsWith(line, cursor, "->") || cursor + 2 > closingBracket) { + return null; + } + cursor = skipWhitespace(line, cursor + 2, closingBracket); + final ParsedTarget target = parseTarget(line, cursor, closingBracket); + if (target == null) { + return null; + } + cursor = skipWhitespace(line, target.endOffset(), closingBracket); + boolean hasNeedsArtifactTypes = false; + if (startsWith(line, cursor, ">>")) { + cursor = skipWhitespace(line, cursor + 2, closingBracket); + final int needsEndOffset = parseNeedsArtifactTypes(line, cursor, closingBracket); + if (needsEndOffset < 0) { + return null; + } + cursor = skipWhitespace(line, needsEndOffset, closingBracket); + hasNeedsArtifactTypes = true; + } + if (cursor != closingBracket) { + return null; + } + return new ParsedCoverageTag( + skipWhitespace(line, openingBracket + 1, closingBracket), + source.artifactType(), + source.name(), + source.revision(), + target.id(), + target.name(), + hasNeedsArtifactTypes + ); + } + + private static @Nullable ParsedSource parseSource(final String line, final int startOffset, final int limit) { + final ParsedToken artifact = parseArtifactType(line, startOffset, limit); + if (artifact == null) { + return null; + } + return parseSourceBody(line, limit, artifact); + } + + private static @Nullable ParsedTarget parseTarget(final String line, final int startOffset, final int limit) { + final ParsedToken artifact = parseArtifactType(line, startOffset, limit); + if (artifact == null || artifact.endOffset() >= limit || line.charAt(artifact.endOffset()) != '~') { + return null; + } + final ParsedToken name = parseName(line, artifact.endOffset() + 1, limit); + if (name == null || name.endOffset() >= limit || line.charAt(name.endOffset()) != '~') { + return null; + } + final ParsedNumber revision = parseUnsignedInteger(line, name.endOffset() + 1, limit); + if (revision == null) { + return null; + } + return new ParsedTarget( + artifact.value() + "~" + name.value() + "~" + revision.value(), + name.value(), + revision.endOffset() + ); + } + + private static int parseNeedsArtifactTypes(final String line, final int startOffset, final int limit) { + final ParsedToken firstArtifact = parseArtifactType(line, startOffset, limit); + if (firstArtifact == null) { + return -1; + } + int cursor = firstArtifact.endOffset(); + while (true) { + final int commaOffset = skipWhitespace(line, cursor, limit); + if (commaOffset >= limit || line.charAt(commaOffset) != ',') { + return cursor; + } + final int nextArtifactOffset = skipWhitespace(line, commaOffset + 1, limit); + final ParsedToken artifact = parseArtifactType(line, nextArtifactOffset, limit); + if (artifact == null) { + return -1; + } + cursor = artifact.endOffset(); + } + } + + private static @Nullable ParsedToken parseArtifactType(final String line, final int startOffset, final int limit) { + return parseToken(line, startOffset, limit, Character::isLetter); + } + + private static @Nullable ParsedToken parseName(final String line, final int startOffset, final int limit) { + if (startOffset >= limit || !Character.isLetter(line.charAt(startOffset))) { + return null; + } + int cursor = advanceWhileLetterOrDigit(line, startOffset + 1, limit); + while (cursor < limit) { + final int nextCursor = advanceAfterNameSeparator(line, cursor, limit); + if (nextCursor < 0) { + break; + } + cursor = nextCursor; + } + return new ParsedToken(line.substring(startOffset, cursor), cursor); + } + + private static @Nullable ParsedNumber parseUnsignedInteger( + final String line, + final int startOffset, + final int limit + ) { + if (startOffset >= limit || !Character.isDigit(line.charAt(startOffset))) { + return null; + } + int cursor = startOffset + 1; + while (cursor < limit && Character.isDigit(line.charAt(cursor))) { + cursor++; + } + return new ParsedNumber(Integer.parseInt(line.substring(startOffset, cursor)), cursor); + } + + private static @Nullable ParsedToken parseToken( + final String line, + final int startOffset, + final int limit, + final CharacterPredicate predicate + ) { + if (startOffset >= limit || !predicate.test(line.charAt(startOffset))) { + return null; + } + int cursor = startOffset + 1; + while (cursor < limit && predicate.test(line.charAt(cursor))) { + cursor++; + } + return new ParsedToken(line.substring(startOffset, cursor), cursor); + } + + private static int skipWhitespace(final String value, final int startOffset, final int limit) { + int cursor = startOffset; + while (cursor < limit && Character.isWhitespace(value.charAt(cursor))) { + cursor++; + } + return cursor; + } + + private static @Nullable ParsedSource parseSourceBody( + final String line, + final int limit, + final ParsedToken artifact + ) { + final int cursor = artifact.endOffset(); + if (!isTildeAt(line, cursor, limit)) { + return new ParsedSource(artifact.value(), null, 0, cursor); + } + final int detailsStartOffset = cursor + 1; + if (detailsStartOffset >= limit) { + return null; + } + if (isTildeAt(line, detailsStartOffset, limit)) { + return parseUnnamedSource(line, limit, artifact.value(), detailsStartOffset); + } + return parseNamedSource(line, limit, artifact.value(), detailsStartOffset); + } + + private static @Nullable ParsedSource parseUnnamedSource( + final String line, + final int limit, + final String artifactType, + final int revisionMarkerOffset + ) { + final ParsedNumber revision = parseUnsignedInteger(line, revisionMarkerOffset + 1, limit); + if (revision == null) { + return null; + } + return new ParsedSource(artifactType, null, revision.value(), revision.endOffset()); + } + + private static @Nullable ParsedSource parseNamedSource( + final String line, + final int limit, + final String artifactType, + final int nameStartOffset + ) { + final ParsedToken sourceName = parseName(line, nameStartOffset, limit); + if (!isTokenFollowedByTilde(sourceName, line, limit)) { + return null; + } + final ParsedNumber revision = parseUnsignedInteger(line, sourceName.endOffset() + 1, limit); + if (revision == null) { + return null; + } + return new ParsedSource(artifactType, sourceName.value(), revision.value(), revision.endOffset()); + } + + private static boolean isTildeAt(final String line, final int offset, final int limit) { + return offset < limit && line.charAt(offset) == '~'; + } + + private static boolean isTokenFollowedByTilde( + final @Nullable ParsedToken token, + final String line, + final int limit + ) { + return token != null && token.endOffset() < limit && line.charAt(token.endOffset()) == '~'; + } + + private static int advanceAfterNameSeparator(final String line, final int cursor, final int limit) { + if (!isNameSeparatorAt(line, cursor, limit) || !hasNameCharacterAfterSeparator(line, cursor, limit)) { + return -1; + } + return advanceWhileLetterOrDigit(line, cursor + 2, limit); + } + + private static boolean isNameSeparatorAt(final String line, final int cursor, final int limit) { + return cursor < limit && isNameSeparator(line.charAt(cursor)); + } + + private static boolean hasNameCharacterAfterSeparator(final String line, final int cursor, final int limit) { + return cursor + 1 < limit && Character.isLetterOrDigit(line.charAt(cursor + 1)); + } + + private static boolean isNameSeparator(final char current) { + return current == '.' || current == '_' || current == '-'; + } + + private static int advanceWhileLetterOrDigit(final String line, final int startOffset, final int limit) { + int cursor = startOffset; + while (cursor < limit && Character.isLetterOrDigit(line.charAt(cursor))) { + cursor++; + } + return cursor; + } + + private static boolean startsWith(final String value, final int offset, final String prefix) { + return offset >= 0 && offset + prefix.length() <= value.length() && value.startsWith(prefix, offset); + } + + @FunctionalInterface + private interface CharacterPredicate { + boolean test(char value); + } + + private record ParsedCoverageTag( + int sourceIdStartOffset, + String sourceArtifact, + @Nullable String sourceName, + int sourceRevision, + String targetId, + String targetName, + boolean hasNeedsArtifactTypes + ) { + } + + private record ParsedSource(String artifactType, @Nullable String name, int revision, int endOffset) { + } + + private record ParsedTarget(String id, String name, int endOffset) { + } + + private record ParsedToken(String value, int endOffset) { + } + + private record ParsedNumber(int value, int endOffset) { + } +} diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceNavigationResolver.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceNavigationResolver.java index 8f6941f..5efdfd1 100644 --- a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceNavigationResolver.java +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceNavigationResolver.java @@ -1,39 +1,34 @@ package org.itsallcode.openfasttrace.intellijplugin.trace; import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.editor.Document; -import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.ProjectFileIndex; import com.intellij.openapi.vfs.LocalFileSystem; -import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.search.FilenameIndex; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.util.indexing.FileBasedIndex; -import org.itsallcode.openfasttrace.intellijplugin.OftSupportedFiles; import org.itsallcode.openfasttrace.intellijplugin.indexing.OftSpecificationIndex; import org.jetbrains.annotations.Nullable; -import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.util.Optional; -import java.util.zip.CRC32; final class OftTraceNavigationResolver { private static final Logger LOG = Logger.getInstance(OftTraceNavigationResolver.class); private final Project project; + private final OftCoverageTagTraceItemNavigator coverageTagTraceItemNavigator; OftTraceNavigationResolver(final Project project) { this.project = project; + this.coverageTagTraceItemNavigator = new OftCoverageTagTraceItemNavigator(project); } Optional resolve(final String specificationId) { return findDeclaredSpecificationTarget(specificationId) - .or(() -> findCoverageTagSourceTarget(specificationId)); + .or(() -> coverageTagTraceItemNavigator.resolve(specificationId)); } // [impl->dsn~navigate-from-test-runner-source-files~1] @@ -66,6 +61,7 @@ private Path sourcePath(final String sourcePath) { ? null : Path.of(projectBasePath).resolve(path).normalize(); } catch (final InvalidPathException exception) { + LOG.debug("Ignoring invalid OFT trace source path: " + sourcePath, exception); return null; } } @@ -88,7 +84,7 @@ private Optional findIndexedSourceFile( .map(file -> new OftTraceNavigationTarget(file, 0)); } - private @Nullable String sourceFileName(final String sourcePath, final @Nullable Path path) { + private static @Nullable String sourceFileName(final String sourcePath, final @Nullable Path path) { if (path != null) { final Path fileName = path.getFileName(); if (fileName != null) { @@ -161,14 +157,30 @@ private boolean matchesSourcePath( final String projectRelativeSourcePath ) { final String filePath = displayPath(file.getPath()); - return filePath.equals(requestedPath) - || filePath.equals(resolvedPath) - || projectRelativePath(file).filter(path -> requestedPath.equals(path) - || projectRelativeSourcePath.equals(path)).isPresent() + return matchesAbsoluteSourcePath(filePath, requestedPath, resolvedPath) + || matchesProjectRelativeSourcePath(file, requestedPath, projectRelativeSourcePath) || matchesRelativeSourcePath(filePath, projectRelativeSourcePath); } - private boolean matchesRelativeSourcePath(final String filePath, final String requestedPath) { + private static boolean matchesAbsoluteSourcePath( + final String filePath, + final String requestedPath, + final @Nullable String resolvedPath + ) { + return filePath.equals(requestedPath) || filePath.equals(resolvedPath); + } + + private boolean matchesProjectRelativeSourcePath( + final VirtualFile file, + final String requestedPath, + final String projectRelativeSourcePath + ) { + return projectRelativePath(file) + .filter(path -> requestedPath.equals(path) || projectRelativeSourcePath.equals(path)) + .isPresent(); + } + + private static boolean matchesRelativeSourcePath(final String filePath, final String requestedPath) { return !requestedPath.startsWith("/") && requestedPath.contains("/") && filePath.endsWith("/" + requestedPath); @@ -209,432 +221,4 @@ private Optional findDeclaredSpecificationTarget(final return Optional.ofNullable(target[0]); } - private Optional findCoverageTagSourceTarget(final String specificationId) { - final OftTraceNavigationTarget[] target = new OftTraceNavigationTarget[1]; - ProjectFileIndex.getInstance(project) - .iterateContent(file -> processCoverageTagFile(specificationId, target, file)); - return Optional.ofNullable(target[0]); - } - - private static boolean processCoverageTagFile( - final String specificationId, - final OftTraceNavigationTarget[] target, - final VirtualFile file - ) { - if (!OftSupportedFiles.isCoverageTagFile(file)) { - return true; - } - final OftTraceNavigationTarget match = findCoverageTagTargetInFile(file, specificationId); - if (match != null) { - target[0] = match; - return false; - } - return true; - } - - private static @Nullable OftTraceNavigationTarget findCoverageTagTargetInFile( - final VirtualFile file, - final String specificationId - ) { - final String text; - try { - text = currentFileText(file); - } catch (final IllegalStateException exception) { - LOG.debug("Failed to read OFT trace navigation source file: " + file.getPath(), exception); - return null; - } - int lineNumber = 1; - int lineStartOffset = 0; - int cursor = 0; - while (cursor < text.length()) { - final int lineEnd = findLineEnd(text, cursor); - final String line = text.substring(cursor, lineEnd); - final OftTraceNavigationTarget target = findCoverageTagTargetInLine( - file, - line, - lineNumber, - lineStartOffset, - specificationId - ); - if (target != null) { - return target; - } - final int nextCursor = skipLineSeparator(text, lineEnd); - lineStartOffset = nextCursor; - cursor = nextCursor; - lineNumber++; - } - return null; - } - - private static String currentFileText(final VirtualFile file) { - final Document document = FileDocumentManager.getInstance().getCachedDocument(file); - if (document != null) { - return document.getText(); - } - try { - return VfsUtilCore.loadText(file); - } catch (final IOException exception) { - throw new IllegalStateException( - "Failed to load OFT trace navigation source file " + file.getPath(), - exception - ); - } - } - - private static @Nullable OftTraceNavigationTarget findCoverageTagTargetInLine( - final VirtualFile file, - final String line, - final int lineNumber, - final int lineStartOffset, - final String specificationId - ) { - int searchStart = 0; - int lineMatchCount = 0; - while (searchStart < line.length()) { - final int openingBracket = line.indexOf('[', searchStart); - if (openingBracket < 0) { - return null; - } - final int closingBracket = line.indexOf(']', openingBracket + 1); - if (closingBracket < 0) { - return null; - } - final ParsedCoverageTag tag = parseCoverageTag(line, openingBracket, closingBracket); - if (tag != null) { - if (createCoverageTagSourceId(file, lineNumber, lineMatchCount, tag).equals(specificationId)) { - return new OftTraceNavigationTarget(file, lineStartOffset + tag.sourceIdStartOffset()); - } - lineMatchCount++; - } - searchStart = closingBracket + 1; - } - return null; - } - - private static String createCoverageTagSourceId( - final VirtualFile file, - final int lineNumber, - final int lineMatchCount, - final ParsedCoverageTag tag - ) { - final String sourceName = tag.sourceName(); - final int revision = tag.sourceRevision(); - final String name = sourceName != null - ? sourceName - : generateCoverageTagName(file, lineNumber, lineMatchCount, tag); - return tag.sourceArtifact() + "~" + name + "~" + revision; - } - - private static String generateCoverageTagName( - final VirtualFile file, - final int lineNumber, - final int lineMatchCount, - final ParsedCoverageTag tag - ) { - final String targetName = tag.targetName(); - if (tag.hasNeedsArtifactTypes()) { - return targetName; - } - final String uniqueName = file.getPath() + lineNumber + lineMatchCount + tag.targetId(); - return targetName + "-" + calculateCrc32(uniqueName); - } - - private static long calculateCrc32(final String value) { - final CRC32 checksum = new CRC32(); - checksum.update(value.getBytes(StandardCharsets.UTF_8)); - return checksum.getValue(); - } - - private static int findLineEnd(final String text, final int cursor) { - int index = cursor; - while (index < text.length()) { - final char current = text.charAt(index); - if (current == '\n' || current == '\r') { - return index; - } - index++; - } - return text.length(); - } - - private static int skipLineSeparator(final String text, final int cursor) { - if (cursor >= text.length()) { - return cursor; - } - if (text.charAt(cursor) == '\r' && cursor + 1 < text.length() && text.charAt(cursor + 1) == '\n') { - return cursor + 2; - } - return cursor + 1; - } - - private static @Nullable ParsedCoverageTag parseCoverageTag( - final String line, - final int openingBracket, - final int closingBracket - ) { - int cursor = skipWhitespace(line, openingBracket + 1, closingBracket); - final ParsedSource source = parseSource(line, cursor, closingBracket); - if (source == null) { - return null; - } - cursor = skipWhitespace(line, source.endOffset(), closingBracket); - if (!startsWith(line, cursor, "->") || cursor + 2 > closingBracket) { - return null; - } - cursor = skipWhitespace(line, cursor + 2, closingBracket); - final ParsedTarget target = parseTarget(line, cursor, closingBracket); - if (target == null) { - return null; - } - cursor = skipWhitespace(line, target.endOffset(), closingBracket); - boolean hasNeedsArtifactTypes = false; - if (startsWith(line, cursor, ">>")) { - cursor = skipWhitespace(line, cursor + 2, closingBracket); - final int needsEndOffset = parseNeedsArtifactTypes(line, cursor, closingBracket); - if (needsEndOffset < 0) { - return null; - } - cursor = skipWhitespace(line, needsEndOffset, closingBracket); - hasNeedsArtifactTypes = true; - } - if (cursor != closingBracket) { - return null; - } - return new ParsedCoverageTag( - skipWhitespace(line, openingBracket + 1, closingBracket), - source.artifactType(), - source.name(), - source.revision(), - target.id(), - target.name(), - hasNeedsArtifactTypes - ); - } - - private static @Nullable ParsedSource parseSource(final String line, final int startOffset, final int limit) { - final ParsedToken artifact = parseArtifactType(line, startOffset, limit); - if (artifact == null) { - return null; - } - return parseSourceBody(line, limit, artifact); - } - - private static @Nullable ParsedTarget parseTarget(final String line, final int startOffset, final int limit) { - final ParsedToken artifact = parseArtifactType(line, startOffset, limit); - if (artifact == null || artifact.endOffset() >= limit || line.charAt(artifact.endOffset()) != '~') { - return null; - } - final ParsedToken name = parseName(line, artifact.endOffset() + 1, limit); - if (name == null || name.endOffset() >= limit || line.charAt(name.endOffset()) != '~') { - return null; - } - final ParsedNumber revision = parseUnsignedInteger(line, name.endOffset() + 1, limit); - if (revision == null) { - return null; - } - return new ParsedTarget( - artifact.value() + "~" + name.value() + "~" + revision.value(), - name.value(), - revision.endOffset() - ); - } - - private static int parseNeedsArtifactTypes(final String line, final int startOffset, final int limit) { - final ParsedToken firstArtifact = parseArtifactType(line, startOffset, limit); - if (firstArtifact == null) { - return -1; - } - int cursor = firstArtifact.endOffset(); - while (true) { - final int commaOffset = skipWhitespace(line, cursor, limit); - if (commaOffset >= limit || line.charAt(commaOffset) != ',') { - return cursor; - } - final int nextArtifactOffset = skipWhitespace(line, commaOffset + 1, limit); - final ParsedToken artifact = parseArtifactType(line, nextArtifactOffset, limit); - if (artifact == null) { - return -1; - } - cursor = artifact.endOffset(); - } - } - - private static @Nullable ParsedToken parseArtifactType(final String line, final int startOffset, final int limit) { - return parseToken(line, startOffset, limit, Character::isLetter); - } - - private static @Nullable ParsedToken parseName(final String line, final int startOffset, final int limit) { - if (startOffset >= limit || !Character.isLetter(line.charAt(startOffset))) { - return null; - } - int cursor = advanceWhileLetterOrDigit(line, startOffset + 1, limit); - while (cursor < limit) { - final int nextCursor = advanceAfterNameSeparator(line, cursor, limit); - if (nextCursor < 0) { - break; - } - cursor = nextCursor; - } - return new ParsedToken(line.substring(startOffset, cursor), cursor); - } - - private static @Nullable ParsedNumber parseUnsignedInteger( - final String line, - final int startOffset, - final int limit - ) { - if (startOffset >= limit || !Character.isDigit(line.charAt(startOffset))) { - return null; - } - int cursor = startOffset + 1; - while (cursor < limit && Character.isDigit(line.charAt(cursor))) { - cursor++; - } - return new ParsedNumber(Integer.parseInt(line.substring(startOffset, cursor)), cursor); - } - - private static @Nullable ParsedToken parseToken( - final String line, - final int startOffset, - final int limit, - final CharacterPredicate predicate - ) { - if (startOffset >= limit || !predicate.test(line.charAt(startOffset))) { - return null; - } - int cursor = startOffset + 1; - while (cursor < limit && predicate.test(line.charAt(cursor))) { - cursor++; - } - return new ParsedToken(line.substring(startOffset, cursor), cursor); - } - - private static int skipWhitespace(final String value, final int startOffset, final int limit) { - int cursor = startOffset; - while (cursor < limit && Character.isWhitespace(value.charAt(cursor))) { - cursor++; - } - return cursor; - } - - private static @Nullable ParsedSource parseSourceBody( - final String line, - final int limit, - final ParsedToken artifact - ) { - final int cursor = artifact.endOffset(); - if (!isTildeAt(line, cursor, limit)) { - return new ParsedSource(artifact.value(), null, 0, cursor); - } - final int detailsStartOffset = cursor + 1; - if (detailsStartOffset >= limit) { - return null; - } - if (isTildeAt(line, detailsStartOffset, limit)) { - return parseUnnamedSource(line, limit, artifact.value(), detailsStartOffset); - } - return parseNamedSource(line, limit, artifact.value(), detailsStartOffset); - } - - private static @Nullable ParsedSource parseUnnamedSource( - final String line, - final int limit, - final String artifactType, - final int revisionMarkerOffset - ) { - final ParsedNumber revision = parseUnsignedInteger(line, revisionMarkerOffset + 1, limit); - if (revision == null) { - return null; - } - return new ParsedSource(artifactType, null, revision.value(), revision.endOffset()); - } - - private static @Nullable ParsedSource parseNamedSource( - final String line, - final int limit, - final String artifactType, - final int nameStartOffset - ) { - final ParsedToken sourceName = parseName(line, nameStartOffset, limit); - if (!isTokenFollowedByTilde(sourceName, line, limit)) { - return null; - } - final ParsedNumber revision = parseUnsignedInteger(line, sourceName.endOffset() + 1, limit); - if (revision == null) { - return null; - } - return new ParsedSource(artifactType, sourceName.value(), revision.value(), revision.endOffset()); - } - - private static boolean isTildeAt(final String line, final int offset, final int limit) { - return offset < limit && line.charAt(offset) == '~'; - } - - private static boolean isTokenFollowedByTilde( - final @Nullable ParsedToken token, - final String line, - final int limit - ) { - return token != null && token.endOffset() < limit && line.charAt(token.endOffset()) == '~'; - } - - private static int advanceAfterNameSeparator(final String line, final int cursor, final int limit) { - if (!isNameSeparatorAt(line, cursor, limit) || !hasNameCharacterAfterSeparator(line, cursor, limit)) { - return -1; - } - return advanceWhileLetterOrDigit(line, cursor + 2, limit); - } - - private static boolean isNameSeparatorAt(final String line, final int cursor, final int limit) { - return cursor < limit && isNameSeparator(line.charAt(cursor)); - } - - private static boolean hasNameCharacterAfterSeparator(final String line, final int cursor, final int limit) { - return cursor + 1 < limit && Character.isLetterOrDigit(line.charAt(cursor + 1)); - } - - private static boolean isNameSeparator(final char current) { - return current == '.' || current == '_' || current == '-'; - } - - private static int advanceWhileLetterOrDigit(final String line, final int startOffset, final int limit) { - int cursor = startOffset; - while (cursor < limit && Character.isLetterOrDigit(line.charAt(cursor))) { - cursor++; - } - return cursor; - } - - private static boolean startsWith(final String value, final int offset, final String prefix) { - return offset >= 0 && offset + prefix.length() <= value.length() && value.startsWith(prefix, offset); - } - - @FunctionalInterface - private interface CharacterPredicate { - boolean test(char value); - } - - private record ParsedCoverageTag( - int sourceIdStartOffset, - String sourceArtifact, - @Nullable String sourceName, - int sourceRevision, - String targetId, - String targetName, - boolean hasNeedsArtifactTypes - ) { - } - - private record ParsedSource(String artifactType, @Nullable String name, int revision, int endOffset) { - } - - private record ParsedTarget(String id, String name, int endOffset) { - } - - private record ParsedToken(String value, int endOffset) { - } - - private record ParsedNumber(int value, int endOffset) { - } } diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestProxy.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestProxy.java index 44a73ac..2bb4dbc 100644 --- a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestProxy.java +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestProxy.java @@ -4,13 +4,9 @@ import com.intellij.openapi.fileEditor.OpenFileDescriptor; import com.intellij.openapi.project.Project; -import java.io.Serial; import java.util.Optional; final class OftTraceTestProxy extends SMTestProxy { - @Serial - private static final long serialVersionUID = 1L; - private final transient Project project; private final String specificationId; private final String sourcePath; @@ -20,11 +16,6 @@ final class OftTraceTestProxy extends SMTestProxy { this(project, name, suite, navigationId, null); } - // [impl->dsn~navigate-from-test-runner-source-files~1] - static OftTraceTestProxy sourceFileSuite(final Project project, final String name, final String sourcePath) { - return new OftTraceTestProxy(project, name, true, null, sourcePath); - } - private OftTraceTestProxy( final Project project, final String name, @@ -39,6 +30,11 @@ private OftTraceTestProxy( this.navigationResolver = new OftTraceNavigationResolver(project); } + // [impl->dsn~navigate-from-test-runner-source-files~1] + static OftTraceTestProxy sourceFileSuite(final Project project, final String name, final String sourcePath) { + return new OftTraceTestProxy(project, name, true, null, sourcePath); + } + @Override public void navigate(final boolean requestFocus) { resolveNavigationTarget() diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerOutputPresenter.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerOutputPresenter.java index 245037e..d32533d 100644 --- a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerOutputPresenter.java +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestRunnerOutputPresenter.java @@ -5,23 +5,17 @@ import com.intellij.execution.testframework.sm.runner.ui.SMTestRunnerResultsForm; import com.intellij.execution.ui.ConsoleViewContentType; import com.intellij.openapi.project.Project; +import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceTestTree.OftTraceItemNode; +import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceTestTree.OftTraceLinkNode; +import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceTestTree.OftTraceSuiteNode; import java.util.function.Function; public final class OftTraceTestRunnerOutputPresenter implements OftTraceOutputPresenter { private final Function consoleFactory; - private final OftTraceTestTreeMapper testTreeMapper; public OftTraceTestRunnerOutputPresenter(final Function consoleFactory) { - this(consoleFactory, new OftTraceTestTreeMapper()); - } - - OftTraceTestRunnerOutputPresenter( - final Function consoleFactory, - final OftTraceTestTreeMapper testTreeMapper - ) { this.consoleFactory = consoleFactory; - this.testTreeMapper = testTreeMapper; } // [impl->dsn~trace-test-runner-presentation~1] @@ -33,7 +27,10 @@ public void show(final Project project, final String contentTitle, final OftTrac root.setPresentation(contentTitle); resultsViewer.onTestingStarted(root); final PresentationOutcome outcome = result.trace() - .map(trace -> showTrace(project, resultsViewer, root, testTreeMapper.map(trace, project.getBasePath()))) + .map(trace -> showTrace(project, resultsViewer, root, OftTraceTestTreeMapper.map( + trace, + project.getBasePath() + ))) .orElseGet(() -> showResultWithoutTrace(console, resultsViewer, root, result)); if (outcome.failed()) { markFailed(root, outcome.details()); @@ -158,7 +155,12 @@ private static PresentationOutcome showResultWithoutTrace( new SMTestProxy(result.statusMessage(), false, null, true); root.addChild(failureProxy); resultsViewer.onTestsCountInSuite(1); - showTestNode(resultsViewer, failureProxy, result.requiresAttention(), OftTraceTestNodeDetails.resultWithoutTrace(result)); + showTestNode( + resultsViewer, + failureProxy, + result.requiresAttention(), + OftTraceTestNodeDetails.resultWithoutTrace(result) + ); return new PresentationOutcome( result.requiresAttention(), result.requiresAttention() diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTree.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTree.java index b400e33..5790622 100644 --- a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTree.java +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTree.java @@ -1,5 +1,7 @@ package org.itsallcode.openfasttrace.intellijplugin.trace; +import org.jetbrains.annotations.Nullable; + import java.util.List; record OftTraceTestTree(List suites) { @@ -12,45 +14,45 @@ int testCount() { boolean failed() { return suites.stream().anyMatch(OftTraceSuiteNode::failed); } -} -record OftTraceSuiteNode(String name, String sourcePath, List items) { - int testCount() { - return items.stream() - .mapToInt(OftTraceItemNode::testCount) - .sum(); - } + record OftTraceSuiteNode(String name, @Nullable String sourcePath, List items) { + int testCount() { + return items.stream() + .mapToInt(OftTraceItemNode::testCount) + .sum(); + } - boolean failed() { - return items.stream().anyMatch(OftTraceItemNode::failed); - } + boolean failed() { + return items.stream().anyMatch(OftTraceItemNode::failed); + } - OftTraceTestNodeDetails failureDetails() { - return OftTraceTestNodeDetails.sourceSuiteFailure(name); + OftTraceTestNodeDetails failureDetails() { + return OftTraceTestNodeDetails.sourceSuiteFailure(name); + } } -} -record OftTraceItemNode( - String name, - String navigationId, - boolean defective, - OftTraceTestNodeDetails details, - List links -) { - int testCount() { - return 1 + links.size(); - } + record OftTraceItemNode( + String name, + String navigationId, + boolean defective, + OftTraceTestNodeDetails details, + List links + ) { + int testCount() { + return 1 + links.size(); + } - boolean failed() { - return defective || links.stream().anyMatch(OftTraceLinkNode::failed); - } + boolean failed() { + return defective || links.stream().anyMatch(OftTraceLinkNode::failed); + } - OftTraceTestNodeDetails failureDetails() { - return defective - ? details - : OftTraceTestNodeDetails.specificationItemLinkFailure(navigationId); + OftTraceTestNodeDetails failureDetails() { + return defective + ? details + : OftTraceTestNodeDetails.specificationItemLinkFailure(navigationId); + } } -} -record OftTraceLinkNode(String name, String navigationId, boolean failed, OftTraceTestNodeDetails details) { + record OftTraceLinkNode(String name, String navigationId, boolean failed, OftTraceTestNodeDetails details) { + } } diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTreeMapper.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTreeMapper.java index bc272ad..38ab8d7 100644 --- a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTreeMapper.java +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTreeMapper.java @@ -1,33 +1,39 @@ package org.itsallcode.openfasttrace.intellijplugin.trace; +import com.intellij.openapi.diagnostic.Logger; import org.itsallcode.openfasttrace.api.core.DeepCoverageStatus; import org.itsallcode.openfasttrace.api.core.LinkedSpecificationItem; import org.itsallcode.openfasttrace.api.core.LinkStatus; import org.itsallcode.openfasttrace.api.core.Location; import org.itsallcode.openfasttrace.api.core.Trace; import org.itsallcode.openfasttrace.api.core.TracedLink; +import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceTestTree.OftTraceItemNode; +import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceTestTree.OftTraceLinkNode; +import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceTestTree.OftTraceSuiteNode; import org.jetbrains.annotations.Nullable; import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.util.ArrayList; -import java.util.Collections; import java.util.Comparator; -import java.util.IdentityHashMap; +import java.util.EnumMap; import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; -import java.util.Set; final class OftTraceTestTreeMapper { + private static final Logger LOG = Logger.getInstance(OftTraceTestTreeMapper.class); private static final String UNKNOWN_SOURCE = "Unknown source"; private static final Comparator ITEM_ORDER = Comparator .comparing(LinkedSpecificationItem::getArtifactType) .thenComparing(LinkedSpecificationItem::getName) .thenComparingInt(LinkedSpecificationItem::getRevision); + private static final EnumMap REVERSE_LINK_STATUSES = createReverseLinkStatuses(); + + private OftTraceTestTreeMapper() { + } // [impl->dsn~trace-test-runner-presentation~1] // [impl->dsn~show-trace-source-files-as-test-runner-suites~1] @@ -48,38 +54,34 @@ final class OftTraceTestTreeMapper { // [impl->dsn~navigate-from-test-runner-source-files~1] // [impl->dsn~navigate-from-test-runner-specification-items~1] // [impl->dsn~navigate-from-test-runner-trace-links~1] - OftTraceTestTree map(final Trace trace) { + static OftTraceTestTree map(final Trace trace) { return map(trace, null); } - OftTraceTestTree map(final Trace trace, final String projectBasePath) { - final Map> visibleLinksByItem = - visibleLinksByItem(trace.getItems()); + static OftTraceTestTree map(final Trace trace, final String projectBasePath) { + final List visibleLinksByItem = visibleLinksByItem(trace.getItems()); final Map itemsBySource = new LinkedHashMap<>(); - for (final LinkedSpecificationItem item : trace.getItems()) { - final SourceFileSuite sourceFileSuite = sourceFileSuite(item, projectBasePath); + for (final TraceItemLinks itemLinks : visibleLinksByItem) { + final SourceFileSuite sourceFileSuite = sourceFileSuite(itemLinks.item(), projectBasePath); itemsBySource.computeIfAbsent( sourceFileSuite.name(), ignored -> new SourceFileItems(sourceFileSuite.name(), sourceFileSuite.sourcePath()) - ).add(item); + ).add(itemLinks); } final List suites = itemsBySource.values().stream() .map(source -> new OftTraceSuiteNode( source.name(), source.sourcePath(), - mapItems(source.items(), visibleLinksByItem) + mapItems(source.items()) )) .toList(); return new OftTraceTestTree(suites); } - private static List mapItems( - final List items, - final Map> visibleLinksByItem - ) { + private static List mapItems(final List items) { return items.stream() - .sorted(ITEM_ORDER) - .map(item -> mapItem(item, visibleLinksByItem.get(item))) + .sorted(Comparator.comparing(TraceItemLinks::item, ITEM_ORDER)) + .map(itemLinks -> mapItem(itemLinks.item(), itemLinks.links())) .toList(); } @@ -102,51 +104,49 @@ private static OftTraceItemNode mapItem( ); } - private static Map> visibleLinksByItem( + private static List visibleLinksByItem( final List items ) { - final Set traceItems = Collections.newSetFromMap(new IdentityHashMap<>()); - final Map> linkSetsByItem = new LinkedHashMap<>(); - for (final LinkedSpecificationItem item : items) { - traceItems.add(item); - linkSetsByItem.put(item, new LinkedHashSet<>()); - } - for (final LinkedSpecificationItem owner : items) { + final List linksByItem = items.stream() + .map(TraceItemLinks::new) + .toList(); + for (final TraceItemLinks ownerLinks : linksByItem) { + final LinkedSpecificationItem owner = ownerLinks.item(); for (final TracedLink link : owner.getTracedLinks()) { - addVisibleLink(linkSetsByItem, owner, link.getOtherLinkEnd(), link.getStatus()); - if (traceItems.contains(link.getOtherLinkEnd())) { - reverseStatus(link.getStatus()) - .ifPresent(status -> addVisibleLink(linkSetsByItem, link.getOtherLinkEnd(), owner, status)); - } + ownerLinks.add(link.getOtherLinkEnd(), link.getStatus()); + findTraceItemLinks(linksByItem, link.getOtherLinkEnd()) + .ifPresent(otherLinks -> reverseStatus(link.getStatus()) + .ifPresent(status -> otherLinks.add(owner, status))); } } - final Map> linksByItem = new LinkedHashMap<>(); - linkSetsByItem.forEach((item, links) -> linksByItem.put(item, List.copyOf(links))); return linksByItem; } - private static void addVisibleLink( - final Map> linkSetsByItem, - final LinkedSpecificationItem owner, - final LinkedSpecificationItem otherItem, - final LinkStatus status + private static Optional findTraceItemLinks( + final List traceItemLinks, + final LinkedSpecificationItem item ) { - linkSetsByItem.get(owner).add(new VisibleTraceLink(otherItem, status)); + return traceItemLinks.stream() + .filter(candidate -> candidate.item() == item) + .findFirst(); } private static Optional reverseStatus(final LinkStatus status) { - return switch (status) { - case COVERS -> Optional.of(LinkStatus.COVERED_SHALLOW); - case COVERED_SHALLOW -> Optional.of(LinkStatus.COVERS); - case PREDATED -> Optional.of(LinkStatus.COVERED_PREDATED); - case COVERED_PREDATED -> Optional.of(LinkStatus.PREDATED); - case OUTDATED -> Optional.of(LinkStatus.COVERED_OUTDATED); - case COVERED_OUTDATED -> Optional.of(LinkStatus.OUTDATED); - case UNWANTED -> Optional.of(LinkStatus.COVERED_UNWANTED); - case COVERED_UNWANTED -> Optional.of(LinkStatus.UNWANTED); - case DUPLICATE -> Optional.of(LinkStatus.DUPLICATE); - case AMBIGUOUS, ORPHANED -> Optional.empty(); - }; + return Optional.ofNullable(REVERSE_LINK_STATUSES.get(status)); + } + + private static EnumMap createReverseLinkStatuses() { + final EnumMap statuses = new EnumMap<>(LinkStatus.class); + statuses.put(LinkStatus.COVERS, LinkStatus.COVERED_SHALLOW); + statuses.put(LinkStatus.COVERED_SHALLOW, LinkStatus.COVERS); + statuses.put(LinkStatus.PREDATED, LinkStatus.COVERED_PREDATED); + statuses.put(LinkStatus.COVERED_PREDATED, LinkStatus.PREDATED); + statuses.put(LinkStatus.OUTDATED, LinkStatus.COVERED_OUTDATED); + statuses.put(LinkStatus.COVERED_OUTDATED, LinkStatus.OUTDATED); + statuses.put(LinkStatus.UNWANTED, LinkStatus.COVERED_UNWANTED); + statuses.put(LinkStatus.COVERED_UNWANTED, LinkStatus.UNWANTED); + statuses.put(LinkStatus.DUPLICATE, LinkStatus.DUPLICATE); + return statuses; } private static OftTraceLinkNode mapLink(final LinkedSpecificationItem owner, final VisibleTraceLink link) { @@ -177,7 +177,7 @@ private static OftTraceLinkNode mapLink(final LinkedSpecificationItem owner, fin private static String itemName(final LinkedSpecificationItem item) { final String title = item.getTitle(); - return title == null || title.isBlank() ? item.getId().toString() : title; + return (title == null || title.isBlank()) ? item.getId().toString() : title; } private static String nodeName(final String baseName, final String status, final boolean clean) { @@ -221,6 +221,7 @@ private static Path pathOrNull(final String path) { try { return Path.of(path); } catch (final InvalidPathException exception) { + LOG.debug("Ignoring invalid OFT trace source path: " + path, exception); return null; } } @@ -269,17 +270,41 @@ private record VisibleTraceLink(LinkedSpecificationItem otherItem, LinkStatus st private record SourceFileSuite(String name, @Nullable String sourcePath) { } + private static final class TraceItemLinks { + private final LinkedSpecificationItem item; + private final List links = new ArrayList<>(); + + private TraceItemLinks(final LinkedSpecificationItem item) { + this.item = item; + } + + private LinkedSpecificationItem item() { + return item; + } + + private void add(final LinkedSpecificationItem otherItem, final LinkStatus status) { + final VisibleTraceLink visibleTraceLink = new VisibleTraceLink(otherItem, status); + if (!links.contains(visibleTraceLink)) { + links.add(visibleTraceLink); + } + } + + private List links() { + return List.copyOf(links); + } + } + private static final class SourceFileItems { private final String name; private final String sourcePath; - private final List items = new ArrayList<>(); + private final List items = new ArrayList<>(); private SourceFileItems(final String name, final String sourcePath) { this.name = name; this.sourcePath = sourcePath; } - private void add(final LinkedSpecificationItem item) { + private void add(final TraceItemLinks item) { items.add(item); } @@ -291,7 +316,7 @@ private String sourcePath() { return sourcePath; } - private List items() { + private List items() { return items; } } diff --git a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunProfileState.java b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunProfileState.java index 683f916..c974f5b 100644 --- a/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunProfileState.java +++ b/src/main/java/org/itsallcode/openfasttrace/intellijplugin/trace/runconfig/OftRunProfileState.java @@ -1,7 +1,6 @@ package org.itsallcode.openfasttrace.intellijplugin.trace.runconfig; import com.intellij.execution.DefaultExecutionResult; -import com.intellij.execution.ExecutionException; import com.intellij.execution.ExecutionResult; import com.intellij.execution.Executor; import com.intellij.execution.configurations.RunProfileState; @@ -26,7 +25,7 @@ import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceSettingsSnapshot; import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceTestRunnerOutputPresenter; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.NonNull; public final class OftRunProfileState implements RunProfileState { private final ExecutionEnvironment environment; @@ -38,8 +37,7 @@ public OftRunProfileState(final ExecutionEnvironment environment, final OftTrace } @Override - public @Nullable ExecutionResult execute(final Executor executor, @NotNull final ProgramRunner runner) - throws ExecutionException { + public @NonNull ExecutionResult execute(final Executor executor, @NotNull final ProgramRunner runner) { final Project project = environment.getProject(); final OftTraceInputResolution resolution = OftTraceInputResolver.resolve(project, settings); @@ -84,7 +82,10 @@ private static ExecutionPresentation createPlainTextExecutionPresentation(final // [impl->dsn~test-runner-as-default-run-configuration-result-view~1] // [impl->dsn~select-test-runner-trace-result-view~1] - private ExecutionPresentation createTestRunnerExecutionPresentation(final Project project, final Executor executor) { + private ExecutionPresentation createTestRunnerExecutionPresentation( + final Project project, + final Executor executor + ) { final SMTRunnerConsoleProperties properties = new SMTRunnerConsoleProperties( project, environment.getRunProfile(), diff --git a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceNavigationResolverParserTest.java b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceNavigationResolverParserTest.java index 46a8cf1..04e7ea1 100644 --- a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceNavigationResolverParserTest.java +++ b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceNavigationResolverParserTest.java @@ -219,7 +219,10 @@ private static Object invokeStatic( final Class[] parameterTypes, final Object... arguments ) throws ReflectiveOperationException { - final Method method = OftTraceNavigationResolver.class.getDeclaredMethod(methodName, parameterTypes); + final Method method = OftCoverageTagTraceItemNavigator.class.getDeclaredMethod( + methodName, + parameterTypes + ); method.setAccessible(true); return method.invoke(null, arguments); } diff --git a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceNavigationResolverTest.java b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceNavigationResolverTest.java index bafb823..9ea0ab8 100644 --- a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceNavigationResolverTest.java +++ b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceNavigationResolverTest.java @@ -66,6 +66,20 @@ public void testGivenVeryLargeMalformedCoverageTagWhenResolvingThenItReturnsWith assertThat(target, nullValue()); } + public void testGivenSourcePathMatchingProjectFileSuffixWhenResolvingSourceFileThenItFindsIndexedFile() { + final PsiFile file = myFixture.addFileToProject("module/doc/source.md", """ + req~runner_suffix_source_navigation~1 + """); + + final OftTraceNavigationTarget target = new OftTraceNavigationResolver(getProject()) + .resolveSourceFile("doc/source.md") + .orElse(null); + + assertThat(target, notNullValue()); + assertThat(target.file(), is(file.getVirtualFile())); + assertThat(target.offset(), is(0)); + } + private String generatedCoverageItemId( final String filePath, final int lineNumber, @@ -73,7 +87,8 @@ private String generatedCoverageItemId( final String sourceArtifactType, final String coveredId ) { - return sourceArtifactType + "~" + coveredId.split("~")[1] + "-" + crc32(filePath + lineNumber + lineMatchCount + coveredId) + "~0"; + return sourceArtifactType + "~" + coveredId.split("~")[1] + "-" + + crc32(filePath + lineNumber + lineMatchCount + coveredId) + "~0"; } private long crc32(final String value) { diff --git a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTreeMapperTest.java b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTreeMapperTest.java index 9ae52d7..3f59c93 100644 --- a/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTreeMapperTest.java +++ b/src/test/java/org/itsallcode/openfasttrace/intellijplugin/trace/OftTraceTestTreeMapperTest.java @@ -6,6 +6,9 @@ import org.itsallcode.openfasttrace.api.core.SpecificationItem; import org.itsallcode.openfasttrace.api.core.SpecificationItemId; import org.itsallcode.openfasttrace.api.core.Trace; +import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceTestTree.OftTraceItemNode; +import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceTestTree.OftTraceLinkNode; +import org.itsallcode.openfasttrace.intellijplugin.trace.OftTraceTestTree.OftTraceSuiteNode; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -21,8 +24,6 @@ class OftTraceTestTreeMapperTest { private static final String PROJECT_BASE = "/workspace/openfasttrace-intellij-plugin"; - private final OftTraceTestTreeMapper mapper = new OftTraceTestTreeMapper(); - // [itest->dsn~trace-test-runner-presentation~1] // [itest->dsn~show-trace-source-files-as-test-runner-suites~1] // [itest->dsn~roll-up-source-file-suite-trace-status~1] @@ -33,7 +34,11 @@ void testGivenTraceItemsFromDifferentSourceFilesWhenMappingThenItCreatesSuitesBy final LinkedSpecificationItem secondRequirement = item("req~second_requirement~1", "doc/requirements.md"); final LinkedSpecificationItem implementation = item("impl~first_requirement~1", "src/Main.java"); - final OftTraceTestTree tree = mapper.map(trace(firstRequirement, secondRequirement, implementation)); + final OftTraceTestTree tree = OftTraceTestTreeMapper.map(trace( + firstRequirement, + secondRequirement, + implementation + )); Assertions.assertAll( () -> assertThat(tree.suites().stream().map(OftTraceSuiteNode::name).toList(), @@ -60,7 +65,7 @@ void testGivenAbsoluteTraceItemPathsBelowProjectWhenMappingThenItCreatesSuitesWi PROJECT_BASE + "/src/main/java/Main.java" ); - final OftTraceTestTree tree = mapper.map(trace(requirement, implementation), PROJECT_BASE); + final OftTraceTestTree tree = OftTraceTestTreeMapper.map(trace(requirement, implementation), PROJECT_BASE); Assertions.assertAll( () -> assertThat( @@ -76,6 +81,20 @@ void testGivenAbsoluteTraceItemPathsBelowProjectWhenMappingThenItCreatesSuitesWi ); } + @Test + void testGivenTraceItemsWithoutUsableSourcePathsWhenMappingThenItCreatesFallbackSuites() { + final LinkedSpecificationItem withoutLocation = itemWithoutLocation("req~unknown_source~1"); + final LinkedSpecificationItem invalidPath = item("req~invalid_source~1", "doc/\0requirements.md"); + + final OftTraceTestTree tree = OftTraceTestTreeMapper.map(trace(withoutLocation, invalidPath), "\0"); + + Assertions.assertAll( + () -> assertThat(suiteNamed(tree, "Unknown source").sourcePath(), is((String) null)), + () -> assertThat(suiteNamed(tree, "doc/\0requirements.md").sourcePath(), is((String) null)), + () -> assertThat(tree.testCount(), is(2)) + ); + } + // [itest->dsn~trace-test-runner-presentation~1] // [itest->dsn~show-trace-specification-items-as-test-runner-tests~1] // [itest->dsn~show-trace-links-as-test-runner-sub-tests~1] @@ -104,7 +123,7 @@ void testGivenCoveredRequirementWithIncomingTraceLinkWhenMappingThenItCreatesPas requirement.addLinkToItemWithStatus(test, LinkStatus.COVERED_SHALLOW); final OftTraceItemNode requirementNode = onlyItem( - mapper.map(trace(requirement, test)), + OftTraceTestTreeMapper.map(trace(requirement, test)), "doc/requirements.md" ); @@ -153,7 +172,7 @@ void testGivenOnlyOutgoingTraceLinkWhenMappingThenItCreatesIncomingLinkOnLinkedI implementation.addLinkToItemWithStatus(requirement, LinkStatus.COVERS); final OftTraceItemNode requirementNode = onlyItem( - mapper.map(trace(implementation, requirement)), + OftTraceTestTreeMapper.map(trace(implementation, requirement)), "doc/requirements.md" ); @@ -165,11 +184,38 @@ void testGivenOnlyOutgoingTraceLinkWhenMappingThenItCreatesIncomingLinkOnLinkedI ); } + @Test + void testGivenDuplicateTraceLinkWhenMappingThenItCreatesNonDirectionalFailedLinksOnBothItems() { + final LinkedSpecificationItem firstRequirement = titledItem( + "req~duplicate_requirement~1", + "doc/first.md", + "First duplicate requirement" + ); + final LinkedSpecificationItem secondRequirement = titledItem( + "req~duplicate_requirement~1", + "doc/second.md", + "Second duplicate requirement" + ); + firstRequirement.addLinkToItemWithStatus(secondRequirement, LinkStatus.DUPLICATE); + + final OftTraceTestTree tree = OftTraceTestTreeMapper.map(trace(firstRequirement, secondRequirement)); + + Assertions.assertAll( + () -> assertThat(onlyItem(tree, "doc/first.md").name(), + is("First duplicate requirement (duplicate)")), + () -> assertThat(onlyItem(tree, "doc/first.md").links().getFirst().name(), + is("↔ Second duplicate requirement (duplicate)")), + () -> assertThat(onlyItem(tree, "doc/second.md").links().getFirst().name(), + is("↔ First duplicate requirement (duplicate)")), + () -> assertThat(tree.failed(), is(true)) + ); + } + // [itest->dsn~trace-test-runner-presentation~1] // [itest->dsn~sort-specification-items-in-test-runner-ui~1] @Test void testGivenUnsortedSpecificationItemsInOneSourceFileWhenMappingThenItSortsItemsByIdParts() { - final OftTraceTestTree tree = mapper.map(trace( + final OftTraceTestTree tree = OftTraceTestTreeMapper.map(trace( item("req~zeta_requirement~1", "doc/requirements.md"), item("impl~zeta_requirement~1", "doc/requirements.md"), item("req~alpha_requirement~2", "doc/requirements.md"), @@ -207,7 +253,7 @@ void testGivenOutgoingOrphanedTraceLinkWhenMappingThenItCreatesFailedItemAndLink final LinkedSpecificationItem missingRequirement = item("req~missing_requirement~1", "doc/requirements.md"); implementation.addLinkToItemWithStatus(missingRequirement, LinkStatus.ORPHANED); - final OftTraceTestTree tree = mapper.map(trace(implementation)); + final OftTraceTestTree tree = OftTraceTestTreeMapper.map(trace(implementation)); final OftTraceItemNode implementationNode = onlyItem( tree, "src/Main.java" @@ -238,7 +284,7 @@ void testGivenUncoveredRequirementWhenMappingThenItCreatesFailedItemNodeWithUnco final LinkedSpecificationItem requirement = item("req~uncovered_requirement~1", "doc/requirements.md", "dsn"); final OftTraceItemNode requirementNode = onlyItem( - mapper.map(trace(requirement)), + OftTraceTestTreeMapper.map(trace(requirement)), "doc/requirements.md" ); @@ -287,6 +333,14 @@ private static LinkedSpecificationItem item( return titledItem(id, locationPath, "", needsArtifactTypes); } + private static LinkedSpecificationItem itemWithoutLocation(final String id) { + return new LinkedSpecificationItem(SpecificationItem.builder() + .id(SpecificationItemId.parseId(id)) + .title("") + .status(ItemStatus.APPROVED) + .build()); + } + private static LinkedSpecificationItem titledItem( final String id, final String locationPath, From 1e0e8629c023259d9ef6cf57fa696938c367cc53 Mon Sep 17 00:00:00 2001 From: redcatbear Date: Tue, 9 Jun 2026 15:39:23 +0200 Subject: [PATCH 10/10] #40: Improved changelog entry. --- doc/changes/changes_0.8.0.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/doc/changes/changes_0.8.0.md b/doc/changes/changes_0.8.0.md index d08f373..e3c508d 100644 --- a/doc/changes/changes_0.8.0.md +++ b/doc/changes/changes_0.8.0.md @@ -1,8 +1,18 @@ # OpenFastTrace IntelliJ Plugin 0.8.0, released 2026-06-09 -With this release of the plugin, OpenFastTrace traces show results in IntelliJ's built-in Test Runner UI by default. +Version 0.7.0 made OpenFastTrace runs configurable, so you could trace only the files, artifact types, or tags that matter for a specific task. Version 0.8.0 makes those trace results much easier to understand and act on. -The global `Tools | OpenFastTrace | Trace Project` action and new or previously unconfigured run configurations use the Test Runner UI to inspect source-file suites, specification-item tests, trace-link sub-tests, pass/fail status, and navigation back to source files, specification declarations, or source-side coverage tags. Run configurations can still opt into the existing plain text output when users prefer the text report. +OpenFastTrace traces now open in IntelliJ's built-in Test Runner UI by default. Instead of scanning a plain text report, you get a structured result tree that groups findings by source file, specification item, and trace link. Clean items show as passed tests, trace defects show as failed tests, and failed source files and the top-level trace result are marked automatically. + +This gives you a faster trace review workflow: + +* see immediately whether the trace passed or where defects remain +* expand a source file to inspect the affected specification items +* inspect incoming and outgoing trace links as sub-tests +* read item IDs, link status, and defect explanations in the test details +* navigate from result nodes back to source files, specification declarations, and source-side coverage tags + +The global `Tools | OpenFastTrace | Trace Project` action and new or previously unconfigured run configurations use the Test Runner UI. Run configurations can still opt into the existing plain text output when you want the raw OpenFastTrace report. ## Features