From 4f6d27e84ba89999a878c06d8c7bf9cff1c237a9 Mon Sep 17 00:00:00 2001 From: Sebastian Wolf <65733509+phanlezz@users.noreply.github.com> Date: Mon, 2 Dec 2024 12:08:58 +0100 Subject: [PATCH 1/5] feat(analysis): add core structure for large merge, WIP (#3743) --- README.md | 2 +- .../codecharta/filter/mergefilter/FatMerge.kt | 55 +++ .../filter/mergefilter/MergeFilter.kt | 49 ++- .../filter/mergefilter/mimo/Mimo.kt | 4 +- .../filter/mergefilter/MergeFilterTest.kt | 66 +++- .../duplicate/testProject.beta.cc.json | 323 ++++++++++++++++++ .../resources/fatMerge/testEdges1.cc.json | 128 +++++++ .../fatMerge/testProject.alpha.cc.json | 323 ++++++++++++++++++ 8 files changed, 943 insertions(+), 7 deletions(-) create mode 100644 analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/FatMerge.kt create mode 100644 analysis/filter/MergeFilter/src/test/resources/fatMerge/duplicate/testProject.beta.cc.json create mode 100644 analysis/filter/MergeFilter/src/test/resources/fatMerge/testEdges1.cc.json create mode 100644 analysis/filter/MergeFilter/src/test/resources/fatMerge/testProject.alpha.cc.json diff --git a/README.md b/README.md index 17e5ac6e4b..e9e17e3821 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

Latest Release:
- Analysis 1.129.0 | Visualization 1.131.2 + Analysis 1.129.0 | Visualization 1.131.2 [comment]: ################################################################################## [comment]: diff --git a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/FatMerge.kt b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/FatMerge.kt new file mode 100644 index 0000000000..98da63bf20 --- /dev/null +++ b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/FatMerge.kt @@ -0,0 +1,55 @@ +package de.maibornwolff.codecharta.filter.mergefilter + +import de.maibornwolff.codecharta.model.BlacklistItem +import de.maibornwolff.codecharta.model.Edge +import de.maibornwolff.codecharta.model.Node +import de.maibornwolff.codecharta.model.NodeType +import de.maibornwolff.codecharta.model.Project + +class FatMerge { + companion object { + fun packageProjectInto(project: Project, prefix: String): Project { + val modifiedProject = Project( + project.projectName, + moveNodesIntoFolder(project.rootNode, prefix), + project.apiVersion, + addFolderToEdgePaths(project.edges, prefix), + project.attributeTypes, + project.attributeDescriptors, + addFolderToBlackListPaths(project.blacklist, prefix) + ) + return modifiedProject + } + + private fun moveNodesIntoFolder(root: Node, folderName: String): List { + val newRoot = Node( + name = folderName, + type = NodeType.Folder, + children = root.children + ) + val mutableRoot = root.toMutableNode() + mutableRoot.children = mutableSetOf(newRoot.toMutableNode()) + return listOf(mutableRoot.toNode()) + } + + private fun addFolderToEdgePaths(edges: List, folderName: String): List { + edges.forEach { + it.fromNodeName = insertFolderIntoPath(it.fromNodeName, folderName) + it.toNodeName = insertFolderIntoPath(it.toNodeName, folderName) + } + return edges + } + + private fun addFolderToBlackListPaths(blacklist: List, folderName: String): List { + blacklist.forEach { + it.path = insertFolderIntoPath(it.path, folderName) + } + return blacklist + } + + private fun insertFolderIntoPath(path: String, folderName: String): String { + require(Regex("^/root/").matches(path)) + return path.replace(Regex("^/root/"), "/root/$folderName/") + } + } +} 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 cded7f991a..abfaf7669a 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 @@ -58,6 +58,12 @@ class MergeFilter( ) private var levenshteinDistance = 3 + @CommandLine.Option( + names = ["--fat"], + description = ["Merges input files into on file divided into project sub-folder as defined per prefix of each input file"] + ) + private var fatMerge = false + override val name = NAME override val description = DESCRIPTION @@ -94,8 +100,11 @@ class MergeFilter( if (mimo) { processMimoMerge(sourceFiles, nodeMergerStrategy) + } else if (fatMerge) { + processFatMerge(sourceFiles, nodeMergerStrategy) } else { val projects = readInputFiles(sourceFiles) + if (!continueIfIncompatibleProjects(projects)) return null val mergedProject = ProjectMerger(projects, nodeMergerStrategy).merge() @@ -155,8 +164,9 @@ class MergeFilter( return@forEach } - val projects = readInputFiles(confirmedFileList) - if (projects.size <= 1) { + val projectsFileNamePairs = readInputFilesKeepFileNames(confirmedFileList) + val projects = projectsFileNamePairs.map { it.second } + if (projectsFileNamePairs.size <= 1) { Logger.warn { "After deserializing there were one or less projects. Continue with next group" } return@forEach } @@ -164,7 +174,7 @@ class MergeFilter( if (!continueIfIncompatibleProjects(projects)) return@forEach val mergedProject = ProjectMerger(projects, nodeMergerStrategy).merge() - val outputFilePrefix = Mimo.retrieveGroupName(confirmedFileList) + val outputFilePrefix = Mimo.retrieveGroupName(projectsFileNamePairs.map { it.first }) ProjectSerializer.serializeToFileOrStream(mergedProject, "$outputFilePrefix.merge.cc.json", output, compress) Logger.info { "Merged files with prefix '$outputFilePrefix' into" + @@ -173,6 +183,27 @@ class MergeFilter( } } + private fun processFatMerge(sourceFiles: List, nodeMergerStrategy: NodeMergerStrategy) { + val projectsFileNamePairs = readInputFilesKeepFileNames(sourceFiles) + val fileNameList = projectsFileNamePairs.map { it.first } + + require(fileNameList.size > 1) { + Logger.warn { "One or less projects in input, merging aborted." } + } + + require(fileNameList.groupingBy { it.substringBefore(".") }.eachCount().all {it.value == 1}) { + Logger.warn { "Make sure that the input prefixes across all input files are unique!" } + } + + val packagedProjects : MutableList = mutableListOf() + projectsFileNamePairs.forEach { + packagedProjects.add(FatMerge.packageProjectInto(it.second, it.first.substringBefore("."))) + } + + val mergedProject = ProjectMerger(packagedProjects, nodeMergerStrategy).merge() + ProjectSerializer.serializeToFileOrStream(mergedProject, outputFile, output, compress) + } + private fun readInputFiles(files: List): List { return files.mapNotNull { val input = it.inputStream() @@ -184,4 +215,16 @@ class MergeFilter( } } } + + private fun readInputFilesKeepFileNames(files: List): List> { + return files.mapNotNull { + try { + Pair(it.name, ProjectDeserializer.deserializeProject(it.inputStream())) + } catch (e: Exception) { + Logger.warn { "${it.name} is not a valid project file and will be skipped." } + null + } + + } + } } diff --git a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/mimo/Mimo.kt b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/mimo/Mimo.kt index bca25bb8c9..e263fc04da 100644 --- a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/mimo/Mimo.kt +++ b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/mimo/Mimo.kt @@ -51,8 +51,8 @@ class Mimo { } } - fun retrieveGroupName(files: List): String { - val filePrefixes = files.map { it.name.substringBefore(".") }.toSet() + fun retrieveGroupName(files: List): String { + val filePrefixes = files.map { it.substringBefore(".") }.toSet() if (filePrefixes.size == 1) return filePrefixes.first() return ParserDialog.askForMimoPrefix(filePrefixes) } diff --git a/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt b/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt index e5ca7fdad9..85eba25ac9 100644 --- a/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt +++ b/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt @@ -243,7 +243,7 @@ class MergeFilterTest { @Nested @DisplayName("MimoModeTests") inner class MimoModeTest { - val testFile1Path = "src/test/resources/test.json" + private val testFile1Path = "src/test/resources/test.json" val testProjectPathA = "src/test/resources/mimoFileSelection/testProject.alpha.cc.json" val testProjectPathB = "src/test/resources/mimoFileSelection/testProject.beta.cc.json" val testProjectPathC = "src/test/resources/mimoFileSelection/testProjectX.notIncl.cc.json" @@ -493,4 +493,68 @@ class MergeFilterTest { outputFile.deleteOnExit() } } + + @Nested + @DisplayName("FatMergeTests") + inner class FatMergeTest { + private val fatMergeTestFolder = "src/test/resources/fatMerge" + private val testFilePath1 = "$fatMergeTestFolder/testEdges1.cc.json" + private val testFilePath2 = "$fatMergeTestFolder/testProject.alpha.cc.json" + private val testFilePathDuplicate = "$fatMergeTestFolder/duplicate/testProject.beta.cc.json" + + @Test + fun `should warn about invalid files during fat merge and abort`() { + System.setErr(PrintStream(errContent)) + CommandLine(MergeFilter()).execute( + "src/test/resources/invalid.cc.json", + "--fat" + ).toString() + System.setErr(originalErr) + + assertThat(errContent.toString()).contains("Input invalid files/folders for MergeFilter, stopping execution...") + } + + @Test + fun `should exit when prefixes are not unique`() { + System.setErr(PrintStream(errContent)) + CommandLine(MergeFilter()).execute( + testFilePath1, testFilePath2, testFilePathDuplicate, + "--fat" + ).toString() + System.setErr(originalErr) + + assertThat(errContent.toString()).contains("Make sure that the input prefixes across all input files are unique!") + } + + @Test + fun `should cancel on single file input`() { + System.setErr(PrintStream(errContent)) + CommandLine(MergeFilter()).execute( + testFilePath1, + "--fat" + ).toString() + System.setErr(originalErr) + + assertThat(errContent.toString()).contains("One or less projects in input, merging aborted.") + } + + @Test + fun `should merge all projects into one file each packaged into a subfolder with input file's prefix`() { + System.setOut(PrintStream(outContent)) + System.setErr(PrintStream(errContent)) + CommandLine(MergeFilter()).execute( + testFilePath1, testFilePath2, + "--fat" + ).toString() + System.setErr(originalErr) + System.setOut(originalOut) + assertThat(errContent.toString()).contains("Input invalid files/folders for MergeFilter, stopping execution...") + assertThat(originalOut.toString()).contains("") + } + + @Test + fun `should output into specified file`() { + + } + } } diff --git a/analysis/filter/MergeFilter/src/test/resources/fatMerge/duplicate/testProject.beta.cc.json b/analysis/filter/MergeFilter/src/test/resources/fatMerge/duplicate/testProject.beta.cc.json new file mode 100644 index 0000000000..e6dd62ac91 --- /dev/null +++ b/analysis/filter/MergeFilter/src/test/resources/fatMerge/duplicate/testProject.beta.cc.json @@ -0,0 +1,323 @@ +{ + "projectName": "SourceMonitorImporter", + "apiVersion": "1.0", + "nodes": [ + { + "name": "root", + "type": "Folder", + "children": [ + { + "name": "src", + "type": "Folder", + "children": [ + { + "name": "main", + "type": "Folder", + "children": [ + { + "name": "java", + "type": "Folder", + "children": [ + { + "name": "de", + "type": "Folder", + "children": [ + { + "name": "mwea", + "type": "Folder", + "children": [ + { + "name": "codecharta", + "type": "Folder", + "children": [ + { + "name": "importer", + "type": "Folder", + "children": [ + { + "name": "sourcemon", + "type": "Folder", + "children": [ + { + "name": "SourceMonCsvConverter.java", + "type": "File", + "attributes": { + "AdditionalAttributeForTest": 0.0, + "Classes and Interfaces": 1.0, + "Statements at block level 9": 0.0, + "Maximum Block Depth": 4.0, + "Statements at block level 8": 0.0, + "Statements at block level 7": 0.0, + "Percent Lines with Comments": 0.0, + "Average Complexity*": 2.0, + "Statements at block level 6": 0.0, + "Statements at block level 5": 0.0, + "Statements at block level 4": 1.0, + "Statements at block level 3": 4.0, + "Line Number of Most Complex Method*": 81.0, + "Statements at block level 2": 30.0, + "Maximum Complexity*": 4.0, + "Statements at block level 1": 15.0, + "Statements at block level 0": 14.0, + "Line Number of Deepest Block": 85.0, + "Methods per Class": 6.0, + "Percent Branch Statements": 7.8, + "Average Statements per Method": 5.83, + "Statements": 64.0, + "Average Block Depth": 1.42, + "Method Call Statements": 38.0 + } + }, + { + "name": "SourcemonCsvImporter.java", + "type": "File", + "attributes": { + "Lines*": 25.0, + "Classes and Interfaces": 1.0, + "Statements at block level 9": 0.0, + "Maximum Block Depth": 4.0, + "Statements at block level 8": 0.0, + "Statements at block level 7": 0.0, + "Percent Lines with Comments": 0.0, + "Average Complexity*": 3.0, + "Statements at block level 6": 0.0, + "Statements at block level 5": 0.0, + "Statements at block level 4": 1.0, + "Statements at block level 3": 3.0, + "Line Number of Most Complex Method*": 18.0, + "Statements at block level 2": 7.0, + "Maximum Complexity*": 4.0, + "Statements at block level 1": 2.0, + "Statements at block level 0": 6.0, + "Line Number of Deepest Block": 24.0, + "Methods per Class": 2.0, + "Percent Branch Statements": 21.1, + "Average Statements per Method": 5.5, + "Statements": 19.0, + "Average Block Depth": 1.53, + "Method Call Statements": 9.0 + } + } + ] + } + ] + }, + { + "name": "json", + "type": "Folder", + "children": [ + { + "name": "JsonAccessor.java", + "type": "File", + "attributes": { + "Lines*": 15.0, + "Classes and Interfaces": 1.0, + "Statements at block level 9": 0.0, + "Maximum Block Depth": 2.0, + "Statements at block level 8": 0.0, + "Statements at block level 7": 0.0, + "Percent Lines with Comments": 0.0, + "Average Complexity*": 1.0, + "Statements at block level 6": 0.0, + "Statements at block level 5": 0.0, + "Statements at block level 4": 0.0, + "Statements at block level 3": 0.0, + "Line Number of Most Complex Method*": 14.0, + "Statements at block level 2": 2.0, + "Maximum Complexity*": 1.0, + "Statements at block level 1": 1.0, + "Statements at block level 0": 10.0, + "Line Number of Deepest Block": 15.0, + "Methods per Class": 1.0, + "Percent Branch Statements": 0.0, + "Average Statements per Method": 2.0, + "Statements": 13.0, + "Average Block Depth": 0.38, + "Method Call Statements": 2.0 + } + } + ] + }, + { + "name": "model", + "type": "Folder", + "children": [ + { + "name": "Node.java", + "type": "File", + "attributes": { + "Lines*": 34.0, + "Classes and Interfaces": 1.0, + "Statements at block level 9": 0.0, + "Maximum Block Depth": 2.0, + "Statements at block level 8": 0.0, + "Statements at block level 7": 0.0, + "Percent Lines with Comments": 0.0, + "Average Complexity*": 1.0, + "Statements at block level 6": 0.0, + "Statements at block level 5": 0.0, + "Statements at block level 4": 0.0, + "Statements at block level 3": 0.0, + "Line Number of Most Complex Method*": 11.0, + "Statements at block level 2": 10.0, + "Maximum Complexity*": 1.0, + "Statements at block level 1": 12.0, + "Statements at block level 0": 3.0, + "Line Number of Deepest Block": 12.0, + "Methods per Class": 8.0, + "Percent Branch Statements": 0.0, + "Average Statements per Method": 1.25, + "Statements": 25.0, + "Average Block Depth": 1.28, + "Method Call Statements": 2.0 + } + }, + { + "name": "NodeType.java", + "type": "File", + "attributes": { + "Lines*": 4.0, + "Classes and Interfaces": 1.0, + "Statements at block level 9": 0.0, + "Maximum Block Depth": 0.0, + "Statements at block level 8": 0.0, + "Statements at block level 7": 0.0, + "Percent Lines with Comments": 0.0, + "Average Complexity*": 0.0, + "Statements at block level 6": 0.0, + "Statements at block level 5": 0.0, + "Statements at block level 4": 0.0, + "Statements at block level 3": 0.0, + "Statements at block level 2": 0.0, + "Maximum Complexity*": 0.0, + "Statements at block level 1": 0.0, + "Statements at block level 0": 2.0, + "Line Number of Deepest Block": 1.0, + "Methods per Class": 0.0, + "Percent Branch Statements": 0.0, + "Average Statements per Method": 0.0, + "Statements": 2.0, + "Average Block Depth": 0.0, + "Method Call Statements": 0.0 + } + }, + { + "name": "Project.java", + "type": "File", + "attributes": { + "Lines*": 17.0, + "Classes and Interfaces": 1.0, + "Statements at block level 9": 0.0, + "Maximum Block Depth": 2.0, + "Statements at block level 8": 0.0, + "Statements at block level 7": 0.0, + "Percent Lines with Comments": 0.0, + "Average Complexity*": 1.0, + "Statements at block level 6": 0.0, + "Statements at block level 5": 0.0, + "Statements at block level 4": 0.0, + "Statements at block level 3": 0.0, + "Line Number of Most Complex Method*": 10.0, + "Statements at block level 2": 3.0, + "Maximum Complexity*": 1.0, + "Statements at block level 1": 6.0, + "Statements at block level 0": 4.0, + "Line Number of Deepest Block": 11.0, + "Methods per Class": 3.0, + "Percent Branch Statements": 0.0, + "Average Statements per Method": 1.0, + "Statements": 13.0, + "Average Block Depth": 0.92, + "Method Call Statements": 0.0 + } + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "name": "test", + "type": "Folder", + "children": [ + { + "name": "java", + "type": "Folder", + "children": [ + { + "name": "de", + "type": "Folder", + "children": [ + { + "name": "mwea", + "type": "Folder", + "children": [ + { + "name": "codecharta", + "type": "Folder", + "children": [ + { + "name": "importer", + "type": "Folder", + "children": [ + { + "name": "sourcemon", + "type": "Folder", + "children": [ + { + "name": "SourceMonCsvConverterTest.java", + "type": "File", + "attributes": { + "Lines*": 18.0, + "Classes and Interfaces": 1.0, + "Statements at block level 9": 0.0, + "Maximum Block Depth": 2.0, + "Statements at block level 8": 0.0, + "Statements at block level 7": 0.0, + "Percent Lines with Comments": 0.0, + "Average Complexity*": 1.0, + "Statements at block level 6": 0.0, + "Statements at block level 5": 0.0, + "Statements at block level 4": 0.0, + "Statements at block level 3": 0.0, + "Line Number of Most Complex Method*": 13.0, + "Statements at block level 2": 4.0, + "Maximum Complexity*": 1.0, + "Statements at block level 1": 3.0, + "Statements at block level 0": 7.0, + "Line Number of Deepest Block": 14.0, + "Methods per Class": 2.0, + "Percent Branch Statements": 0.0, + "Average Statements per Method": 2.0, + "Statements": 14.0, + "Average Block Depth": 0.79, + "Method Call Statements": 5.0 + } + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/analysis/filter/MergeFilter/src/test/resources/fatMerge/testEdges1.cc.json b/analysis/filter/MergeFilter/src/test/resources/fatMerge/testEdges1.cc.json new file mode 100644 index 0000000000..8bd4746be1 --- /dev/null +++ b/analysis/filter/MergeFilter/src/test/resources/fatMerge/testEdges1.cc.json @@ -0,0 +1,128 @@ +{ + "projectName": "Sample Project 1", + "apiVersion": "1.0", + "nodes": [ + { + "name": "root", + "type": "Folder", + "attributes": {}, + "children": [ + { + "name": "file1", + "type": "File", + "attributes": { + "number_of_commits": 160.0, + "range_of_weeks_with_commits": 68.0, + "successive_weeks_with_commits": 10.0, + "weeks_with_commits": 51.0, + "highly_coupled_files": 0.0, + "median_coupled_files": 2.0, + "abs_coupled_churn": 33700.0, + "avg_code_churn": 3.0 + }, + "link": "", + "children": [] + }, + { + "name": "file4", + "type": "File", + "attributes": { + "number_of_authors": 22.0, + "number_of_commits": 130.0, + "range_of_weeks_with_commits": 18.0, + "successive_weeks_with_commits": 20.0, + "weeks_with_commits": 61.0, + "highly_coupled_files": 1.0, + "median_coupled_files": 4.0, + "abs_coupled_churn": 22000.0, + "avg_code_churn": 4.0 + }, + "link": "", + "children": [] + }, + { + "name": "visualization", + "type": "Folder", + "attributes": {}, + "link": "", + "children": [ + { + "name": "file2", + "type": "File", + "attributes": { + "number_of_authors": 12.0, + "number_of_commits": 120.0, + "range_of_weeks_with_commits": 30.0, + "successive_weeks_with_commits": 20.0, + "weeks_with_commits": 76.0, + "highly_coupled_files": 0.0, + "median_coupled_files": 2.0, + "abs_coupled_churn": 33700.0, + "avg_code_churn": 7.0 + }, + "link": "", + "children": [] + }, + { + "name": "file3", + "type": "File", + "attributes": { + "number_of_authors": 40.0, + "number_of_commits": 230.0, + "range_of_weeks_with_commits": 50.0, + "successive_weeks_with_commits": 80.0, + "weeks_with_commits": 10.0, + "highly_coupled_files": 5.0, + "median_coupled_files": 2.0, + "abs_coupled_churn": 44000.0, + "avg_code_churn": 4.0 + }, + "link": "", + "children": [] + } + ] + } + ] + } + ], + "edges": [ + { + "fromNodeName": "/root/file1", + "toNodeName": "/root/visualization/file2", + "attributes": { + "pairingRate": 90, + "avgCommits": 5, + "anotherMetric": 25 + } + }, + { + "fromNodeName": "/root/visualization/file2", + "toNodeName": "/root/visualization/file3", + "attributes": { + "pairingRate": 80, + "avgCommits": 3 + } + } + ], + "attributeTypes": { + "nodes": { + "attributeA": "absolute", + "attributeB": "absolute", + "attributeZ": "absolute" + }, + "edges": { + "attributeD": "absolute", + "attributeE": "relative" + } + }, + "blacklist": [ + { + "path": "/root/file1", + "type": "hide" + }, + { + "path": "/root/visualization/file2", + "type": "exclude" + } + ] +} diff --git a/analysis/filter/MergeFilter/src/test/resources/fatMerge/testProject.alpha.cc.json b/analysis/filter/MergeFilter/src/test/resources/fatMerge/testProject.alpha.cc.json new file mode 100644 index 0000000000..ad84fc1762 --- /dev/null +++ b/analysis/filter/MergeFilter/src/test/resources/fatMerge/testProject.alpha.cc.json @@ -0,0 +1,323 @@ +{ + "projectName": "SourceMonitorImporter", + "apiVersion": "1.0", + "nodes": [ + { + "name": "root", + "type": "Folder", + "children": [ + { + "name": "src", + "type": "Folder", + "children": [ + { + "name": "main", + "type": "Folder", + "children": [ + { + "name": "java", + "type": "Folder", + "children": [ + { + "name": "de", + "type": "Folder", + "children": [ + { + "name": "mwea", + "type": "Folder", + "children": [ + { + "name": "codecharta", + "type": "Folder", + "children": [ + { + "name": "importer", + "type": "Folder", + "children": [ + { + "name": "sourcemon", + "type": "Folder", + "children": [ + { + "name": "SourceMonCsvConverter.java", + "type": "File", + "attributes": { + "Lines*": 80.0, + "Classes and Interfaces": 1.0, + "Statements at block level 9": 0.0, + "Maximum Block Depth": 4.0, + "Statements at block level 8": 0.0, + "Statements at block level 7": 0.0, + "Percent Lines with Comments": 0.0, + "Average Complexity*": 2.0, + "Statements at block level 6": 0.0, + "Statements at block level 5": 0.0, + "Statements at block level 4": 1.0, + "Statements at block level 3": 4.0, + "Line Number of Most Complex Method*": 81.0, + "Statements at block level 2": 30.0, + "Maximum Complexity*": 4.0, + "Statements at block level 1": 15.0, + "Statements at block level 0": 14.0, + "Line Number of Deepest Block": 85.0, + "Methods per Class": 6.0, + "Percent Branch Statements": 7.8, + "Average Statements per Method": 5.83, + "Statements": 64.0, + "Average Block Depth": 1.42, + "Method Call Statements": 38.0 + } + }, + { + "name": "SourcemonCsvImporter.java", + "type": "File", + "attributes": { + "Lines*": 25.0, + "Classes and Interfaces": 1.0, + "Statements at block level 9": 0.0, + "Maximum Block Depth": 4.0, + "Statements at block level 8": 0.0, + "Statements at block level 7": 0.0, + "Percent Lines with Comments": 0.0, + "Average Complexity*": 3.0, + "Statements at block level 6": 0.0, + "Statements at block level 5": 0.0, + "Statements at block level 4": 1.0, + "Statements at block level 3": 3.0, + "Line Number of Most Complex Method*": 18.0, + "Statements at block level 2": 7.0, + "Maximum Complexity*": 4.0, + "Statements at block level 1": 2.0, + "Statements at block level 0": 6.0, + "Line Number of Deepest Block": 24.0, + "Methods per Class": 2.0, + "Percent Branch Statements": 21.1, + "Average Statements per Method": 5.5, + "Statements": 19.0, + "Average Block Depth": 1.53, + "Method Call Statements": 9.0 + } + } + ] + } + ] + }, + { + "name": "json", + "type": "Folder", + "children": [ + { + "name": "JsonAccessor.java", + "type": "File", + "attributes": { + "Lines*": 15.0, + "Classes and Interfaces": 1.0, + "Statements at block level 9": 0.0, + "Maximum Block Depth": 2.0, + "Statements at block level 8": 0.0, + "Statements at block level 7": 0.0, + "Percent Lines with Comments": 0.0, + "Average Complexity*": 1.0, + "Statements at block level 6": 0.0, + "Statements at block level 5": 0.0, + "Statements at block level 4": 0.0, + "Statements at block level 3": 0.0, + "Line Number of Most Complex Method*": 14.0, + "Statements at block level 2": 2.0, + "Maximum Complexity*": 1.0, + "Statements at block level 1": 1.0, + "Statements at block level 0": 10.0, + "Line Number of Deepest Block": 15.0, + "Methods per Class": 1.0, + "Percent Branch Statements": 0.0, + "Average Statements per Method": 2.0, + "Statements": 13.0, + "Average Block Depth": 0.38, + "Method Call Statements": 2.0 + } + } + ] + }, + { + "name": "model", + "type": "Folder", + "children": [ + { + "name": "Node.java", + "type": "File", + "attributes": { + "Lines*": 34.0, + "Classes and Interfaces": 1.0, + "Statements at block level 9": 0.0, + "Maximum Block Depth": 2.0, + "Statements at block level 8": 0.0, + "Statements at block level 7": 0.0, + "Percent Lines with Comments": 0.0, + "Average Complexity*": 1.0, + "Statements at block level 6": 0.0, + "Statements at block level 5": 0.0, + "Statements at block level 4": 0.0, + "Statements at block level 3": 0.0, + "Line Number of Most Complex Method*": 11.0, + "Statements at block level 2": 10.0, + "Maximum Complexity*": 1.0, + "Statements at block level 1": 12.0, + "Statements at block level 0": 3.0, + "Line Number of Deepest Block": 12.0, + "Methods per Class": 8.0, + "Percent Branch Statements": 0.0, + "Average Statements per Method": 1.25, + "Statements": 25.0, + "Average Block Depth": 1.28, + "Method Call Statements": 2.0 + } + }, + { + "name": "NodeType.java", + "type": "File", + "attributes": { + "Lines*": 4.0, + "Classes and Interfaces": 1.0, + "Statements at block level 9": 0.0, + "Maximum Block Depth": 0.0, + "Statements at block level 8": 0.0, + "Statements at block level 7": 0.0, + "Percent Lines with Comments": 0.0, + "Average Complexity*": 0.0, + "Statements at block level 6": 0.0, + "Statements at block level 5": 0.0, + "Statements at block level 4": 0.0, + "Statements at block level 3": 0.0, + "Statements at block level 2": 0.0, + "Maximum Complexity*": 0.0, + "Statements at block level 1": 0.0, + "Statements at block level 0": 2.0, + "Line Number of Deepest Block": 1.0, + "Methods per Class": 0.0, + "Percent Branch Statements": 0.0, + "Average Statements per Method": 0.0, + "Statements": 2.0, + "Average Block Depth": 0.0, + "Method Call Statements": 0.0 + } + }, + { + "name": "Project.java", + "type": "File", + "attributes": { + "Lines*": 17.0, + "Classes and Interfaces": 1.0, + "Statements at block level 9": 0.0, + "Maximum Block Depth": 2.0, + "Statements at block level 8": 0.0, + "Statements at block level 7": 0.0, + "Percent Lines with Comments": 0.0, + "Average Complexity*": 1.0, + "Statements at block level 6": 0.0, + "Statements at block level 5": 0.0, + "Statements at block level 4": 0.0, + "Statements at block level 3": 0.0, + "Line Number of Most Complex Method*": 10.0, + "Statements at block level 2": 3.0, + "Maximum Complexity*": 1.0, + "Statements at block level 1": 6.0, + "Statements at block level 0": 4.0, + "Line Number of Deepest Block": 11.0, + "Methods per Class": 3.0, + "Percent Branch Statements": 0.0, + "Average Statements per Method": 1.0, + "Statements": 13.0, + "Average Block Depth": 0.92, + "Method Call Statements": 0.0 + } + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "name": "test", + "type": "Folder", + "children": [ + { + "name": "java", + "type": "Folder", + "children": [ + { + "name": "de", + "type": "Folder", + "children": [ + { + "name": "mwea", + "type": "Folder", + "children": [ + { + "name": "codecharta", + "type": "Folder", + "children": [ + { + "name": "importer", + "type": "Folder", + "children": [ + { + "name": "sourcemon", + "type": "Folder", + "children": [ + { + "name": "SourceMonCsvConverterTest.java", + "type": "File", + "attributes": { + "Lines*": 18.0, + "Classes and Interfaces": 1.0, + "Statements at block level 9": 0.0, + "Maximum Block Depth": 2.0, + "Statements at block level 8": 0.0, + "Statements at block level 7": 0.0, + "Percent Lines with Comments": 0.0, + "Average Complexity*": 1.0, + "Statements at block level 6": 0.0, + "Statements at block level 5": 0.0, + "Statements at block level 4": 0.0, + "Statements at block level 3": 0.0, + "Line Number of Most Complex Method*": 13.0, + "Statements at block level 2": 4.0, + "Maximum Complexity*": 1.0, + "Statements at block level 1": 3.0, + "Statements at block level 0": 7.0, + "Line Number of Deepest Block": 14.0, + "Methods per Class": 2.0, + "Percent Branch Statements": 0.0, + "Average Statements per Method": 2.0, + "Statements": 14.0, + "Average Block Depth": 0.79, + "Method Call Statements": 5.0 + } + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] +} \ No newline at end of file From d8155c599fca0f1c66eba5e32288c9d15cbbaa59 Mon Sep 17 00:00:00 2001 From: Sebastian Wolf <65733509+phanlezz@users.noreply.github.com> Date: Tue, 3 Dec 2024 09:28:08 +0100 Subject: [PATCH 2/5] feat(analysis): apply new name (#3743) --- .../mergefilter/{FatMerge.kt => LargeMerge.kt} | 2 +- .../filter/mergefilter/MergeFilter.kt | 17 ++++++++--------- .../filter/mergefilter/MergeFilterTest.kt | 18 ++++++++++-------- .../duplicate/testProject.beta.cc.json | 0 .../testEdges1.cc.json | 0 .../testProject.alpha.cc.json | 0 6 files changed, 19 insertions(+), 18 deletions(-) rename analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/{FatMerge.kt => LargeMerge.kt} (99%) rename analysis/filter/MergeFilter/src/test/resources/{fatMerge => largeMerge}/duplicate/testProject.beta.cc.json (100%) rename analysis/filter/MergeFilter/src/test/resources/{fatMerge => largeMerge}/testEdges1.cc.json (100%) rename analysis/filter/MergeFilter/src/test/resources/{fatMerge => largeMerge}/testProject.alpha.cc.json (100%) diff --git a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/FatMerge.kt b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/LargeMerge.kt similarity index 99% rename from analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/FatMerge.kt rename to analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/LargeMerge.kt index 98da63bf20..a956f74720 100644 --- a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/FatMerge.kt +++ b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/LargeMerge.kt @@ -6,7 +6,7 @@ import de.maibornwolff.codecharta.model.Node import de.maibornwolff.codecharta.model.NodeType import de.maibornwolff.codecharta.model.Project -class FatMerge { +class LargeMerge { companion object { fun packageProjectInto(project: Project, prefix: String): Project { val modifiedProject = Project( 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 abfaf7669a..b2a09ed87c 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 @@ -59,10 +59,10 @@ class MergeFilter( private var levenshteinDistance = 3 @CommandLine.Option( - names = ["--fat"], + names = ["--large"], description = ["Merges input files into on file divided into project sub-folder as defined per prefix of each input file"] ) - private var fatMerge = false + private var largeMerge = false override val name = NAME override val description = DESCRIPTION @@ -100,8 +100,8 @@ class MergeFilter( if (mimo) { processMimoMerge(sourceFiles, nodeMergerStrategy) - } else if (fatMerge) { - processFatMerge(sourceFiles, nodeMergerStrategy) + } else if (largeMerge) { + processLargeMerge(sourceFiles, nodeMergerStrategy) } else { val projects = readInputFiles(sourceFiles) @@ -183,7 +183,7 @@ class MergeFilter( } } - private fun processFatMerge(sourceFiles: List, nodeMergerStrategy: NodeMergerStrategy) { + private fun processLargeMerge(sourceFiles: List, nodeMergerStrategy: NodeMergerStrategy) { val projectsFileNamePairs = readInputFilesKeepFileNames(sourceFiles) val fileNameList = projectsFileNamePairs.map { it.first } @@ -191,13 +191,13 @@ class MergeFilter( Logger.warn { "One or less projects in input, merging aborted." } } - require(fileNameList.groupingBy { it.substringBefore(".") }.eachCount().all {it.value == 1}) { + require(fileNameList.groupingBy { it.substringBefore(".") }.eachCount().all { it.value == 1 }) { Logger.warn { "Make sure that the input prefixes across all input files are unique!" } } - val packagedProjects : MutableList = mutableListOf() + val packagedProjects: MutableList = mutableListOf() projectsFileNamePairs.forEach { - packagedProjects.add(FatMerge.packageProjectInto(it.second, it.first.substringBefore("."))) + packagedProjects.add(LargeMerge.packageProjectInto(it.second, it.first.substringBefore("."))) } val mergedProject = ProjectMerger(packagedProjects, nodeMergerStrategy).merge() @@ -224,7 +224,6 @@ class MergeFilter( Logger.warn { "${it.name} is not a valid project file and will be skipped." } null } - } } } diff --git a/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt b/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt index 85eba25ac9..ba594ca4cb 100644 --- a/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt +++ b/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt @@ -497,7 +497,7 @@ class MergeFilterTest { @Nested @DisplayName("FatMergeTests") inner class FatMergeTest { - private val fatMergeTestFolder = "src/test/resources/fatMerge" + private val fatMergeTestFolder = "src/test/resources/largeMerge" private val testFilePath1 = "$fatMergeTestFolder/testEdges1.cc.json" private val testFilePath2 = "$fatMergeTestFolder/testProject.alpha.cc.json" private val testFilePathDuplicate = "$fatMergeTestFolder/duplicate/testProject.beta.cc.json" @@ -507,7 +507,7 @@ class MergeFilterTest { System.setErr(PrintStream(errContent)) CommandLine(MergeFilter()).execute( "src/test/resources/invalid.cc.json", - "--fat" + "--large" ).toString() System.setErr(originalErr) @@ -518,8 +518,10 @@ class MergeFilterTest { fun `should exit when prefixes are not unique`() { System.setErr(PrintStream(errContent)) CommandLine(MergeFilter()).execute( - testFilePath1, testFilePath2, testFilePathDuplicate, - "--fat" + testFilePath1, + testFilePath2, + testFilePathDuplicate, + "--large" ).toString() System.setErr(originalErr) @@ -531,7 +533,7 @@ class MergeFilterTest { System.setErr(PrintStream(errContent)) CommandLine(MergeFilter()).execute( testFilePath1, - "--fat" + "--large" ).toString() System.setErr(originalErr) @@ -543,8 +545,9 @@ class MergeFilterTest { System.setOut(PrintStream(outContent)) System.setErr(PrintStream(errContent)) CommandLine(MergeFilter()).execute( - testFilePath1, testFilePath2, - "--fat" + testFilePath1, + testFilePath2, + "--large" ).toString() System.setErr(originalErr) System.setOut(originalOut) @@ -554,7 +557,6 @@ class MergeFilterTest { @Test fun `should output into specified file`() { - } } } diff --git a/analysis/filter/MergeFilter/src/test/resources/fatMerge/duplicate/testProject.beta.cc.json b/analysis/filter/MergeFilter/src/test/resources/largeMerge/duplicate/testProject.beta.cc.json similarity index 100% rename from analysis/filter/MergeFilter/src/test/resources/fatMerge/duplicate/testProject.beta.cc.json rename to analysis/filter/MergeFilter/src/test/resources/largeMerge/duplicate/testProject.beta.cc.json diff --git a/analysis/filter/MergeFilter/src/test/resources/fatMerge/testEdges1.cc.json b/analysis/filter/MergeFilter/src/test/resources/largeMerge/testEdges1.cc.json similarity index 100% rename from analysis/filter/MergeFilter/src/test/resources/fatMerge/testEdges1.cc.json rename to analysis/filter/MergeFilter/src/test/resources/largeMerge/testEdges1.cc.json diff --git a/analysis/filter/MergeFilter/src/test/resources/fatMerge/testProject.alpha.cc.json b/analysis/filter/MergeFilter/src/test/resources/largeMerge/testProject.alpha.cc.json similarity index 100% rename from analysis/filter/MergeFilter/src/test/resources/fatMerge/testProject.alpha.cc.json rename to analysis/filter/MergeFilter/src/test/resources/largeMerge/testProject.alpha.cc.json From 8ed1597ccdc73d9d62b2a218e747ef80f8859655 Mon Sep 17 00:00:00 2001 From: Sebastian Wolf <65733509+phanlezz@users.noreply.github.com> Date: Tue, 3 Dec 2024 12:02:54 +0100 Subject: [PATCH 3/5] feat(analysis): add test to verify output in file (#3743) --- .../filter/mergefilter/LargeMerge.kt | 1 - .../filter/mergefilter/MergeFilterTest.kt | 33 ++++++++++++++++--- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/LargeMerge.kt b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/LargeMerge.kt index a956f74720..49442485d7 100644 --- a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/LargeMerge.kt +++ b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/LargeMerge.kt @@ -48,7 +48,6 @@ class LargeMerge { } private fun insertFolderIntoPath(path: String, folderName: String): String { - require(Regex("^/root/").matches(path)) return path.replace(Regex("^/root/"), "/root/$folderName/") } } diff --git a/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt b/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt index ba594ca4cb..0a641e021f 100644 --- a/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt +++ b/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt @@ -5,6 +5,7 @@ import com.github.kinquirer.components.promptCheckboxObject import com.github.kinquirer.components.promptList import com.github.kinquirer.core.Choice import de.maibornwolff.codecharta.filter.mergefilter.MergeFilter.Companion.main +import de.maibornwolff.codecharta.serialization.ProjectDeserializer import de.maibornwolff.codecharta.tools.interactiveparser.InputType import de.maibornwolff.codecharta.util.InputHelper import io.mockk.every @@ -543,20 +544,44 @@ class MergeFilterTest { @Test fun `should merge all projects into one file each packaged into a subfolder with input file's prefix`() { System.setOut(PrintStream(outContent)) - System.setErr(PrintStream(errContent)) CommandLine(MergeFilter()).execute( testFilePath1, testFilePath2, "--large" ).toString() - System.setErr(originalErr) System.setOut(originalOut) - assertThat(errContent.toString()).contains("Input invalid files/folders for MergeFilter, stopping execution...") - assertThat(originalOut.toString()).contains("") + val outputString = outContent.toString() + assertThat(outputString).contains("testProject", "testEdges1") + assertThat(outputString).contains("SourceMonCsvConverter", "number_of_commits") + assertThat( + outputString + ).contains("/root/testEdges1/visualization/file2", "/root/testEdges1/visualization/file3", "/root/testEdges1/file1") } @Test fun `should output into specified file`() { + val outPutFilePath = "$fatMergeTestFolder/largeOutputToFile.cc.json" + CommandLine(MergeFilter()).execute( + testFilePath1, + testFilePath2, + "--large", + "-o=$outPutFilePath", + "-nc" + ).toString() + val outPutFile = File(outPutFilePath) + assertThat(outPutFile).exists() + val project = ProjectDeserializer.deserializeProject(outPutFile.inputStream()) + val projectInput1 = ProjectDeserializer.deserializeProject(File(testFilePath1).inputStream()) + val projectInput2 = ProjectDeserializer.deserializeProject(File(testFilePath2).inputStream()) + assertThat(project.sizeOfEdges()).isEqualTo(2) + assertThat(project.sizeOfBlacklist()).isEqualTo(2) + assertThat(project.edges.toString()).contains("/root/testEdges1/visualization/file2", "/root/testEdges1/visualization/file3") + assertThat(project.rootNode.children.size).isEqualTo(2) + val outputProject1 = project.rootNode.children.first { it.name == "testEdges1" } + val outputProject2 = project.rootNode.children.first { it.name == "testProject" } + assertThat(outputProject1.children.toString()).isEqualTo(projectInput1.rootNode.children.toString()) + assertThat(outputProject2.children.toString()).isEqualTo(projectInput2.rootNode.children.toString()) + outPutFile.deleteOnExit() } } } From 53abd5d79e2e9eca4d59760efb2668240622ca54 Mon Sep 17 00:00:00 2001 From: Sebastian Wolf <65733509+phanlezz@users.noreply.github.com> Date: Tue, 3 Dec 2024 14:27:47 +0100 Subject: [PATCH 4/5] feat(analysis): add dialog option for large and -o for mimo (#3743) Also updates the MergeFilter README (copied into gh-pages), updates tests for dialog and adds test for output file during mimo --- analysis/CHANGELOG.md | 9 ++ analysis/filter/MergeFilter/README.md | 45 ++++++---- .../filter/mergefilter/MergeFilter.kt | 6 +- .../filter/mergefilter/ParserDialog.kt | 42 ++++++--- .../filter/mergefilter/mimo/Mimo.kt | 10 +++ .../filter/mergefilter/MergeFilterTest.kt | 47 +++++++++- .../filter/mergefilter/ParserDialogTest.kt | 86 ++++++++++++++++--- gh-pages/_docs/07-filter/02-merge-filter.md | 45 ++++++---- 8 files changed, 228 insertions(+), 62 deletions(-) diff --git a/analysis/CHANGELOG.md b/analysis/CHANGELOG.md index 37b0517958..0a9d529893 100644 --- a/analysis/CHANGELOG.md +++ b/analysis/CHANGELOG.md @@ -7,8 +7,17 @@ and this project adheres to [Semantic Versioning](http://semver.org/) ## [unreleased] (Added 🚀 | Changed | Removed 🗑 | Fixed 🐞 | Chore 👨‍💻 👩‍💻) +### Added 🚀 + +- Add a new `--large` flat to the MergeFilter that merges projects into one file each in its own subfolder depending on the input file's dot-prefix name [#3841](https://github.com/MaibornWolff/codecharta/pull/3841) +- Add the ability to the MergeFilter to specify the output file during `--mimo` operation [#3841](https://github.com/MaibornWolff/codecharta/pull/3841) + ## [1.129.0] - 2024-11-29 +### Added 🚀 + +- Add a new `--mimo` flag to the MergeFilter that merges multiple project files into multiple output files depending on the input file's dot-prefix name [#3800](https://github.com/MaibornWolff/codecharta/pull/3800) + ### Changed - Moved the structure print functionality to a new tool called 'inspection' [#3826](https://github.com/MaibornWolff/codecharta/pull/3826) diff --git a/analysis/filter/MergeFilter/README.md b/analysis/filter/MergeFilter/README.md index 82c8980b3e..acc6680f15 100644 --- a/analysis/filter/MergeFilter/README.md +++ b/analysis/filter/MergeFilter/README.md @@ -14,19 +14,20 @@ Both strategies will merge the unique list entries for `attributeTypes` and `bla ## Usage and Parameters -| Parameters | Description | -|---------------------------------|----------------------------------------------------------------------| -| `FILE` | files to merge | -| `-a, --add-missing` | [Leaf Merging Strategy] enable adding missing nodes to reference | -| `-h, --help` | displays help and exits | -| `--ignore-case` | ignores case when checking node names | -| `--leaf` | use leaf merging strategy | -| `-nc, --not-compressed` | save uncompressed output File | -| `-o, --outputFile=` | output File (or empty for stdout; ignored in [MIMO mode])) | -| `--recursive` | use recursive merging strategy (default) | -| `--mimo` | merge multiple files with the same prefix into multiple output files | -| `-ld, --levenshtein-distance` | [MIMO mode] levenshtein distance for name match suggestions | -| `-f` | force merge non-overlapping modules at the top-level structure | +| Parameters | Description | +|---------------------------------|----------------------------------------------------------------------------------| +| `FILE` | files to merge | +| `-a, --add-missing` | [Leaf Merging Strategy] enable adding missing nodes to reference | +| `-h, --help` | displays help and exits | +| `--ignore-case` | ignores case when checking node names | +| `--leaf` | use leaf merging strategy | +| `-nc, --not-compressed` | save uncompressed output File | +| `-o, --outputFile=` | output File (or empty for stdout; [MIMO mode] output folder)) | +| `--recursive` | use recursive merging strategy (default) | +| `--mimo` | merge multiple files with the same prefix into multiple output files | +| `-ld, --levenshtein-distance` | [MIMO mode] levenshtein distance for name match suggestions | +| `-f` | force merge non-overlapping modules at the top-level structure | +| `--large` | merge multiple project files into one output file, separated by their dot-prefix | ``` Usage: ccsh merge [-ah] [--ignore-case] [--leaf] [-nc] [--recursive] @@ -53,10 +54,24 @@ This last example inputs the folder foo, which will result in all project files ccsh merge myProjectFolder/ --mimo -ld 0 -f ``` -## MIMO - Multiply Inputs Multiple Outputs +## MIMO Merge - Multiply Inputs Multiple Outputs Matches multiple `cc.json` files based on their prefix (e.g. **myProject**.git.cc.json). Tries to match project names with typos and asks which to add to the output. If you want to use this in a CI/CD pipeline environment you may find it useful to specify `-ld` and `-f` to not prompt any user input. The output file name follows the following schema: `myProject.merge.cc.json`. -> IMPORTANT: Output is always the current working directory. +## Large Merge + +Merges multiple `.cc.json` files into one projects, but separates them into sub-folders, with names defined through the dot-prefixes of the input files: + +``` +ccsh merge aa.cc.json bb.cc.json cc.cc.json --large -o myOutputFile -nc +# myOutputFile.cc.json: +# - root +# - - aa +# - - - * +# - - bb +# - - - * +# - - cc +# - - - * +``` 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 b2a09ed87c..6d5fb8c9f2 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 @@ -175,10 +175,12 @@ class MergeFilter( val mergedProject = ProjectMerger(projects, nodeMergerStrategy).merge() val outputFilePrefix = Mimo.retrieveGroupName(projectsFileNamePairs.map { it.first }) - ProjectSerializer.serializeToFileOrStream(mergedProject, "$outputFilePrefix.merge.cc.json", output, compress) + val outputFileName = "$outputFilePrefix.merge.cc.json" + val outputFilePath = Mimo.assembleOutputFilePath(outputFile, outputFileName) + ProjectSerializer.serializeToFileOrStream(mergedProject, outputFilePath, output, compress) Logger.info { "Merged files with prefix '$outputFilePrefix' into" + - " '$outputFilePrefix.merge.cc.json${if (compress) ".gz" else ""}'" + " '$outputFileName${if (compress) ".gz" else ""}'" } } } diff --git a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/ParserDialog.kt b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/ParserDialog.kt index 36db9fd2f1..d77aee4b1b 100644 --- a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/ParserDialog.kt +++ b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/ParserDialog.kt @@ -20,24 +20,32 @@ class ParserDialog { inputFolderName = getInputFileName("cc.json", InputType.FOLDER) } while (!InputHelper.isInputValidAndNotNull(arrayOf(File(inputFolderName)), canInputContainFolders = true)) - val isMimoMode = KInquirer.promptConfirm( - message = "Do you want to use MIMO mode? (multiple inputs multiple outputs)", - default = false + val defaultMerge = "Default merging..." + val mimoMerge = "Mimo Merge" + val largeMerge = "Large Merge" + val mergeMode = KInquirer.promptList( + message = "Do you want to use a special merge mode?", + choices = listOf(defaultMerge, mimoMerge, largeMerge) ) - var outputFileName = "" + val outputFileName: String val isCompressed: Boolean var levenshteinDistance = 0 - if (isMimoMode) { - levenshteinDistance = KInquirer.promptInputNumber( - message = "Select Levenshtein Distance for name match suggestions (0 for no suggestions)", - default = "3" - ).toInt() + if (mergeMode == mimoMerge) { + outputFileName = KInquirer.promptInput( + message = "What is the output folder path?", + hint = "Uses the current working directory if empty" + ) isCompressed = KInquirer.promptConfirm( message = "Do you want to compress the output file(s)?", default = true ) + + levenshteinDistance = KInquirer.promptInputNumber( + message = "Select Levenshtein Distance for name match suggestions (0 for no suggestions)", + default = "3" + ).toInt() } else { outputFileName = KInquirer.promptInput( @@ -82,18 +90,24 @@ class ParserDialog { "--recursive=${!leafFlag}", "--leaf=$leafFlag", "--ignore-case=$ignoreCase", - "--not-compressed=$isCompressed" + "--not-compressed=$isCompressed", + "--output-file=$outputFileName" ) - if (isMimoMode) { + if (mergeMode == mimoMerge) { return basicMergeConfig + listOf( "--mimo=true", "--levenshtein-distance=$levenshteinDistance" ) } - return basicMergeConfig + listOf( - "--output-file=$outputFileName" - ) + + if (mergeMode == largeMerge) { + return basicMergeConfig + listOf( + "--large=true" + ) + } + + return basicMergeConfig } fun askForceMerge(): Boolean { diff --git a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/mimo/Mimo.kt b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/mimo/Mimo.kt index e263fc04da..4fa3374654 100644 --- a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/mimo/Mimo.kt +++ b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/mimo/Mimo.kt @@ -83,5 +83,15 @@ class Mimo { return cost[rhsLength] } + + fun assembleOutputFilePath(filePath: String?, fileName: String): String { + return if (filePath.isNullOrEmpty()) { + fileName + } else if (File(filePath).isDirectory) { + "${File(filePath).path}/$fileName" + } else { + throw IllegalArgumentException("Please specify a folder for MIMO output or nothing") + } + } } } diff --git a/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt b/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt index 0a641e021f..99159cc39b 100644 --- a/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt +++ b/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt @@ -493,11 +493,54 @@ class MergeFilterTest { outputFile.deleteOnExit() } + + @Test + fun `should merge two projects with output at the given location`() { + val prefix = "testProject" + val outputFolder = "src/test/resources" + + System.setErr(PrintStream(errContent)) + CommandLine(MergeFilter()).execute( + testProjectPathA, + testProjectPathB, + "--mimo", + "--levenshtein-distance=0", + "-o=$outputFolder" + ).toString() + + System.setErr(originalErr) + + val outputFileName = "$prefix.merge.cc.json.gz" + val outputFile = File("$outputFolder/$outputFileName") + + assertThat(errContent.toString()).contains("Merged files with prefix '$prefix' into '$outputFileName'") + assertThat(outputFile).exists() + + outputFile.deleteOnExit() + } + + @Test + fun `should throw error if output-file is not a folder`() { + val invalidOutputPath = "src/test/resources/invalid.cc.json" + + System.setErr(PrintStream(errContent)) + CommandLine(MergeFilter()).execute( + testProjectPathA, + testProjectPathB, + "--mimo", + "--levenshtein-distance=0", + "-o=$invalidOutputPath" + ).toString() + + System.setErr(originalErr) + + assertThat(errContent.toString()).contains("Please specify a folder for MIMO output or nothing") + } } @Nested - @DisplayName("FatMergeTests") - inner class FatMergeTest { + @DisplayName("LargeMergeTests") + inner class LargeMergeTest { private val fatMergeTestFolder = "src/test/resources/largeMerge" private val testFilePath1 = "$fatMergeTestFolder/testEdges1.cc.json" private val testFilePath2 = "$fatMergeTestFolder/testProject.alpha.cc.json" diff --git a/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/ParserDialogTest.kt b/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/ParserDialogTest.kt index 209056a447..3e383871a1 100644 --- a/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/ParserDialogTest.kt +++ b/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/ParserDialogTest.kt @@ -26,14 +26,15 @@ class ParserDialogTest { } @Test - fun `should output correct arguments and skip questions (leaf, no mimo)`() { + fun `should output correct arguments and skip questions (leaf, no mimo or large)`() { // given val inputFolderName = "folder" val outputFileName = "sampleOutputFile" val compress = false val addMissing = true val ignoreCase = false - val mimo = false + + val mergeMode = "Default merging..." // set indirectly val recursive = false @@ -51,11 +52,11 @@ class ParserDialogTest { mockkStatic("com.github.kinquirer.components.ConfirmKt") every { KInquirer.promptConfirm(any(), any()) - } returns mimo andThen compress andThen addMissing andThen ignoreCase + } returns compress andThen addMissing andThen ignoreCase mockkStatic("com.github.kinquirer.components.ListKt") every { KInquirer.promptList(any(), any(), any(), any(), any()) - } returns "Leaf Merging Strategy" + } returns mergeMode andThen "Leaf Merging Strategy" // when val parserArguments = ParserDialog.collectParserArgs() @@ -77,14 +78,15 @@ class ParserDialogTest { } @Test - fun `should output correct arguments and skip questions (recursive, no mimo)`() { + fun `should output correct arguments and skip questions (recursive, no mimo or large)`() { // given val inputFolderName = "folder" val outputFileName = "sampleOutputFile" val compress = true val addMissing = false val ignoreCase = false - val mimo = false + + val mergeMode = "Default merging..." // set indirectly val recursive = true @@ -102,11 +104,11 @@ class ParserDialogTest { mockkStatic("com.github.kinquirer.components.ConfirmKt") every { KInquirer.promptConfirm(any(), any()) - } returns mimo andThen compress andThen ignoreCase + } returns compress andThen ignoreCase mockkStatic("com.github.kinquirer.components.ListKt") every { KInquirer.promptList(any(), any(), any(), any(), any()) - } returns "Recursive Merging Strategy" + } returns mergeMode andThen "Recursive Merging Strategy" // when val parserArguments = ParserDialog.collectParserArgs() @@ -128,15 +130,16 @@ class ParserDialogTest { } @Test - fun `should output correct arguments when mimo`() { + fun `should output correct arguments when using Mimo Merge`() { // given val inputFolderName = "folder" val addMissing = false val ignoreCase = true - val mimo = true val levenshteinDistance = BigDecimal(3) val compress = true + val mergeMode = "Mimo Merge" + // set indirectly val recursive = true val leaf = false @@ -149,7 +152,7 @@ class ParserDialogTest { mockkStatic("com.github.kinquirer.components.InputKt") every { KInquirer.promptInput(any(), any(), any()) - } returns inputFolderName + } returns inputFolderName andThen "" every { KInquirer.promptInputNumber(any(), any(), any(), any()) } returns levenshteinDistance @@ -157,11 +160,11 @@ class ParserDialogTest { mockkStatic("com.github.kinquirer.components.ConfirmKt") every { KInquirer.promptConfirm(any(), any()) - } returns mimo andThen compress andThen ignoreCase + } returns compress andThen ignoreCase mockkStatic("com.github.kinquirer.components.ListKt") every { KInquirer.promptList(any(), any(), any(), any(), any()) - } returns "Recursive Merging Strategy" + } returns mergeMode andThen "Recursive Merging Strategy" // when val parserArguments = ParserDialog.collectParserArgs() @@ -174,15 +177,70 @@ class ParserDialogTest { it.name } ).isEqualTo(listOf(inputFolderName)) + Assertions.assertThat(parseResult.matchedOption("output-file").getValue()).isEqualTo("") Assertions.assertThat(parseResult.matchedOption("not-compressed").getValue()).isEqualTo(compress) Assertions.assertThat(parseResult.matchedOption("add-missing").getValue()).isEqualTo(addMissing) Assertions.assertThat(parseResult.matchedOption("recursive").getValue()).isEqualTo(recursive) Assertions.assertThat(parseResult.matchedOption("leaf").getValue()).isEqualTo(leaf) Assertions.assertThat(parseResult.matchedOption("ignore-case").getValue()).isEqualTo(ignoreCase) - Assertions.assertThat(parseResult.matchedOption("mimo").getValue()).isEqualTo(mimo) + Assertions.assertThat(parseResult.matchedOption("mimo").getValue()).isTrue() Assertions.assertThat(parseResult.matchedOption("levenshtein-distance").getValue()).isEqualTo(levenshteinDistance.toInt()) } + @Test + fun `should output correct arguments when using Large Merge`() { + // given + val inputFolderName = "folder" + val outputFileName = "sampleOutputFile" + val addMissing = false + val ignoreCase = true + val compress = true + + val mergeMode = "Large Merge" + + // set indirectly + val recursive = true + val leaf = false + + mockkObject(InputHelper) + every { + InputHelper.isInputValidAndNotNull(any(), any()) + } returns true + + mockkStatic("com.github.kinquirer.components.InputKt") + every { + KInquirer.promptInput(any(), any(), any()) + } returns inputFolderName andThen outputFileName + + mockkStatic("com.github.kinquirer.components.ConfirmKt") + every { + KInquirer.promptConfirm(any(), any()) + } returns compress andThen ignoreCase + mockkStatic("com.github.kinquirer.components.ListKt") + every { + KInquirer.promptList(any(), any(), any(), any(), any()) + } returns mergeMode andThen "Recursive Merging Strategy" + + // when + val parserArguments = ParserDialog.collectParserArgs() + val commandLine = CommandLine(MergeFilter()) + val parseResult = commandLine.parseArgs(*parserArguments.toTypedArray()) + + // then + Assertions.assertThat( + parseResult.matchedPositional(0).getValue>().map { + it.name + } + ).isEqualTo(listOf(inputFolderName)) + Assertions.assertThat(parseResult.matchedOption("output-file").getValue()).isEqualTo(outputFileName) + Assertions.assertThat(parseResult.matchedOption("not-compressed").getValue()).isEqualTo(compress) + Assertions.assertThat(parseResult.matchedOption("add-missing").getValue()).isEqualTo(addMissing) + Assertions.assertThat(parseResult.matchedOption("recursive").getValue()).isEqualTo(recursive) + Assertions.assertThat(parseResult.matchedOption("leaf").getValue()).isEqualTo(leaf) + Assertions.assertThat(parseResult.matchedOption("ignore-case").getValue()).isEqualTo(ignoreCase) + Assertions.assertThat(parseResult.matchedOption("large").getValue()).isTrue() + } + @Test fun `should prompt user twice for input file when first input file is invalid`() { // given val invalidInputFolderName = "" diff --git a/gh-pages/_docs/07-filter/02-merge-filter.md b/gh-pages/_docs/07-filter/02-merge-filter.md index 6639c51c52..b7aa286aba 100644 --- a/gh-pages/_docs/07-filter/02-merge-filter.md +++ b/gh-pages/_docs/07-filter/02-merge-filter.md @@ -21,19 +21,20 @@ Both strategies will merge the unique list entries for `attributeTypes` and `bla ## Usage and Parameters -| Parameters | Description | -|---------------------------------|----------------------------------------------------------------------| -| `FILE` | files to merge | -| `-a, --add-missing` | [Leaf Merging Strategy] enable adding missing nodes to reference | -| `-h, --help` | displays help and exits | -| `--ignore-case` | ignores case when checking node names | -| `--leaf` | use leaf merging strategy | -| `-nc, --not-compressed` | save uncompressed output File | -| `-o, --outputFile=` | output File (or empty for stdout; ignored in [MIMO mode])) | -| `--recursive` | use recursive merging strategy (default) | -| `--mimo` | merge multiple files with the same prefix into multiple output files | -| `-ld, --levenshtein-distance` | [MIMO mode] levenshtein distance for name match suggestions | -| `-f` | force merge non-overlapping modules at the top-level structure | +| Parameters | Description | +|---------------------------------|----------------------------------------------------------------------------------| +| `FILE` | files to merge | +| `-a, --add-missing` | [Leaf Merging Strategy] enable adding missing nodes to reference | +| `-h, --help` | displays help and exits | +| `--ignore-case` | ignores case when checking node names | +| `--leaf` | use leaf merging strategy | +| `-nc, --not-compressed` | save uncompressed output File | +| `-o, --outputFile=` | output File (or empty for stdout; [MIMO mode] output folder)) | +| `--recursive` | use recursive merging strategy (default) | +| `--mimo` | merge multiple files with the same prefix into multiple output files | +| `-ld, --levenshtein-distance` | [MIMO mode] levenshtein distance for name match suggestions | +| `-f` | force merge non-overlapping modules at the top-level structure | +| `--large` | merge multiple project files into one output file, separated by their dot-prefix | ``` Usage: ccsh merge [-ah] [--ignore-case] [--leaf] [-nc] [--recursive] @@ -60,10 +61,24 @@ This last example inputs the folder foo, which will result in all project files ccsh merge myProjectFolder/ --mimo -ld 0 -f ``` -## MIMO - Multiply Inputs Multiple Outputs +## MIMO Merge - Multiply Inputs Multiple Outputs Matches multiple `cc.json` files based on their prefix (e.g. **myProject**.git.cc.json). Tries to match project names with typos and asks which to add to the output. If you want to use this in a CI/CD pipeline environment you may find it useful to specify `-ld` and `-f` to not prompt any user input. The output file name follows the following schema: `myProject.merge.cc.json`. -> IMPORTANT: Output is always the current working directory. +## Large Merge + +Merges multiple `.cc.json` files into one projects, but separates them into sub-folders, with names defined through the dot-prefixes of the input files: + +``` +ccsh merge aa.cc.json bb.cc.json cc.cc.json --large -o myOutputFile -nc +# myOutputFile.cc.json: +# - root +# - - aa +# - - - * +# - - bb +# - - - * +# - - cc +# - - - * +``` From 40df04b50e148792fe6a2241f1c0fcc28cf5140b Mon Sep 17 00:00:00 2001 From: Sebastian Wolf <65733509+phanlezz@users.noreply.github.com> Date: Thu, 5 Dec 2024 14:04:28 +0100 Subject: [PATCH 5/5] feat(analysis): implement review notes (#3743) --- analysis/CHANGELOG.md | 2 +- .../filter/mergefilter/LargeMerge.kt | 28 ++-- .../filter/mergefilter/MergeFilter.kt | 2 +- .../filter/mergefilter/MergeFilterTest.kt | 16 +++ .../duplicate/customRootFailure.cc.json | 128 ++++++++++++++++++ 5 files changed, 164 insertions(+), 12 deletions(-) create mode 100644 analysis/filter/MergeFilter/src/test/resources/largeMerge/duplicate/customRootFailure.cc.json diff --git a/analysis/CHANGELOG.md b/analysis/CHANGELOG.md index 0a9d529893..1f6851b6ca 100644 --- a/analysis/CHANGELOG.md +++ b/analysis/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/) ### Added 🚀 -- Add a new `--large` flat to the MergeFilter that merges projects into one file each in its own subfolder depending on the input file's dot-prefix name [#3841](https://github.com/MaibornWolff/codecharta/pull/3841) +- Add a new `--large` flag to the MergeFilter that merges projects into one file each in its own subfolder depending on the input file's dot-prefix name [#3841](https://github.com/MaibornWolff/codecharta/pull/3841) - Add the ability to the MergeFilter to specify the output file during `--mimo` operation [#3841](https://github.com/MaibornWolff/codecharta/pull/3841) ## [1.129.0] - 2024-11-29 diff --git a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/LargeMerge.kt b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/LargeMerge.kt index 49442485d7..dcc7712eda 100644 --- a/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/LargeMerge.kt +++ b/analysis/filter/MergeFilter/src/main/kotlin/de/maibornwolff/codecharta/filter/mergefilter/LargeMerge.kt @@ -8,7 +8,7 @@ import de.maibornwolff.codecharta.model.Project class LargeMerge { companion object { - fun packageProjectInto(project: Project, prefix: String): Project { + fun wrapProjectInFolder(project: Project, prefix: String): Project { val modifiedProject = Project( project.projectName, moveNodesIntoFolder(project.rootNode, prefix), @@ -22,32 +22,40 @@ class LargeMerge { } private fun moveNodesIntoFolder(root: Node, folderName: String): List { - val newRoot = Node( + val folderNode = Node( name = folderName, type = NodeType.Folder, children = root.children ) val mutableRoot = root.toMutableNode() - mutableRoot.children = mutableSetOf(newRoot.toMutableNode()) + mutableRoot.children = mutableSetOf(folderNode.toMutableNode()) return listOf(mutableRoot.toNode()) } private fun addFolderToEdgePaths(edges: List, folderName: String): List { - edges.forEach { - it.fromNodeName = insertFolderIntoPath(it.fromNodeName, folderName) - it.toNodeName = insertFolderIntoPath(it.toNodeName, folderName) + return edges.map { edge -> + Edge( + fromNodeName = insertFolderIntoPath(edge.fromNodeName, folderName), + toNodeName = insertFolderIntoPath(edge.toNodeName, folderName), + attributes = edge.attributes + ) } - return edges } private fun addFolderToBlackListPaths(blacklist: List, folderName: String): List { - blacklist.forEach { - it.path = insertFolderIntoPath(it.path, folderName) + return blacklist.map { item -> + BlacklistItem( + path = insertFolderIntoPath(item.path, folderName), + type = item.type + ) } - return blacklist } private fun insertFolderIntoPath(path: String, folderName: String): String { + val rootRegex = Regex("^/root/") + require(rootRegex.containsMatchIn(path)) { + "Input project structure doesn't have '/root/' as a base folder. If that's intended open an issue." + } return path.replace(Regex("^/root/"), "/root/$folderName/") } } 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 6d5fb8c9f2..5afdcefbb6 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 @@ -199,7 +199,7 @@ class MergeFilter( val packagedProjects: MutableList = mutableListOf() projectsFileNamePairs.forEach { - packagedProjects.add(LargeMerge.packageProjectInto(it.second, it.first.substringBefore("."))) + packagedProjects.add(LargeMerge.wrapProjectInFolder(it.second, it.first.substringBefore("."))) } val mergedProject = ProjectMerger(packagedProjects, nodeMergerStrategy).merge() diff --git a/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt b/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt index 99159cc39b..dc5a7db54e 100644 --- a/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt +++ b/analysis/filter/MergeFilter/src/test/kotlin/de/maibornwolff/codecharta/filter/mergefilter/MergeFilterTest.kt @@ -626,5 +626,21 @@ class MergeFilterTest { assertThat(outputProject2.children.toString()).isEqualTo(projectInput2.rootNode.children.toString()) outPutFile.deleteOnExit() } + + @Test + fun `should throw error if input project does not contain a strict root node`() { + val customRootProject = "$fatMergeTestFolder/duplicate/customRootFailure.cc.json" + System.setErr(PrintStream(errContent)) + CommandLine(MergeFilter()).execute( + testFilePath1, + testFilePath2, + customRootProject, + "--large" + ).toString() + System.setErr(originalErr) + assertThat( + errContent.toString() + ).contains("Input project structure doesn't have '/root/' as a base folder. If that's intended open an issue.") + } } } diff --git a/analysis/filter/MergeFilter/src/test/resources/largeMerge/duplicate/customRootFailure.cc.json b/analysis/filter/MergeFilter/src/test/resources/largeMerge/duplicate/customRootFailure.cc.json new file mode 100644 index 0000000000..fae0f5ee94 --- /dev/null +++ b/analysis/filter/MergeFilter/src/test/resources/largeMerge/duplicate/customRootFailure.cc.json @@ -0,0 +1,128 @@ +{ + "projectName": "Sample Project 1", + "apiVersion": "1.0", + "nodes": [ + { + "name": "customRoot", + "type": "Folder", + "attributes": {}, + "children": [ + { + "name": "file1", + "type": "File", + "attributes": { + "number_of_commits": 160.0, + "range_of_weeks_with_commits": 68.0, + "successive_weeks_with_commits": 10.0, + "weeks_with_commits": 51.0, + "highly_coupled_files": 0.0, + "median_coupled_files": 2.0, + "abs_coupled_churn": 33700.0, + "avg_code_churn": 3.0 + }, + "link": "", + "children": [] + }, + { + "name": "file4", + "type": "File", + "attributes": { + "number_of_authors": 22.0, + "number_of_commits": 130.0, + "range_of_weeks_with_commits": 18.0, + "successive_weeks_with_commits": 20.0, + "weeks_with_commits": 61.0, + "highly_coupled_files": 1.0, + "median_coupled_files": 4.0, + "abs_coupled_churn": 22000.0, + "avg_code_churn": 4.0 + }, + "link": "", + "children": [] + }, + { + "name": "visualization", + "type": "Folder", + "attributes": {}, + "link": "", + "children": [ + { + "name": "file2", + "type": "File", + "attributes": { + "number_of_authors": 12.0, + "number_of_commits": 120.0, + "range_of_weeks_with_commits": 30.0, + "successive_weeks_with_commits": 20.0, + "weeks_with_commits": 76.0, + "highly_coupled_files": 0.0, + "median_coupled_files": 2.0, + "abs_coupled_churn": 33700.0, + "avg_code_churn": 7.0 + }, + "link": "", + "children": [] + }, + { + "name": "file3", + "type": "File", + "attributes": { + "number_of_authors": 40.0, + "number_of_commits": 230.0, + "range_of_weeks_with_commits": 50.0, + "successive_weeks_with_commits": 80.0, + "weeks_with_commits": 10.0, + "highly_coupled_files": 5.0, + "median_coupled_files": 2.0, + "abs_coupled_churn": 44000.0, + "avg_code_churn": 4.0 + }, + "link": "", + "children": [] + } + ] + } + ] + } + ], + "edges": [ + { + "fromNodeName": "/customRoot/file1", + "toNodeName": "/customRoot/visualization/file2", + "attributes": { + "pairingRate": 90, + "avgCommits": 5, + "anotherMetric": 25 + } + }, + { + "fromNodeName": "/customRoot/visualization/file2", + "toNodeName": "/customRoot/visualization/file3", + "attributes": { + "pairingRate": 80, + "avgCommits": 3 + } + } + ], + "attributeTypes": { + "nodes": { + "attributeA": "absolute", + "attributeB": "absolute", + "attributeZ": "absolute" + }, + "edges": { + "attributeD": "absolute", + "attributeE": "relative" + } + }, + "blacklist": [ + { + "path": "/customRoot/file1", + "type": "hide" + }, + { + "path": "/customRoot/visualization/file2", + "type": "exclude" + } + ] +}