diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 68e9fbc6..49c7048e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -68,13 +68,6 @@ jobs: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} MAVEN_OPTS: "-Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn" - - name: Publish Test Report for Java ${{ matrix.java }} on ${{ matrix.os }} - uses: scacap/action-surefire-report@v1 - if: ${{ always() && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' }} - with: - report_paths: '**/target/surefire-reports/TEST-*.xml' - github_token: ${{ secrets.GITHUB_TOKEN }} - - name: Archive oft binary uses: actions/upload-artifact@v3 if: ${{ env.DEFAULT_OS == matrix.os && env.DEFAULT_JAVA == matrix.java }} diff --git a/api/src/main/java/org/itsallcode/openfasttrace/api/ColorScheme.java b/api/src/main/java/org/itsallcode/openfasttrace/api/ColorScheme.java new file mode 100644 index 00000000..9793e627 --- /dev/null +++ b/api/src/main/java/org/itsallcode/openfasttrace/api/ColorScheme.java @@ -0,0 +1,13 @@ +package org.itsallcode.openfasttrace.api; + +/** + * Color schemes + */ +public enum ColorScheme { + /** Black and white */ + BLACK_AND_WHITE, + /** Monochrome (e.g for printers) */ + MONOCHROME, + /** COLOR */ + COLOR +} diff --git a/api/src/main/java/org/itsallcode/openfasttrace/api/ReportSettings.java b/api/src/main/java/org/itsallcode/openfasttrace/api/ReportSettings.java index c7feb9f3..53a938fc 100644 --- a/api/src/main/java/org/itsallcode/openfasttrace/api/ReportSettings.java +++ b/api/src/main/java/org/itsallcode/openfasttrace/api/ReportSettings.java @@ -4,6 +4,8 @@ import org.itsallcode.openfasttrace.api.report.ReportConstants; import org.itsallcode.openfasttrace.api.report.ReportVerbosity; +import java.util.Objects; + /** * This class implements a parameter object to control the settings of OFT's * report mode. @@ -14,6 +16,7 @@ public class ReportSettings private final boolean showOrigin; private final String outputFormat; private final Newline newline; + private final ColorScheme colorScheme; private ReportSettings(final Builder builder) { @@ -21,6 +24,7 @@ private ReportSettings(final Builder builder) this.showOrigin = builder.showOrigin; this.outputFormat = builder.outputFormat; this.newline = builder.newline; + this.colorScheme = Objects.requireNonNull(builder.colorScheme); } /** @@ -63,6 +67,15 @@ public Newline getNewline() return this.newline; } + /** + * Get the color scheme + * + * @return color scheme + */ + public ColorScheme getColorScheme() { + return this.colorScheme; + } + /** * Create default report settings * @@ -92,6 +105,7 @@ public static class Builder private String outputFormat = ReportConstants.DEFAULT_REPORT_FORMAT; private boolean showOrigin = false; private ReportVerbosity verbosity = ReportVerbosity.FAILURE_DETAILS; + private ColorScheme colorScheme = ColorScheme.BLACK_AND_WHITE; private Builder() { @@ -160,5 +174,16 @@ public Builder newline(final Newline newline) this.newline = newline; return this; } + + /** + * Set the desired color scheme + * + * @param colorScheme color scheme to use + * @return this for fluent programming + */ + public Builder colorScheme(final ColorScheme colorScheme) { + this.colorScheme = Objects.requireNonNull(colorScheme); + return this; + } } } diff --git a/core/src/main/java/org/itsallcode/openfasttrace/core/cli/ArgumentValidator.java b/core/src/main/java/org/itsallcode/openfasttrace/core/cli/ArgumentValidator.java index 2a1ebe58..25066781 100644 --- a/core/src/main/java/org/itsallcode/openfasttrace/core/cli/ArgumentValidator.java +++ b/core/src/main/java/org/itsallcode/openfasttrace/core/cli/ArgumentValidator.java @@ -40,7 +40,7 @@ private boolean validate() { final Optional command = this.arguments.getCommand(); boolean ok = false; - if (!command.isPresent()) + if (command.isEmpty()) { this.error = "Missing command"; this.suggestion = "Add one of " + listCommands(); @@ -72,7 +72,7 @@ private boolean validateTraceCommand() if (this.arguments.getReportVerbosity() == ReportVerbosity.QUIET && this.arguments.getOutputPath() != null) { - this.error = "combining stream verbosity 'quiet' and ouput to file is not supported."; + this.error = "combining stream verbosity 'quiet' and output to file is not supported."; this.suggestion = "remove output file parameter."; } else @@ -132,5 +132,4 @@ public String getSuggestion() { return this.suggestion; } - } diff --git a/core/src/main/java/org/itsallcode/openfasttrace/core/cli/CliArguments.java b/core/src/main/java/org/itsallcode/openfasttrace/core/cli/CliArguments.java index 4dd6887d..6efdfecf 100644 --- a/core/src/main/java/org/itsallcode/openfasttrace/core/cli/CliArguments.java +++ b/core/src/main/java/org/itsallcode/openfasttrace/core/cli/CliArguments.java @@ -1,11 +1,10 @@ package org.itsallcode.openfasttrace.core.cli; -import static java.util.Arrays.asList; - import java.nio.file.Path; import java.nio.file.Paths; import java.util.*; +import org.itsallcode.openfasttrace.api.ColorScheme; import org.itsallcode.openfasttrace.api.cli.DirectoryService; import org.itsallcode.openfasttrace.api.core.Newline; import org.itsallcode.openfasttrace.api.report.ReportConstants; @@ -37,6 +36,7 @@ public class CliArguments // [impl->dsn~reporting.html.linked-specification-item-origin~1] private boolean showOrigin; private final DirectoryService directoryService; + private ColorScheme colorScheme; /** * Create new {@link CliArguments}. @@ -111,7 +111,7 @@ public List getInputs() { if (this.unnamedValues == null || this.unnamedValues.size() <= 1) { - return asList(this.directoryService.getCurrent()); + return List.of(this.directoryService.getCurrent()); } return this.unnamedValues.subList(1, this.unnamedValues.size()); } @@ -293,6 +293,25 @@ public Set getWantedTags() return this.wantedTags; } + /** + * Get the color scheme. + *

+ * Defaults to {@link ColorScheme#COLOR}. The switch -f overrides this setting, so that the color + * scheme is always {@link ColorScheme#BLACK_AND_WHITE}. + *

+ * + * @return the color scheme + */ + public ColorScheme getColorScheme() + { + if (this.getOutputPath() == null) { + return (this.colorScheme == null) ? ColorScheme.COLOR : this.colorScheme; + } + else { + return ColorScheme.BLACK_AND_WHITE; + } + } + /** * Set a list of tags to be applied as filter during import * @@ -349,4 +368,24 @@ public void setS(final boolean showOrigin) { setShowOrigin(showOrigin); } + + /** + * Choose the color scheme. + * + * @param colorScheme color scheme to use for console output + */ + public void setColorScheme(final ColorScheme colorScheme) + { + this.colorScheme = colorScheme; + } + + /** + * Choose the color scheme. + * + * @param colorScheme color scheme to use for console output + */ + public void setC(final ColorScheme colorScheme) + { + this.setColorScheme(colorScheme); + } } diff --git a/core/src/main/java/org/itsallcode/openfasttrace/core/cli/commands/TraceCommand.java b/core/src/main/java/org/itsallcode/openfasttrace/core/cli/commands/TraceCommand.java index 86625ecc..74401dbd 100644 --- a/core/src/main/java/org/itsallcode/openfasttrace/core/cli/commands/TraceCommand.java +++ b/core/src/main/java/org/itsallcode/openfasttrace/core/cli/commands/TraceCommand.java @@ -71,6 +71,7 @@ private ReportSettings convertCommandLineArgumentsToReportSettings() .verbosity(this.arguments.getReportVerbosity()) // .newline(this.arguments.getNewline()) // .showOrigin(this.arguments.getShowOrigin()) // + .colorScheme(this.arguments.getColorScheme()) // .build(); } } \ No newline at end of file diff --git a/core/src/main/resources/usage.txt b/core/src/main/resources/usage.txt index 04e7fe5f..8ffe112c 100644 --- a/core/src/main/resources/usage.txt +++ b/core/src/main/resources/usage.txt @@ -20,10 +20,14 @@ Converting options: (e.g. file and line number) Common options: - -f, --file path The output file. Defaults to STDOUT. - -n, --newline format Newline format one of "unix", "windows", "oldmac" -a, --wanted-artifact-types Import only specification items contained in the comma-separated list + -c, --color-scheme scheme Color scheme for output. One of "black-and-white", + "monochrome", "color". Defaults to "color". + Note that this option is ignored when -f is also + set. + -f, --file path The output file. Defaults to STDOUT. + -n, --newline format Newline format. One of "unix", "windows", "oldmac" -t, --wanted-tags Import only specification items that have at least one tag contained in the comma-separated list. Add a single underscore as first item in diff --git a/core/src/test/java/org/itsallcode/openfasttrace/core/cli/TestArgumentValidator.java b/core/src/test/java/org/itsallcode/openfasttrace/core/cli/TestArgumentValidator.java index f61a6341..e9f8d13b 100644 --- a/core/src/test/java/org/itsallcode/openfasttrace/core/cli/TestArgumentValidator.java +++ b/core/src/test/java/org/itsallcode/openfasttrace/core/cli/TestArgumentValidator.java @@ -54,7 +54,7 @@ void testTraceCommandQuietAndOutputFileGivenIsNotValid() cliArgs.setV(ReportVerbosity.QUIET); cliArgs.setOutputFile("outputFile"); assertValidatorResult( - "combining stream verbosity 'quiet' and ouput to file is not supported.", + "combining stream verbosity 'quiet' and output to file is not supported.", "remove output file parameter."); } diff --git a/core/src/test/java/org/itsallcode/openfasttrace/core/cli/TestCliArguments.java b/core/src/test/java/org/itsallcode/openfasttrace/core/cli/TestCliArguments.java index 3cc74a06..1a0904eb 100644 --- a/core/src/test/java/org/itsallcode/openfasttrace/core/cli/TestCliArguments.java +++ b/core/src/test/java/org/itsallcode/openfasttrace/core/cli/TestCliArguments.java @@ -1,12 +1,13 @@ package org.itsallcode.openfasttrace.core.cli; -import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; import java.nio.file.Paths; +import java.util.List; +import org.itsallcode.openfasttrace.api.ColorScheme; import org.itsallcode.openfasttrace.api.core.Newline; import org.itsallcode.openfasttrace.api.report.ReportConstants; import org.itsallcode.openfasttrace.api.report.ReportVerbosity; @@ -54,7 +55,7 @@ void testSetOutputFormat() @Test void getStandardOutputFormatForExport() { - this.arguments.setUnnamedValues(asList(ConvertCommand.COMMAND_NAME)); + this.arguments.setUnnamedValues(List.of(ConvertCommand.COMMAND_NAME)); assertThat(this.arguments.getOutputFormat(), equalTo(ExporterConstants.DEFAULT_OUTPUT_FORMAT)); } @@ -63,7 +64,7 @@ void getStandardOutputFormatForExport() @Test void getStandardOutputFormatForReport() { - this.arguments.setUnnamedValues(asList(TraceCommand.COMMAND_NAME)); + this.arguments.setUnnamedValues(List.of(TraceCommand.COMMAND_NAME)); assertThat(this.arguments.getOutputFormat(), equalTo(ReportConstants.DEFAULT_REPORT_FORMAT)); } @@ -146,7 +147,7 @@ void testWantedArtifactTypesEmptyByDefault() // [utest->dsn~filtering-by-artifact-types-during-import~1] @Test - void testSetIngnoreArtifactTypes() + void testSetIgnoreArtifactTypes() { final String value = "impl,utest"; this.arguments.setWantedArtifactTypes(value); @@ -183,7 +184,7 @@ void testSetWantedTags() // [utest->dsn~filtering-by-tags-during-import~1] @Test - void testSetd() + void testSetD() { final String value = "client,server"; this.arguments.setT(value); @@ -232,4 +233,37 @@ void testSetS() this.arguments.setS(true); assertThat(this.arguments.getShowOrigin(), is(true)); } + + // [dsn~reporting.plain-text.ansi-color~1] + // [dsn~reporting.plain-text.ansi-font-style~1] + @Test + void testSetColorScheme() + { + this.arguments.setColorScheme(ColorScheme.MONOCHROME); + assertThat(this.arguments.getColorScheme(), is(ColorScheme.MONOCHROME)); + } + + // [dsn~reporting.plain-text.ansi-color~1] + // [dsn~reporting.plain-text.ansi-font-style~1] + @Test + void testSetC() + { + this.arguments.setC(ColorScheme.MONOCHROME); + assertThat(this.arguments.getColorScheme(), is(ColorScheme.MONOCHROME)); + } + + + @Test + void testColorSchemeDefaultsToColor() + { + assertThat(this.arguments.getColorScheme(), is(ColorScheme.COLOR)); + } + + @Test + void testSetOutputFileOverridesColorSchemeSetting() + { + this.arguments.setColorScheme(ColorScheme.MONOCHROME); + this.arguments.setOutputFile("something"); + assertThat(this.arguments.getColorScheme(), is(ColorScheme.BLACK_AND_WHITE)); + } } \ No newline at end of file diff --git a/doc/spec/design.md b/doc/spec/design.md index b3bb6f1d..4716fc9b 100644 --- a/doc/spec/design.md +++ b/doc/spec/design.md @@ -361,6 +361,29 @@ Covers: Needs: impl, utest +### Plain Text Report ANSI Color +`dsn~reporting.plain-text.ansi-color~1` + +The plain text report uses ANSI escape sequences to color the output. + +Covers: + +* `req~colored-plain-text-report~1` + +Needs: impl, utest + +### Plain Text Report ANSI Font Style +`dsn~reporting.plain-text.ansi-font-style~1` + +The plain text report uses ANSI escape sequences to modify the font style of the output. + +Covers: + +* `req~colored-plain-text-report~1` +* `req~monochrome-plain-text-report-with-font-style~1` + +Needs: impl, utest + ### HTML Report #### HTML Report Inlines CSS diff --git a/doc/spec/system_requirements.md b/doc/spec/system_requirements.md index 93626ead..a503a697 100644 --- a/doc/spec/system_requirements.md +++ b/doc/spec/system_requirements.md @@ -119,7 +119,9 @@ Needs: req A tracing report is a representation of the results of the requirement tracing OFT performs. Depending on their use, reports can be designed to be human readable, machine readable or both. -#### Plain Text Report +#### Console Reports + +##### Plain Text Report `feat~plain-text-report~1` OFT produces a tracing report in plain text. @@ -321,7 +323,7 @@ The possible results are: 2. Outdated: link points to a specification item which has a higher revision number 3. Predated: link points to a specification item which has a lower revision number 4. Ambiguous: link points to a specification item that has duplicates - 5. Unwanted: coverage provider has an artifact type the provider does not want + 5. Unwanted: coverage provider has an artifact type the requester does not want 6. Orphaned: link is broken - there is no matching coverage requester Covers: @@ -528,6 +530,36 @@ Covers: Needs: dsn +##### Monochrome Plain Text Report With Font Style +`req~monochrome-plain-text-report-with-font-style~1` + +The plain text report supports different font styles to visually separate report elements. + +Rationale: + +This makes the report easier to read and works for people who are colorblind. + +Covers: + +* [feat~plain-text-report~1](#plain-text-report) + +Needs: dsn + +##### Colored Plain Text Report +`req~colored-plain-text-report~1` + +The plain text report supports color to visually separate report elements. + +Rationale: + +This makes the report easier to read. + +Covers: + +* [feat~plain-text-report~1](#plain-text-report) + +Needs: dsn + #### HTML Report ##### HTML Report is a Single File diff --git a/doc/user_guide.md b/doc/user_guide.md index 09e3c3a3..f5226ffd 100644 --- a/doc/user_guide.md +++ b/doc/user_guide.md @@ -5,11 +5,11 @@ ## In a Nutshell OFT is a requirement tracing tool. It helps you make sure that all defined requirements are covered in your code. It -also helps finding outdated code passages. +also helps you find outdated code passages. 1. Create requirement and specification documents in Markdown including OFT-readable specification items -1. Put tags into your source code that mark the coverage of items from the specification -1. Use OFT to trace the requirements from the source to the final implementation +2. Put tags into your source code that mark the coverage of items from the specification +3. Use OFT to trace the requirements from the source to the final implementation ## Introduction @@ -36,7 +36,7 @@ Of course requirement engineering and tracing are useful outside the software do ### What is Requirement Tracing? -OpenFastTrace is a requirement tracing suite. Requirement tracing helps you keeping track of whether you actually implemented everything you planned to in your specifications. It also identifies obsolete parts of your product and helps you getting rid of them. +OpenFastTrace is a requirement tracing suite. Requirement tracing helps you to keep track of whether you actually implemented everything you planned to in your specifications. It also identifies obsolete parts of your product and helps you to get rid of them. The foundation of all requirement tracing are links between documents, implementation, test, reports and whatever other artifacts your product consists of. @@ -133,11 +133,11 @@ If on the other hand you only added a missing period at the end of a sentence, t #### Informative Passages -Informative passages of a specification provide explanations and context that is necessary for understanding the subject matter. They do not require coverage though. +Informative passages of a specification provide explanations and context that is necessary for understanding the subject. They do not require coverage though. #### Normative Passages -Normative passages contain requirements (or in OFT terms ["specification items"](#specification-item). Unlike [informative passages](#informative-passages) they require that someone details, implements or verifies the contained specification items. +Normative passages contain requirements (or in OFT terms ["specification items"](#specification-item)). Unlike [informative passages](#informative-passages) they require that someone details, implements or verifies the contained specification items. #### Coverage @@ -176,7 +176,7 @@ Let's start with a minimal requirement: This is the description of the requirement. -Simple as this. This is already a valid and complete OFT requirement. Of course you can enrich the requirement with other information but at the heart of it every requirement is an ID and a description. +Simple as this. This is already a valid and complete OFT requirement. Of course, you can enrich the requirement with other information but at the heart of it every requirement is an ID and a description. It is mostly a matter of taste whether you prefer your specification items to have a title or not. The same requirement above with a title looks like this: @@ -200,7 +200,7 @@ At the moment the specification item above is a [terminating item](#terminating- Now the item must be covered in the design ("dsn") and user manual ("uman"). Remember you can introduce your own artifact types depending on the needs of your project. -Of course you can embed specification items into normal Markdown text. This adds the necessary [informative](#informative-passages) context that is required to understand the [normative passages](#normative-passages). +Of course, you can embed specification items into normal Markdown text. This adds the necessary [informative](#informative-passages) context that is required to understand the [normative passages](#normative-passages). # ACME portable hole @@ -314,7 +314,7 @@ Tags are described in detail later in this document. Consider a situation where you are responsible for the high-level software architecture of your project. You define the component breakdown, the interfaces and the interworking of the components. You get your requirements from a system requirement specification, but it turns out many of those incoming requirements are at a detail level that does not require design decisions on inter-component-level but rather affects the internals of a single component. -In those cases it would be a waste of time to repeat the original requirement in your architecture just to hand them down to the detailed design of a component. Instead what you need is a fast way to express "yes, I read that requirement and I am sure it does not need design decisions in the high-level architecture." +In those cases it would be a waste of time to repeat the original requirement in your architecture just to hand them down to the detailed design of a component. Instead, what you need is a fast way to express "yes, I read that requirement, and I am sure it does not need design decisions in the high-level architecture." To achieve this OFT features a shorthand notation for delegating the job of covering a specification item to one or more different artifact types. @@ -324,7 +324,7 @@ In the following example a requirement in the system requirement specification ( ### Distributing the Detailing Work -In projects of a certain size you always reach the point where a single team is not enough to process the workload. As a consequence the teams must find a way to distribute the work. A popular approach is splitting the architecture into components that are as independent as possible. Each team is then responsible for one or more distinct components. While the act of assigning the work should never be done inside of the specification, at least the specification can prepare criteria on which to split the work. +In projects of a certain size you always reach the point where a single team is not enough to process the workload. As a consequence the teams must find a way to distribute the work. A popular approach is splitting the architecture into components that are as independent as possible. Each team is then responsible for one or more distinct components. While the act of assigning the work should never be done inside the specification, at least the specification can prepare criteria on which to split the work. One proven way to do this is to use tags. The teams then decide for which specification items with which tags they are responsible. @@ -397,7 +397,7 @@ oft trace doc src/main/java src/test/java The first variant is better suited for integration into scripts where you usually want to avoid changing the directory. -By default this will produce a plain text trace that displays details of all defect specification items and a summary. +By default, this will produce a plain text trace that displays details of all defect specification items and a summary. See also: * [Tracing Options](#tracing-options) for controlling the tracing output @@ -491,6 +491,21 @@ Newline format, one of Defaults to the platform standard if not given. +You can change the output color scheme. + + -c, --color= + +The available color schemes are + +`black-and-white` +: Plain black and white. On the console this also means no font styles used. + +`monochrome` +: Black, white and shades of grey. Also enables font style on the console. + +`color` +: Color output. Also enables font style on the console. + ### Input Format Support #### Tags in Programming Language or Markup Files @@ -511,6 +526,9 @@ Here is an example of a tag embedded into a Java comment: ```java // [impl->dsn~validate-authentication-request~1] +private validate(final AuthenticationRequest request){ + // ... +} ``` When using UML models as design document files like UML models it is useful to add needed coverage as well. To do this, you can use the following format: @@ -521,8 +539,9 @@ When using UML models as design document files like UML models it is useful to a Example: -```plantuml +``` ' [dsn->req~1password-login~1>>impl,test] +user -> system : login(token: OAuthToken) ``` The Tag Importer recognizes the supported format by the file extension. The following list shows the standard set of @@ -588,95 +607,71 @@ The Console Tracing Report is the standard report format of OFT. Its main purpos Below you see a typical example of a requirement from a design document. - ok - 0/2>0>0/1 - dsn~cli.tracing.default-format~1 (impl, utest) - | - | The CLI uses plain text as requirement tracing report format if none is given as a parameter. - | - |<-- ( ) impl~cli.tracing.default-format-2215031703~0 - |--> ( ) req~cli.tracing.default-output-format~1 - |<-- ( ) utest~cli.tracing.default-format-3750270139~0 + ok [ in: 2 / 2 ✔ | out: 1 / 1 ✔ ] dsn~cli.tracing.default-format~1 (impl, utest) + + The CLI uses plain text as requirement tracing report format if none is given as a parameter. + + [covered shallow ] ← impl~cli.tracing.default-format-2215031703~0 + [covers ] → req~cli.tracing.default-output-format~1 + [covered shallow ] ← utest~cli.tracing.default-format-3750270139~0 Let's go through its elements one by one. The first line is the summary. -It starts with the status of the requirement — ok in this case. +It starts with the status of the requirement — OK in this case. -> **ok** - 0/2>0>0/1 - `dsn~cli.tracing.default-format~1` (impl, utest) +> **ok** [ in: 2 / 2 ✔ | out: 1 / 1 ✔ ] dsn~cli.tracing.default-format~1 (impl, utest) Next we have a couple of numbers. -The first pair shows how many of the broken incoming links this requirement has (zero), and how many in total (two). +The first pair shows how many of the incoming good links this requirement has (two), and how many in total (two). -> ok - **0/2**>0>0/1 - `dsn~cli.tracing.default-format~1` (impl, utest) +> ok [ **in: 2 / 2 ✔** | out: 1 / 1 ✔ ] dsn~cli.tracing.default-format~1 (impl, utest) -The number in the middle tells you how many duplicates this requirement has (also zero). +Consequently, the next pair informs you how many (one) of the overall (one) outgoing links are good. -> ok - 0/2>**0**>0/1 - `dsn~cli.tracing.default-format~1` (impl, utest) +Please note that OFT cannot predict the exact number of required incoming links, because often we are talking about one-to-many relations. So OFT does not try to. The checkmark and crossmark in the square brackets are only a quick indicator of if the existing links are okay. This goes so far that in case of zero links, no mark is displayed at all. -Consequently the next pair informs you how many (zero) of the overall (one) outgoing links are broken. - -> ok - 0/2>0>**0/1** - `dsn~cli.tracing.default-format~1` (impl, utest) +> ok [ in: 2 / 2 ✔ | **out: 1 / 1 ✔** ] dsn~cli.tracing.default-format~1 (impl, utest) The [Specification Item ID](#specification-item-id) in the middle is the unique technical ID of this requirement. -> ok - 0/2>0>0/1 - **`dsn~cli.tracing.default-format~1`** (impl, utest) +> ok [ in: 2 / 2 ✔ | out: 1 / 1 ✔ ] **dsn~cli.tracing.default-format~1** (impl, utest) In the brackets you find, which artifact types this item expects as coverage. If the type is covered correctly, you see just the name there. -> ok - 0/2>0>0/1 - `dsn~cli.tracing.default-format~1` (**impl, utest**) +> ok [ in: 2 / 2 ✔ | out: 1 / 1 ✔ ] dsn~cli.tracing.default-format~1 (**impl, utest**) If it is not covered, the name is lead in by a minus: -> not ok - 0/2>0>0/1 - `dsn~cli.tracing.default-format~1` (**-impl**, utest) - +> **not ok** … (**-impl**, utest) + If an artifact type provides coverage that is not requested, you find this indicated with a plus in front. -> not ok - 0/2>0>0/1 - `dsn~cli.tracing.default-format~1` (impl, **+itest**, utest) +> **not ok** … (impl, **+itest**, utest) -Everything after that line is details of the requirement. A bar symbol in the first position indicates this. Everything -that does not start with an arrow symbol is part of the description. +If there were any other specification objects defined with the same ID, you would see the following at the end of the summary line: - | - | The CLI uses plain text as requirement tracing report format if none is given as a parameter. - | - -The section with the arrows provides details about incoming and outgoing links. Arrows pointing to the bar (i.e. to the left) are incoming links, arrows pointing away from the bar are outgoing. +> [has 3 duplicates] -The following line means that this design requirement is covered in the implementation. +Everything after that line are details of the requirement. Indented text indicates this. The first part of the details is the description. ->|**<--** ( ) **`impl`**`~cli.tracing.default-format-2215031703~0` + The CLI uses plain text as requirement tracing report format if none is given as a parameter. -The ID of the implementation comes from the Tag Importer and is for it's most part auto-generated. The artifact type `dsn` is simply replaced by `impl` here and a number is attached for disambiguation. +The section with the arrows provides details about incoming and outgoing links. Arrows pointing to the left are incoming links, arrows pointing to the right are outgoing. You can easily remember this, since the arrows either point towards the ID of the connected specification item or away from it. ->|<-- ( ) **`impl`**`~cli.tracing.default-format-`**`2215031703~0`** - -In the brackets you find the status of the link. Empty brackets are good because they mean that the link is okay. Any other character indicates a broken link. - -The following incoming link statuses exist: +The following line means that this design requirement is covered in the implementation. -| Symbol | Link Status | -|--------|-------------------| -| | Covered shallow | -| + | Covered unwanted | -| > | Covered predated | -| < | Covered outdated | +> [covered shallow ] ← impl~cli.tracing.default-format-2215031703~0 -Outgoing links can have one of the following statuses: +The ID of the implementation comes from the Tag Importer and is for its most part auto-generated. The artifact type `dsn` is simply replaced by `impl` here and a number is attached for disambiguation. -| Symbol | Link Status | -|--------|-------------------| -| | Covers | -| > | Predated | -| < | Outdated | -| + | Unwanted | -| / | Orphaned | +> [covered shallow ] ← **impl**~cli.tracing.default-format-**2215031703**~0 -Finally duplicate links can exist. They have no real direction. the closest match would be a bi-directional link. +In the square brackets you find the status of the link. -| Symbol | Link Status | -|--------|-------------------| -| ? | Duplicate | +Just in case you are wondering about the extra spaces in some places of the report, those exist as padding to align multiple similar items in lists. ## XML Tracing Report @@ -798,7 +793,7 @@ The element `` describes all covering requirements: 1 dsn COVERED - COVERED + COVERED COVERING ... @@ -873,7 +868,7 @@ or run a report. #### Converting File from Java -The following example code use a OFT as a converter that scans the current working directory recursively (default import setting) and exports the found artifacts with the standard settings to a ReqM2 file. +The following example code use OFT as a converter that scans the current working directory recursively (default import setting) and exports the found artifacts with the standard settings to a ReqM2 file. ```JAVA import org.itsallcode.openfasttrace.Oft; @@ -964,10 +959,9 @@ oft.reportToPath(trace, reportPath, reportSettings); oft.reportToStdOut(trace); ``` - #### Configuring the Steps -Import, export and report each have a overloaded variant that can be configured using the following classes +Import, export and report each have an overloaded variant that can be configured using the following classes * [org.itsallcode.openfasttrace.api.importer.ImportSettings](../api/src/main/java/org/itsallcode/openfasttrace/api/importer/ImportSettings.java) * [org.itsallcode.openfasttrace.core.ExportSettings](../core/src/main/java/org/itsallcode/openfasttrace/core/ExportSettings.java) @@ -993,10 +987,11 @@ The OFT command line interface returns the following exit codes: The following editors and integrated development environments are well suited for authoring OFT documents. The list is not exhaustive, any editor with Markdown capabilities can be used. -Editor / IDE | Syntax highl. | Preview | Outline | HTML export ----------------------------------------------------------|---------------|---------|---------|------------ -[Gedit](https://wiki.gnome.org/Apps/Gedit) | y | | | -[Eclipse](https://eclipse.org) with WikiText plug-in | y | y | y | y -[Eclipse](https://eclipse.org) with GMF plug-in | | y | | -[Vim](https://www.vim.org/) | y | | | -[Visual Studio Code](https://code.visualstudio.com/) | y | y | y | +| Editor / IDE | Syntax highl. | Preview | Outline | HTML export | +|--------------------------------------------------------|---------------|---------|---------|-------------| +| [Gedit](https://wiki.gnome.org/Apps/Gedit) | y | | | | +| [Eclipse](https://eclipse.org) with WikiText plug-in | y | y | y | y | +| [Eclipse](https://eclipse.org) with GMF plug-in | | y | | | +| [IntelliJ](https://www.jetbrains.com/idea/) | y | y | y | y | +| [Vim](https://www.vim.org/) | y | | | | +| [Visual Studio Code](https://code.visualstudio.com/) | y | y | y | | diff --git a/importer/specobject/src/main/java/org/itsallcode/openfasttrace/importer/specobject/handler/SpecDocumentHandlerBuilder.java b/importer/specobject/src/main/java/org/itsallcode/openfasttrace/importer/specobject/handler/SpecDocumentHandlerBuilder.java index e0fe0a80..01667160 100644 --- a/importer/specobject/src/main/java/org/itsallcode/openfasttrace/importer/specobject/handler/SpecDocumentHandlerBuilder.java +++ b/importer/specobject/src/main/java/org/itsallcode/openfasttrace/importer/specobject/handler/SpecDocumentHandlerBuilder.java @@ -46,11 +46,10 @@ public TreeContentHandler build() this.handler.setDefaultStartElementListener(startElement -> { if (startElement.isRootElement()) { - LOG.info(() -> "Found unknown root element " + startElement + ": skip file"); + LOG.fine(() -> "Found unknown root element '" + startElement + "': skip file"); this.handler.stopParsing(); } LOG.warning(() -> "Found unknown element " + startElement); - return; }); this.handler.addElementListener("specdocument", elem -> { diff --git a/product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliStarter.java b/product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliStarter.java index 97f7ae8b..f5dfdfba 100644 --- a/product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliStarter.java +++ b/product/src/test/java/org/itsallcode/openfasttrace/cli/TestCliStarter.java @@ -37,6 +37,7 @@ class TestCliStarter private static final String REPORT_VERBOSITY_PARAMETER = "--report-verbosity"; private static final String OUTPUT_FORMAT_PARAMETER = "--output-format"; private static final String WANTED_ARTIFACT_TYPES_PARAMETER = "--wanted-artifact-types"; + private static final String COLOR_SCHEME_PARAMETER = "--color-scheme"; private static final String CARRIAGE_RETURN = "\r"; private static final String NEWLINE = "\n"; @@ -115,7 +116,8 @@ void testConvertToSpecobjectFile() throws IOException final Runnable runnable = () -> runCliStarter( // CONVERT_COMMAND, this.DOC_DIR.toString(), // OUTPUT_FORMAT_PARAMETER, "specobject", // - OUTPUT_FILE_PARAMETER, this.outputFile.toString()); + OUTPUT_FILE_PARAMETER, this.outputFile.toString(), // + COLOR_SCHEME_PARAMETER, "BLACK_AND_WHITE"); assertExitOkWithOutputFileStart(runnable, SPECOBJECT_PREAMBLE + "\n runCliStarter( // TRACE_COMMAND, this.DOC_DIR.toString(), // OUTPUT_FILE_PARAMETER, this.outputFile.toString(), // - WANTED_ARTIFACT_TYPES_PARAMETER, "feat,req" - // + WANTED_ARTIFACT_TYPES_PARAMETER, "feat,req" // ); assertExitOkWithOutputFileStart(runnable, "ok - 3 total"); } @@ -381,7 +382,7 @@ private String getOutputFileContent() } catch (final IOException exception) { - // Need to convert this to an unchecked exception. Otherwise we get + // Need to convert this to an unchecked exception. Otherwise, we get // stuck with the checked exceptions in the assertion lambdas. throw new RuntimeException(exception); } diff --git a/reporter/plaintext/src/main/java/org/itsallcode/openfasttrace/report/plaintext/AnsiSequence.java b/reporter/plaintext/src/main/java/org/itsallcode/openfasttrace/report/plaintext/AnsiSequence.java new file mode 100644 index 00000000..682a613e --- /dev/null +++ b/reporter/plaintext/src/main/java/org/itsallcode/openfasttrace/report/plaintext/AnsiSequence.java @@ -0,0 +1,76 @@ +package org.itsallcode.openfasttrace.report.plaintext; + +/** + * ANSI console font effect sequences + */ +// [impl->dsn~reporting.plain-text.ansi-color~1] +// [impl-> dsn~reporting.plain-text.ansi-font-style~1] +enum AnsiSequence { + /** Reset all font effects */ + RESET(0), + /** Bold font */ + BOLD(1), + /** Italic font */ + ITALIC(3), + /** Underlined */ + UNDERLINE(4), + /** Inverted foreground and background color */ + INVERSE(7), + /** Black */ + BLACK(30), + /** Red */ + RED(31), + /** Green */ + GREEN(32), + /** Yellow */ + YELLOW(33), + /** Blue */ + BLUE(34), + /** Magenta */ + MAGENTA(35), + /** Cyan */ + CYAN(36), + /** White */ + WHITE(37), + /** Bright Red */ + BRIGHT_RED(91); + + public static final String PREFIX = "\u001B["; + public static final String SUFFIX = "m"; + private final int id; + + private AnsiSequence(final int id) { + this.id = id; + } + + @Override + public String toString() { + return PREFIX + id + SUFFIX; + } + + /** + * Get the ID of the ANSI sequence. + * @return ANSI sequence ID + */ + public int getId() { + return this.id; + } + + /** + * Combine the given ANSI sequences. + * + * @param ids IDs of the font effects that should be combined + * @return sequence that represents the combined font effect. + */ + public static String combine(AnsiSequence ... ids) { + StringBuilder builder = new StringBuilder(PREFIX); + for(int i = 0; i < ids.length ; ++i) { + if (i > 0) { + builder.append(";"); + } + builder.append(ids[i].id); + } + builder.append(SUFFIX); + return builder.toString(); + } +} diff --git a/reporter/plaintext/src/main/java/org/itsallcode/openfasttrace/report/plaintext/ConsoleColorFormatter.java b/reporter/plaintext/src/main/java/org/itsallcode/openfasttrace/report/plaintext/ConsoleColorFormatter.java new file mode 100644 index 00000000..61bc3663 --- /dev/null +++ b/reporter/plaintext/src/main/java/org/itsallcode/openfasttrace/report/plaintext/ConsoleColorFormatter.java @@ -0,0 +1,31 @@ +package org.itsallcode.openfasttrace.report.plaintext; + +import static org.itsallcode.openfasttrace.report.plaintext.AnsiSequence.*; + +/** + * Formatter that uses ANSI code sequences to color the output. + */ +// [impl->dsn~reporting.plain-text.ansi-color~1] +final class ConsoleColorFormatter implements TextFormatter { + /** + * Create a new instance of a {@link ConsoleColorFormatter}. + */ + public ConsoleColorFormatter() { + // Added for JavaDoc. + } + + @Override + public String formatOk(final String text) { + return GREEN + text + RESET; + } + + @Override + public String formatNotOk(final String text) { + return BRIGHT_RED + text + RESET; + } + + @Override + public String formatStrong(final String text) { + return AnsiSequence.combine(BOLD, CYAN) + text + RESET; + } +} \ No newline at end of file diff --git a/reporter/plaintext/src/main/java/org/itsallcode/openfasttrace/report/plaintext/MonochromeTextFormatter.java b/reporter/plaintext/src/main/java/org/itsallcode/openfasttrace/report/plaintext/MonochromeTextFormatter.java new file mode 100644 index 00000000..78681223 --- /dev/null +++ b/reporter/plaintext/src/main/java/org/itsallcode/openfasttrace/report/plaintext/MonochromeTextFormatter.java @@ -0,0 +1,35 @@ +package org.itsallcode.openfasttrace.report.plaintext; + +import static org.itsallcode.openfasttrace.report.plaintext.AnsiSequence.*; + +/** + * Formatter that only uses font weight and style + *

+ * Useful for people who cannot distinguish colors or terminals that don't display colors or if colors are hard to + * discern with a console color scheme. + *

+ */ +// [impl->dsn~reporting.plain-text.ansi-font-style~1] +public class MonochromeTextFormatter implements TextFormatter { + /** + * Create a new instance of a {@link MonochromeTextFormatter}. + */ + public MonochromeTextFormatter() { + // Added for JavaDoc. + } + + @Override + public String formatOk(final String text) { + return text; + } + + @Override + public String formatNotOk(final String text) { + return INVERSE + text + RESET; + } + + @Override + public String formatStrong(final String text) { + return BOLD + text + RESET; + } +} diff --git a/reporter/plaintext/src/main/java/org/itsallcode/openfasttrace/report/plaintext/NullTextFormatter.java b/reporter/plaintext/src/main/java/org/itsallcode/openfasttrace/report/plaintext/NullTextFormatter.java new file mode 100644 index 00000000..36771e76 --- /dev/null +++ b/reporter/plaintext/src/main/java/org/itsallcode/openfasttrace/report/plaintext/NullTextFormatter.java @@ -0,0 +1,31 @@ +package org.itsallcode.openfasttrace.report.plaintext; + +/** + * Pseudo text formatter that returns everything exactly as it was given. + *

+ * This is useful for reports that are piped into other tools or written to files. + *

+ */ +class NullTextFormatter implements TextFormatter { + /** + * Create a new instance of a {@link NullTextFormatter}. + */ + public NullTextFormatter() { + // Added for JavaDoc. + } + + @Override + public String formatOk(final String text) { + return text; + } + + @Override + public String formatNotOk(final String text) { + return text; + } + + @Override + public String formatStrong(final String text) { + return text; + } +} \ No newline at end of file diff --git a/reporter/plaintext/src/main/java/org/itsallcode/openfasttrace/report/plaintext/PlainTextReport.java b/reporter/plaintext/src/main/java/org/itsallcode/openfasttrace/report/plaintext/PlainTextReport.java index bcc91c6a..16250bc5 100644 --- a/reporter/plaintext/src/main/java/org/itsallcode/openfasttrace/report/plaintext/PlainTextReport.java +++ b/reporter/plaintext/src/main/java/org/itsallcode/openfasttrace/report/plaintext/PlainTextReport.java @@ -2,7 +2,6 @@ import java.io.OutputStream; import java.io.PrintStream; -import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Collections; @@ -13,7 +12,6 @@ import org.itsallcode.openfasttrace.api.ReportSettings; import org.itsallcode.openfasttrace.api.core.*; -import org.itsallcode.openfasttrace.api.report.ReportException; import org.itsallcode.openfasttrace.api.report.Reportable; /** @@ -27,6 +25,7 @@ public class PlainTextReport implements Reportable .comparing(LinkedSpecificationItem::getId); private int nonEmptySections = 0; private final ReportSettings settings; + private final TextFormatter formatter; /** * Create a new instance of {@link PlainTextReport} @@ -41,20 +40,17 @@ public PlainTextReport(final Trace trace, final ReportSettings settings) { this.trace = trace; this.settings = settings; + this.formatter = TextFormatterFactory.createFormatter(settings.getColorScheme()); } @Override public void renderToStream(final OutputStream outputStream) { final Charset charset = StandardCharsets.UTF_8; - try (final PrintStream report = new PrintStream(outputStream, false, charset.displayName())) + try (final PrintStream report = new PrintStream(outputStream, false, charset)) { renderToPrintStream(report); } - catch (final UnsupportedEncodingException e) - { - throw new ReportException("Encoding charset '" + charset + "' not supported", e); - } } private void renderToPrintStream(final PrintStream report) @@ -109,7 +105,7 @@ private void renderResultStatus(final PrintStream report) private String translateStatus(final boolean ok) { - return ok ? "ok" : "not ok"; + return ok ? this.formatter.formatOk("ok") : this.formatter.formatNotOk("not ok"); } // [impl->dsn~reporting.plain-text.summary~2] @@ -149,13 +145,12 @@ private void renderFailureSummaries(final PrintStream report) private void renderItemSummary(final PrintStream report, final LinkedSpecificationItem item) { report.print(translateStatus(!item.isDefect())); - report.print(" - "); renderItemLinkCounts(report, item); - report.print(" - "); - report.print(item.getId().toString()); + report.print(this.formatter.formatStrong(item.getId().toString())); report.print(" "); renderMaturity(report, item); report.print(translateArtifactTypeCoverage(item)); + renderDuplicatesCount(report, item); report.print(this.settings.getNewline()); } @@ -164,13 +159,13 @@ private String translateArtifactTypeCoverage(final LinkedSpecificationItem item) final Comparator byTypeName = Comparator.comparing(a -> a.replaceFirst("[-+]", "")); final Stream uncoveredStream = item.getUncoveredArtifactTypes().stream() - .map(x -> "-" + x); + .map(x -> this.formatter.formatNotOk("-" + x)); return "(" + Stream.concat( // Stream.concat( // uncoveredStream, // - item.getCoveredArtifactTypes().stream() // + item.getCoveredArtifactTypes().stream().map(this.formatter::formatOk) // ), // - item.getOverCoveredArtifactTypes().stream().map(x -> "+" + x) // + item.getOverCoveredArtifactTypes().stream().map(x -> this.formatter.formatNotOk("+" + x)) // ) // .sorted(byTypeName) // .collect(Collectors.joining(", ")) + ")"; @@ -178,15 +173,36 @@ private String translateArtifactTypeCoverage(final LinkedSpecificationItem item) private void renderItemLinkCounts(final PrintStream report, final LinkedSpecificationItem item) { - report.print(item.countIncomingBadLinks()); - report.print("/"); - report.print(item.countIncomingLinks()); - report.print(">"); - report.print(item.countDuplicateLinks()); - report.print(">"); - report.print(item.countOutgoingBadLinks()); - report.print("/"); - report.print(item.countOutgoingLinks()); + final int incomingLinks = item.countIncomingLinks(); + final int incomingBadLinks = item.countIncomingBadLinks(); + final int incomingGoodLinks = incomingLinks - incomingBadLinks; + final int outgoingLinks = item.countOutgoingLinks(); + final int outgoingBadLinks = item.countOutgoingBadLinks(); + final int outgoingGoodLinks = outgoingLinks - outgoingBadLinks; + report.print(" [ in: "); + report.print(formatCountXofY(incomingGoodLinks, incomingLinks)); + report.print(" | out: "); + report.print(formatCountXofY(outgoingGoodLinks, outgoingLinks)); + report.print(" ] "); + } + + private void renderDuplicatesCount(final PrintStream report, final LinkedSpecificationItem item) { + final int duplicateLinks = item.countDuplicateLinks(); + if(duplicateLinks != 0) { + report.print(" [has "); + report.print(this.formatter.formatNotOk(duplicateLinks + " duplicate" + (duplicateLinks > 1 ? "s" : ""))); + report.print("]"); + } + } + + private String formatCountXofY(final int countGood, final int count) { + if((countGood == 0) && (count == 0)) { + return " 0 / 0 "; + } else { + return (countGood == count) + ? this.formatter.formatOk(String.format("%2d / %2d ✔", countGood, count)) + : this.formatter.formatNotOk(String.format("%2d / %2d ✘", countGood, count)); + } } private void renderMaturity(final PrintStream report, final LinkedSpecificationItem item) @@ -239,7 +255,6 @@ private void renderOrigin(final PrintStream report, final Location location) private void renderEmptyItemDetailsLine(final PrintStream report) { - report.print("|"); report.print(this.settings.getNewline()); } @@ -251,7 +266,7 @@ private void renderDescription(final PrintStream report, final LinkedSpecificati renderEmptyItemDetailsLine(report); for (final String line : description.split(Newline.anyNewlineReqEx())) { - report.print("| "); + report.print(" "); report.print(line); report.print(this.settings.getNewline()); } @@ -284,9 +299,14 @@ private void renderLink(final PrintStream report, final TracedLink link, final boolean showOrigin) { final LinkStatus status = link.getStatus(); - report.print(status.isIncoming() ? "|<-- (" : "|--> ("); - report.print(status.getShortTag()); - report.print(") "); + report.print(" ["); + if(status == LinkStatus.COVERS || (status == LinkStatus.COVERED_SHALLOW)) { + report.print(formatter.formatOk(padStatus(status))); + } else { + report.print(formatter.formatNotOk(padStatus(status))); + } + report.print("] "); + report.print(status.isIncoming() ? "← " : "→ "); report.print(link.getOtherLinkEnd().getId()); report.print(this.settings.getNewline()); if (showOrigin) @@ -294,21 +314,25 @@ private void renderLink(final PrintStream report, final TracedLink link, final Location location = link.getOtherLinkEnd().getLocation(); if (location != null) { - report.print("| "); + report.print(" "); renderOrigin(report, location); report.print(this.settings.getNewline()); } } } + private static String padStatus(final LinkStatus status) { + return String.format("%-17s", status.toString().toLowerCase()); + } + private void renderTags(final PrintStream report, final LinkedSpecificationItem item) { final List tags = item.getTags(); if (tags != null && !tags.equals(Collections.emptyList())) { renderEmptyItemDetailsLine(report); - report.print("| #: "); - report.print(tags.stream().collect(Collectors.joining(", "))); + report.print(" #: "); + report.print(String.join(", ", tags)); report.print(this.settings.getNewline()); ++this.nonEmptySections; } @@ -320,7 +344,7 @@ private void renderOrigin(final PrintStream report, final LinkedSpecificationIte if (location != null) { renderEmptyItemDetailsLine(report); - report.print("| ("); + report.print(" ("); report.print(location.getPath()); report.print(":"); report.print(location.getLine()); diff --git a/reporter/plaintext/src/main/java/org/itsallcode/openfasttrace/report/plaintext/TextFormatter.java b/reporter/plaintext/src/main/java/org/itsallcode/openfasttrace/report/plaintext/TextFormatter.java new file mode 100644 index 00000000..e717b523 --- /dev/null +++ b/reporter/plaintext/src/main/java/org/itsallcode/openfasttrace/report/plaintext/TextFormatter.java @@ -0,0 +1,29 @@ +package org.itsallcode.openfasttrace.report.plaintext; + +/** + * Interface for text formatters. + */ +interface TextFormatter { + /** + * Format a text span that represents a good result. + * @param text text span to be formatted + * @return formatted text + */ + public String formatOk(final String text); + + /** + * Format a text span that represents a bad result. + * + * @param text text span to be formatted + * @return formatted text + */ + public String formatNotOk(final String text); + + /** + * Format a text span that represents a strongly emphasized text. + * + * @param text text span to be formatted + * @return formatted text + */ + public String formatStrong(final String text); +} \ No newline at end of file diff --git a/reporter/plaintext/src/main/java/org/itsallcode/openfasttrace/report/plaintext/TextFormatterFactory.java b/reporter/plaintext/src/main/java/org/itsallcode/openfasttrace/report/plaintext/TextFormatterFactory.java new file mode 100644 index 00000000..8e93d881 --- /dev/null +++ b/reporter/plaintext/src/main/java/org/itsallcode/openfasttrace/report/plaintext/TextFormatterFactory.java @@ -0,0 +1,35 @@ +package org.itsallcode.openfasttrace.report.plaintext; + +import org.itsallcode.openfasttrace.api.ColorScheme; + +/** + * Factory for text formatters + */ +final class TextFormatterFactory { + private TextFormatterFactory() { + // prevent instantiation + } + + /** + * Create a text formatter + * @param colorScheme color scheme that the formatter should apply + * @return text formatter + */ + public static TextFormatter createFormatter(ColorScheme colorScheme) { + if(colorScheme == null) + { + return new NullTextFormatter(); + } + switch (colorScheme) { + case BLACK_AND_WHITE: + return new NullTextFormatter(); + case MONOCHROME: + return new MonochromeTextFormatter(); + case COLOR: + return new ConsoleColorFormatter(); + default: + throw new IllegalArgumentException("Unable to create text formatter for unknown color scheme '" + + colorScheme + "'."); + } + } +} diff --git a/reporter/plaintext/src/test/java/org/itsallcode/openfasttrace/report/plaintext/AnsiSequenceTest.java b/reporter/plaintext/src/test/java/org/itsallcode/openfasttrace/report/plaintext/AnsiSequenceTest.java new file mode 100644 index 00000000..6390ba1c --- /dev/null +++ b/reporter/plaintext/src/test/java/org/itsallcode/openfasttrace/report/plaintext/AnsiSequenceTest.java @@ -0,0 +1,38 @@ +package org.itsallcode.openfasttrace.report.plaintext; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.itsallcode.openfasttrace.report.plaintext.AnsiSequence.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +class AnsiSequenceTest { + public static Stream getAnsiSequenceIds() { + return Stream.of( + Arguments.of(RESET, 0), + Arguments.of(BOLD, 1), + Arguments.of(ITALIC, 3), + Arguments.of(UNDERLINE, 4), + Arguments.of(INVERSE, 7), + Arguments.of(BLACK, 30), + Arguments.of(RED, 31), + Arguments.of(GREEN, 32), + Arguments.of(YELLOW, 33), + Arguments.of(BLUE, 34), + Arguments.of(MAGENTA, 35), + Arguments.of(CYAN, 36), + Arguments.of(WHITE, 37), + Arguments.of(BRIGHT_RED, 91) + ); + } + + @MethodSource("getAnsiSequenceIds") + @ParameterizedTest + void testValues(final AnsiSequence sequence, final int id) { + assertThat(sequence.getId(), equalTo(id)); + } +} \ No newline at end of file diff --git a/reporter/plaintext/src/test/java/org/itsallcode/openfasttrace/report/plaintext/ConsoleColorFormatterTest.java b/reporter/plaintext/src/test/java/org/itsallcode/openfasttrace/report/plaintext/ConsoleColorFormatterTest.java new file mode 100644 index 00000000..ceedc1d9 --- /dev/null +++ b/reporter/plaintext/src/test/java/org/itsallcode/openfasttrace/report/plaintext/ConsoleColorFormatterTest.java @@ -0,0 +1,29 @@ +package org.itsallcode.openfasttrace.report.plaintext; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +class ConsoleColorFormatterTest { + private static final TextFormatter FORMATTER = new ConsoleColorFormatter(); + + // [utest->dsn~reporting.plain-text.ansi-color~1] + @Test + void testFormatOk() { + assertThat(FORMATTER.formatOk("ok"), equalTo("\u001B[32mok\u001B[0m")); + } + + // [utest->dsn~reporting.plain-text.ansi-color~1] + @Test + void testFormatNotOk() { + assertThat(FORMATTER.formatNotOk("not ok"), equalTo("\u001B[91mnot ok\u001B[0m")); + } + + // [utest->dsn~reporting.plain-text.ansi-color~1] + // [utest-> dsn~reporting.plain-text.ansi-font-style~1] + @Test + void testFormatStrong() { + assertThat(FORMATTER.formatStrong("strong"), equalTo("\u001B[1;36mstrong\u001B[0m")); + } +} \ No newline at end of file diff --git a/reporter/plaintext/src/test/java/org/itsallcode/openfasttrace/report/plaintext/TestNullTextFormatter.java b/reporter/plaintext/src/test/java/org/itsallcode/openfasttrace/report/plaintext/TestNullTextFormatter.java new file mode 100644 index 00000000..5827b487 --- /dev/null +++ b/reporter/plaintext/src/test/java/org/itsallcode/openfasttrace/report/plaintext/TestNullTextFormatter.java @@ -0,0 +1,25 @@ +package org.itsallcode.openfasttrace.report.plaintext; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +class TestNullTextFormatter { + private static final TextFormatter FORMATTER = new NullTextFormatter(); + + @Test + void testFormatOk() { + assertThat(FORMATTER.formatOk("ok"), equalTo("ok")); + } + + @Test + void testFormatNotOk() { + assertThat(FORMATTER.formatNotOk("not ok"), equalTo("not ok")); + } + + @Test + void testFormatStrong() { + assertThat(FORMATTER.formatStrong("strong"), equalTo("strong")); + } +} \ No newline at end of file diff --git a/reporter/plaintext/src/test/java/org/itsallcode/openfasttrace/report/plaintext/TestPlainTextReport.java b/reporter/plaintext/src/test/java/org/itsallcode/openfasttrace/report/plaintext/TestPlainTextReport.java index a27b1118..866456fa 100644 --- a/reporter/plaintext/src/test/java/org/itsallcode/openfasttrace/report/plaintext/TestPlainTextReport.java +++ b/reporter/plaintext/src/test/java/org/itsallcode/openfasttrace/report/plaintext/TestPlainTextReport.java @@ -3,11 +3,11 @@ import static java.util.Arrays.asList; import static java.util.stream.Collectors.joining; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; import static org.itsallcode.openfasttrace.testutil.core.SampleArtifactTypes.*; import static org.itsallcode.openfasttrace.testutil.matcher.MultilineTextMatcher.matchesAllLines; import static org.mockito.Mockito.*; +import java.nio.charset.StandardCharsets; import java.io.*; import java.util.*; @@ -80,20 +80,19 @@ private String getExpectedReportText(final String... expectedReportLines) private String getReportOutput(final ReportVerbosity verbosity, final boolean showOrigin) { - final Newline newline = NEWLINE_SEPARATOR; - return getReportOutputWithNewline(verbosity, newline, showOrigin); + return getReportOutputWithNewline(verbosity, NEWLINE_SEPARATOR, showOrigin); } private String getReportOutputWithNewline(final ReportVerbosity verbosity, final Newline newline, final boolean showOrigin) { - final OutputStream outputStream = new ByteArrayOutputStream(); + final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); final ReportSettings settings = ReportSettings.builder().verbosity(verbosity) .newline(newline).showOrigin(showOrigin).build(); final PlaintextReporterFactory factory = createFactory(settings); final Reportable report = factory.createImporter(this.traceMock); report.renderToStream(outputStream); - return outputStream.toString(); + return outputStream.toString(StandardCharsets.UTF_8); } private PlaintextReporterFactory createFactory(final ReportSettings settings) @@ -158,10 +157,10 @@ void testReport_LevelFailureSummaries_NotOK() prepareFailedItemDetails(); assertReportOutput(ReportVerbosity.FAILURE_SUMMARIES, // - "not ok - 0/0>0>2/4 - dsn~bar~1 [proposed] (impl, -uman, utest)", // - "not ok - 0/3>1>0/2 - req~foo~1 (dsn)", // - "not ok - 3/7>1>2/3 - req~zoo~1 [rejected] (-impl, -utest)", // - "not ok - 1/6>0>0/0 - req~zoo~2 [draft] (dsn, +utest)", // + "not ok [ in: 0 / 0 | out: 2 / 4 ✘ ] dsn~bar~1 [proposed] (impl, -uman, utest)", // + "not ok [ in: 3 / 3 ✔ | out: 2 / 2 ✔ ] req~foo~1 (dsn) [has 1 duplicate]", // + "not ok [ in: 4 / 7 ✘ | out: 1 / 3 ✘ ] req~zoo~1 [rejected] (-impl, -utest) [has 1 duplicate]", // + "not ok [ in: 5 / 6 ✘ | out: 0 / 0 ] req~zoo~2 [draft] (dsn, +utest)", // "", // "not ok - 6 total, 4 defect"); } @@ -179,16 +178,16 @@ private void prepareFailedItemDetails() final LinkedSpecificationItem itemDMock = createLinkedItemMock("req~zoo~1", ItemStatus.REJECTED, "desc D1", 3, 7, 1, 2, 3); - when(itemAMock.getNeedsArtifactTypes()).thenReturn(asList(DSN)); - when(itemAMock.getCoveredArtifactTypes()).thenReturn(new HashSet<>(asList(DSN))); - when(itemBMock.getCoveredArtifactTypes()).thenReturn(new HashSet<>(asList(IMPL, UTEST))); - when(itemBMock.getUncoveredArtifactTypes()).thenReturn(asList(UMAN)); - when(itemCMock.getCoveredArtifactTypes()).thenReturn(new HashSet<>(asList(DSN))); - when(itemCMock.getOverCoveredArtifactTypes()).thenReturn(new HashSet<>(asList(UTEST))); + when(itemAMock.getNeedsArtifactTypes()).thenReturn(List.of(DSN)); + when(itemAMock.getCoveredArtifactTypes()).thenReturn(new HashSet<>(List.of(DSN))); + when(itemBMock.getCoveredArtifactTypes()).thenReturn(new HashSet<>(List.of(IMPL, UTEST))); + when(itemBMock.getUncoveredArtifactTypes()).thenReturn(List.of(UMAN)); + when(itemCMock.getCoveredArtifactTypes()).thenReturn(new HashSet<>(List.of(DSN))); + when(itemCMock.getOverCoveredArtifactTypes()).thenReturn(new HashSet<>(List.of(UTEST))); when(itemDMock.getCoveredArtifactTypes()).thenReturn(Collections.emptySet()); - when(itemDMock.getUncoveredArtifactTypes()).thenReturn(asList(IMPL, UTEST)); + when(itemDMock.getUncoveredArtifactTypes()).thenReturn(List.of(IMPL, UTEST)); when(this.traceMock.getDefectItems()) - .thenReturn(asList(itemAMock, itemBMock, itemCMock, itemDMock)); + .thenReturn(List.of(itemAMock, itemBMock, itemCMock, itemDMock)); when(itemAMock.getLocation()).thenReturn(Location.create("/tmp/foo.md", 1)); when(itemBMock.getLocation()).thenReturn(Location.create("/tmp/bar.md", 2)); when(itemCMock.getLocation()).thenReturn(Location.create("/tmp/zoo.xml", 13)); @@ -204,17 +203,17 @@ void testReport_LevelFailureDetails() prepareMixedItemDetails(); assertReportOutput(ReportVerbosity.FAILURE_DETAILS, // - "not ok - 0/1>3>2/4 - dsn~failure~0 (impl, uman, -utest)", // - "|", // - "| This is a failure.", // - "|", // - "|<-- ( ) imp~failure~0", // - "|--> ( ) req~bar~1", // - "|--> (+) req~baz~1", // - "|--> ( ) req~foo~1", // - "|--> (<) req~zoo~1", // - "|--> (/) req~zoo~2", // - "|", // + "not ok [ in: 1 / 1 ✔ | out: 2 / 4 ✘ ] dsn~failure~0 (impl, uman, -utest) [has 3 duplicates]", // + "", // + " This is a failure.", // + "", // + " [covered shallow ] ← imp~failure~0", // + " [covers ] → req~bar~1", // + " [unwanted ] → req~baz~1", // + " [covers ] → req~foo~1", // + " [outdated ] → req~zoo~1", // + " [orphaned ] → req~zoo~2", // + "", // "", // "not ok - 2 total, 1 defect"); } @@ -228,23 +227,23 @@ void testReport_LevelAll() prepareMixedItemDetails(); assertReportOutput(ReportVerbosity.ALL, // - "not ok - 0/1>3>2/4 - dsn~failure~0 (impl, uman, -utest)", // - "|", // - "| This is a failure.", // - "|", // - "|<-- ( ) imp~failure~0", // - "|--> ( ) req~bar~1", // - "|--> (+) req~baz~1", // - "|--> ( ) req~foo~1", // - "|--> (<) req~zoo~1", // - "|--> (/) req~zoo~2", // - "|", // - "ok - 0/0>0>0/0 - req~success~20170126 (dsn)", // - "|", // - "| This is a success.", // - "|", // - "| #: tag, another tag", // - "|", // + "not ok [ in: 1 / 1 ✔ | out: 2 / 4 ✘ ] dsn~failure~0 (impl, uman, -utest) [has 3 duplicates]", // + "", // + " This is a failure.", // + "", // + " [covered shallow ] ← imp~failure~0", // + " [covers ] → req~bar~1", // + " [unwanted ] → req~baz~1", // + " [covers ] → req~foo~1", // + " [outdated ] → req~zoo~1", // + " [orphaned ] → req~zoo~2", // + "", // + "ok [ in: 0 / 0 | out: 0 / 0 ] req~success~20170126 (dsn)", // + "", // + " This is a success.", // + "", // + " #: tag, another tag", // + "", // "", // "not ok - 2 total, 1 defect"); } @@ -258,14 +257,14 @@ private void prepareMixedItemDetails() "This is a failure.", // 0, 1, 3, 2, 4); - when(itemAMock.getNeedsArtifactTypes()).thenReturn(asList(DSN)); - when(itemAMock.getCoveredArtifactTypes()).thenReturn(new HashSet<>(asList(DSN))); - when(itemAMock.getTags()).thenReturn(asList("tag", "another tag")); - when(itemBMock.getCoveredArtifactTypes()).thenReturn(new HashSet<>(asList(IMPL, UMAN))); - when(itemBMock.getUncoveredArtifactTypes()).thenReturn(asList(UTEST)); + when(itemAMock.getNeedsArtifactTypes()).thenReturn(List.of(DSN)); + when(itemAMock.getCoveredArtifactTypes()).thenReturn(new HashSet<>(List.of(DSN))); + when(itemAMock.getTags()).thenReturn(List.of("tag", "another tag")); + when(itemBMock.getCoveredArtifactTypes()).thenReturn(new HashSet<>(List.of(IMPL, UMAN))); + when(itemBMock.getUncoveredArtifactTypes()).thenReturn(List.of(UTEST)); prepareLinks(itemBMock); - when(this.traceMock.getItems()).thenReturn(asList(itemAMock, itemBMock)); - when(this.traceMock.getDefectItems()).thenReturn(asList(itemBMock)); + when(this.traceMock.getItems()).thenReturn(List.of(itemAMock, itemBMock)); + when(this.traceMock.getDefectItems()).thenReturn(List.of(itemBMock)); } private void prepareLinks(final LinkedSpecificationItem itemMock) @@ -336,24 +335,24 @@ void testReportWithDifferentLineSeparator() final LinkedSpecificationItem itemBMock = createLinkedItemMock("b~b~2", // "Yet another" + separator + "multiline text", // 0, 0, 0, 0, 0); - when(itemAMock.getNeedsArtifactTypes()).thenReturn(asList(DSN)); - when(itemAMock.getCoveredArtifactTypes()).thenReturn(new HashSet<>(asList(DSN))); - when(itemBMock.getCoveredArtifactTypes()).thenReturn(new HashSet<>(asList(IMPL))); + when(itemAMock.getNeedsArtifactTypes()).thenReturn(List.of(DSN)); + when(itemAMock.getCoveredArtifactTypes()).thenReturn(new HashSet<>(List.of(DSN))); + when(itemBMock.getCoveredArtifactTypes()).thenReturn(new HashSet<>(List.of(IMPL))); when(this.traceMock.hasNoDefects()).thenReturn(true); - when(this.traceMock.getItems()).thenReturn(asList(itemAMock, itemBMock)); + when(this.traceMock.getItems()).thenReturn(List.of(itemAMock, itemBMock)); assertThat(getReportOutputWithNewline(ReportVerbosity.ALL, separator, false), // - equalTo("ok - 0/0>0>0/0 - a~a~1 (dsn)" + separator// - + "|" + separator // - + "| This is" + separator // - + "| a multiline description" + separator // - + "|" + separator // - + "ok - 0/0>0>0/0 - b~b~2 (impl)" + separator // - + "|" + separator // - + "| Yet another" + separator // - + "| multiline text" + separator // - + "|" + separator // + matchesAllLines("ok [ in: 0 / 0 | out: 0 / 0 ] a~a~1 (dsn)" + separator// + "" + separator // + + " This is" + separator // + + " a multiline description" + separator // + + separator // + + "ok [ in: 0 / 0 | out: 0 / 0 ] b~b~2 (impl)" + separator // + + "" + separator // + + " Yet another" + separator // + + " multiline text" + separator // + + separator // + + separator // + "ok - 2 total" + separator)); } @@ -365,8 +364,8 @@ void testReportWithOriginDisplayEnabled() { final LinkedSpecificationItem itemMock = createLinkedItemMock("req~item.with-source~77", "Description", 0, 1, 0, 0, 0); - when(itemMock.getNeedsArtifactTypes()).thenReturn(asList(DSN)); - when(itemMock.getCoveredArtifactTypes()).thenReturn(new HashSet<>(asList(DSN))); + when(itemMock.getNeedsArtifactTypes()).thenReturn(List.of(DSN)); + when(itemMock.getCoveredArtifactTypes()).thenReturn(new HashSet<>(List.of(DSN))); final LinkedSpecificationItem other = createOtherItemMock("dsn~the-other~1"); when(other.getLocation()).thenReturn(Location.create("baz/zoo", 10)); final List links = new ArrayList<>(); @@ -377,17 +376,17 @@ void testReportWithOriginDisplayEnabled() when(this.traceMock.count()).thenReturn(1); when(this.traceMock.countDefects()).thenReturn(0); when(this.traceMock.hasNoDefects()).thenReturn(true); - when(this.traceMock.getItems()).thenReturn(asList(itemMock)); + when(this.traceMock.getItems()).thenReturn(List.of(itemMock)); assertReportOutputWithOrigin(ReportVerbosity.ALL, // - "ok - 0/1>0>0/0 - req~item.with-source~77 (dsn)", // - "|", // - "| Description", // - "|", // - "| (/foo/bar:42)", // - "|", // - "|<-- ( ) dsn~the-other~1", // - "| (baz/zoo:10)", // - "|", // + "ok [ in: 1 / 1 ✔ | out: 0 / 0 ] req~item.with-source~77 (dsn)", // + "", // + " Description", // + "", // + " (/foo/bar:42)", // + "", // + " [covered shallow ] ← dsn~the-other~1", // + " (baz/zoo:10)", // + "", // "", // "ok - 1 total"); } diff --git a/reporter/plaintext/src/test/java/org/itsallcode/openfasttrace/report/plaintext/TestTextFormatterFactory.java b/reporter/plaintext/src/test/java/org/itsallcode/openfasttrace/report/plaintext/TestTextFormatterFactory.java new file mode 100644 index 00000000..8f5aea87 --- /dev/null +++ b/reporter/plaintext/src/test/java/org/itsallcode/openfasttrace/report/plaintext/TestTextFormatterFactory.java @@ -0,0 +1,32 @@ +package org.itsallcode.openfasttrace.report.plaintext; + +import org.itsallcode.openfasttrace.api.ColorScheme; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsInstanceOf.instanceOf; +import static org.itsallcode.openfasttrace.api.ColorScheme.*; +import static org.itsallcode.openfasttrace.report.plaintext.TextFormatterFactory.createFormatter; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class TestTextFormatterFactory { + @Test + void testCreateNullTextFormatter() { + assertThat(createFormatter(BLACK_AND_WHITE), instanceOf(NullTextFormatter.class)); + } + + @Test + void testCreateMonochromeTextFormatter() { + assertThat(createFormatter(MONOCHROME), instanceOf(MonochromeTextFormatter.class)); + } + + @Test + void testCreateConsoleColorFormatter() { + assertThat(createFormatter(COLOR), instanceOf(ConsoleColorFormatter.class)); + } + + @Test + void testCreateDefaultFormatter() { + assertThat(createFormatter(null), instanceOf(NullTextFormatter.class)); + } +} \ No newline at end of file diff --git a/testutil/pom.xml b/testutil/pom.xml index f065cdb4..19fc271d 100644 --- a/testutil/pom.xml +++ b/testutil/pom.xml @@ -68,6 +68,13 @@ true + + org.apache.maven.plugins + maven-surefire-plugin + + false + + \ No newline at end of file diff --git a/testutil/src/main/java/module-info.java b/testutil/src/main/java/module-info.java new file mode 100644 index 00000000..84fc9f62 --- /dev/null +++ b/testutil/src/main/java/module-info.java @@ -0,0 +1,21 @@ +/** + * Shared test utilities. + */ +module org.itsallcode.openfasttrace.testutil +{ + exports org.itsallcode.openfasttrace.testutil; + exports org.itsallcode.openfasttrace.testutil.cli; + exports org.itsallcode.openfasttrace.testutil.core; + exports org.itsallcode.openfasttrace.testutil.importer; + exports org.itsallcode.openfasttrace.testutil.log; + exports org.itsallcode.openfasttrace.testutil.matcher; + exports org.itsallcode.openfasttrace.testutil.xml; + + requires hamcrest.all; + requires transitive org.junit.jupiter.api; + requires org.mockito; + requires org.mockito.junit.jupiter; + requires java.logging; + requires transitive java.xml; + requires org.itsallcode.openfasttrace.api; +}