diff --git a/README.md b/README.md index 6833532..45229bc 100644 --- a/README.md +++ b/README.md @@ -10,18 +10,18 @@ we theoretically support all ecosystems currently implemented in the ORT and dep ### Usage -For development purposes, we recommend to import the project into IntelliJ and use one of the preconfigured -run configurations to get started. -Currently, we support two different commands: \ -`create-dependency-graph`, which creates the dependency graphs for the given projects and annotates the graph's -nodes with their version information.\ -`calculate-technical-lag`, which takes a directory with annotated dependency graphs as an input and calculates and -stores their corresponding technical lag. - +Import the project to IntelliJ as a gradle project and add a custom run configuration. +For manual use run `./gradlew run` to get a list of all available commands. Run `./gradlew run --args "{{COMMAND_NAME}} --help"` to +get usage instructions. To run the technical lag calculator you can either use the included `Dockerfile` or build it using `./gradlew installDist` and then run the resulting artifact with `./build/install/libyear-ort/bin/technical-lag-calculator`. +### Display results + +The tool results are created in the provided output folder. The results contain a visualization of the output in the +form of an HTML report. To display the report locally you need to start a local webserver (e.g. by running `python3 -m http.server`) in the folder containing the +files and access the URL (e.g., `localhost:8000/htmlReport.html`). ### Architecture overview diff --git a/src/main/kotlin/commands/calculateTechnicalLag/TechnicalLagCommand.kt b/src/main/kotlin/commands/calculateTechnicalLag/TechnicalLagCommand.kt index 60ded97..66298b0 100644 --- a/src/main/kotlin/commands/calculateTechnicalLag/TechnicalLagCommand.kt +++ b/src/main/kotlin/commands/calculateTechnicalLag/TechnicalLagCommand.kt @@ -9,11 +9,11 @@ import commands.calculateTechnicalLag.visualization.Visualizer import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json import org.apache.logging.log4j.kotlin.logger +import org.jetbrains.kotlinx.dataframe.api.toDataFrame import shared.analyzerResultDtos.AnalyzerResultDto import shared.analyzerResultDtos.ProjectDto import shared.project.Project -import shared.project.ProjectPaths -import util.StoreResultHelper +import reporter.Reporter import java.nio.file.Files import kotlin.io.path.createDirectories import kotlin.io.path.extension @@ -41,11 +41,10 @@ class CalculateTechnicalLag : CliktCommand() { override fun run(): Unit = runBlocking { outputPath.createDirectories() - + val reporter = Reporter(outputPath) val technicalLagStatisticsService = TechnicalLagStatisticsService() logger.info { "Running libyears for projects in $dependencyGraphDirs and output path $outputPath" } - outputPath.createDirectories() val techLagExport = mutableListOf() @@ -87,10 +86,13 @@ class CalculateTechnicalLag : CliktCommand() { environmentInfo = analyzerResult.environmentInfo, ) // TODO: store direct and transitive stats for the graph. need to extend the serialization and DTO - StoreResultHelper.storeAnalyzerResultInFile(outputPath.toFile(), result) + reporter.storeAnalyzerResultInFile(result) } - Visualizer.createAndStoreBoxplotFromTechLag(data = techLagExport, outputPath = outputPath) + val df = techLagExport.toDataFrame() + Visualizer.createAndStoreBoxplotFromTechLag(df, outputPath = outputPath) + reporter.storeCsvExport(df) + reporter.generateHtmlReport() } diff --git a/src/main/kotlin/commands/calculateTechnicalLag/visualization/Visualizer.kt b/src/main/kotlin/commands/calculateTechnicalLag/visualization/Visualizer.kt index d74a10f..745bd14 100644 --- a/src/main/kotlin/commands/calculateTechnicalLag/visualization/Visualizer.kt +++ b/src/main/kotlin/commands/calculateTechnicalLag/visualization/Visualizer.kt @@ -1,5 +1,6 @@ package commands.calculateTechnicalLag.visualization +import org.jetbrains.kotlinx.dataframe.DataFrame import org.jetbrains.kotlinx.dataframe.api.toDataFrame import org.jetbrains.kotlinx.dataframe.io.writeCSV import org.jetbrains.kotlinx.kandy.dsl.plot @@ -39,11 +40,9 @@ object Visualizer { val version: String ) - fun createAndStoreBoxplotFromTechLag(data: List, outputPath: Path) { + fun createAndStoreBoxplotFromTechLag(df: DataFrame, outputPath: Path) { - val df = data.toDataFrame() - df.writeCSV(outputPath.toAbsolutePath().resolve("boxplot-data.csv").toString()) val outputFilePath = outputPath.toAbsolutePath().resolve("${Date().time}-boxplot.png").toString() df.plot { boxplot("scope", "libdays") { diff --git a/src/main/kotlin/commands/createDependencyGraph/DependencyGraphCommand.kt b/src/main/kotlin/commands/createDependencyGraph/DependencyGraphCommand.kt index 8d06ea4..637e3aa 100644 --- a/src/main/kotlin/commands/createDependencyGraph/DependencyGraphCommand.kt +++ b/src/main/kotlin/commands/createDependencyGraph/DependencyGraphCommand.kt @@ -8,7 +8,7 @@ import kotlinx.coroutines.runBlocking import org.apache.logging.log4j.kotlin.logger import shared.analyzerResultDtos.AnalyzerResultDto import shared.analyzerResultDtos.ProjectDto -import util.StoreResultHelper +import reporter.Reporter import java.io.File import java.nio.file.Files import kotlin.io.path.createDirectories @@ -81,8 +81,8 @@ class CreateDependencyGraph : CliktCommand() { repositoryInfo = rawResult.repositoryInfo, environmentInfo = rawResult.environmentInfo, ) - - StoreResultHelper.storeAnalyzerResultInFile(outputPath.toFile(), result) + val reporter = Reporter(outputPath) + reporter.storeAnalyzerResultInFile(result) } catch (error: Exception) { logger.error("Dependency Analyzer failed with error $error") } diff --git a/src/main/kotlin/reporter/Reporter.kt b/src/main/kotlin/reporter/Reporter.kt new file mode 100644 index 0000000..265e8e7 --- /dev/null +++ b/src/main/kotlin/reporter/Reporter.kt @@ -0,0 +1,76 @@ +package reporter + +import commands.calculateTechnicalLag.visualization.Visualizer +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import org.jetbrains.kotlinx.dataframe.DataFrame +import org.jetbrains.kotlinx.dataframe.io.writeCSV +import shared.analyzerResultDtos.AnalyzerResultDto +import shared.project.ProjectPaths +import java.io.File +import java.nio.file.Path +import java.util.* + + +class Reporter(outputPath: Path) { + + private val json = Json { prettyPrint = true } + private val outputFile = outputPath.toAbsolutePath().toFile() + + private val analyzerResultFile by lazy { + createFileInOutputPath("${Date().time}-analyzerResult.json") + } + private val htmlReportFile by lazy { + createFileInOutputPath("htmlReport.html") + } + private val csvFileName = "csvReport.csv" + private val csvReportFile by lazy { + outputPath.resolve(csvFileName) + } + + private fun createFileInOutputPath(fileName: String): File { + val file = outputFile.resolve(fileName) + file.createNewFile() + return file + } + + fun storeAnalyzerResultInFile( + result: AnalyzerResultDto, + ) { + val jsonString = + json.encodeToString(AnalyzerResultDto.serializer(), result) + analyzerResultFile.writeText(jsonString) + } + + fun storeCsvExport(df: DataFrame) { + df.writeCSV(csvReportFile.toString()) + } + + fun generateHtmlReport( + ) { + val resource = + this.javaClass.classLoader.getResource("html-report-template.html") ?: return + + val htmlReportTemplate = resource.readText(Charsets.UTF_8) + val htmlReport = htmlReportTemplate.replace("{{ PATH_TO_STATISTICS }}", "\"$csvFileName\"") + + htmlReportFile.writeText(htmlReport, Charsets.UTF_8) + } + + suspend fun storeResultFilePathsInFile( + outputDirectory: File, + paths: ProjectPaths, + dispatcher: CoroutineDispatcher = Dispatchers.IO + ): File { + val outputFile = outputDirectory.resolve("dependencyGraphs.json") + return withContext(dispatcher) { + outputFile.createNewFile() + val jsonString = + json.encodeToString(ProjectPaths.serializer(), paths) + outputFile.writeText(jsonString) + return@withContext outputFile + } + } +} diff --git a/src/main/kotlin/util/StoreResultsHelper.kt b/src/main/kotlin/util/StoreResultsHelper.kt deleted file mode 100644 index f7be0c0..0000000 --- a/src/main/kotlin/util/StoreResultsHelper.kt +++ /dev/null @@ -1,49 +0,0 @@ -package util - -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlinx.serialization.json.Json -import shared.analyzerResultDtos.AnalyzerResultDto -import shared.project.ProjectPaths -import java.io.File -import java.util.* - - -class StoreResultHelper { - companion object { - private val json = Json { prettyPrint = true } - - suspend fun storeAnalyzerResultInFile( - outputDirectory: File, - result: AnalyzerResultDto, - dispatcher: CoroutineDispatcher = Dispatchers.IO - ): File { - val outputFile = outputDirectory.resolve("${Date().time}-analyzerResult.json") - return withContext(dispatcher) { - outputFile.createNewFile() - - val jsonString = - json.encodeToString(AnalyzerResultDto.serializer(), result) - outputFile.writeText(jsonString) - return@withContext outputFile - } - } - - - suspend fun storeResultFilePathsInFile( - outputDirectory: File, - paths: ProjectPaths, - dispatcher: CoroutineDispatcher = Dispatchers.IO - ): File { - val outputFile = outputDirectory.resolve("dependencyGraphs.json") - return withContext(dispatcher) { - outputFile.createNewFile() - val jsonString = - json.encodeToString(ProjectPaths.serializer(), paths) - outputFile.writeText(jsonString) - return@withContext outputFile - } - } - } -} diff --git a/src/main/resources/html-report-template.html b/src/main/resources/html-report-template.html new file mode 100644 index 0000000..41904f9 --- /dev/null +++ b/src/main/resources/html-report-template.html @@ -0,0 +1,115 @@ + + + + + + + +
+ + + \ No newline at end of file