From c067e0c2977adbfb59c9da190afdfae62bbb3268 Mon Sep 17 00:00:00 2001 From: moritz-suckow <129938897+moritz-suckow@users.noreply.github.com> Date: Wed, 26 Apr 2023 08:46:48 +0200 Subject: [PATCH] Feature/1884/auto suggest parsers (#3275) Expand ccsh to auto suggest useful parsers #1884 --- CHANGELOG.md | 10 + .../codecharta/exporter/csv/CSVExporter.kt | 14 +- .../filter/edgefilter/EdgeFilter.kt | 14 +- .../filter/mergefilter/MergeFilter.kt | 14 +- .../structuremodifier/StructureModifier.kt | 14 +- .../codecharta/importer/csv/CSVImporter.kt | 13 +- .../sourcemonitor/SourceMonitorImporter.kt | 13 +- .../importer/codemaat/CodeMaatImporter.kt | 14 +- .../importer/gitlogparser/GitLogParser.kt | 29 ++- .../subcommands/LogScanCommand.kt | 7 + .../subcommands/RepoScanCommand.kt | 7 + .../importer/gitlogparser/GitLogParserTest.kt | 35 ++++ .../src/test/resources/my/git/repo/Test.git | 0 .../MetricGardenerImporter.kt | 14 +- .../importer/svnlogparser/SVNLogParser.kt | 15 +- .../importer/sonar/SonarImporterMain.kt | 23 ++- .../importer/sonar/SonarImporterMainTest.kt | 49 ++++- .../my/sonar/repo/sonar-project.properties | 0 .../sourcecodeparser/SourceCodeParserMain.kt | 14 +- .../importer/tokeiimporter/TokeiImporter.kt | 14 +- .../parser/rawtextparser/RawTextParser.kt | 13 +- .../interactiveparser/InteractiveParser.kt | 4 + .../util/InteractiveParserHelper.kt | 84 +++++++++ .../tools/validation/ValidationTool.kt | 13 +- .../codecharta/tools/ccsh/Ccsh.kt | 75 +++++++- .../InteractiveParserSuggestionDialog.kt | 59 ++++++ .../tools/ccsh/parser/ParserService.kt | 61 ++++-- .../parser/repository/ParserRepository.kt | 11 ++ .../repository/PicocliParserRepository.kt | 68 +++++++ .../maibornwolff/codecharta/ccsh/CcshTest.kt | 132 ++++++++++++- .../InteractiveParserSuggestionDialogTest.kt | 140 ++++++++++++++ .../ccsh/parser/ParserServiceTest.kt | 113 +++++++++-- .../repository/PicocliParserRepositoryTest.kt | 176 ++++++++++++++++++ 33 files changed, 1171 insertions(+), 91 deletions(-) create mode 100644 analysis/import/GitLogParser/src/test/resources/my/git/repo/Test.git create mode 100644 analysis/import/SonarImporter/src/test/resources/my/sonar/repo/sonar-project.properties create mode 100644 analysis/tools/InteractiveParser/src/main/kotlin/de/maibornwolff/codecharta/tools/interactiveparser/util/InteractiveParserHelper.kt create mode 100644 analysis/tools/ccsh/src/main/kotlin/de/maibornwolff/codecharta/tools/ccsh/parser/InteractiveParserSuggestionDialog.kt create mode 100644 analysis/tools/ccsh/src/main/kotlin/de/maibornwolff/codecharta/tools/ccsh/parser/repository/ParserRepository.kt create mode 100644 analysis/tools/ccsh/src/main/kotlin/de/maibornwolff/codecharta/tools/ccsh/parser/repository/PicocliParserRepository.kt create mode 100644 analysis/tools/ccsh/src/test/kotlin/de/maibornwolff/codecharta/ccsh/parser/InteractiveParserSuggestionDialogTest.kt create mode 100644 analysis/tools/ccsh/src/test/kotlin/de/maibornwolff/codecharta/ccsh/parser/repository/PicocliParserRepositoryTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index eed949832f..38f4f7b423 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/) ## [unreleased] (Added 🚀 | Changed | Removed 🗑 | Fixed 🐞 | Chore 👨‍💻 👩‍💻) +### Added 🚀 + +- Add automatic parser suggestions to recommend usable parsers for a codebase (supports GitLogParser and SonarImporter) when running `ccsh` command [#3275](https://github.com/MaibornWolff/codecharta/pull/3275)
+ ![image](https://user-images.githubusercontent.com/129938897/234309117-c9edd4e7-7c53-4ba7-b849-ec9c3f8f3215.png) + +### Changed + +- Changed default behavior when launching ccsh without arguments to parser suggestions [#3275](https://github.com/MaibornWolff/codecharta/pull/3275) +- Old interactive parser selection now reachable by passing -i or --interactive as arguments [#3275](https://github.com/MaibornWolff/codecharta/pull/3275) + ### Fixed 🐞 - Fix suspicious metrics and risk profile docs not loading [#3272](https://github.com/MaibornWolff/codecharta/pull/3272) diff --git a/analysis/export/CSVExporter/src/main/kotlin/de/maibornwolff/codecharta/exporter/csv/CSVExporter.kt b/analysis/export/CSVExporter/src/main/kotlin/de/maibornwolff/codecharta/exporter/csv/CSVExporter.kt index 42654c3db1..d8d68e2db4 100644 --- a/analysis/export/CSVExporter/src/main/kotlin/de/maibornwolff/codecharta/exporter/csv/CSVExporter.kt +++ b/analysis/export/CSVExporter/src/main/kotlin/de/maibornwolff/codecharta/exporter/csv/CSVExporter.kt @@ -8,6 +8,7 @@ import de.maibornwolff.codecharta.model.Project import de.maibornwolff.codecharta.serialization.ProjectDeserializer import de.maibornwolff.codecharta.tools.interactiveparser.InteractiveParser import de.maibornwolff.codecharta.tools.interactiveparser.ParserDialogInterface +import de.maibornwolff.codecharta.tools.interactiveparser.util.InteractiveParserHelper import picocli.CommandLine import java.io.BufferedWriter import java.io.File @@ -18,9 +19,9 @@ import java.io.Writer import java.util.concurrent.Callable @CommandLine.Command( - name = "csvexport", - description = ["generates csv file with header"], - footer = ["Copyright(c) 2022, MaibornWolff GmbH"] + name = InteractiveParserHelper.CSVExporterConstants.name, + description = [InteractiveParserHelper.CSVExporterConstants.description], + footer = [InteractiveParserHelper.GeneralConstants.GenericFooter] ) class CSVExporter : Callable, InteractiveParser { @@ -98,6 +99,13 @@ class CSVExporter : Callable, InteractiveParser { } override fun getDialog(): ParserDialogInterface = ParserDialog + override fun isApplicable(resourceToBeParsed: String): Boolean { + return false + } + + override fun getName(): String { + return InteractiveParserHelper.CSVExporterConstants.name + } } private fun Node.toAttributeList(attributeNames: List): List { diff --git a/analysis/filter/EdgeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/edgefilter/EdgeFilter.kt b/analysis/filter/EdgeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/edgefilter/EdgeFilter.kt index 59aa4eba69..24221444c2 100644 --- a/analysis/filter/EdgeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/edgefilter/EdgeFilter.kt +++ b/analysis/filter/EdgeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/edgefilter/EdgeFilter.kt @@ -4,15 +4,16 @@ import de.maibornwolff.codecharta.serialization.ProjectDeserializer import de.maibornwolff.codecharta.serialization.ProjectSerializer import de.maibornwolff.codecharta.tools.interactiveparser.InteractiveParser import de.maibornwolff.codecharta.tools.interactiveparser.ParserDialogInterface +import de.maibornwolff.codecharta.tools.interactiveparser.util.InteractiveParserHelper import picocli.CommandLine import java.io.File import java.io.PrintStream import java.util.concurrent.Callable @CommandLine.Command( - name = "edgefilter", - description = ["aggregates edgeAttributes as nodeAttributes into a new cc.json file"], - footer = ["Copyright(c) 2022, MaibornWolff GmbH"] + name = InteractiveParserHelper.EdgeFilterConstants.name, + description = [InteractiveParserHelper.EdgeFilterConstants.description], + footer = [InteractiveParserHelper.GeneralConstants.GenericFooter] ) class EdgeFilter( private val output: PrintStream = System.out @@ -48,4 +49,11 @@ class EdgeFilter( } override fun getDialog(): ParserDialogInterface = ParserDialog + override fun isApplicable(resourceToBeParsed: String): Boolean { + return false + } + + override fun getName(): String { + return InteractiveParserHelper.EdgeFilterConstants.name + } } diff --git a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilter.kt b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilter.kt index c615bbf105..e141832d6d 100644 --- a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilter.kt +++ b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilter.kt @@ -5,6 +5,7 @@ import de.maibornwolff.codecharta.serialization.ProjectDeserializer import de.maibornwolff.codecharta.serialization.ProjectSerializer import de.maibornwolff.codecharta.tools.interactiveparser.InteractiveParser import de.maibornwolff.codecharta.tools.interactiveparser.ParserDialogInterface +import de.maibornwolff.codecharta.tools.interactiveparser.util.InteractiveParserHelper import mu.KotlinLogging import picocli.CommandLine import java.io.File @@ -12,9 +13,9 @@ import java.io.PrintStream import java.util.concurrent.Callable @CommandLine.Command( - name = "merge", - description = ["merges multiple cc.json files"], - footer = ["Copyright(c) 2020, MaibornWolff GmbH"] + name = InteractiveParserHelper.MergeFilterConstants.name, + description = [InteractiveParserHelper.MergeFilterConstants.description], + footer = [InteractiveParserHelper.GeneralConstants.GenericFooter] ) class MergeFilter( private val output: PrintStream = System.out @@ -99,4 +100,11 @@ class MergeFilter( } override fun getDialog(): ParserDialogInterface = ParserDialog + override fun isApplicable(resourceToBeParsed: String): Boolean { + return false + } + + override fun getName(): String { + return InteractiveParserHelper.MergeFilterConstants.name + } } diff --git a/analysis/filter/StructureModifier/src/main/kotlin/de/maibornwolff/codecharta/filter/structuremodifier/StructureModifier.kt b/analysis/filter/StructureModifier/src/main/kotlin/de/maibornwolff/codecharta/filter/structuremodifier/StructureModifier.kt index db6062f559..3038a82f4f 100644 --- a/analysis/filter/StructureModifier/src/main/kotlin/de/maibornwolff/codecharta/filter/structuremodifier/StructureModifier.kt +++ b/analysis/filter/StructureModifier/src/main/kotlin/de/maibornwolff/codecharta/filter/structuremodifier/StructureModifier.kt @@ -5,6 +5,7 @@ import de.maibornwolff.codecharta.serialization.ProjectDeserializer import de.maibornwolff.codecharta.serialization.ProjectSerializer import de.maibornwolff.codecharta.tools.interactiveparser.InteractiveParser import de.maibornwolff.codecharta.tools.interactiveparser.ParserDialogInterface +import de.maibornwolff.codecharta.tools.interactiveparser.util.InteractiveParserHelper import mu.KotlinLogging import picocli.CommandLine import java.io.File @@ -13,9 +14,9 @@ import java.io.PrintStream import java.util.concurrent.Callable @CommandLine.Command( - name = "modify", - description = ["changes the structure of cc.json files"], - footer = ["Copyright(c) 2022, MaibornWolff GmbH"] + name = InteractiveParserHelper.StructureModifierConstants.name, + description = [InteractiveParserHelper.StructureModifierConstants.description], + footer = [InteractiveParserHelper.GeneralConstants.GenericFooter] ) class StructureModifier( private val input: InputStream = System.`in`, @@ -100,4 +101,11 @@ class StructureModifier( } override fun getDialog(): ParserDialogInterface = ParserDialog + override fun isApplicable(resourceToBeParsed: String): Boolean { + return false + } + + override fun getName(): String { + return InteractiveParserHelper.StructureModifierConstants.name + } } diff --git a/analysis/import/CSVImporter/src/main/kotlin/de/maibornwolff/codecharta/importer/csv/CSVImporter.kt b/analysis/import/CSVImporter/src/main/kotlin/de/maibornwolff/codecharta/importer/csv/CSVImporter.kt index 66d9fa835f..8c25b91b80 100644 --- a/analysis/import/CSVImporter/src/main/kotlin/de/maibornwolff/codecharta/importer/csv/CSVImporter.kt +++ b/analysis/import/CSVImporter/src/main/kotlin/de/maibornwolff/codecharta/importer/csv/CSVImporter.kt @@ -3,6 +3,7 @@ package de.maibornwolff.codecharta.importer.csv import de.maibornwolff.codecharta.serialization.ProjectSerializer import de.maibornwolff.codecharta.tools.interactiveparser.InteractiveParser import de.maibornwolff.codecharta.tools.interactiveparser.ParserDialogInterface +import de.maibornwolff.codecharta.tools.interactiveparser.util.InteractiveParserHelper import picocli.CommandLine import java.io.File import java.io.IOException @@ -11,9 +12,9 @@ import java.io.PrintStream import java.util.concurrent.Callable @CommandLine.Command( - name = "csvimport", - description = ["generates cc.json from csv with header"], - footer = ["Copyright(c) 2022, MaibornWolff GmbH"] + name = InteractiveParserHelper.CSVImporterConstants.name, + description = [InteractiveParserHelper.CSVImporterConstants.description], + footer = [InteractiveParserHelper.GeneralConstants.GenericFooter] ) class CSVImporter( private val output: PrintStream = System.out @@ -60,4 +61,10 @@ class CSVImporter( } override fun getDialog(): ParserDialogInterface = ParserDialog + override fun isApplicable(resourceToBeParsed: String): Boolean { + return false + } + override fun getName(): String { + return InteractiveParserHelper.CSVImporterConstants.name + } } diff --git a/analysis/import/CSVImporter/src/main/kotlin/de/maibornwolff/codecharta/importer/sourcemonitor/SourceMonitorImporter.kt b/analysis/import/CSVImporter/src/main/kotlin/de/maibornwolff/codecharta/importer/sourcemonitor/SourceMonitorImporter.kt index c49f3506fd..3674ed0057 100644 --- a/analysis/import/CSVImporter/src/main/kotlin/de/maibornwolff/codecharta/importer/sourcemonitor/SourceMonitorImporter.kt +++ b/analysis/import/CSVImporter/src/main/kotlin/de/maibornwolff/codecharta/importer/sourcemonitor/SourceMonitorImporter.kt @@ -4,6 +4,7 @@ import de.maibornwolff.codecharta.importer.csv.CSVProjectBuilder import de.maibornwolff.codecharta.serialization.ProjectSerializer import de.maibornwolff.codecharta.tools.interactiveparser.InteractiveParser import de.maibornwolff.codecharta.tools.interactiveparser.ParserDialogInterface +import de.maibornwolff.codecharta.tools.interactiveparser.util.InteractiveParserHelper import de.maibornwolff.codecharta.translator.MetricNameTranslator import picocli.CommandLine import java.io.File @@ -13,9 +14,9 @@ import java.io.PrintStream import java.util.concurrent.Callable @CommandLine.Command( - name = "sourcemonitorimport", - description = ["generates cc.json from sourcemonitor csv"], - footer = ["Copyright(c) 2020, MaibornWolff GmbH"] + name = InteractiveParserHelper.SourceMonitorImporterConstants.name, + description = [InteractiveParserHelper.SourceMonitorImporterConstants.description], + footer = [InteractiveParserHelper.GeneralConstants.GenericFooter] ) class SourceMonitorImporter( private val output: PrintStream = System.out @@ -84,4 +85,10 @@ class SourceMonitorImporter( } override fun getDialog(): ParserDialogInterface = ParserDialog + override fun isApplicable(resourceToBeParsed: String): Boolean { + return false + } + override fun getName(): String { + return InteractiveParserHelper.SourceMonitorImporterConstants.name + } } diff --git a/analysis/import/CodeMaatImporter/src/main/kotlin/de/maibornwolff/codecharta/importer/codemaat/CodeMaatImporter.kt b/analysis/import/CodeMaatImporter/src/main/kotlin/de/maibornwolff/codecharta/importer/codemaat/CodeMaatImporter.kt index f07e9d1236..1ddd936775 100644 --- a/analysis/import/CodeMaatImporter/src/main/kotlin/de/maibornwolff/codecharta/importer/codemaat/CodeMaatImporter.kt +++ b/analysis/import/CodeMaatImporter/src/main/kotlin/de/maibornwolff/codecharta/importer/codemaat/CodeMaatImporter.kt @@ -5,6 +5,7 @@ import de.maibornwolff.codecharta.model.AttributeTypes import de.maibornwolff.codecharta.serialization.ProjectSerializer import de.maibornwolff.codecharta.tools.interactiveparser.InteractiveParser import de.maibornwolff.codecharta.tools.interactiveparser.ParserDialogInterface +import de.maibornwolff.codecharta.tools.interactiveparser.util.InteractiveParserHelper import de.maibornwolff.codecharta.translator.MetricNameTranslator import picocli.CommandLine import java.io.File @@ -14,9 +15,9 @@ import java.io.PrintStream import java.util.concurrent.Callable @CommandLine.Command( - name = "codemaatimport", - description = ["generates cc.json from codemaat coupling csv"], - footer = ["Copyright(c) 2022, MaibornWolff GmbH"] + name = InteractiveParserHelper.CodeMaatImporterConstants.name, + description = [InteractiveParserHelper.CodeMaatImporterConstants.description], + footer = [InteractiveParserHelper.GeneralConstants.GenericFooter] ) class CodeMaatImporter( private val output: PrintStream = System.out) : Callable, InteractiveParser { @@ -79,4 +80,11 @@ class CodeMaatImporter( } override fun getDialog(): ParserDialogInterface = ParserDialog + override fun isApplicable(resourceToBeParsed: String): Boolean { + return false + } + + override fun getName(): String { + return InteractiveParserHelper.CodeMaatImporterConstants.name + } } diff --git a/analysis/import/GitLogParser/src/main/kotlin/de/maibornwolff/codecharta/importer/gitlogparser/GitLogParser.kt b/analysis/import/GitLogParser/src/main/kotlin/de/maibornwolff/codecharta/importer/gitlogparser/GitLogParser.kt index 49db61301f..46912e8151 100644 --- a/analysis/import/GitLogParser/src/main/kotlin/de/maibornwolff/codecharta/importer/gitlogparser/GitLogParser.kt +++ b/analysis/import/GitLogParser/src/main/kotlin/de/maibornwolff/codecharta/importer/gitlogparser/GitLogParser.kt @@ -13,6 +13,7 @@ import de.maibornwolff.codecharta.serialization.ProjectDeserializer import de.maibornwolff.codecharta.serialization.ProjectSerializer import de.maibornwolff.codecharta.tools.interactiveparser.InteractiveParser import de.maibornwolff.codecharta.tools.interactiveparser.ParserDialogInterface +import de.maibornwolff.codecharta.tools.interactiveparser.util.InteractiveParserHelper import org.mozilla.universalchardet.UniversalDetector import picocli.CommandLine import java.io.File @@ -21,14 +22,15 @@ import java.io.InputStream import java.io.PrintStream import java.nio.charset.Charset import java.nio.file.Files +import java.nio.file.Paths import java.util.concurrent.Callable import java.util.stream.Stream @CommandLine.Command( - name = "gitlogparser", - description = ["git log parser - generates cc.json from git-log files"], + name = InteractiveParserHelper.GitLogParserConstants.name, + description = [InteractiveParserHelper.GitLogParserConstants.description], subcommands = [LogScanCommand::class, RepoScanCommand::class], - footer = ["Copyright(c) 2022, MaibornWolff GmbH"] + footer = [InteractiveParserHelper.GeneralConstants.GenericFooter] ) class GitLogParser( private val input: InputStream = System.`in`, @@ -152,4 +154,25 @@ class GitLogParser( } override fun getDialog(): ParserDialogInterface = ParserDialog + override fun isApplicable(resourceToBeParsed: String): Boolean { + if (resourceToBeParsed.endsWith(".git")) { + return true + } + + val searchFile = if (resourceToBeParsed == "") { + File(Paths.get("").toAbsolutePath().toString()) + } else { + File(resourceToBeParsed) + } + + return searchFile.walk() + .maxDepth(1) + .asSequence() + .map { it.name } + .filter { it.endsWith(".git") } + .any() + } + override fun getName(): String { + return InteractiveParserHelper.GitLogParserConstants.name + } } diff --git a/analysis/import/GitLogParser/src/main/kotlin/de/maibornwolff/codecharta/importer/gitlogparser/subcommands/LogScanCommand.kt b/analysis/import/GitLogParser/src/main/kotlin/de/maibornwolff/codecharta/importer/gitlogparser/subcommands/LogScanCommand.kt index 19fc8946aa..b7c6fef01d 100644 --- a/analysis/import/GitLogParser/src/main/kotlin/de/maibornwolff/codecharta/importer/gitlogparser/subcommands/LogScanCommand.kt +++ b/analysis/import/GitLogParser/src/main/kotlin/de/maibornwolff/codecharta/importer/gitlogparser/subcommands/LogScanCommand.kt @@ -58,4 +58,11 @@ class LogScanCommand : Callable, InteractiveParser { } override fun getDialog(): ParserDialogInterface = LogScanParserDialog + override fun isApplicable(resourceToBeParsed: String): Boolean { + return false + } + + override fun getName(): String { + return "log-scan" + } } diff --git a/analysis/import/GitLogParser/src/main/kotlin/de/maibornwolff/codecharta/importer/gitlogparser/subcommands/RepoScanCommand.kt b/analysis/import/GitLogParser/src/main/kotlin/de/maibornwolff/codecharta/importer/gitlogparser/subcommands/RepoScanCommand.kt index 400e16b00f..caaccb49d9 100644 --- a/analysis/import/GitLogParser/src/main/kotlin/de/maibornwolff/codecharta/importer/gitlogparser/subcommands/RepoScanCommand.kt +++ b/analysis/import/GitLogParser/src/main/kotlin/de/maibornwolff/codecharta/importer/gitlogparser/subcommands/RepoScanCommand.kt @@ -82,4 +82,11 @@ class RepoScanCommand : Callable, InteractiveParser { } override fun getDialog(): ParserDialogInterface = RepoScanParserDialog + override fun isApplicable(resourceToBeParsed: String): Boolean { + return false + } + + override fun getName(): String { + return "repo-scan" + } } diff --git a/analysis/import/GitLogParser/src/test/kotlin/de/maibornwolff/codecharta/importer/gitlogparser/GitLogParserTest.kt b/analysis/import/GitLogParser/src/test/kotlin/de/maibornwolff/codecharta/importer/gitlogparser/GitLogParserTest.kt index 30c39b6b9a..839f39bf30 100644 --- a/analysis/import/GitLogParser/src/test/kotlin/de/maibornwolff/codecharta/importer/gitlogparser/GitLogParserTest.kt +++ b/analysis/import/GitLogParser/src/test/kotlin/de/maibornwolff/codecharta/importer/gitlogparser/GitLogParserTest.kt @@ -2,12 +2,33 @@ package de.maibornwolff.codecharta.importer.gitlogparser import de.maibornwolff.codecharta.importer.gitlogparser.GitLogParser.Companion.main import de.maibornwolff.codecharta.serialization.ProjectDeserializer +import org.assertj.core.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource import java.io.File class GitLogParserTest { + companion object { + @JvmStatic + fun provideValidInputFiles(): List { + return listOf( + Arguments.of("src/test/resources/my/git/repo"), + Arguments.of("src/test/resources/my/git/repo/Test.git")) + } + + @JvmStatic + fun provideInvalidInputFiles(): List { + return listOf( + Arguments.of("src/test/resources/my/empty/repo"), + Arguments.of("src/test/resources/this/does/not/exist"), + Arguments.of(""), + Arguments.of("src/test/resources/my")) + } + } @Test fun `should create json uncompressed file repo-scan`() { @@ -62,4 +83,18 @@ class GitLogParserTest { assertTrue(file.exists()) } + + @ParameterizedTest + @MethodSource("provideValidInputFiles") + fun `should be identified as applicable for given directory path containing a git folder`(resourceToBeParsed: String) { + val isUsable = GitLogParser().isApplicable(resourceToBeParsed) + Assertions.assertThat(isUsable).isTrue() + } + + @ParameterizedTest + @MethodSource("provideInvalidInputFiles") + fun `should NOT be identified as applicable if no git folder is present at given path`(resourceToBeParsed: String) { + val isUsable = GitLogParser().isApplicable(resourceToBeParsed) + Assertions.assertThat(isUsable).isFalse() + } } diff --git a/analysis/import/GitLogParser/src/test/resources/my/git/repo/Test.git b/analysis/import/GitLogParser/src/test/resources/my/git/repo/Test.git new file mode 100644 index 0000000000..e69de29bb2 diff --git a/analysis/import/MetricGardenerImporter/src/main/kotlin/de/maibornwolff/codecharta/importer/metricgardenerimporter/MetricGardenerImporter.kt b/analysis/import/MetricGardenerImporter/src/main/kotlin/de/maibornwolff/codecharta/importer/metricgardenerimporter/MetricGardenerImporter.kt index c9e8d3b432..0274db2409 100644 --- a/analysis/import/MetricGardenerImporter/src/main/kotlin/de/maibornwolff/codecharta/importer/metricgardenerimporter/MetricGardenerImporter.kt +++ b/analysis/import/MetricGardenerImporter/src/main/kotlin/de/maibornwolff/codecharta/importer/metricgardenerimporter/MetricGardenerImporter.kt @@ -8,6 +8,7 @@ import de.maibornwolff.codecharta.importer.metricgardenerimporter.model.MetricGa import de.maibornwolff.codecharta.serialization.ProjectSerializer import de.maibornwolff.codecharta.tools.interactiveparser.InteractiveParser import de.maibornwolff.codecharta.tools.interactiveparser.ParserDialogInterface +import de.maibornwolff.codecharta.tools.interactiveparser.util.InteractiveParserHelper import mu.KotlinLogging import picocli.CommandLine import java.io.File @@ -18,9 +19,9 @@ import java.nio.file.Paths import java.util.concurrent.Callable @CommandLine.Command( - name = "metricgardenerimport", - description = ["generates a cc.json file from a project parsed with metric-gardener"], - footer = ["Copyright(c) 2022, MaibornWolff GmbH"] + name = InteractiveParserHelper.MetricGardenerImporterConstants.name, + description = [InteractiveParserHelper.MetricGardenerImporterConstants.description], + footer = [InteractiveParserHelper.GeneralConstants.GenericFooter] ) class MetricGardenerImporter( @@ -101,4 +102,11 @@ class MetricGardenerImporter( } override fun getDialog(): ParserDialogInterface = ParserDialog + override fun isApplicable(resourceToBeParsed: String): Boolean { + return false + } + + override fun getName(): String { + return InteractiveParserHelper.MetricGardenerImporterConstants.name + } } diff --git a/analysis/import/SVNLogParser/src/main/kotlin/de/maibornwolff/codecharta/importer/svnlogparser/SVNLogParser.kt b/analysis/import/SVNLogParser/src/main/kotlin/de/maibornwolff/codecharta/importer/svnlogparser/SVNLogParser.kt index 876ff72745..d2592db0dd 100644 --- a/analysis/import/SVNLogParser/src/main/kotlin/de/maibornwolff/codecharta/importer/svnlogparser/SVNLogParser.kt +++ b/analysis/import/SVNLogParser/src/main/kotlin/de/maibornwolff/codecharta/importer/svnlogparser/SVNLogParser.kt @@ -11,6 +11,7 @@ import de.maibornwolff.codecharta.serialization.ProjectDeserializer import de.maibornwolff.codecharta.serialization.ProjectSerializer import de.maibornwolff.codecharta.tools.interactiveparser.InteractiveParser import de.maibornwolff.codecharta.tools.interactiveparser.ParserDialogInterface +import de.maibornwolff.codecharta.tools.interactiveparser.util.InteractiveParserHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -27,9 +28,9 @@ import java.util.concurrent.Callable import java.util.stream.Stream @CommandLine.Command( - name = "svnlogparser", - description = ["generates cc.json from svn log file"], - footer = ["Copyright(c) 2020, MaibornWolff GmbH"] + name = InteractiveParserHelper.SVNLogParserConstants.name, + description = [InteractiveParserHelper.SVNLogParserConstants.description], + footer = [InteractiveParserHelper.GeneralConstants.GenericFooter] ) class SVNLogParser( private val input: InputStream = System.`in`, @@ -150,7 +151,6 @@ class SVNLogParser( } companion object { - @JvmStatic fun main(args: Array) { CommandLine(SVNLogParser()).execute(*args) @@ -173,4 +173,11 @@ class SVNLogParser( } override fun getDialog(): ParserDialogInterface = ParserDialog + override fun isApplicable(resourceToBeParsed: String): Boolean { + return false + } + + override fun getName(): String { + return InteractiveParserHelper.SVNLogParserConstants.name + } } diff --git a/analysis/import/SonarImporter/src/main/kotlin/de/maibornwolff/codecharta/importer/sonar/SonarImporterMain.kt b/analysis/import/SonarImporter/src/main/kotlin/de/maibornwolff/codecharta/importer/sonar/SonarImporterMain.kt index 7f04a3a5a0..c8079cc759 100644 --- a/analysis/import/SonarImporter/src/main/kotlin/de/maibornwolff/codecharta/importer/sonar/SonarImporterMain.kt +++ b/analysis/import/SonarImporter/src/main/kotlin/de/maibornwolff/codecharta/importer/sonar/SonarImporterMain.kt @@ -8,16 +8,18 @@ import de.maibornwolff.codecharta.serialization.ProjectDeserializer import de.maibornwolff.codecharta.serialization.ProjectSerializer import de.maibornwolff.codecharta.tools.interactiveparser.InteractiveParser import de.maibornwolff.codecharta.tools.interactiveparser.ParserDialogInterface +import de.maibornwolff.codecharta.tools.interactiveparser.util.InteractiveParserHelper import picocli.CommandLine +import java.io.File import java.io.InputStream import java.io.PrintStream import java.net.URL import java.util.concurrent.Callable @CommandLine.Command( - name = "sonarimport", - description = ["generates cc.json from metric data from SonarQube"], - footer = ["Copyright(c) 2022, MaibornWolff GmbH"] + name = InteractiveParserHelper.SonarImporterConstants.name, + description = [InteractiveParserHelper.SonarImporterConstants.description], + footer = [InteractiveParserHelper.GeneralConstants.GenericFooter] ) class SonarImporterMain( private val input: InputStream = System.`in`, @@ -93,4 +95,19 @@ class SonarImporterMain( } override fun getDialog(): ParserDialogInterface = ParserDialog + override fun isApplicable(resourceToBeParsed: String): Boolean { + val trimmedInput = resourceToBeParsed.trim() + val searchFile = File(resourceToBeParsed) + + return (trimmedInput.startsWith("http://") || trimmedInput.startsWith("https://") || + trimmedInput == ("sonar-project.properties") || + searchFile.walk() + .asSequence().filter { it.isFile } + .map { it.name }.filter { it == "sonar-project.properties" } + .any()) + } + + override fun getName(): String { + return InteractiveParserHelper.SonarImporterConstants.name + } } diff --git a/analysis/import/SonarImporter/src/test/kotlin/de/maibornwolff/codecharta/importer/sonar/SonarImporterMainTest.kt b/analysis/import/SonarImporter/src/test/kotlin/de/maibornwolff/codecharta/importer/sonar/SonarImporterMainTest.kt index 7f2531438e..fd7a0de59b 100644 --- a/analysis/import/SonarImporter/src/test/kotlin/de/maibornwolff/codecharta/importer/sonar/SonarImporterMainTest.kt +++ b/analysis/import/SonarImporter/src/test/kotlin/de/maibornwolff/codecharta/importer/sonar/SonarImporterMainTest.kt @@ -6,11 +6,14 @@ import com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo import com.github.tomakehurst.wiremock.client.WireMock.verify import com.github.tomakehurst.wiremock.junit5.WireMockTest import de.maibornwolff.codecharta.importer.sonar.dataaccess.SonarMetricsAPIDatasource +import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource import picocli.CommandLine import javax.ws.rs.core.MediaType -import kotlin.jvm.Throws private const val PORT = 8089 @@ -19,6 +22,29 @@ class SonarImporterMainTest { companion object { private const val METRIC_LIST_URL_PATH = "/api/metrics/search?f=hidden,decimalScale&p=1&ps=${SonarMetricsAPIDatasource.PAGE_SIZE}" + + @JvmStatic + fun provideValidUrls(): List { + return listOf( + Arguments.of("https://thisisatesturl.com"), + Arguments.of("http://thisisatesturl.com")) + } + + @JvmStatic + fun provideValidInputFiles(): List { + return listOf( + Arguments.of("src/test/resources/my/sonar/repo"), + Arguments.of("src/test/resources/my/sonar/repo/sonar-project.properties"), + Arguments.of("src/test/resources/my/sonar")) + } + + @JvmStatic + fun provideInvalidInputFiles(): List { + return listOf( + Arguments.of("src/test/resources/my/nonsonar/repo"), + Arguments.of("src/test/resources/this/does/not/exist"), + Arguments.of("")) + } } @BeforeEach @@ -69,4 +95,25 @@ class SonarImporterMainTest { verify(1, getRequestedFor(urlEqualTo(METRIC_LIST_URL_PATH))) } + + @ParameterizedTest + @MethodSource("provideValidUrls") + fun `should be identified as applicable for given directory path being an url`(resourceToBeParsed: String) { + val isApplicable = SonarImporterMain().isApplicable(resourceToBeParsed) + Assertions.assertTrue(isApplicable) + } + + @ParameterizedTest + @MethodSource("provideValidInputFiles") + fun `should be identified as applicable for given directory path containing a sonar properties file`(resourceToBeParsed: String) { + val isApplicable = SonarImporterMain().isApplicable(resourceToBeParsed) + Assertions.assertTrue(isApplicable) + } + + @ParameterizedTest + @MethodSource("provideInvalidInputFiles") + fun `should NOT be identified as applicable if no sonar properties file is present at given path`(resourceToBeParsed: String) { + val isApplicable = SonarImporterMain().isApplicable(resourceToBeParsed) + Assertions.assertFalse(isApplicable) + } } diff --git a/analysis/import/SonarImporter/src/test/resources/my/sonar/repo/sonar-project.properties b/analysis/import/SonarImporter/src/test/resources/my/sonar/repo/sonar-project.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/analysis/import/SourceCodeParser/src/main/kotlin/de/maibornwolff/codecharta/importer/sourcecodeparser/SourceCodeParserMain.kt b/analysis/import/SourceCodeParser/src/main/kotlin/de/maibornwolff/codecharta/importer/sourcecodeparser/SourceCodeParserMain.kt index 7f51882dfc..b60df0ee5c 100644 --- a/analysis/import/SourceCodeParser/src/main/kotlin/de/maibornwolff/codecharta/importer/sourcecodeparser/SourceCodeParserMain.kt +++ b/analysis/import/SourceCodeParser/src/main/kotlin/de/maibornwolff/codecharta/importer/sourcecodeparser/SourceCodeParserMain.kt @@ -7,6 +7,7 @@ import de.maibornwolff.codecharta.serialization.OutputFileHandler import de.maibornwolff.codecharta.serialization.ProjectDeserializer import de.maibornwolff.codecharta.tools.interactiveparser.InteractiveParser import de.maibornwolff.codecharta.tools.interactiveparser.ParserDialogInterface +import de.maibornwolff.codecharta.tools.interactiveparser.util.InteractiveParserHelper import picocli.CommandLine import java.io.BufferedWriter import java.io.File @@ -21,9 +22,9 @@ import java.nio.file.Paths import java.util.concurrent.Callable @CommandLine.Command( - name = "sourcecodeparser", - description = ["generates cc.json from source code"], - footer = ["This program uses the SonarJava, which is licensed under the GNU Lesser General Public Library, version 3.\nCopyright(c) 2020, MaibornWolff GmbH"] + name = InteractiveParserHelper.SourceCodeParserConstants.name, + description = [InteractiveParserHelper.SourceCodeParserConstants.description], + footer = [InteractiveParserHelper.SourceCodeParserConstants.footer] ) class SourceCodeParserMain( private val outputStream: PrintStream, @@ -131,4 +132,11 @@ class SourceCodeParserMain( } override fun getDialog(): ParserDialogInterface = ParserDialog + override fun isApplicable(resourceToBeParsed: String): Boolean { + return false + } + + override fun getName(): String { + return InteractiveParserHelper.SourceCodeParserConstants.name + } } diff --git a/analysis/import/TokeiImporter/src/main/kotlin/de/maibornwolff/codecharta/importer/tokeiimporter/TokeiImporter.kt b/analysis/import/TokeiImporter/src/main/kotlin/de/maibornwolff/codecharta/importer/tokeiimporter/TokeiImporter.kt index 2f2069c849..d0ae61a5ae 100644 --- a/analysis/import/TokeiImporter/src/main/kotlin/de/maibornwolff/codecharta/importer/tokeiimporter/TokeiImporter.kt +++ b/analysis/import/TokeiImporter/src/main/kotlin/de/maibornwolff/codecharta/importer/tokeiimporter/TokeiImporter.kt @@ -12,6 +12,7 @@ import de.maibornwolff.codecharta.serialization.ProjectSerializer import de.maibornwolff.codecharta.serialization.mapLines import de.maibornwolff.codecharta.tools.interactiveparser.InteractiveParser import de.maibornwolff.codecharta.tools.interactiveparser.ParserDialogInterface +import de.maibornwolff.codecharta.tools.interactiveparser.util.InteractiveParserHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -25,9 +26,9 @@ import java.io.PrintWriter import java.util.concurrent.Callable @CommandLine.Command( - name = "tokeiimporter", - description = ["generates cc.json from tokei json"], - footer = ["Copyright(c) 2020, MaibornWolff GmbH"] + name = InteractiveParserHelper.TokeiImporterConstants.name, + description = [InteractiveParserHelper.TokeiImporterConstants.description], + footer = [InteractiveParserHelper.GeneralConstants.GenericFooter] ) class TokeiImporter( private val input: InputStream = System.`in`, @@ -131,4 +132,11 @@ class TokeiImporter( } override fun getDialog(): ParserDialogInterface = ParserDialog + override fun isApplicable(resourceToBeParsed: String): Boolean { + return false + } + + override fun getName(): String { + return InteractiveParserHelper.TokeiImporterConstants.name + } } diff --git a/analysis/parser/RawTextParser/src/main/kotlin/de/maibornwolff/codecharta/parser/rawtextparser/RawTextParser.kt b/analysis/parser/RawTextParser/src/main/kotlin/de/maibornwolff/codecharta/parser/rawtextparser/RawTextParser.kt index 4f03449dee..afdd9d9974 100644 --- a/analysis/parser/RawTextParser/src/main/kotlin/de/maibornwolff/codecharta/parser/rawtextparser/RawTextParser.kt +++ b/analysis/parser/RawTextParser/src/main/kotlin/de/maibornwolff/codecharta/parser/rawtextparser/RawTextParser.kt @@ -6,6 +6,7 @@ import de.maibornwolff.codecharta.serialization.ProjectDeserializer import de.maibornwolff.codecharta.serialization.ProjectSerializer import de.maibornwolff.codecharta.tools.interactiveparser.InteractiveParser import de.maibornwolff.codecharta.tools.interactiveparser.ParserDialogInterface +import de.maibornwolff.codecharta.tools.interactiveparser.util.InteractiveParserHelper import picocli.CommandLine import java.io.File import java.io.IOException @@ -16,9 +17,9 @@ import java.nio.file.Paths import java.util.concurrent.Callable @CommandLine.Command( - name = "rawtextparser", - description = ["generates cc.json from projects or source code files"], - footer = ["Copyright(c) 2022, MaibornWolff GmbH"] + name = InteractiveParserHelper.RawTextParserConstants.name, + description = [InteractiveParserHelper.RawTextParserConstants.description], + footer = [InteractiveParserHelper.GeneralConstants.GenericFooter] ) class RawTextParser( private val input: InputStream = System.`in`, @@ -115,4 +116,10 @@ class RawTextParser( } override fun getDialog(): ParserDialogInterface = ParserDialog + override fun isApplicable(resourceToBeParsed: String): Boolean { + return false + } + override fun getName(): String { + return InteractiveParserHelper.RawTextParserConstants.name + } } diff --git a/analysis/tools/InteractiveParser/src/main/kotlin/de/maibornwolff/codecharta/tools/interactiveparser/InteractiveParser.kt b/analysis/tools/InteractiveParser/src/main/kotlin/de/maibornwolff/codecharta/tools/interactiveparser/InteractiveParser.kt index a594783823..cec0b23ab9 100644 --- a/analysis/tools/InteractiveParser/src/main/kotlin/de/maibornwolff/codecharta/tools/interactiveparser/InteractiveParser.kt +++ b/analysis/tools/InteractiveParser/src/main/kotlin/de/maibornwolff/codecharta/tools/interactiveparser/InteractiveParser.kt @@ -2,4 +2,8 @@ package de.maibornwolff.codecharta.tools.interactiveparser interface InteractiveParser { fun getDialog(): ParserDialogInterface + + fun isApplicable(resourceToBeParsed: String): Boolean + + fun getName(): String } diff --git a/analysis/tools/InteractiveParser/src/main/kotlin/de/maibornwolff/codecharta/tools/interactiveparser/util/InteractiveParserHelper.kt b/analysis/tools/InteractiveParser/src/main/kotlin/de/maibornwolff/codecharta/tools/interactiveparser/util/InteractiveParserHelper.kt new file mode 100644 index 0000000000..e5c07b6c49 --- /dev/null +++ b/analysis/tools/InteractiveParser/src/main/kotlin/de/maibornwolff/codecharta/tools/interactiveparser/util/InteractiveParserHelper.kt @@ -0,0 +1,84 @@ +package de.maibornwolff.codecharta.tools.interactiveparser.util + +class InteractiveParserHelper { + + object GeneralConstants { + const val GenericFooter = "Copyright(c) 2023, MaibornWolff GmbH" + } + object CSVExporterConstants { + const val name = "csvexport" + const val description = "generates csv file with header" + } + + object EdgeFilterConstants { + const val name = "edgefilter" + const val description = "aggregates edgeAttributes as nodeAttributes into a new cc.json file" + } + + object MergeFilterConstants { + const val name = "merge" + const val description = "merges multiple cc.json files" + } + + object StructureModifierConstants { + const val name = "modify" + const val description = "changes the structure of cc.json files" + } + + object CodeMaatImporterConstants { + const val name = "codemaatimport" + const val description = "generates cc.json from codemaat coupling csv" + } + + object CSVImporterConstants { + const val name = "csvimport" + const val description = "generates cc.json from csv with header" + } + + object SourceMonitorImporterConstants { + const val name = "sourcemonitorimport" + const val description = "generates cc.json from sourcemonitor csv" + } + + object GitLogParserConstants { + const val name = "gitlogparser" + const val description = "generates cc.json from git-log files" + } + + object MetricGardenerImporterConstants { + const val name = "metricgardenerimport" + const val description = "generates a cc.json file from a project parsed with metric-gardener" + } + + object SonarImporterConstants { + const val name = "sonarimport" + const val description = "generates cc.json from metric data from SonarQube" + } + + object SourceCodeParserConstants { + const val name = "sourcecodeparser" + const val description = "generates cc.json from source code" + const val footer = "This program uses the SonarJava, which is licensed under the GNU Lesser General Public Library, version 3.\n" + + "Copyright(c) 2020, MaibornWolff GmbH" + } + + object SVNLogParserConstants { + const val name = "svnlogparser" + const val description = "generates cc.json from svn log file" + } + + object TokeiImporterConstants { + const val name = "tokeiimporter" + const val description = "generates cc.json from tokei json" + } + + object RawTextParserConstants { + const val name = "rawtextparser" + const val description = "generates cc.json from projects or source code files" + } + + object ValidationToolConstants { + const val name = "check" + const val description = "validates cc.json files" + } +} diff --git a/analysis/tools/ValidationTool/src/main/kotlin/de/maibornwolff/codecharta/tools/validation/ValidationTool.kt b/analysis/tools/ValidationTool/src/main/kotlin/de/maibornwolff/codecharta/tools/validation/ValidationTool.kt index 1b222b4a32..77e948f0e6 100644 --- a/analysis/tools/ValidationTool/src/main/kotlin/de/maibornwolff/codecharta/tools/validation/ValidationTool.kt +++ b/analysis/tools/ValidationTool/src/main/kotlin/de/maibornwolff/codecharta/tools/validation/ValidationTool.kt @@ -2,15 +2,16 @@ package de.maibornwolff.codecharta.tools.validation import de.maibornwolff.codecharta.tools.interactiveparser.InteractiveParser import de.maibornwolff.codecharta.tools.interactiveparser.ParserDialogInterface +import de.maibornwolff.codecharta.tools.interactiveparser.util.InteractiveParserHelper import picocli.CommandLine import java.io.File import java.io.FileInputStream import java.util.concurrent.Callable @CommandLine.Command( - name = "check", - description = ["validates cc.json files"], - footer = ["Copyright(c) 2020, MaibornWolff GmbH"] + name = InteractiveParserHelper.ValidationToolConstants.name, + description = [InteractiveParserHelper.ValidationToolConstants.description], + footer = [InteractiveParserHelper.GeneralConstants.GenericFooter] ) class ValidationTool : Callable, InteractiveParser { @@ -31,4 +32,10 @@ class ValidationTool : Callable, InteractiveParser { } override fun getDialog(): ParserDialogInterface = ParserDialog + override fun isApplicable(resourceToBeParsed: String): Boolean { + return false + } + override fun getName(): String { + return InteractiveParserHelper.ValidationToolConstants.name + } } diff --git a/analysis/tools/ccsh/src/main/kotlin/de/maibornwolff/codecharta/tools/ccsh/Ccsh.kt b/analysis/tools/ccsh/src/main/kotlin/de/maibornwolff/codecharta/tools/ccsh/Ccsh.kt index e246b71deb..0576e6ddef 100644 --- a/analysis/tools/ccsh/src/main/kotlin/de/maibornwolff/codecharta/tools/ccsh/Ccsh.kt +++ b/analysis/tools/ccsh/src/main/kotlin/de/maibornwolff/codecharta/tools/ccsh/Ccsh.kt @@ -1,5 +1,7 @@ package de.maibornwolff.codecharta.tools.ccsh +import com.github.kinquirer.KInquirer +import com.github.kinquirer.components.promptConfirm import de.maibornwolff.codecharta.exporter.csv.CSVExporter import de.maibornwolff.codecharta.filter.edgefilter.EdgeFilter import de.maibornwolff.codecharta.filter.mergefilter.MergeFilter @@ -14,11 +16,17 @@ import de.maibornwolff.codecharta.importer.sourcemonitor.SourceMonitorImporter import de.maibornwolff.codecharta.importer.svnlogparser.SVNLogParser import de.maibornwolff.codecharta.importer.tokeiimporter.TokeiImporter import de.maibornwolff.codecharta.parser.rawtextparser.RawTextParser +import de.maibornwolff.codecharta.tools.ccsh.parser.InteractiveParserSuggestionDialog import de.maibornwolff.codecharta.tools.ccsh.parser.ParserService +import de.maibornwolff.codecharta.tools.ccsh.parser.repository.PicocliParserRepository import de.maibornwolff.codecharta.tools.validation.ValidationTool import mu.KotlinLogging import picocli.CommandLine import java.util.concurrent.Callable +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger +import kotlin.math.min import kotlin.system.exitProcess @CommandLine.Command( @@ -58,6 +66,9 @@ class Ccsh : Callable { @CommandLine.Option(names = ["-h", "--help"], usageHelp = true, description = ["displays this help and exit"]) var help: Boolean = false + @CommandLine.Option(names = ["-i", "--interactive"], description = ["starts interactive parser"]) + var shouldUseInteractiveShell: Boolean = false + override fun call(): Void? { // info: always run @@ -67,6 +78,8 @@ class Ccsh : Callable { companion object { private val logger = KotlinLogging.logger {} + const val NO_USABLE_PARSER_FOUND_MESSAGE = "No usable parser was found for the input file path!" + @JvmStatic fun main(args: Array) { exitProcess(executeCommandLine(args)) @@ -75,15 +88,69 @@ class Ccsh : Callable { fun executeCommandLine(args: Array): Int { val commandLine = CommandLine(Ccsh()) commandLine.executionStrategy = CommandLine.RunAll() - if (args.isEmpty() || isParserUnknown(args, commandLine)) { - return executeInteractiveParser(commandLine) + return if (args.isEmpty()) { + val configuredParsers = InteractiveParserSuggestionDialog.offerAndGetInteractiveParserSuggestionsAndConfigurations(commandLine) + if (configuredParsers.isEmpty()) { + return 0 + } + + val shouldRunConfiguredParsers: Boolean = + KInquirer.promptConfirm( + message = "Do you want to run all configured parsers now?", + default = true) + + return if (shouldRunConfiguredParsers) { + executeConfiguredParsers(commandLine, configuredParsers) + } else { + 0 + } + } else if (isParserUnknown(args, commandLine) || args.contains("--interactive") || args.contains("-i")) { + executeInteractiveParser(commandLine) } else { - return commandLine.execute(*sanitizeArgs(args)) + commandLine.execute(*sanitizeArgs(args)) } } + fun executeConfiguredParsers(commandLine: CommandLine, configuredParsers: Map>): Int { + val exitCode = AtomicInteger(0) + val numberOfThreadsToBeStarted = min(configuredParsers.size, Runtime.getRuntime().availableProcessors()) + val threadPool = Executors.newFixedThreadPool(numberOfThreadsToBeStarted) + for (configuredParser in configuredParsers) { + threadPool.execute { + val currentExitCode = executeConfiguredParser(commandLine, configuredParser) + if (currentExitCode != 0) { + exitCode.set(currentExitCode) + logger.info("Code: $currentExitCode") + } + } + } + threadPool.shutdown() + threadPool.awaitTermination(1, TimeUnit.DAYS) + + val finalExitCode = exitCode.get() + logger.info("Code: $finalExitCode") + if (finalExitCode != 0) { + return finalExitCode + } + // Improvement: Try to extract merge commands before so user does not have to configure merge args? + logger.info("Each parser was successfully executed and created a cc.json file. " + + "You can merge all results by making sure they are in one folder and executing the merging tool.") + return 0 + } + + private fun executeConfiguredParser(commandLine: CommandLine, configuredParser: Map.Entry>): Int { + logger.info { "Executing ${configuredParser.key}" } + val exitCode = ParserService.executePreconfiguredParser(commandLine, Pair(configuredParser.key, configuredParser.value)) + + if (exitCode != 0) { + logger.info("Error executing ${configuredParser.key}, code $exitCode") + } + + return exitCode + } + private fun executeInteractiveParser(commandLine: CommandLine): Int { - val selectedParser = ParserService.selectParser(commandLine) + val selectedParser = ParserService.selectParser(commandLine, PicocliParserRepository()) logger.info { "Executing $selectedParser" } return ParserService.executeSelectedParser(commandLine, selectedParser) } diff --git a/analysis/tools/ccsh/src/main/kotlin/de/maibornwolff/codecharta/tools/ccsh/parser/InteractiveParserSuggestionDialog.kt b/analysis/tools/ccsh/src/main/kotlin/de/maibornwolff/codecharta/tools/ccsh/parser/InteractiveParserSuggestionDialog.kt new file mode 100644 index 0000000000..9b70566b9d --- /dev/null +++ b/analysis/tools/ccsh/src/main/kotlin/de/maibornwolff/codecharta/tools/ccsh/parser/InteractiveParserSuggestionDialog.kt @@ -0,0 +1,59 @@ +package de.maibornwolff.codecharta.tools.ccsh.parser + +import com.github.kinquirer.KInquirer +import com.github.kinquirer.components.promptCheckbox +import com.github.kinquirer.components.promptInput +import de.maibornwolff.codecharta.tools.ccsh.Ccsh +import de.maibornwolff.codecharta.tools.ccsh.parser.repository.PicocliParserRepository +import mu.KotlinLogging +import picocli.CommandLine + +class InteractiveParserSuggestionDialog { + companion object { + private val logger = KotlinLogging.logger {} + + fun offerAndGetInteractiveParserSuggestionsAndConfigurations(commandLine: CommandLine): Map> { + val applicableParsers = getApplicableInteractiveParsers(commandLine) + + if (applicableParsers.isEmpty()) { + return emptyMap() + } + + val selectedParsers = selectToBeExecutedInteractiveParsers(applicableParsers) + + return if (selectedParsers.isEmpty()) { + emptyMap() + } else { + ParserService.configureParserSelection(commandLine, PicocliParserRepository(), selectedParsers) + } + } + + private fun getApplicableInteractiveParsers(commandLine: CommandLine): List { + val inputFile: String = KInquirer.promptInput( + message = "Which path should be scanned?", + hint = "You can provide a directory path / file path / sonar url.") + + val applicableParsers = + ParserService.getParserSuggestions(commandLine, PicocliParserRepository(), inputFile) + + if (applicableParsers.isEmpty()) { + logger.info(Ccsh.NO_USABLE_PARSER_FOUND_MESSAGE) + return emptyList() + } + + return applicableParsers + } + + private fun selectToBeExecutedInteractiveParsers(applicableParsers: List): List { + val selectedParsers = KInquirer.promptCheckbox( + message = "Choose from this list of applicable parsers", + choices = applicableParsers) + + if (selectedParsers.isEmpty()) { + logger.info("Did not select any parser to be configured!") + return emptyList() + } + return selectedParsers + } + } +} diff --git a/analysis/tools/ccsh/src/main/kotlin/de/maibornwolff/codecharta/tools/ccsh/parser/ParserService.kt b/analysis/tools/ccsh/src/main/kotlin/de/maibornwolff/codecharta/tools/ccsh/parser/ParserService.kt index 7258da213a..ca3e05bcd4 100644 --- a/analysis/tools/ccsh/src/main/kotlin/de/maibornwolff/codecharta/tools/ccsh/parser/ParserService.kt +++ b/analysis/tools/ccsh/src/main/kotlin/de/maibornwolff/codecharta/tools/ccsh/parser/ParserService.kt @@ -2,6 +2,7 @@ package de.maibornwolff.codecharta.tools.ccsh.parser import com.github.kinquirer.KInquirer import com.github.kinquirer.components.promptList +import de.maibornwolff.codecharta.tools.ccsh.parser.repository.PicocliParserRepository import de.maibornwolff.codecharta.tools.interactiveparser.InteractiveParser import picocli.CommandLine @@ -9,47 +10,67 @@ class ParserService { companion object { private const val EXIT_CODE_PARSER_NOT_SUPPORTED = 42 - fun selectParser(commandLine: CommandLine): String { + fun getParserSuggestions(commandLine: CommandLine, parserRepository: PicocliParserRepository, inputFile: String): List { + val allParsers = parserRepository.getAllInteractiveParsers(commandLine) + val usableParsers = parserRepository.getApplicableInteractiveParserNames(inputFile, allParsers) + + return usableParsers.ifEmpty { emptyList() } + } + + fun configureParserSelection(commandLine: CommandLine, parserRepository: PicocliParserRepository, selectedParsers: List): Map> { + val configuredParsers = mutableMapOf>() + for (selectedParser in selectedParsers) { + val interactiveParser = parserRepository.getInteractiveParser(commandLine, selectedParser) + if (interactiveParser == null) { + throw IllegalArgumentException("Tried to configure non existing parser!") + } else { + val configuration = interactiveParser.getDialog().collectParserArgs() + configuredParsers[selectedParser] = configuration + + println("You can run the same command again by using the following command line arguments:") + println("ccsh " + selectedParser + " " + configuration.map { x -> '"' + x + '"' }.joinToString(" ")) + } + } + return configuredParsers + } + + fun selectParser(commandLine: CommandLine, parserRepository: PicocliParserRepository): String { val selectedParser: String = KInquirer.promptList( message = "Which parser do you want to execute?", - choices = getParserNamesWithDescription(commandLine) + choices = parserRepository.getInteractiveParserNamesWithDescription(commandLine) ) - - return extractParserName(selectedParser) + return parserRepository.extractParserName(selectedParser) } fun executeSelectedParser(commandLine: CommandLine, selectedParser: String): Int { val subCommand = commandLine.subcommands.getValue(selectedParser) val parserObject = subCommand.commandSpec.userObject() val interactive = parserObject as? InteractiveParser - if (interactive != null) { + return if (interactive != null) { val collectedArgs = interactive.getDialog().collectParserArgs() val subCommandLine = CommandLine(interactive) println("You can run the same command again by using the following command line arguments:") println("ccsh " + selectedParser + " " + collectedArgs.map { x -> '"' + x + '"' }.joinToString(" ")) - return subCommandLine.execute(*collectedArgs.toTypedArray()) + subCommandLine.execute(*collectedArgs.toTypedArray()) } else { printNotSupported(selectedParser) - return EXIT_CODE_PARSER_NOT_SUPPORTED + EXIT_CODE_PARSER_NOT_SUPPORTED } } - private fun getParserNamesWithDescription(commandLine: CommandLine): List { - val subCommands = commandLine.subcommands.values - return subCommands.mapNotNull { subCommand -> - val parserName = subCommand.commandName - if (subCommand.commandSpec.userObject() is InteractiveParser) { - val parserDescriptions = subCommand.commandSpec.usageMessage().description() - val parserDescription = parserDescriptions[0] - "$parserName - $parserDescription" - } else null + fun executePreconfiguredParser(commandLine: CommandLine, configuredParser: Pair>): Int { + val subCommand = commandLine.subcommands.getValue(configuredParser.first) + val parserObject = subCommand.commandSpec.userObject() + val interactive = parserObject as? InteractiveParser + return if (interactive != null) { + val subCommandLine = CommandLine(interactive) + subCommandLine.execute(*configuredParser.second.toTypedArray()) + } else { + printNotSupported(configuredParser.first) + EXIT_CODE_PARSER_NOT_SUPPORTED } } - private fun extractParserName(parserNameWithDescription: String): String { - return parserNameWithDescription.substringBefore(' ') - } - private fun printNotSupported(parserName: String) { println( "The interactive usage of $parserName is not supported yet.\n" + diff --git a/analysis/tools/ccsh/src/main/kotlin/de/maibornwolff/codecharta/tools/ccsh/parser/repository/ParserRepository.kt b/analysis/tools/ccsh/src/main/kotlin/de/maibornwolff/codecharta/tools/ccsh/parser/repository/ParserRepository.kt new file mode 100644 index 0000000000..0c3d7a9bd5 --- /dev/null +++ b/analysis/tools/ccsh/src/main/kotlin/de/maibornwolff/codecharta/tools/ccsh/parser/repository/ParserRepository.kt @@ -0,0 +1,11 @@ +package de.maibornwolff.codecharta.tools.ccsh.parser.repository + +import de.maibornwolff.codecharta.tools.interactiveparser.InteractiveParser +interface ParserRepository { + fun getInteractiveParserNames(dataSource: T): List + fun getInteractiveParserNamesWithDescription(dataSource: T): List + fun extractParserName(parserNameWithDescription: String): String + fun getAllInteractiveParsers(dataSource: T): List + fun getApplicableInteractiveParserNames(inputFile: String, allParsers: List): List + fun getInteractiveParser(dataSource: T, name: String): InteractiveParser? +} diff --git a/analysis/tools/ccsh/src/main/kotlin/de/maibornwolff/codecharta/tools/ccsh/parser/repository/PicocliParserRepository.kt b/analysis/tools/ccsh/src/main/kotlin/de/maibornwolff/codecharta/tools/ccsh/parser/repository/PicocliParserRepository.kt new file mode 100644 index 0000000000..25277c7fb7 --- /dev/null +++ b/analysis/tools/ccsh/src/main/kotlin/de/maibornwolff/codecharta/tools/ccsh/parser/repository/PicocliParserRepository.kt @@ -0,0 +1,68 @@ +package de.maibornwolff.codecharta.tools.ccsh.parser.repository + +import de.maibornwolff.codecharta.tools.interactiveparser.InteractiveParser +import picocli.CommandLine + +class PicocliParserRepository : ParserRepository { + + override fun getInteractiveParserNames(dataSource: CommandLine): List { + val subCommands = dataSource.subcommands.values + return subCommands.mapNotNull { subCommand -> + val parserName = subCommand.commandName + if (subCommand.commandSpec.userObject() is InteractiveParser) { + parserName + } else null + } + } + + override fun getInteractiveParserNamesWithDescription(dataSource: CommandLine): List { + val subCommands = dataSource.subcommands.values + return subCommands.mapNotNull { subCommand -> + val parserName = subCommand.commandName + if (subCommand.commandSpec.userObject() is InteractiveParser) { + val parserDescriptions = subCommand.commandSpec.usageMessage().description() + val parserDescription = parserDescriptions[0] + "$parserName - $parserDescription" + } else null + } + } + + override fun extractParserName(parserNameWithDescription: String): String { + return parserNameWithDescription.substringBefore(' ') + } + + override fun getAllInteractiveParsers(dataSource: CommandLine): List { + val allParserNames = getInteractiveParserNames(dataSource) + val allParsers = mutableListOf() + for (parserName in allParserNames) { + val interactive = getInteractiveParser(dataSource, parserName) + + if (interactive != null) { + allParsers.add(interactive) + } + } + return allParsers + } + + override fun getApplicableInteractiveParserNames(inputFile: String, allParsers: List): List { + val usableParsers = mutableListOf() + + for (parser in allParsers) { + if (parser.isApplicable(inputFile)) { + usableParsers.add(parser.getName()) + } + } + return usableParsers + } + + override fun getInteractiveParser(dataSource: CommandLine, name: String): InteractiveParser? { + return try { + val subCommand = dataSource.subcommands.getValue(name) + val parserObject = subCommand.commandSpec.userObject() + parserObject as? InteractiveParser + } catch (exception: NoSuchElementException) { + println("Could not find the specified parser with the name '$name'!") + null + } + } +} diff --git a/analysis/tools/ccsh/src/test/kotlin/de/maibornwolff/codecharta/ccsh/CcshTest.kt b/analysis/tools/ccsh/src/test/kotlin/de/maibornwolff/codecharta/ccsh/CcshTest.kt index 1eee84c6db..8314f773e9 100644 --- a/analysis/tools/ccsh/src/test/kotlin/de/maibornwolff/codecharta/ccsh/CcshTest.kt +++ b/analysis/tools/ccsh/src/test/kotlin/de/maibornwolff/codecharta/ccsh/CcshTest.kt @@ -1,13 +1,18 @@ package de.maibornwolff.codecharta.ccsh +import com.github.kinquirer.KInquirer +import com.github.kinquirer.components.promptConfirm import de.maibornwolff.codecharta.tools.ccsh.Ccsh +import de.maibornwolff.codecharta.tools.ccsh.parser.InteractiveParserSuggestionDialog import de.maibornwolff.codecharta.tools.ccsh.parser.ParserService import io.mockk.every import io.mockk.mockkObject +import io.mockk.mockkStatic import io.mockk.unmockkAll import io.mockk.verify import org.assertj.core.api.Assertions import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import picocli.CommandLine @@ -18,6 +23,19 @@ import java.io.StringWriter @TestInstance(TestInstance.Lifecycle.PER_CLASS) class CcshTest { + private val outContent = ByteArrayOutputStream() + private val originalOut = System.out + private val cmdLine = CommandLine(Ccsh()) + + @BeforeAll + fun setUpStreams() { + System.setOut(PrintStream(outContent)) + } + + @AfterAll + fun restoreStreams() { + System.setOut(originalOut) + } @AfterAll fun afterTest() { @@ -47,37 +65,137 @@ class CcshTest { Assertions.assertThat(exitCode).isEqualTo(0) Assertions.assertThat(contentOutput.toString()) - .contains("Usage: ccsh [-hv] [COMMAND]", "Command Line Interface for CodeCharta analysis") + .contains("Usage: ccsh [-hiv] [COMMAND]", "Command Line Interface for CodeCharta analysis") + verify(exactly = 0) { ParserService.executePreconfiguredParser(any(), any()) } + } + + @Test + fun `should execute parser suggestions and all selected parsers when no options are passed`() { + val selectedParsers = listOf("parser1", "parser2") + val args = listOf(listOf("dummyArg1"), listOf("dummyArg2")) + + mockkObject(InteractiveParserSuggestionDialog) + every { + InteractiveParserSuggestionDialog.offerAndGetInteractiveParserSuggestionsAndConfigurations(any()) + } returns mapOf(selectedParsers[0] to args[0], selectedParsers[1] to args[1]) + + mockkObject(ParserService) + every { + ParserService.executeSelectedParser(any(), any()) + } returns 0 + + every { + ParserService.executePreconfiguredParser(any(), any()) + } returns 0 + + mockkStatic("com.github.kinquirer.components.ConfirmKt") + every { + KInquirer.promptConfirm(any(), any()) + } returns true + + val exitCode = Ccsh.executeCommandLine(emptyArray()) + Assertions.assertThat(exitCode).isZero + + verify(exactly = 2) { ParserService.executePreconfiguredParser(any(), any()) } + } + + @Test + fun `should only execute parsers when configuration was successful`() { + mockkObject(InteractiveParserSuggestionDialog) + every { + InteractiveParserSuggestionDialog.offerAndGetInteractiveParserSuggestionsAndConfigurations(any()) + } returns emptyMap() + + mockkObject(ParserService) + every { + ParserService.executeSelectedParser(any(), any()) + } returns 0 + every { + ParserService.executePreconfiguredParser(any(), any()) + } returns 0 + + val exitCode = Ccsh.executeCommandLine(emptyArray()) + + Assertions.assertThat(exitCode == 0).isTrue() + verify(exactly = 0) { ParserService.executeSelectedParser(any(), any()) } + verify(exactly = 0) { ParserService.executePreconfiguredParser(any(), any()) } + } + + @Test + fun `should only execute parsers when user does confirm execution`() { + val selectedParsers = listOf("parser1", "parser2") + val args = listOf(listOf("dummyArg1"), listOf("dummyArg2")) + + mockkObject(InteractiveParserSuggestionDialog) + every { + InteractiveParserSuggestionDialog.offerAndGetInteractiveParserSuggestionsAndConfigurations(any()) + } returns mapOf(selectedParsers[0] to args[0], selectedParsers[1] to args[1]) + + mockkObject(ParserService) + every { + ParserService.executeSelectedParser(any(), any()) + } returns 0 + every { + ParserService.executePreconfiguredParser(any(), any()) + } returns 0 + + mockkStatic("com.github.kinquirer.components.ConfirmKt") + every { + KInquirer.promptConfirm(any(), any()) + } returns false + + val exitCode = Ccsh.executeCommandLine(emptyArray()) + + Assertions.assertThat(exitCode == 0).isTrue() + verify(exactly = 0) { ParserService.executeSelectedParser(any(), any()) } + verify(exactly = 0) { ParserService.executePreconfiguredParser(any(), any()) } + } + + @Test + fun `should continue executing parsers even if there is an error while executing one`() { + mockkObject(ParserService) + every { + ParserService.executeSelectedParser(any(), any()) + } returns -1 + every { + ParserService.executePreconfiguredParser(any(), any()) + } returns -1 + + val dummyConfiguredParsers = mapOf("dummyParser1" to listOf("dummyArg1", "dummyArg2"), "dummyParser2" to listOf("dummyArg1", "dummyArg2")) + + Ccsh.executeConfiguredParsers(cmdLine, dummyConfiguredParsers) + + verify(exactly = 2) { ParserService.executePreconfiguredParser(any(), any()) } verify(exactly = 0) { ParserService.executeSelectedParser(any(), any()) } } @Test - fun `should start interactive parser when no arguments or parameters are passed`() { + fun `should execute interactive parser when passed parser is unknown`() { mockkObject(ParserService) every { - ParserService.selectParser(any()) + ParserService.selectParser(any(), any()) } returns "someparser" every { ParserService.executeSelectedParser(any(), any()) } returns 0 - val exitCode = Ccsh.executeCommandLine(emptyArray()) + val exitCode = Ccsh.executeCommandLine(arrayOf("unknownparser")) Assertions.assertThat(exitCode).isZero verify { ParserService.executeSelectedParser(any(), any()) } } @Test - fun `should execute interactive parser when passed parser is unknown`() { + fun `should execute interactive parser when -i option is passed`() { mockkObject(ParserService) every { - ParserService.selectParser(any()) + ParserService.selectParser(any(), any()) } returns "someparser" every { ParserService.executeSelectedParser(any(), any()) } returns 0 - val exitCode = Ccsh.executeCommandLine(arrayOf("unknownparser")) + val exitCode = Ccsh.executeCommandLine(arrayOf("-i")) Assertions.assertThat(exitCode).isZero verify { ParserService.executeSelectedParser(any(), any()) } diff --git a/analysis/tools/ccsh/src/test/kotlin/de/maibornwolff/codecharta/ccsh/parser/InteractiveParserSuggestionDialogTest.kt b/analysis/tools/ccsh/src/test/kotlin/de/maibornwolff/codecharta/ccsh/parser/InteractiveParserSuggestionDialogTest.kt new file mode 100644 index 0000000000..08d70061c6 --- /dev/null +++ b/analysis/tools/ccsh/src/test/kotlin/de/maibornwolff/codecharta/ccsh/parser/InteractiveParserSuggestionDialogTest.kt @@ -0,0 +1,140 @@ +package de.maibornwolff.codecharta.ccsh.parser + +import com.github.kinquirer.KInquirer +import com.github.kinquirer.components.promptCheckbox +import com.github.kinquirer.components.promptInput +import com.github.kinquirer.components.promptList +import de.maibornwolff.codecharta.tools.ccsh.Ccsh +import de.maibornwolff.codecharta.tools.ccsh.parser.InteractiveParserSuggestionDialog +import de.maibornwolff.codecharta.tools.ccsh.parser.ParserService +import io.mockk.every +import io.mockk.mockkObject +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import picocli.CommandLine +import java.io.ByteArrayOutputStream +import java.io.PrintStream + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class InteractiveParserSuggestionDialogTest { + private val outContent = ByteArrayOutputStream() + private val originalOut = System.out + private val errorOut = ByteArrayOutputStream() + private val originalErrorOut = System.err + private val cmdLine = CommandLine(Ccsh()) + + @BeforeAll + fun setUpStreams() { + System.setOut(PrintStream(outContent)) + System.setErr(PrintStream(errorOut)) + } + + @AfterAll + fun restoreStreams() { + System.setOut(originalOut) + System.setErr(originalErrorOut) + } + + @AfterAll + fun afterTest() { + unmockkAll() + } + + @Test + fun `should only output message when no usable parsers were found`() { + mockkStatic("com.github.kinquirer.components.InputKt") + every { + KInquirer.promptInput(any(), any(), any(), any(), any()) + } returns "" + + mockkStatic("com.github.kinquirer.components.ListKt") + every { + KInquirer.promptList(any(), any(), any(), any(), any()) + } returns "" + + mockkObject(ParserService) + every { + ParserService.getParserSuggestions(any(), any(), any()) + } returns emptyList() + + val usableParsers = InteractiveParserSuggestionDialog.offerAndGetInteractiveParserSuggestionsAndConfigurations(cmdLine) + + Assertions.assertThat(errorOut.toString()).contains(Ccsh.NO_USABLE_PARSER_FOUND_MESSAGE) + Assertions.assertThat(usableParsers).isNotNull + Assertions.assertThat(usableParsers).isEmpty() + } + + @Test + fun `should return empty map when user does not select any parser`() { + mockkStatic("com.github.kinquirer.components.InputKt") + every { + KInquirer.promptInput(any(), any(), any(), any(), any()) + } returns "" + + mockkStatic("com.github.kinquirer.components.ListKt") + every { + KInquirer.promptList(any(), any(), any(), any(), any()) + } returns "" + + mockkStatic("com.github.kinquirer.components.CheckboxKt") + every { + KInquirer.promptCheckbox(any(), any(), any(), any(), any()) + } returns emptyList() + + val parser = "dummyParser" + + mockkObject(ParserService) + every { + ParserService.getParserSuggestions(any(), any(), any()) + } returns listOf(parser) + + val selectedParsers = InteractiveParserSuggestionDialog.offerAndGetInteractiveParserSuggestionsAndConfigurations(cmdLine) + + Assertions.assertThat(selectedParsers).isNotNull + Assertions.assertThat(selectedParsers).isEmpty() + } + + @Test + fun `should return configured parsers after user finished configuring selection`() { + mockkStatic("com.github.kinquirer.components.InputKt") + every { + KInquirer.promptInput(any(), any(), any(), any(), any()) + } returns "" + + mockkStatic("com.github.kinquirer.components.ListKt") + every { + KInquirer.promptList(any(), any(), any(), any(), any()) + } returns "" + + val parser = "dummyParser" + val configuration = listOf("dummyArg") + val parserList = listOf(parser) + + mockkStatic("com.github.kinquirer.components.CheckboxKt") + every { + KInquirer.promptCheckbox(any(), any(), any(), any(), any()) + } returns parserList + + mockkObject(ParserService) + every { + ParserService.getParserSuggestions(any(), any(), any()) + } returns parserList + + every { + ParserService.configureParserSelection(any(), any(), any()) + } returns mapOf(parser to configuration) + + val configuredParsers = InteractiveParserSuggestionDialog.offerAndGetInteractiveParserSuggestionsAndConfigurations(cmdLine) + + Assertions.assertThat(configuredParsers).isNotNull + Assertions.assertThat(configuredParsers).isNotEmpty + + Assertions.assertThat(configuredParsers).containsKey(parser) + Assertions.assertThat(configuredParsers[parser] == configuration).isTrue() + } +} diff --git a/analysis/tools/ccsh/src/test/kotlin/de/maibornwolff/codecharta/ccsh/parser/ParserServiceTest.kt b/analysis/tools/ccsh/src/test/kotlin/de/maibornwolff/codecharta/ccsh/parser/ParserServiceTest.kt index cc6518d4f5..27a5fafd72 100644 --- a/analysis/tools/ccsh/src/test/kotlin/de/maibornwolff/codecharta/ccsh/parser/ParserServiceTest.kt +++ b/analysis/tools/ccsh/src/test/kotlin/de/maibornwolff/codecharta/ccsh/parser/ParserServiceTest.kt @@ -1,16 +1,14 @@ package de.maibornwolff.codecharta.ccsh.parser -import com.github.kinquirer.KInquirer -import com.github.kinquirer.components.promptList import de.maibornwolff.codecharta.tools.ccsh.Ccsh import de.maibornwolff.codecharta.tools.ccsh.parser.ParserService +import de.maibornwolff.codecharta.tools.ccsh.parser.repository.PicocliParserRepository import de.maibornwolff.codecharta.tools.interactiveparser.InteractiveParser import de.maibornwolff.codecharta.tools.interactiveparser.ParserDialogInterface import io.mockk.every import io.mockk.mockkClass import io.mockk.mockkConstructor import io.mockk.mockkObject -import io.mockk.mockkStatic import io.mockk.unmockkAll import io.mockk.verify import org.assertj.core.api.Assertions @@ -50,7 +48,7 @@ class ParserServiceTest { companion object { @JvmStatic - fun provideArguments(): List { + fun providerParserArguments(): List { return listOf( Arguments.of("check"), Arguments.of("edgefilter"), @@ -69,20 +67,78 @@ class ParserServiceTest { } } + @ParameterizedTest + @MethodSource("providerParserArguments") + fun `should output generated parser command for each configured parser`(selectedParser: String) { + val parser = mockParserObject(selectedParser) + + val collectedArgs = parser.getDialog().collectParserArgs() + val expectedParserCommand = "ccsh " + selectedParser + " " + collectedArgs.map { x -> '"' + x + '"' }.joinToString(" ") + + val selectedParserList = listOf(selectedParser) + val mockPicocliParserRepository = mockParserRepository(selectedParser, emptyList()) + + ParserService.configureParserSelection(cmdLine, mockPicocliParserRepository, selectedParserList) + + Assertions.assertThat(outContent.toString()) + .contains(expectedParserCommand) + } + @Test - fun `should return the selected parser name`() { - mockkStatic("com.github.kinquirer.components.ListKt") - every { - KInquirer.promptList(any(), any(), any(), any(), any()) - } returns "parser - description" + fun `should output empty list for parser suggestions if no usable parsers were found`() { + // Parser name is chosen arbitrarily + val mockParserRepository = mockParserRepository("check", emptyList()) - val selectedParserName = ParserService.selectParser(CommandLine(Ccsh())) + val usableParsers = ParserService.getParserSuggestions(cmdLine, mockParserRepository, "dummy") - Assertions.assertThat(selectedParserName).isEqualTo("parser") + Assertions.assertThat(usableParsers).isNotNull + Assertions.assertThat(usableParsers).isEmpty() + } + + @Test + fun `should output parser name list of user selected parsers from parser suggestions`() { + val expectedUsualParsers = listOf("check", "validate") + // Parser name is chosen arbitrarily + val mockParserRepository = mockParserRepository("check", expectedUsualParsers) + val actualUsableParsers = ParserService.getParserSuggestions(cmdLine, mockParserRepository, "dummy") + + Assertions.assertThat(actualUsableParsers).isNotNull + Assertions.assertThat(actualUsableParsers).isNotEmpty + + Assertions.assertThat(actualUsableParsers).contains("check") + Assertions.assertThat(actualUsableParsers).contains("validate") + } + + @Test + fun `should start configuration for each selected parser`() { + val selectedParserList = listOf("check", + "edgefilter", + "sonarimport", + "svnlogparser", + "merge", + "gitlogparser", + "rawtextparser", + "sourcemonitorimport", + "tokeiimporter", + "sourcecodeparser", + "modify", + "csvexport", + "codemaatimport") + val mockPicocliParserRepository = mockParserRepository(selectedParserList[0], emptyList()) + + val configuredParsers = ParserService.configureParserSelection(cmdLine, mockPicocliParserRepository, selectedParserList) + + Assertions.assertThat(configuredParsers).isNotEmpty + Assertions.assertThat(configuredParsers).size().isEqualTo(selectedParserList.size) + + for (entry in configuredParsers) { + Assertions.assertThat(entry.value).isNotEmpty + Assertions.assertThat(entry.value[0] == "dummyArg").isTrue() + } } @ParameterizedTest - @MethodSource("provideArguments") + @MethodSource("providerParserArguments") fun `should execute parser`(parser: String) { mockParserObject(parser) @@ -91,12 +147,25 @@ class ParserServiceTest { verify { anyConstructed().execute(any()) } } + @ParameterizedTest + @MethodSource("providerParserArguments") + fun `should execute preconfigured parser`(parser: String) { + val parserObject = mockParserObject(parser) + + ParserService.executePreconfiguredParser(cmdLine, Pair(parser, parserObject.getDialog().collectParserArgs())) + + verify { anyConstructed().execute(any()) } + } + @Test fun `should not execute any parser`() { - Assertions.assertThatExceptionOfType(NoSuchElementException::class.java).isThrownBy { ParserService.executeSelectedParser(cmdLine, "unknownparser") } + + Assertions.assertThatExceptionOfType(NoSuchElementException::class.java).isThrownBy { + ParserService.executePreconfiguredParser(cmdLine, Pair("unknownparser", listOf("dummyArg"))) + } } private fun mockParserObject(name: String): InteractiveParser { @@ -114,4 +183,22 @@ class ParserServiceTest { every { anyConstructed().execute(*dummyArgs.toTypedArray()) } returns 0 return obj } + + private fun mockParserRepository(mockParserName: String, usableParsers: List): PicocliParserRepository { + val obj = mockkClass(PicocliParserRepository::class) + + every { + obj.getInteractiveParser(any(), any()) + } returns mockParserObject(mockParserName) + + every { + obj.getAllInteractiveParsers(any()) + } returns emptyList() + + every { + obj.getApplicableInteractiveParserNames(any(), any()) + } returns usableParsers + + return obj + } } diff --git a/analysis/tools/ccsh/src/test/kotlin/de/maibornwolff/codecharta/ccsh/parser/repository/PicocliParserRepositoryTest.kt b/analysis/tools/ccsh/src/test/kotlin/de/maibornwolff/codecharta/ccsh/parser/repository/PicocliParserRepositoryTest.kt new file mode 100644 index 0000000000..87089276cd --- /dev/null +++ b/analysis/tools/ccsh/src/test/kotlin/de/maibornwolff/codecharta/ccsh/parser/repository/PicocliParserRepositoryTest.kt @@ -0,0 +1,176 @@ +package de.maibornwolff.codecharta.ccsh.parser.repository + +import de.maibornwolff.codecharta.exporter.csv.CSVExporter +import de.maibornwolff.codecharta.filter.edgefilter.EdgeFilter +import de.maibornwolff.codecharta.filter.mergefilter.MergeFilter +import de.maibornwolff.codecharta.filter.structuremodifier.StructureModifier +import de.maibornwolff.codecharta.importer.codemaat.CodeMaatImporter +import de.maibornwolff.codecharta.importer.csv.CSVImporter +import de.maibornwolff.codecharta.importer.gitlogparser.GitLogParser +import de.maibornwolff.codecharta.importer.metricgardenerimporter.MetricGardenerImporter +import de.maibornwolff.codecharta.importer.sonar.SonarImporterMain +import de.maibornwolff.codecharta.importer.sourcecodeparser.SourceCodeParserMain +import de.maibornwolff.codecharta.importer.sourcemonitor.SourceMonitorImporter +import de.maibornwolff.codecharta.importer.svnlogparser.SVNLogParser +import de.maibornwolff.codecharta.importer.tokeiimporter.TokeiImporter +import de.maibornwolff.codecharta.parser.rawtextparser.RawTextParser +import de.maibornwolff.codecharta.tools.ccsh.Ccsh +import de.maibornwolff.codecharta.tools.ccsh.parser.repository.PicocliParserRepository +import de.maibornwolff.codecharta.tools.interactiveparser.InteractiveParser +import de.maibornwolff.codecharta.tools.interactiveparser.ParserDialogInterface +import io.mockk.every +import io.mockk.mockkClass +import io.mockk.mockkConstructor +import io.mockk.mockkObject +import io.mockk.unmockkAll +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import picocli.CommandLine +import java.io.ByteArrayOutputStream +import java.io.PrintStream + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class PicocliParserRepositoryTest { + + private val outContent = ByteArrayOutputStream() + private val originalOut = System.out + private val cmdLine = CommandLine(Ccsh()) + + private val picocliParserRepository = PicocliParserRepository() + + @BeforeAll + fun setUpStreams() { + System.setOut(PrintStream(outContent)) + } + + @AfterAll + fun restoreStreams() { + System.setOut(originalOut) + } + + @AfterEach + fun afterTest() { + unmockkAll() + } + + private fun getExpectedParsers(): List { + return listOf(CSVExporter(), + EdgeFilter(), MergeFilter(), + StructureModifier(), CSVImporter(), + SonarImporterMain(), SourceMonitorImporter(), + SVNLogParser(), GitLogParser(), + SourceCodeParserMain(), CodeMaatImporter(), + TokeiImporter(), RawTextParser(), MetricGardenerImporter()) + } + + private fun getExpectedParserNamesWithDescription(): List { + return listOf(CSVExporter().getName() + " - generates csv file with header", + EdgeFilter().getName() + " - aggregates edgeAttributes as nodeAttributes into a new cc.json file", + MergeFilter().getName() + " - merges multiple cc.json files", + StructureModifier().getName() + " - changes the structure of cc.json files", + CSVImporter().getName() + " - generates cc.json from csv with header", + SonarImporterMain().getName() + " - generates cc.json from metric data from SonarQube", + SourceMonitorImporter().getName() + " - generates cc.json from sourcemonitor csv", + SVNLogParser().getName() + " - generates cc.json from svn log file", + GitLogParser().getName() + " - generates cc.json from git-log files", + SourceCodeParserMain().getName() + " - generates cc.json from source code", + CodeMaatImporter().getName() + " - generates cc.json from codemaat coupling csv", + TokeiImporter().getName() + " - generates cc.json from tokei json", + RawTextParser().getName() + " - generates cc.json from projects or source code files", + MetricGardenerImporter().getName() + " - generates a cc.json file from a project parsed with metric-gardener") + } + + @Test + fun `should return all interactive parsers`() { + val expectedParsers = getExpectedParsers() + val expectedParserNames = expectedParsers.map { it.getName() } + + val actualParsers = picocliParserRepository.getAllInteractiveParsers(cmdLine) + val actualParserNames = actualParsers.map { it.getName() } + + for (parser in expectedParserNames) { + Assertions.assertTrue(actualParserNames.contains(parser)) + } + } + + @Test + fun `should return all interactive parser names`() { + val expectedParsers = getExpectedParsers() + val expectedParserNames = expectedParsers.map { it.getName() } + + val actualParserNames = picocliParserRepository.getInteractiveParserNames(cmdLine) + + for (parser in expectedParserNames) { + Assertions.assertTrue(actualParserNames.contains(parser)) + } + } + + @Test + fun `should return all usable parser names`() { + // Names are chosen arbitrarily + val usableParser = mockParserObject("gitlogparser", true) + val unusableParser = mockParserObject("sonarimport", false) + + val usableParsers = picocliParserRepository.getApplicableInteractiveParserNames("input", listOf(usableParser, unusableParser)) + + Assertions.assertTrue(usableParsers.contains("gitlogparser")) + Assertions.assertFalse(usableParsers.contains("sonarimport")) + } + + @Test + fun `should return all parser names with description`() { + // Names are chosen arbitrarily + val expectedParserNamesWithDescription = getExpectedParserNamesWithDescription() + + val actualParserNamesWithDescription = picocliParserRepository.getInteractiveParserNamesWithDescription(cmdLine) + + for (parserNameWithDescription in expectedParserNamesWithDescription) { + Assertions.assertTrue(actualParserNamesWithDescription.contains(parserNameWithDescription)) + } + } + + @Test + fun `should return the selected parser name`() { + val expectedParserNamesWithDescription = getExpectedParserNamesWithDescription() + val expectedParsers = getExpectedParsers() + val expectedParserNames = expectedParsers.map { it.getName() } + + for (parser in expectedParserNamesWithDescription) { + Assertions.assertTrue(expectedParserNames.contains(picocliParserRepository.extractParserName(parser))) + } + } + + @Test + fun `should not crash when trying to get invalid parser name and output message`() { + val parserName = picocliParserRepository.getInteractiveParser(cmdLine, "nonexistent") + + Assertions.assertNull(parserName) + Assertions.assertTrue(outContent.toString().contains("Could not find the specified parser with the name 'nonexistent'!")) + } + + private fun mockParserObject(name: String, isUsable: Boolean): InteractiveParser { + val obj = cmdLine.subcommands[name]!!.commandSpec.userObject() as InteractiveParser + mockkObject(obj) + val dialogInterface = mockkClass(ParserDialogInterface::class) + val dummyArgs = listOf("dummyArg") + every { + dialogInterface.collectParserArgs() + } returns dummyArgs + every { + obj.getDialog() + } returns dialogInterface + every { + obj.isApplicable(any()) + } returns isUsable + every { + obj.getName() + } returns name + mockkConstructor(CommandLine::class) + every { anyConstructed().execute(*dummyArgs.toTypedArray()) } returns 0 + return obj + } +}