From b2c202a8dcca3bd5dede2dda4bc5966be4500f7b Mon Sep 17 00:00:00 2001 From: Abhijit Sarkar Date: Sun, 9 May 2021 04:42:50 -0700 Subject: [PATCH 1/2] closes #22; support for output to CSV file --- .github/workflows/stale.yml | 19 + README.md | 34 +- build.gradle.kts | 8 +- gradle.properties | 4 +- src/main/kotlin/com/asarkar/gradle/Printer.kt | 19 - .../BuildTimeTrackerPlugin.kt | 16 +- .../{ => buildtimetracker}/Constants.kt | 2 +- .../Printer.kt} | 49 ++- .../gradle/buildtimetracker/Printers.kt | 26 ++ .../{ => buildtimetracker}/TimingRecorder.kt | 8 +- .../BuildTimeTrackerPluginFunctionalTest.kt | 85 ----- .../com/asarkar/gradle/ConsolePrinterTest.kt | 274 -------------- .../BuildTimeTrackerPluginFunctionalTest.kt | 137 +++++++ .../gradle/buildtimetracker/PrinterTest.kt | 334 ++++++++++++++++++ 14 files changed, 599 insertions(+), 416 deletions(-) create mode 100644 .github/workflows/stale.yml delete mode 100644 src/main/kotlin/com/asarkar/gradle/Printer.kt rename src/main/kotlin/com/asarkar/gradle/{ => buildtimetracker}/BuildTimeTrackerPlugin.kt (69%) rename src/main/kotlin/com/asarkar/gradle/{ => buildtimetracker}/Constants.kt (78%) rename src/main/kotlin/com/asarkar/gradle/{ConsolePrinter.kt => buildtimetracker/Printer.kt} (52%) create mode 100644 src/main/kotlin/com/asarkar/gradle/buildtimetracker/Printers.kt rename src/main/kotlin/com/asarkar/gradle/{ => buildtimetracker}/TimingRecorder.kt (90%) delete mode 100644 src/test/kotlin/com/asarkar/gradle/BuildTimeTrackerPluginFunctionalTest.kt delete mode 100644 src/test/kotlin/com/asarkar/gradle/ConsolePrinterTest.kt create mode 100644 src/test/kotlin/com/asarkar/gradle/buildtimetracker/BuildTimeTrackerPluginFunctionalTest.kt create mode 100644 src/test/kotlin/com/asarkar/gradle/buildtimetracker/PrinterTest.kt diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..7a39e2b --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,19 @@ +name: "Close stale issues and PRs" +on: + schedule: + - cron: "0 0 * * *" + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v3 + with: + stale-issue-message: "This issue is stale and will be closed in 5 days." + stale-pr-message: "This PR is stale and will be closed in 5 days." + days-before-stale: 30 + days-before-close: 5 + any-of-labels: "waiting-feedback" + exempt-all-milestones: true + debug-only: false + enable-statistics: true \ No newline at end of file diff --git a/README.md b/README.md index 7234970..f925c50 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # build-time-tracker -Like [passy/build-time-tracker-plugin](https://github.com/passy/build-time-tracker-plugin), but actively maintained. -Requires Java 8 or later. +Gradle plugin that prints the time taken by the tasks in a build. Requires Java 8 or later. [![](https://github.com/asarkar/build-time-tracker/workflows/CI%20Pipeline/badge.svg)](https://github.com/asarkar/build-time-tracker/actions?query=workflow%3A%22CI+Pipeline%22) @@ -16,35 +15,42 @@ Requires Java 8 or later. :webapp:dockerPushImage | 4S | 14% | ████ ``` -See [Gradle Plugin Portal](https://plugins.gradle.org/plugin/com.asarkar.gradle.build-time-tracker) for usage +See [Gradle Plugin Portal](https://plugins.gradle.org/plugin/com.asarkar.gradle.build-time-tracker) for usage instructions. If you are the fiddling type, you can customize the plugin as follows: ``` -import com.asarkar.gradle.BuildTimeTrackerPluginExtension +import com.asarkar.gradle.buildtimetracker.BuildTimeTrackerPluginExtension // bunch of code configure { // or buildTimeTracker {...}, for Groovy barPosition = TRAILING or LEADING, default is TRAILING sort = false or true, default is false - output = CONSOLE, other options may be added in the future - maxWidth = 80, so that your build logs don't look like Craigslist - minTaskDuration = Duration.ofSeconds(1), don't worry about tasks that take less than a second to execute + output = CONSOLE or CSV, default is CONSOLE + maxWidth = 120, default is 80 + minTaskDuration = Duration.ofSeconds(1), don't show tasks that take less than a second to execute showBars = false or true, default is true + csvFilePath = /path/to/csv, only relevant if output = CSV, default build/reports/buildTimeTracker/build.csv } ``` -:information_source: Due to a [Gradle limitation](https://docs.gradle.org/6.5.1/userguide/upgrading_version_5.html#apis_buildlistener_buildstarted_and_gradle_buildstarted_have_been_deprecated), -the build duration can't be calculated precisely. -The bars and percentages are rounded off such that the output provides a good indication of how long individual -tasks took to complete relative to the build, but are not meant to be correct up to the milliseconds. +:information_source: Due to a +[Gradle limitation](https://docs.gradle.org/6.5.1/userguide/upgrading_version_5.html#apis_buildlistener_buildstarted_and_gradle_buildstarted_have_been_deprecated) +, the build duration can't be calculated precisely. The bars and percentages are rounded off such that the output +provides a good indication of how long individual tasks took to complete relative to the build, but are not meant to be +correct up to the milliseconds. -:information_source: It is sufficient to apply the plugin to the root project; applying to subprojects will result -in duplication of the report. +:information_source: It is sufficient to apply the plugin to the root project; applying to subprojects will result in +duplication of the report. -:warning: If the output console does not support UTF-8 encoding, the bars may appear as weird characters. If you are +:warning: If the output terminal does not support UTF-8 encoding, the bars may appear as weird characters. If you are running Windows, make sure the terminal encoding is set to UTF-8, or turn off the bars as explained above. +:warning: If exporting to CSV, and bars are enabled, the resulting file must be imported as UTF-8 encoded CSV data in +Microsoft Excel. How to do this depends on the Operating System, and Excel version, but +[here](https://answers.microsoft.com/en-us/msoffice/forum/msoffice_excel-mso_mac-mso_365hp/how-to-open-utf-8-csv-file-in-excel-without-mis/1eb15700-d235-441e-8b99-db10fafff3c2) +is one way. + ## Contribute This project is a volunteer effort. You are welcome to send pull requests, ask questions, or create issues. If you like diff --git a/build.gradle.kts b/build.gradle.kts index c4f1b29..0ad6e3b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -53,21 +53,21 @@ dependencies { testImplementation("org.assertj:assertj-core:$assertjVersion") } -tasks.withType { +tasks.withType().configureEach { kotlinOptions { - freeCompilerArgs = listOf("-Xjsr305=strict") + freeCompilerArgs = listOf("-Xjsr305=strict", "-Xopt-in=kotlin.RequiresOptIn") jvmTarget = "1.8" } } -plugins.withType { +plugins.withType().configureEach { extensions.configure { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } } -tasks.withType { +tasks.withType().configureEach { useJUnitPlatform() testLogging { showStandardStreams = true diff --git a/gradle.properties b/gradle.properties index 335ae93..fce1007 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,11 +4,11 @@ pluginTags = performance, build, metrics pluginId = com.asarkar.gradle.build-time-tracker pluginDisplayName = build-time-tracker pluginDescription = Gradle plugin that prints the time taken by the tasks in a build -pluginImplementationClass = com.asarkar.gradle.BuildTimeTrackerPlugin +pluginImplementationClass = com.asarkar.gradle.buildtimetracker.BuildTimeTrackerPlugin pluginDeclarationName = buildTimeTrackerPlugin projectGroup = com.asarkar.gradle -projectVersion = 2.1.0 +projectVersion = 3.0.0-rc junitVersion = latest.release assertjVersion = latest.release diff --git a/src/main/kotlin/com/asarkar/gradle/Printer.kt b/src/main/kotlin/com/asarkar/gradle/Printer.kt deleted file mode 100644 index 6241d27..0000000 --- a/src/main/kotlin/com/asarkar/gradle/Printer.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.asarkar.gradle - -data class PrinterInput( - val buildDuration: Long, - val taskDurations: List>, - val ext: BuildTimeTrackerPluginExtension -) - -interface Printer { - fun print(input: PrinterInput) - - companion object { - fun newInstance(output: Output): Printer { - return when (output) { - Output.CONSOLE -> ConsolePrinter() - } - } - } -} diff --git a/src/main/kotlin/com/asarkar/gradle/BuildTimeTrackerPlugin.kt b/src/main/kotlin/com/asarkar/gradle/buildtimetracker/BuildTimeTrackerPlugin.kt similarity index 69% rename from src/main/kotlin/com/asarkar/gradle/BuildTimeTrackerPlugin.kt rename to src/main/kotlin/com/asarkar/gradle/buildtimetracker/BuildTimeTrackerPlugin.kt index d6d579e..456c158 100644 --- a/src/main/kotlin/com/asarkar/gradle/BuildTimeTrackerPlugin.kt +++ b/src/main/kotlin/com/asarkar/gradle/buildtimetracker/BuildTimeTrackerPlugin.kt @@ -1,12 +1,14 @@ -package com.asarkar.gradle +package com.asarkar.gradle.buildtimetracker -import com.asarkar.gradle.Constants.EXTRA_EXTENSION_NAME -import com.asarkar.gradle.Constants.LOGGER_KEY -import com.asarkar.gradle.Constants.PLUGIN_EXTENSION_NAME +import com.asarkar.gradle.buildtimetracker.Constants.EXTRA_EXTENSION_NAME +import com.asarkar.gradle.buildtimetracker.Constants.LOGGER_KEY +import com.asarkar.gradle.buildtimetracker.Constants.PLUGIN_EXTENSION_NAME import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.plugins.ExtensionAware import org.gradle.api.reflect.TypeOf +import java.nio.file.Path +import java.nio.file.Paths import java.time.Duration enum class BarPosition { @@ -14,7 +16,7 @@ enum class BarPosition { } enum class Output { - CONSOLE + CONSOLE, CSV } open class BuildTimeTrackerPluginExtension { @@ -24,6 +26,10 @@ open class BuildTimeTrackerPluginExtension { var maxWidth: Int = 80 var minTaskDuration: Duration = Duration.ofSeconds(1) var showBars: Boolean = true + var csvFilePath: Path = Paths.get("build") + .resolve("reports") + .resolve(PLUGIN_EXTENSION_NAME) + .resolve("build.csv") } class BuildTimeTrackerPlugin : Plugin { diff --git a/src/main/kotlin/com/asarkar/gradle/Constants.kt b/src/main/kotlin/com/asarkar/gradle/buildtimetracker/Constants.kt similarity index 78% rename from src/main/kotlin/com/asarkar/gradle/Constants.kt rename to src/main/kotlin/com/asarkar/gradle/buildtimetracker/Constants.kt index 31807c1..9dbf1f1 100644 --- a/src/main/kotlin/com/asarkar/gradle/Constants.kt +++ b/src/main/kotlin/com/asarkar/gradle/buildtimetracker/Constants.kt @@ -1,4 +1,4 @@ -package com.asarkar.gradle +package com.asarkar.gradle.buildtimetracker object Constants { const val PLUGIN_EXTENSION_NAME = "buildTimeTracker" diff --git a/src/main/kotlin/com/asarkar/gradle/ConsolePrinter.kt b/src/main/kotlin/com/asarkar/gradle/buildtimetracker/Printer.kt similarity index 52% rename from src/main/kotlin/com/asarkar/gradle/ConsolePrinter.kt rename to src/main/kotlin/com/asarkar/gradle/buildtimetracker/Printer.kt index cd9492b..785d7db 100644 --- a/src/main/kotlin/com/asarkar/gradle/ConsolePrinter.kt +++ b/src/main/kotlin/com/asarkar/gradle/buildtimetracker/Printer.kt @@ -1,11 +1,23 @@ -package com.asarkar.gradle +package com.asarkar.gradle.buildtimetracker +import java.io.Closeable import java.io.PrintStream +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.StandardOpenOption.CREATE +import java.nio.file.StandardOpenOption.TRUNCATE_EXISTING +import java.nio.file.StandardOpenOption.WRITE import java.time.Duration import kotlin.math.round -class ConsolePrinter(private val out: PrintStream = System.out) : Printer { - override fun print(input: PrinterInput) { +data class PrinterInput( + val buildDuration: Long, + val taskDurations: List>, + val ext: BuildTimeTrackerPluginExtension +) + +interface Printer : Closeable { + fun print(input: PrinterInput) { // find the maxes needed for formatting val (maxLabelLen, maxDuration, maxFormattedDurationLen) = input.taskDurations.fold( Triple(-1, -1L, -1) @@ -21,28 +33,30 @@ class ConsolePrinter(private val out: PrintStream = System.out) : Printer { .format() .length - out.println("== Build time summary ==") input.taskDurations.forEach { val numBlocks = round(it.second * scalingFraction).toInt() val percent = it.second.percentOf(input.buildDuration) val common = String.format( - "%${maxLabelLen}s | %${maxFormattedDurationLen}s | %${maxFormattedPercentLen}s", - it.first, it.second.format(), percent.format() + "%${maxLabelLen}s%s%${maxFormattedDurationLen}s%s%${maxFormattedPercentLen}s", + it.first, delimiter, it.second.format(), delimiter, percent.format() ) if (!input.ext.showBars) { out.println(common) } else if (input.ext.barPosition == BarPosition.TRAILING) { - out.printf("%s | %s\n", common, BLOCK_STR.repeat(numBlocks)) + out.printf("%s%s%s\n", common, delimiter, "$BLOCK_CHAR".repeat(numBlocks)) } else { - out.printf("%${maxNumBlocks}s | %s\n", BLOCK_STR.repeat(numBlocks), common) + out.printf("%${maxNumBlocks}s%s%s\n", "$BLOCK_CHAR".repeat(numBlocks), delimiter, common) } } } + val out: PrintStream + val delimiter: String + companion object { - const val BLOCK_STR = "\u2588" + const val BLOCK_CHAR = '\u2588' private fun Long.percentOf(buildDuration: Long): Int = round(this / buildDuration.toDouble() * 100).toInt() internal fun Long.format(): String { @@ -50,6 +64,23 @@ class ConsolePrinter(private val out: PrintStream = System.out) : Printer { return Duration.ofSeconds(this).toString() .filterNot { it in separators } } + private fun Int.format(): String = String.format("%d%%", this) + + fun newInstance(ext: BuildTimeTrackerPluginExtension): Printer { + return when (ext.output) { + Output.CONSOLE -> ConsolePrinter() + Output.CSV -> { + ext.csvFilePath.parent.toFile().mkdirs() + CsvPrinter( + PrintStream( + Files.newOutputStream(ext.csvFilePath, CREATE, WRITE, TRUNCATE_EXISTING), + false, + StandardCharsets.UTF_8.name() + ) + ) + } + } + } } } diff --git a/src/main/kotlin/com/asarkar/gradle/buildtimetracker/Printers.kt b/src/main/kotlin/com/asarkar/gradle/buildtimetracker/Printers.kt new file mode 100644 index 0000000..60be83e --- /dev/null +++ b/src/main/kotlin/com/asarkar/gradle/buildtimetracker/Printers.kt @@ -0,0 +1,26 @@ +package com.asarkar.gradle.buildtimetracker + +import java.io.PrintStream + +class ConsolePrinter( + override val out: PrintStream = System.out, + override val delimiter: String = " | " +) : Printer { + override fun print(input: PrinterInput) { + out.println("== Build time summary ==") + super.print(input) + } + + override fun close() { + // Do nothing + } +} + +class CsvPrinter( + override val out: PrintStream, + override val delimiter: String = "," +) : Printer { + override fun close() { + out.close() + } +} diff --git a/src/main/kotlin/com/asarkar/gradle/TimingRecorder.kt b/src/main/kotlin/com/asarkar/gradle/buildtimetracker/TimingRecorder.kt similarity index 90% rename from src/main/kotlin/com/asarkar/gradle/TimingRecorder.kt rename to src/main/kotlin/com/asarkar/gradle/buildtimetracker/TimingRecorder.kt index 674229e..bfcf490 100644 --- a/src/main/kotlin/com/asarkar/gradle/TimingRecorder.kt +++ b/src/main/kotlin/com/asarkar/gradle/buildtimetracker/TimingRecorder.kt @@ -1,4 +1,4 @@ -package com.asarkar.gradle +package com.asarkar.gradle.buildtimetracker import org.gradle.BuildAdapter import org.gradle.BuildResult @@ -43,8 +43,10 @@ class TimingRecorder(val ext: BuildTimeTrackerPluginExtension) : TaskExecutionLi if (ext.sort) { taskDurations.sortBy { -it.second } } - Printer.newInstance(ext.output) - .print(PrinterInput(buildDuration, taskDurations, ext)) + Printer.newInstance(ext) + .use { + it.print(PrinterInput(buildDuration, taskDurations, ext)) + } } override fun projectsEvaluated(gradle: Gradle) { diff --git a/src/test/kotlin/com/asarkar/gradle/BuildTimeTrackerPluginFunctionalTest.kt b/src/test/kotlin/com/asarkar/gradle/BuildTimeTrackerPluginFunctionalTest.kt deleted file mode 100644 index 1c9a543..0000000 --- a/src/test/kotlin/com/asarkar/gradle/BuildTimeTrackerPluginFunctionalTest.kt +++ /dev/null @@ -1,85 +0,0 @@ -package com.asarkar.gradle - -import org.assertj.core.api.Assertions.assertThat -import org.gradle.testkit.runner.GradleRunner -import org.gradle.testkit.runner.TaskOutcome.SUCCESS -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.io.TempDir -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.Paths -import java.nio.file.StandardOpenOption.APPEND -import java.nio.file.StandardOpenOption.CREATE -import java.nio.file.StandardOpenOption.TRUNCATE_EXISTING -import java.nio.file.StandardOpenOption.WRITE -import java.util.Properties - -class BuildTimeTrackerPluginFunctionalTest { - lateinit var buildFile: Path - - @BeforeEach - fun beforeEach(@TempDir testProjectDir: Path) { - val propFile = generateSequence(Paths.get(javaClass.protectionDomain.codeSource.location.path)) { - val props = it.resolve("gradle.properties") - if (Files.exists(props)) props else it.parent - } - .dropWhile { Files.isDirectory(it) } - .take(1) - .iterator() - .next() - - Files.newInputStream(propFile).use { - val props = Properties().apply { load(it) } - buildFile = Files.createFile(testProjectDir.resolve("build.gradle.kts")) - Files.newBufferedWriter(buildFile, CREATE, WRITE, TRUNCATE_EXISTING).use { - it.write( - """ - import java.lang.Thread.sleep - import com.asarkar.gradle.BuildTimeTrackerPluginExtension - import java.time.Duration - plugins { - id("${props.getProperty("pluginId")}") - } - """.trimIndent() - ) - it.newLine() - } - } - } - - @Test - fun testPluginLoads() { - val taskName = "hello" - Files.newBufferedWriter(buildFile, APPEND).use { - it.write( - """ - tasks.register("$taskName") { - doLast { - sleep(2000) - println("Hello, World!") - } - } - - configure { - minTaskDuration = Duration.ofSeconds(1) - } - """.trimIndent() - ) - } - - println(buildFile.toFile().readText()) - - val result = GradleRunner.create() - .withProjectDir(buildFile.parent.toFile()) - .withArguments(taskName, "--warning-mode=all", "--stacktrace") - .withPluginClasspath() - .forwardOutput() - .withDebug(true) - .build() - - assertThat(result.task(taskName)?.outcome == SUCCESS) - assertThat(result.output).contains("== Build time summary ==") - assertThat(result.output).contains(":hello") - } -} diff --git a/src/test/kotlin/com/asarkar/gradle/ConsolePrinterTest.kt b/src/test/kotlin/com/asarkar/gradle/ConsolePrinterTest.kt deleted file mode 100644 index 7cecc9d..0000000 --- a/src/test/kotlin/com/asarkar/gradle/ConsolePrinterTest.kt +++ /dev/null @@ -1,274 +0,0 @@ -package com.asarkar.gradle - -import com.asarkar.gradle.ConsolePrinter.Companion.BLOCK_STR -import com.asarkar.gradle.ConsolePrinter.Companion.format -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.fail -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.Arguments -import org.junit.jupiter.params.provider.Arguments.arguments -import org.junit.jupiter.params.provider.EnumSource -import org.junit.jupiter.params.provider.MethodSource -import java.io.ByteArrayOutputStream -import java.io.PrintStream -import java.util.stream.Stream - -private fun ByteArray.lines(): Sequence { - val iterator = this.iterator() - - return generateSequence { - val buffer = mutableListOf() - if (iterator.hasNext()) { - var next = iterator.next() - while (next != '\n'.toByte()) { - buffer.add(next) - next = iterator.next() - } - String(buffer.toByteArray()) - } else { - null - } - } -} - -private data class Line(val task: String, val duration: String, val percent: String, val bar: String) - -private fun String.toLine(barPosition: BarPosition): Line { - val tokens = this.split("|") - .map { it.trim() } - .takeIf { it.size == 4 } ?: fail("Unexpected line format: $this") - return if (barPosition == BarPosition.TRAILING) { - Line(tokens[0], tokens[1], tokens[2], tokens[3]) - } else { - Line(tokens[1], tokens[2], tokens[3], tokens[0]) - } -} - -class ConsolePrinterTest { - private lateinit var out: ByteArrayOutputStream - private lateinit var ext: BuildTimeTrackerPluginExtension - - @BeforeEach - fun beforeEach() { - out = ByteArrayOutputStream() - ext = BuildTimeTrackerPluginExtension() - } - - private val taskDurations = listOf( - ":commons:extractIncludeProto" to 4L, - ":commons:compileKotlin" to 2L, - ":commons:compileJava" to 6L, - ":service-client:compileKotlin" to 1L, - ":webapp:compileKotlin" to 1L, - ":webapp:dockerBuildImage" to 4L, - ":webapp:dockerPushImage" to 4L - ) - - @Test - fun testConsolePrinterDefault() { - val buildDuration = 28L - - ConsolePrinter(PrintStream(out)).print( - PrinterInput( - buildDuration, - taskDurations, - ext - ) - ) - - val iterator = out.toByteArray().lines().withIndex().iterator() - iterator.next() // ignore summary header - val line = iterator - .next() - .value - .toLine(ext.barPosition) - assertThat(line.task).isEqualTo(taskDurations[0].first) - assertThat(line.duration).isEqualTo("4S") - assertThat(line.percent).isEqualTo("14%") - assertThat(line.bar.toCharArray().all { it == '█' }).isTrue() - - iterator.forEach { - assertThat(it.value.startsWith(taskDurations[it.index - 1].first) && it.value.endsWith(BLOCK_STR)) - } - } - - @Test - fun testConsolePrinterLeading() { - val buildDuration = 28L - ConsolePrinter(PrintStream(out)).print( - PrinterInput( - buildDuration, - taskDurations, - ext.apply { barPosition = BarPosition.LEADING } - ) - ) - - val iterator = out.toByteArray().lines().withIndex().iterator() - iterator.next() // ignore summary header - - val line = iterator - .next() - .value - .toLine(ext.barPosition) - assertThat(line.bar.toCharArray().all { it == '█' }).isTrue() - assertThat(line.duration).isEqualTo("4S") - assertThat(line.percent).isEqualTo("14%") - assertThat(line.task).isEqualTo(taskDurations[0].first) - - iterator.forEach { - assertThat(it.value.endsWith(taskDurations[it.index - 1].first) && it.value.startsWith(BLOCK_STR)) - } - } - - @Test - fun testConsolePrinterScaled() { - ConsolePrinter(PrintStream(out)).print( - PrinterInput( - 28L, - taskDurations, - ext.apply { maxWidth = 5 } - ) - ) - - out.toByteArray().lines() - .drop(1) // ignore summary header - .map { it.toLine(ext.barPosition) } - .forEach { assertThat(it.bar.length <= ext.maxWidth) } - } - - @Test - // https://github.com/asarkar/build-time-tracker/issues/1 - fun testConsolePrinterHideBars() { - ConsolePrinter(PrintStream(out)).print( - PrinterInput( - 28L, - taskDurations, - ext.apply { showBars = false } - ) - ) - - assertThat(out.toByteArray().lines().all { !it.contains(BLOCK_STR) }) - } - - @ParameterizedTest - @EnumSource(BarPosition::class) - // https://github.com/asarkar/build-time-tracker/issues/3 - fun testFormatting(position: BarPosition) { - ConsolePrinter(PrintStream(out)).print( - PrinterInput( - 18L, - listOf( - ":service-client:compileKotlin" to 1L, - "webapp:test" to 13L - ), - ext.apply { barPosition = position } - ) - ) - - val lines = out.toByteArray().lines().toList() - assertThat(lines).hasSize(3) - val first = lines[1].mapIndexed { i, ch -> if (ch == '|') i else -1 } - .filterNot { it == -1 } - val second = lines[2].mapIndexed { i, ch -> if (ch == '|') i else -1 } - .filterNot { it == -1 } - assertThat(first).containsExactlyElementsOf(second) - } - - @Test - fun testFormattingHideBars() { - ConsolePrinter(PrintStream(out)).print( - PrinterInput( - 18L, - listOf( - ":service-client:compileKotlin" to 1L, - "webapp:test" to 13L - ), - ext.apply { showBars = false } - ) - ) - - val lines = out.toByteArray().lines().toList() - assertThat(lines).hasSize(3) - val first = lines[1].mapIndexed { i, ch -> if (ch == '|') i else -1 } - .filterNot { it == -1 } - val second = lines[2].mapIndexed { i, ch -> if (ch == '|') i else -1 } - .filterNot { it == -1 } - assertThat(first).containsExactlyElementsOf(second) - } - - // The following "tests" do not verify anything, but prints the output for manual inspection - - @Test - fun testConsoleOutputDefault() { - ConsolePrinter().print( - PrinterInput( - 28L, - taskDurations, - ext - ) - ) - } - - @Test - fun testConsoleOutputLeading() { - ConsolePrinter().print( - PrinterInput( - 28L, - taskDurations, - ext.apply { barPosition = BarPosition.LEADING } - ) - ) - } - - @Test - fun testConsoleOutputHideBars() { - ConsolePrinter().print( - PrinterInput( - 28L, - taskDurations, - ext.apply { showBars = false } - ) - ) - } - - @ParameterizedTest - @MethodSource("durationProvider") - fun testDurationFormatting(duration: Long, formatted: String) { - assertThat(duration.format()).isEqualTo(formatted) - } - - companion object { - @JvmStatic - fun durationProvider(): Stream { - return Stream.of( - arguments(14, "14S"), - arguments(7200, "2H"), - arguments(120, "2M"), - arguments(3905, "1H5M5S"), - arguments(3900, "1H5M"), - arguments(61, "1M1S"), - arguments(172800, "48H"), - arguments(172859, "48H59S") - ) - } - } - - @Test - fun testFormatHundredPercent() { - ConsolePrinter(PrintStream(out)).print( - PrinterInput( - 10L, - listOf( - ":service-client:compileKotlin" to 10L - ), - ext - ) - ) - - val lines = out.toByteArray().lines().toList() - assertThat(lines).hasSize(2) - assertThat(lines.last().toLine(ext.barPosition).percent).isEqualTo("100%") - } -} diff --git a/src/test/kotlin/com/asarkar/gradle/buildtimetracker/BuildTimeTrackerPluginFunctionalTest.kt b/src/test/kotlin/com/asarkar/gradle/buildtimetracker/BuildTimeTrackerPluginFunctionalTest.kt new file mode 100644 index 0000000..4812d09 --- /dev/null +++ b/src/test/kotlin/com/asarkar/gradle/buildtimetracker/BuildTimeTrackerPluginFunctionalTest.kt @@ -0,0 +1,137 @@ +package com.asarkar.gradle.buildtimetracker + +import org.assertj.core.api.Assertions.assertThat +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.TaskOutcome.SUCCESS +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.StandardOpenOption.APPEND +import java.nio.file.StandardOpenOption.CREATE +import java.nio.file.StandardOpenOption.TRUNCATE_EXISTING +import java.nio.file.StandardOpenOption.WRITE +import java.time.Duration +import java.util.Properties +import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.absolutePathString +import kotlin.io.path.inputStream +import kotlin.io.path.readLines +import kotlin.io.path.readText + +@OptIn(ExperimentalPathApi::class) +class BuildTimeTrackerPluginFunctionalTest { + private lateinit var buildFile: Path + private val taskName = "hello" + + @TempDir + lateinit var testProjectDir: Path + + private val props = generateSequence(Paths.get(javaClass.protectionDomain.codeSource.location.path)) { + val props = it.resolve("gradle.properties") + if (Files.exists(props)) props else it.parent + } + .dropWhile { Files.isDirectory(it) } + .take(1) + .iterator() + .next() + .inputStream() + .use { + Properties().apply { load(it) } + } + + @BeforeEach + fun beforeEach() { + buildFile = Files.createFile(testProjectDir.resolve("build.gradle.kts")) + Files.newBufferedWriter(buildFile, CREATE, WRITE, TRUNCATE_EXISTING).use { + it.write( + """ + import ${Thread::class.qualifiedName} + import ${BuildTimeTrackerPluginExtension::class.qualifiedName} + import ${Output::class.qualifiedName} + import ${Duration::class.qualifiedName} + import ${Paths::class.qualifiedName} + + plugins { + id("${props.getProperty("pluginId")}") + } + + tasks.register("$taskName") { + doLast { + Thread.sleep(200) + println("Hello, World!") + } + } + """.trimIndent() + ) + it.newLine() + } + } + + @Test + fun testConsoleOutput() { + Files.newBufferedWriter(buildFile, APPEND).use { + it.write( + """ + configure<${BuildTimeTrackerPluginExtension::class.simpleName}> { + minTaskDuration = Duration.ofMillis(100) + } + """.trimIndent() + ) + } + + println(buildFile.readText()) + + val result = run() + + assertThat(result.task(taskName)?.outcome == SUCCESS) + val lines = result.output + .lines() + .filter { it.isNotEmpty() } + assertThat(lines).hasSizeGreaterThanOrEqualTo(4) + assertThat(lines[0]).isEqualTo("> Task :$taskName") + assertThat(lines[1]).isEqualTo("Hello, World!") + assertThat(lines[2]).isEqualTo("== Build time summary ==") + assertThat(lines[3]).isEqualTo(":$taskName | 0S | 0% | ") + } + + @Test + fun testCsvOutput() { + val csvFilePath = testProjectDir.resolve(BuildTimeTrackerPluginExtension().csvFilePath) + + Files.newBufferedWriter(buildFile, APPEND).use { + it.write( + """ + configure<${BuildTimeTrackerPluginExtension::class.simpleName}> { + minTaskDuration = Duration.ofMillis(100) + output = Output.CSV + csvFilePath = Paths.get("${csvFilePath.absolutePathString()}") + } + """.trimIndent() + ) + } + + println(buildFile.readText()) + + val result = run() + + assertThat(result.task(taskName)?.outcome == SUCCESS) + assertThat(Files.exists(csvFilePath)).isTrue + val lines = csvFilePath.readLines() + assertThat(lines).hasSize(1) + assertThat(lines.first()).isEqualTo(":$taskName,0S,0%,") + } + + private fun run(): BuildResult { + return GradleRunner.create() + .withProjectDir(testProjectDir.toFile()) + .withArguments(taskName, "--warning-mode=all", "--stacktrace") + .withPluginClasspath() + .withDebug(false) + .forwardOutput() + .build() + } +} diff --git a/src/test/kotlin/com/asarkar/gradle/buildtimetracker/PrinterTest.kt b/src/test/kotlin/com/asarkar/gradle/buildtimetracker/PrinterTest.kt new file mode 100644 index 0000000..7802583 --- /dev/null +++ b/src/test/kotlin/com/asarkar/gradle/buildtimetracker/PrinterTest.kt @@ -0,0 +1,334 @@ +package com.asarkar.gradle.buildtimetracker + +import com.asarkar.gradle.buildtimetracker.Printer.Companion.BLOCK_CHAR +import com.asarkar.gradle.buildtimetracker.Printer.Companion.format +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.fail +import org.junit.jupiter.api.io.TempDir +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.EnumSource +import org.junit.jupiter.params.provider.MethodSource +import java.io.ByteArrayOutputStream +import java.io.PrintStream +import java.nio.file.Files +import java.nio.file.Path +import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.readLines + +private class PrinterWrapper(val output: Output) { + private val out = ByteArrayOutputStream() + val delegate = when (output) { + Output.CONSOLE -> ConsolePrinter(PrintStream(out)) + Output.CSV -> CsvPrinter(PrintStream(out)) + } + + fun lines(): List { + val list = out.use { baos -> + String(baos.toByteArray()) + .lines() + .filter { it.isNotEmpty() } + } + + if (output == Output.CONSOLE) { + return list.drop(1) + } + return list + } +} + +private data class Line(val task: String, val duration: String, val percent: String, val bar: String) + +private fun String.toLine(barPosition: BarPosition, delimiter: String): Line { + val tokens = this.split(delimiter) + .map { it.trim() } + .takeIf { it.size == 4 } ?: fail("Unexpected line format: $this") + return if (barPosition == BarPosition.TRAILING) { + Line(tokens[0], tokens[1], tokens[2], tokens[3]) + } else { + Line(tokens[1], tokens[2], tokens[3], tokens[0]) + } +} + +class PrinterTest { + private lateinit var ext: BuildTimeTrackerPluginExtension + private lateinit var printerWrapper: PrinterWrapper + + @BeforeEach + fun beforeEach() { + ext = BuildTimeTrackerPluginExtension() + } + + @AfterEach + fun afterEach() { + if (this::printerWrapper.isInitialized) { + printerWrapper.delegate.close() + } + } + + private val taskDurations = listOf( + ":commons:extractIncludeProto" to 4L, + ":commons:compileKotlin" to 2L, + ":commons:compileJava" to 6L, + ":service-client:compileKotlin" to 1L, + ":webapp:compileKotlin" to 1L, + ":webapp:dockerBuildImage" to 4L, + ":webapp:dockerPushImage" to 4L + ) + + @ParameterizedTest + @EnumSource(Output::class) + fun testPrintDefault(output: Output) { + printerWrapper = PrinterWrapper(output) + printerWrapper.delegate.print( + PrinterInput( + 28L, + taskDurations, + ext + ) + ) + + val lines = printerWrapper.lines() + val line = lines + .first() + .toLine(ext.barPosition, printerWrapper.delegate.delimiter) + assertThat(line.task).isEqualTo(taskDurations[0].first) + assertThat(line.duration).isEqualTo("4S") + assertThat(line.percent).isEqualTo("14%") + assertThat(line.bar.toCharArray().all { it == BLOCK_CHAR }).isTrue + + lines + .drop(1) + .withIndex() + .forEach { + assertThat(it.value.startsWith(taskDurations[it.index].first) && it.value.endsWith(BLOCK_CHAR)) + } + } + + @ParameterizedTest + @EnumSource(Output::class) + fun testPrintLeadingBar(output: Output) { + printerWrapper = PrinterWrapper(output) + printerWrapper.delegate.print( + PrinterInput( + 28L, + taskDurations, + ext.apply { barPosition = BarPosition.LEADING } + ) + ) + + val lines = printerWrapper.lines() + val line = lines + .first() + .toLine(ext.barPosition, printerWrapper.delegate.delimiter) + assertThat(line.bar.toCharArray().all { it == BLOCK_CHAR }).isTrue + assertThat(line.duration).isEqualTo("4S") + assertThat(line.percent).isEqualTo("14%") + assertThat(line.task).isEqualTo(taskDurations[0].first) + + lines + .drop(1) + .withIndex() + .forEach { + assertThat(it.value.endsWith(taskDurations[it.index].first) && it.value.startsWith(BLOCK_CHAR)) + } + } + + @ParameterizedTest + @EnumSource(Output::class) + fun testPrintScaled(output: Output) { + printerWrapper = PrinterWrapper(output) + printerWrapper.delegate.print( + PrinterInput( + 28L, + taskDurations, + ext.apply { maxWidth = 5 } + ) + ) + + printerWrapper.lines() + .forEach { + val line = it.toLine(ext.barPosition, printerWrapper.delegate.delimiter) + assertThat(line.bar.length <= ext.maxWidth) + } + } + + @ParameterizedTest + @EnumSource(Output::class) + // https://github.com/asarkar/build-time-tracker/issues/1 + fun testPrintHideBars(output: Output) { + printerWrapper = PrinterWrapper(output) + printerWrapper.delegate.print( + PrinterInput( + 28L, + taskDurations, + ext.apply { showBars = false } + ) + ) + + printerWrapper.lines() + .forEach { + assertThat(it).doesNotContain("$BLOCK_CHAR") + } + } + + @ParameterizedTest + @MethodSource("argsProvider") + // https://github.com/asarkar/build-time-tracker/issues/3 + fun testFormatting(output: Output, position: BarPosition) { + printerWrapper = PrinterWrapper(output) + printerWrapper.delegate.print( + PrinterInput( + 18L, + listOf( + ":service-client:compileKotlin" to 1L, + "webapp:test" to 13L + ), + ext.apply { barPosition = position } + ) + ) + + val lines = printerWrapper.lines() + assertThat(lines).hasSize(2) + val pattern = printerWrapper.delegate.delimiter + .trim() + .toRegex(RegexOption.LITERAL) + val firstDelimiterRanges = pattern.findAll(lines.first()) + .map { it.range.first to it.range.last } + .toList() + val secondDelimiterRanges = pattern.findAll(lines.last()) + .map { it.range.first to it.range.last } + .toList() + assertThat(firstDelimiterRanges).containsExactlyElementsOf(secondDelimiterRanges) + } + + @ParameterizedTest + @EnumSource(Output::class) + fun testFormattingHideBars(output: Output) { + printerWrapper = PrinterWrapper(output) + printerWrapper.delegate.print( + PrinterInput( + 18L, + listOf( + ":service-client:compileKotlin" to 1L, + "webapp:test" to 13L + ), + ext.apply { showBars = false } + ) + ) + + val lines = printerWrapper.lines() + assertThat(lines).hasSize(2) + val pattern = printerWrapper.delegate.delimiter + .trim() + .toRegex(RegexOption.LITERAL) + val firstDelimiterRanges = pattern.findAll(lines.first()) + .map { it.range.first to it.range.last } + .toList() + val secondDelimiterRanges = pattern.findAll(lines.last()) + .map { it.range.first to it.range.last } + .toList() + assertThat(firstDelimiterRanges).containsExactlyElementsOf(secondDelimiterRanges) + } + + @ParameterizedTest + @EnumSource(Output::class) + fun testFormatHundredPercent(output: Output) { + printerWrapper = PrinterWrapper(output) + printerWrapper.delegate.print( + PrinterInput( + 10L, + listOf( + ":service-client:compileKotlin" to 10L + ), + ext + ) + ) + + val lines = printerWrapper.lines() + assertThat(lines).hasSize(1) + assertThat(lines.last().toLine(ext.barPosition, printerWrapper.delegate.delimiter).percent) + .isEqualTo("100%") + } + + @ParameterizedTest + @MethodSource("durationProvider") + fun testDurationFormatting(duration: Long, formatted: String) { + assertThat(duration.format()).isEqualTo(formatted) + } + + @Test + fun testCreateConsolePrinter() { + Printer.newInstance(ext) + .use { + assertThat(it).isInstanceOf(ConsolePrinter::class.java) + } + } + + @OptIn(ExperimentalPathApi::class) + @Test + fun testCsvPrinter(@TempDir testProjectDir: Path) { + val testFilePath = testProjectDir.resolve(ext.csvFilePath.resolveSibling("test.csv")) + val extCopy = ext.apply { + output = Output.CSV + csvFilePath = testFilePath + } + + Printer.newInstance(extCopy).use { printer -> + assertThat(printer).isInstanceOf(CsvPrinter::class.java) + printer.print( + PrinterInput( + 28L, + taskDurations, + extCopy + ) + ) + assertThat(Files.exists(testFilePath)).isTrue + val lines = testFilePath.readLines() + val line = lines + .first() + .toLine(ext.barPosition, printer.delimiter) + assertThat(line.task).isEqualTo(taskDurations[0].first) + assertThat(line.duration).isEqualTo("4S") + assertThat(line.percent).isEqualTo("14%") + assertThat(line.bar.toCharArray().all { it == BLOCK_CHAR }).isTrue + + lines + .drop(1) + .withIndex() + .forEach { + assertThat(it.value.startsWith(taskDurations[it.index].first) && it.value.endsWith(BLOCK_CHAR)) + } + } + } + + companion object { + @JvmStatic + fun argsProvider(): List { + return Output.values() + .flatMap { out -> + BarPosition.values() + .map { pos -> + Arguments.arguments(out, pos) + } + } + } + + @JvmStatic + fun durationProvider(): List { + return listOf( + Arguments.arguments(14, "14S"), + Arguments.arguments(7200, "2H"), + Arguments.arguments(120, "2M"), + Arguments.arguments(3905, "1H5M5S"), + Arguments.arguments(3900, "1H5M"), + Arguments.arguments(61, "1M1S"), + Arguments.arguments(172800, "48H"), + Arguments.arguments(172859, "48H59S") + ) + } + } +} From 90d68d9d7c6adda04344b644223cb6ff1a23caa2 Mon Sep 17 00:00:00 2001 From: Abhijit Sarkar Date: Tue, 11 May 2021 19:31:27 -0700 Subject: [PATCH 2/2] Resolve relative csvFilePath relative to project --- README.md | 8 +- gradle.properties | 2 +- .../BuildTimeTrackerPlugin.kt | 28 +--- .../BuildTimeTrackerPluginExtension.kt | 36 ++++ .../gradle/buildtimetracker/Constants.kt | 7 + .../gradle/buildtimetracker/Printer.kt | 34 ++-- .../gradle/buildtimetracker/TimingRecorder.kt | 17 +- .../BuildTimeTrackerPluginFunctionalTest.kt | 80 +++++++-- .../gradle/buildtimetracker/PrinterTest.kt | 154 ++++++------------ 9 files changed, 203 insertions(+), 163 deletions(-) create mode 100644 src/main/kotlin/com/asarkar/gradle/buildtimetracker/BuildTimeTrackerPluginExtension.kt diff --git a/README.md b/README.md index f925c50..8dd110e 100644 --- a/README.md +++ b/README.md @@ -21,19 +21,19 @@ instructions. If you are the fiddling type, you can customize the plugin as follows: ``` -import com.asarkar.gradle.buildtimetracker.BuildTimeTrackerPluginExtension -// bunch of code -configure { // or buildTimeTracker {...}, for Groovy +buildTimeTracker { barPosition = TRAILING or LEADING, default is TRAILING sort = false or true, default is false output = CONSOLE or CSV, default is CONSOLE maxWidth = 120, default is 80 minTaskDuration = Duration.ofSeconds(1), don't show tasks that take less than a second to execute showBars = false or true, default is true - csvFilePath = /path/to/csv, only relevant if output = CSV, default build/reports/buildTimeTracker/build.csv + reportsDir = only relevant if output = CSV, default $buildDir/reports/buildTimeTracker } ``` +> If you are using Kotlin build script, set the configuration properties using `property.set()` method. + :information_source: Due to a [Gradle limitation](https://docs.gradle.org/6.5.1/userguide/upgrading_version_5.html#apis_buildlistener_buildstarted_and_gradle_buildstarted_have_been_deprecated) , the build duration can't be calculated precisely. The bars and percentages are rounded off such that the output diff --git a/gradle.properties b/gradle.properties index fce1007..6d12581 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ pluginImplementationClass = com.asarkar.gradle.buildtimetracker.BuildTimeTracker pluginDeclarationName = buildTimeTrackerPlugin projectGroup = com.asarkar.gradle -projectVersion = 3.0.0-rc +projectVersion = 3.0.0 junitVersion = latest.release assertjVersion = latest.release diff --git a/src/main/kotlin/com/asarkar/gradle/buildtimetracker/BuildTimeTrackerPlugin.kt b/src/main/kotlin/com/asarkar/gradle/buildtimetracker/BuildTimeTrackerPlugin.kt index 456c158..4fe91ba 100644 --- a/src/main/kotlin/com/asarkar/gradle/buildtimetracker/BuildTimeTrackerPlugin.kt +++ b/src/main/kotlin/com/asarkar/gradle/buildtimetracker/BuildTimeTrackerPlugin.kt @@ -6,36 +6,14 @@ import com.asarkar.gradle.buildtimetracker.Constants.PLUGIN_EXTENSION_NAME import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.plugins.ExtensionAware +import org.gradle.api.plugins.ReportingBasePlugin import org.gradle.api.reflect.TypeOf -import java.nio.file.Path -import java.nio.file.Paths -import java.time.Duration - -enum class BarPosition { - LEADING, TRAILING -} - -enum class Output { - CONSOLE, CSV -} - -open class BuildTimeTrackerPluginExtension { - var barPosition: BarPosition = BarPosition.TRAILING - var sort: Boolean = false - var output: Output = Output.CONSOLE - var maxWidth: Int = 80 - var minTaskDuration: Duration = Duration.ofSeconds(1) - var showBars: Boolean = true - var csvFilePath: Path = Paths.get("build") - .resolve("reports") - .resolve(PLUGIN_EXTENSION_NAME) - .resolve("build.csv") -} class BuildTimeTrackerPlugin : Plugin { override fun apply(project: Project) { + project.pluginManager.apply(ReportingBasePlugin::class.java) val ext = project.extensions.create( - PLUGIN_EXTENSION_NAME, BuildTimeTrackerPluginExtension::class.java + PLUGIN_EXTENSION_NAME, BuildTimeTrackerPluginExtension::class.java, project ) (ext as ExtensionAware).extensions.add( object : TypeOf>() {}, diff --git a/src/main/kotlin/com/asarkar/gradle/buildtimetracker/BuildTimeTrackerPluginExtension.kt b/src/main/kotlin/com/asarkar/gradle/buildtimetracker/BuildTimeTrackerPluginExtension.kt new file mode 100644 index 0000000..cb5c488 --- /dev/null +++ b/src/main/kotlin/com/asarkar/gradle/buildtimetracker/BuildTimeTrackerPluginExtension.kt @@ -0,0 +1,36 @@ +package com.asarkar.gradle.buildtimetracker + +import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.Property +import org.gradle.api.reporting.ReportingExtension +import java.time.Duration + +enum class BarPosition { + LEADING, TRAILING +} + +enum class Output { + CONSOLE, CSV +} + +open class BuildTimeTrackerPluginExtension(private val project: Project) { + val barPosition: Property = project.objects.property(BarPosition::class.java) + .convention(Constants.DEFAULT_BAR_POSITION) + val sort: Property = project.objects.property(Boolean::class.java) + .convention(Constants.DEFAULT_SORT) + val output: Property = project.objects.property(Output::class.java) + .convention(Constants.DEFAULT_OUTPUT) + val maxWidth: Property = project.objects.property(Int::class.java) + .convention(Constants.DEFAULT_MAX_WIDTH) + val minTaskDuration: Property = project.objects.property(Duration::class.java) + .convention(Duration.ofSeconds(Constants.DEFAULT_MIN_TASK_DURATION)) + val showBars: Property = project.objects.property(Boolean::class.java) + .convention(Constants.DEFAULT_SHOW_BARS) + val reportsDir: DirectoryProperty = project.objects.directoryProperty() + .convention(baseReportsDir.map { it.dir(Constants.PLUGIN_EXTENSION_NAME) }) + + private val baseReportsDir: DirectoryProperty + get() = project.extensions.getByType(ReportingExtension::class.java) + .baseDirectory +} diff --git a/src/main/kotlin/com/asarkar/gradle/buildtimetracker/Constants.kt b/src/main/kotlin/com/asarkar/gradle/buildtimetracker/Constants.kt index 9dbf1f1..8c8b3de 100644 --- a/src/main/kotlin/com/asarkar/gradle/buildtimetracker/Constants.kt +++ b/src/main/kotlin/com/asarkar/gradle/buildtimetracker/Constants.kt @@ -4,4 +4,11 @@ object Constants { const val PLUGIN_EXTENSION_NAME = "buildTimeTracker" const val EXTRA_EXTENSION_NAME = "extra" const val LOGGER_KEY = "logger" + val DEFAULT_BAR_POSITION = BarPosition.TRAILING + const val DEFAULT_SORT = false + val DEFAULT_OUTPUT = Output.CONSOLE + const val DEFAULT_MAX_WIDTH = 80 + const val DEFAULT_MIN_TASK_DURATION = 1L + const val DEFAULT_SHOW_BARS = true + const val CSV_FILENAME = "build.csv" } diff --git a/src/main/kotlin/com/asarkar/gradle/buildtimetracker/Printer.kt b/src/main/kotlin/com/asarkar/gradle/buildtimetracker/Printer.kt index 785d7db..bb9dbfa 100644 --- a/src/main/kotlin/com/asarkar/gradle/buildtimetracker/Printer.kt +++ b/src/main/kotlin/com/asarkar/gradle/buildtimetracker/Printer.kt @@ -1,6 +1,7 @@ package com.asarkar.gradle.buildtimetracker import java.io.Closeable +import java.io.File import java.io.PrintStream import java.nio.charset.StandardCharsets import java.nio.file.Files @@ -13,7 +14,9 @@ import kotlin.math.round data class PrinterInput( val buildDuration: Long, val taskDurations: List>, - val ext: BuildTimeTrackerPluginExtension + val maxWidth: Int, + val showBars: Boolean, + val barPosition: BarPosition ) interface Printer : Closeable { @@ -27,7 +30,7 @@ interface Printer : Closeable { } // scale the values to max column width so that the corresponding bars don't shoot out of the screen - val scalingFraction = minOf(input.ext.maxWidth.toLong(), maxDuration) / maxDuration.toDouble() + val scalingFraction = minOf(input.maxWidth.toLong(), maxDuration) / maxDuration.toDouble() val maxNumBlocks = round(maxDuration * scalingFraction).toInt() val maxFormattedPercentLen = maxDuration.percentOf(input.buildDuration) .format() @@ -42,9 +45,9 @@ interface Printer : Closeable { it.first, delimiter, it.second.format(), delimiter, percent.format() ) - if (!input.ext.showBars) { + if (!input.showBars) { out.println(common) - } else if (input.ext.barPosition == BarPosition.TRAILING) { + } else if (input.barPosition == BarPosition.TRAILING) { out.printf("%s%s%s\n", common, delimiter, "$BLOCK_CHAR".repeat(numBlocks)) } else { out.printf("%${maxNumBlocks}s%s%s\n", "$BLOCK_CHAR".repeat(numBlocks), delimiter, common) @@ -68,19 +71,24 @@ interface Printer : Closeable { private fun Int.format(): String = String.format("%d%%", this) fun newInstance(ext: BuildTimeTrackerPluginExtension): Printer { - return when (ext.output) { + return when (ext.output.get()) { Output.CONSOLE -> ConsolePrinter() Output.CSV -> { - ext.csvFilePath.parent.toFile().mkdirs() - CsvPrinter( - PrintStream( - Files.newOutputStream(ext.csvFilePath, CREATE, WRITE, TRUNCATE_EXISTING), - false, - StandardCharsets.UTF_8.name() - ) - ) + val csvFile = ext.reportsDir.get() + .file(Constants.CSV_FILENAME) + .asFile + CsvPrinter(newOutputStream(csvFile)) } } } + + internal fun newOutputStream(csvFile: File): PrintStream { + csvFile.parentFile.mkdirs() + return PrintStream( + Files.newOutputStream(csvFile.toPath(), CREATE, WRITE, TRUNCATE_EXISTING), + false, + StandardCharsets.UTF_8.name() + ) + } } } diff --git a/src/main/kotlin/com/asarkar/gradle/buildtimetracker/TimingRecorder.kt b/src/main/kotlin/com/asarkar/gradle/buildtimetracker/TimingRecorder.kt index bfcf490..2cc1f0f 100644 --- a/src/main/kotlin/com/asarkar/gradle/buildtimetracker/TimingRecorder.kt +++ b/src/main/kotlin/com/asarkar/gradle/buildtimetracker/TimingRecorder.kt @@ -12,7 +12,7 @@ import org.gradle.api.tasks.TaskState import java.time.Duration import java.time.Instant -class TimingRecorder(val ext: BuildTimeTrackerPluginExtension) : TaskExecutionListener, BuildAdapter() { +class TimingRecorder(private val ext: BuildTimeTrackerPluginExtension) : TaskExecutionListener, BuildAdapter() { private lateinit var taskStarted: Instant private lateinit var buildStarted: Instant private val taskDurations = mutableListOf>() @@ -23,7 +23,7 @@ class TimingRecorder(val ext: BuildTimeTrackerPluginExtension) : TaskExecutionLi override fun afterExecute(task: Task, state: TaskState) { val duration = Duration.between(taskStarted, Instant.now()).seconds - if (duration >= ext.minTaskDuration.seconds) { + if (duration >= ext.minTaskDuration.get().seconds) { taskDurations.add(task.path to duration) } } @@ -35,17 +35,24 @@ class TimingRecorder(val ext: BuildTimeTrackerPluginExtension) : TaskExecutionLi ) (extra[Constants.LOGGER_KEY] as Logger).lifecycle( "All tasks completed within the minimum threshold: {}s, no build summary to show", - ext.minTaskDuration.seconds + ext.minTaskDuration.get().seconds ) return } val buildDuration = Duration.between(buildStarted, Instant.now()).seconds - if (ext.sort) { + if (ext.sort.get()) { taskDurations.sortBy { -it.second } } Printer.newInstance(ext) .use { - it.print(PrinterInput(buildDuration, taskDurations, ext)) + val input = PrinterInput( + buildDuration, + taskDurations, + ext.maxWidth.get(), + ext.showBars.get(), + ext.barPosition.get() + ) + it.print(input) } } diff --git a/src/test/kotlin/com/asarkar/gradle/buildtimetracker/BuildTimeTrackerPluginFunctionalTest.kt b/src/test/kotlin/com/asarkar/gradle/buildtimetracker/BuildTimeTrackerPluginFunctionalTest.kt index 4812d09..6a12eb6 100644 --- a/src/test/kotlin/com/asarkar/gradle/buildtimetracker/BuildTimeTrackerPluginFunctionalTest.kt +++ b/src/test/kotlin/com/asarkar/gradle/buildtimetracker/BuildTimeTrackerPluginFunctionalTest.kt @@ -4,7 +4,6 @@ import org.assertj.core.api.Assertions.assertThat import org.gradle.testkit.runner.BuildResult import org.gradle.testkit.runner.GradleRunner import org.gradle.testkit.runner.TaskOutcome.SUCCESS -import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import java.nio.file.Files @@ -43,17 +42,14 @@ class BuildTimeTrackerPluginFunctionalTest { Properties().apply { load(it) } } - @BeforeEach - fun beforeEach() { - buildFile = Files.createFile(testProjectDir.resolve("build.gradle.kts")) + private fun newBuildFile(name: String) { + buildFile = Files.createFile(testProjectDir.resolve(name)) Files.newBufferedWriter(buildFile, CREATE, WRITE, TRUNCATE_EXISTING).use { it.write( """ import ${Thread::class.qualifiedName} - import ${BuildTimeTrackerPluginExtension::class.qualifiedName} import ${Output::class.qualifiedName} import ${Duration::class.qualifiedName} - import ${Paths::class.qualifiedName} plugins { id("${props.getProperty("pluginId")}") @@ -72,11 +68,40 @@ class BuildTimeTrackerPluginFunctionalTest { } @Test - fun testConsoleOutput() { + fun testConsoleOutputKotlin() { + newBuildFile("build.gradle.kts") Files.newBufferedWriter(buildFile, APPEND).use { it.write( """ - configure<${BuildTimeTrackerPluginExtension::class.simpleName}> { + ${Constants.PLUGIN_EXTENSION_NAME} { + minTaskDuration.set(Duration.ofMillis(100)) + } + """.trimIndent() + ) + } + + println(buildFile.readText()) + + val result = run() + + assertThat(result.task(taskName)?.outcome == SUCCESS) + val lines = result.output + .lines() + .filter { it.isNotEmpty() } + assertThat(lines).hasSizeGreaterThanOrEqualTo(4) + assertThat(lines[0]).isEqualTo("> Task :$taskName") + assertThat(lines[1]).isEqualTo("Hello, World!") + assertThat(lines[2]).isEqualTo("== Build time summary ==") + assertThat(lines[3]).isEqualTo(":$taskName | 0S | 0% | ") + } + + @Test + fun testConsoleOutputGroovy() { + newBuildFile("build.gradle") + Files.newBufferedWriter(buildFile, APPEND).use { + it.write( + """ + ${Constants.PLUGIN_EXTENSION_NAME} { minTaskDuration = Duration.ofMillis(100) } """.trimIndent() @@ -99,16 +124,41 @@ class BuildTimeTrackerPluginFunctionalTest { } @Test - fun testCsvOutput() { - val csvFilePath = testProjectDir.resolve(BuildTimeTrackerPluginExtension().csvFilePath) + fun testCsvOutputKotlin() { + newBuildFile("build.gradle.kts") + Files.newBufferedWriter(buildFile, APPEND).use { + it.write( + """ + ${Constants.PLUGIN_EXTENSION_NAME} { + minTaskDuration.set(Duration.ofMillis(100)) + output.set(Output.CSV) + reportsDir.set(file("${testProjectDir.absolutePathString()}")) + } + """.trimIndent() + ) + } + println(buildFile.readText()) + + val result = run() + val csvFile = testProjectDir.resolve(Constants.CSV_FILENAME) + assertThat(result.task(taskName)?.outcome == SUCCESS) + assertThat(Files.exists(csvFile)).isTrue + val lines = csvFile.readLines() + assertThat(lines).hasSize(1) + assertThat(lines.first()).isEqualTo(":$taskName,0S,0%,") + } + + @Test + fun testCsvOutputGroovy() { + newBuildFile("build.gradle") Files.newBufferedWriter(buildFile, APPEND).use { it.write( """ - configure<${BuildTimeTrackerPluginExtension::class.simpleName}> { + ${Constants.PLUGIN_EXTENSION_NAME} { minTaskDuration = Duration.ofMillis(100) output = Output.CSV - csvFilePath = Paths.get("${csvFilePath.absolutePathString()}") + reportsDir = file("${testProjectDir.absolutePathString()}") } """.trimIndent() ) @@ -117,10 +167,10 @@ class BuildTimeTrackerPluginFunctionalTest { println(buildFile.readText()) val result = run() - + val csvFile = testProjectDir.resolve(Constants.CSV_FILENAME) assertThat(result.task(taskName)?.outcome == SUCCESS) - assertThat(Files.exists(csvFilePath)).isTrue - val lines = csvFilePath.readLines() + assertThat(Files.exists(csvFile)).isTrue + val lines = csvFile.readLines() assertThat(lines).hasSize(1) assertThat(lines.first()).isEqualTo(":$taskName,0S,0%,") } diff --git a/src/test/kotlin/com/asarkar/gradle/buildtimetracker/PrinterTest.kt b/src/test/kotlin/com/asarkar/gradle/buildtimetracker/PrinterTest.kt index 7802583..2c74c9d 100644 --- a/src/test/kotlin/com/asarkar/gradle/buildtimetracker/PrinterTest.kt +++ b/src/test/kotlin/com/asarkar/gradle/buildtimetracker/PrinterTest.kt @@ -4,7 +4,6 @@ import com.asarkar.gradle.buildtimetracker.Printer.Companion.BLOCK_CHAR import com.asarkar.gradle.buildtimetracker.Printer.Companion.format import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.fail import org.junit.jupiter.api.io.TempDir @@ -54,21 +53,6 @@ private fun String.toLine(barPosition: BarPosition, delimiter: String): Line { } class PrinterTest { - private lateinit var ext: BuildTimeTrackerPluginExtension - private lateinit var printerWrapper: PrinterWrapper - - @BeforeEach - fun beforeEach() { - ext = BuildTimeTrackerPluginExtension() - } - - @AfterEach - fun afterEach() { - if (this::printerWrapper.isInitialized) { - printerWrapper.delegate.close() - } - } - private val taskDurations = listOf( ":commons:extractIncludeProto" to 4L, ":commons:compileKotlin" to 2L, @@ -79,22 +63,32 @@ class PrinterTest { ":webapp:dockerPushImage" to 4L ) + private val defaultInput = PrinterInput( + 28L, + taskDurations, + Constants.DEFAULT_MAX_WIDTH, + Constants.DEFAULT_SHOW_BARS, + Constants.DEFAULT_BAR_POSITION + ) + private lateinit var printerWrapper: PrinterWrapper + + @AfterEach + fun afterEach() { + if (this::printerWrapper.isInitialized) { + printerWrapper.delegate.close() + } + } + @ParameterizedTest @EnumSource(Output::class) fun testPrintDefault(output: Output) { printerWrapper = PrinterWrapper(output) - printerWrapper.delegate.print( - PrinterInput( - 28L, - taskDurations, - ext - ) - ) + printerWrapper.delegate.print(defaultInput) val lines = printerWrapper.lines() val line = lines .first() - .toLine(ext.barPosition, printerWrapper.delegate.delimiter) + .toLine(defaultInput.barPosition, printerWrapper.delegate.delimiter) assertThat(line.task).isEqualTo(taskDurations[0].first) assertThat(line.duration).isEqualTo("4S") assertThat(line.percent).isEqualTo("14%") @@ -112,18 +106,13 @@ class PrinterTest { @EnumSource(Output::class) fun testPrintLeadingBar(output: Output) { printerWrapper = PrinterWrapper(output) - printerWrapper.delegate.print( - PrinterInput( - 28L, - taskDurations, - ext.apply { barPosition = BarPosition.LEADING } - ) - ) + val input = defaultInput.copy(barPosition = BarPosition.LEADING) + printerWrapper.delegate.print(input) val lines = printerWrapper.lines() val line = lines .first() - .toLine(ext.barPosition, printerWrapper.delegate.delimiter) + .toLine(input.barPosition, printerWrapper.delegate.delimiter) assertThat(line.bar.toCharArray().all { it == BLOCK_CHAR }).isTrue assertThat(line.duration).isEqualTo("4S") assertThat(line.percent).isEqualTo("14%") @@ -141,18 +130,13 @@ class PrinterTest { @EnumSource(Output::class) fun testPrintScaled(output: Output) { printerWrapper = PrinterWrapper(output) - printerWrapper.delegate.print( - PrinterInput( - 28L, - taskDurations, - ext.apply { maxWidth = 5 } - ) - ) + val input = defaultInput.copy(maxWidth = 5) + printerWrapper.delegate.print(input) printerWrapper.lines() .forEach { - val line = it.toLine(ext.barPosition, printerWrapper.delegate.delimiter) - assertThat(line.bar.length <= ext.maxWidth) + val line = it.toLine(input.barPosition, printerWrapper.delegate.delimiter) + assertThat(line.bar.length <= input.maxWidth) } } @@ -161,13 +145,8 @@ class PrinterTest { // https://github.com/asarkar/build-time-tracker/issues/1 fun testPrintHideBars(output: Output) { printerWrapper = PrinterWrapper(output) - printerWrapper.delegate.print( - PrinterInput( - 28L, - taskDurations, - ext.apply { showBars = false } - ) - ) + val input = defaultInput.copy(showBars = false) + printerWrapper.delegate.print(input) printerWrapper.lines() .forEach { @@ -180,16 +159,14 @@ class PrinterTest { // https://github.com/asarkar/build-time-tracker/issues/3 fun testFormatting(output: Output, position: BarPosition) { printerWrapper = PrinterWrapper(output) - printerWrapper.delegate.print( - PrinterInput( - 18L, - listOf( - ":service-client:compileKotlin" to 1L, - "webapp:test" to 13L - ), - ext.apply { barPosition = position } - ) + val input = defaultInput.copy( + taskDurations = listOf( + ":service-client:compileKotlin" to 1L, + "webapp:test" to 13L + ), + barPosition = position ) + printerWrapper.delegate.print(input) val lines = printerWrapper.lines() assertThat(lines).hasSize(2) @@ -209,16 +186,14 @@ class PrinterTest { @EnumSource(Output::class) fun testFormattingHideBars(output: Output) { printerWrapper = PrinterWrapper(output) - printerWrapper.delegate.print( - PrinterInput( - 18L, - listOf( - ":service-client:compileKotlin" to 1L, - "webapp:test" to 13L - ), - ext.apply { showBars = false } - ) + val input = defaultInput.copy( + taskDurations = listOf( + ":service-client:compileKotlin" to 1L, + "webapp:test" to 13L + ), + showBars = false ) + printerWrapper.delegate.print(input) val lines = printerWrapper.lines() assertThat(lines).hasSize(2) @@ -238,19 +213,16 @@ class PrinterTest { @EnumSource(Output::class) fun testFormatHundredPercent(output: Output) { printerWrapper = PrinterWrapper(output) - printerWrapper.delegate.print( - PrinterInput( - 10L, - listOf( - ":service-client:compileKotlin" to 10L - ), - ext + val input = defaultInput.copy( + buildDuration = 10L, + taskDurations = listOf( + ":service-client:compileKotlin" to 10L ) ) - + printerWrapper.delegate.print(input) val lines = printerWrapper.lines() assertThat(lines).hasSize(1) - assertThat(lines.last().toLine(ext.barPosition, printerWrapper.delegate.delimiter).percent) + assertThat(lines.last().toLine(input.barPosition, printerWrapper.delegate.delimiter).percent) .isEqualTo("100%") } @@ -260,37 +232,19 @@ class PrinterTest { assertThat(duration.format()).isEqualTo(formatted) } - @Test - fun testCreateConsolePrinter() { - Printer.newInstance(ext) - .use { - assertThat(it).isInstanceOf(ConsolePrinter::class.java) - } - } - @OptIn(ExperimentalPathApi::class) @Test fun testCsvPrinter(@TempDir testProjectDir: Path) { - val testFilePath = testProjectDir.resolve(ext.csvFilePath.resolveSibling("test.csv")) - val extCopy = ext.apply { - output = Output.CSV - csvFilePath = testFilePath - } + val csvFile = testProjectDir.resolve(Constants.CSV_FILENAME) + val csvPrinter = CsvPrinter(Printer.newOutputStream(csvFile.toFile())) - Printer.newInstance(extCopy).use { printer -> - assertThat(printer).isInstanceOf(CsvPrinter::class.java) - printer.print( - PrinterInput( - 28L, - taskDurations, - extCopy - ) - ) - assertThat(Files.exists(testFilePath)).isTrue - val lines = testFilePath.readLines() + csvPrinter.use { printer -> + printer.print(defaultInput) + assertThat(Files.exists(csvFile)).isTrue + val lines = csvFile.readLines() val line = lines .first() - .toLine(ext.barPosition, printer.delimiter) + .toLine(defaultInput.barPosition, printer.delimiter) assertThat(line.task).isEqualTo(taskDurations[0].first) assertThat(line.duration).isEqualTo("4S") assertThat(line.percent).isEqualTo("14%")