├── src ├── test │ ├── resources │ │ ├── empty.csv │ │ ├── timesWithHeaders.csv │ │ └── times.csv │ └── groovy │ │ └── net │ │ └── rdrei │ │ └── android │ │ └── buildtimetracker │ │ ├── FormattingUtilsTest.groovy │ │ ├── TaskStateBuilder.groovy │ │ ├── PluginTest.groovy │ │ ├── TimingRecorderTest.groovy │ │ └── reporters │ │ ├── CSVSummaryReporterTest.groovy │ │ ├── JSONReporterTest.groovy │ │ ├── SummaryReporterTest.groovy │ │ └── CSVReporterTest.groovy └── main │ ├── resources │ └── META-INF │ │ └── gradle-plugins │ │ └── build-time-tracker.properties │ └── groovy │ └── net │ └── rdrei │ └── android │ └── buildtimetracker │ ├── reporters │ ├── TrueTimeProvider.groovy │ ├── DateUtils.groovy │ ├── MemoryUtil.java │ ├── AbstractBuildTimeTrackerReporter.groovy │ ├── FormattingUtils.groovy │ ├── SysInfo.groovy │ ├── TerminalInfo.groovy │ ├── ReporterConfigurationError.groovy │ ├── JSONReporter.groovy │ ├── CSVReporter.groovy │ ├── CSVSummaryReporter.groovy │ └── SummaryReporter.groovy │ ├── util │ └── Clock.groovy │ ├── BuildAndTaskExecutionListenerAdapter.groovy │ ├── TimingRecorder.groovy │ └── BuildTimeTrackerPlugin.groovy ├── assets ├── logo.png ├── screenshot.png └── logo.svg ├── AUTHORS ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties.example ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── report.Rmd ├── gradlew ├── README.markdown └── LICENSE /src/test/resources/empty.csv: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/passy/build-time-tracker-plugin/HEAD/assets/logo.png -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Adam Dougal 2 | Daithi O Crualaoich 3 | Kasper Kondzielski 4 | Pascal Hartig 5 | Sindre Sorhus 6 | -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/passy/build-time-tracker-plugin/HEAD/assets/screenshot.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/passy/build-time-tracker-plugin/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle.properties.example: -------------------------------------------------------------------------------- 1 | signing.keyId=yourkey 2 | signing.password=yoursecret 3 | signing.secretKeyRingFile=/home/you/.gnupg/secring.gpg 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | local.properties 3 | .DS_Store 4 | .idea 5 | build 6 | *.iml 7 | classes 8 | /times.csv 9 | .Rhistory 10 | report.html 11 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/gradle-plugins/build-time-tracker.properties: -------------------------------------------------------------------------------- 1 | implementation-class=net.rdrei.android.buildtimetracker.BuildTimeTrackerPlugin 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: groovy 2 | 3 | jdk: 4 | - oraclejdk8 5 | 6 | env: 7 | global: 8 | - TERM=dumb 9 | 10 | notifications: 11 | email: false 12 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=http\://services.gradle.org/distributions/gradle-4.2-all.zip 6 | -------------------------------------------------------------------------------- /src/main/groovy/net/rdrei/android/buildtimetracker/reporters/TrueTimeProvider.groovy: -------------------------------------------------------------------------------- 1 | package net.rdrei.android.buildtimetracker.reporters; 2 | 3 | public class TrueTimeProvider { 4 | public long getCurrentTime() { 5 | return System.currentTimeMillis(); 6 | } 7 | } -------------------------------------------------------------------------------- /src/main/groovy/net/rdrei/android/buildtimetracker/reporters/DateUtils.groovy: -------------------------------------------------------------------------------- 1 | package net.rdrei.android.buildtimetracker.reporters; 2 | 3 | import org.joda.time.DateTime; 4 | import org.joda.time.DateTimeZone; 5 | 6 | public class DateUtils { 7 | public DateUtils() {} 8 | 9 | long getLocalMidnightUTCTimestamp() { 10 | DateTime.now().withTime(0, 0, 0, 0).withZone(DateTimeZone.UTC).getMillis() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/groovy/net/rdrei/android/buildtimetracker/util/Clock.groovy: -------------------------------------------------------------------------------- 1 | package net.rdrei.android.buildtimetracker.util 2 | 3 | class Clock { 4 | long startTimeInMs 5 | 6 | Clock() { 7 | this(System.currentTimeMillis()) 8 | } 9 | 10 | Clock(long startTimeInMs) { 11 | this.startTimeInMs = startTimeInMs 12 | } 13 | 14 | long getTimeInMs() { 15 | return System.currentTimeMillis() - startTimeInMs 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/groovy/net/rdrei/android/buildtimetracker/reporters/MemoryUtil.java: -------------------------------------------------------------------------------- 1 | package net.rdrei.android.buildtimetracker.reporters; 2 | 3 | /** 4 | * This hackery doesn't work in Groovy, so we have to 5 | * implement this in Java. 6 | */ 7 | public class MemoryUtil { 8 | public static long getPhysicalMemoryAvailable() { 9 | com.sun.management.OperatingSystemMXBean bean = 10 | (com.sun.management.OperatingSystemMXBean) 11 | java.lang.management.ManagementFactory.getOperatingSystemMXBean(); 12 | return bean.getTotalPhysicalMemorySize(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/groovy/net/rdrei/android/buildtimetracker/FormattingUtilsTest.groovy: -------------------------------------------------------------------------------- 1 | package net.rdrei.android.buildtimetracker 2 | 3 | import net.rdrei.android.buildtimetracker.reporters.FormattingUtils 4 | import org.junit.Test 5 | 6 | import static org.junit.Assert.* 7 | 8 | class FormattingUtilsTest { 9 | @Test 10 | void testMinutesFormatting() { 11 | assertEquals "1:57.006", FormattingUtils.formatDuration(117006) 12 | } 13 | 14 | @Test 15 | void testHoursFormatting() { 16 | assertEquals "13:37:03", FormattingUtils.formatDuration(49023001) 17 | } 18 | 19 | @Test 20 | void testHoursZeroFill() { 21 | assertEquals "3:07:03", FormattingUtils.formatDuration(11223123) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/groovy/net/rdrei/android/buildtimetracker/reporters/AbstractBuildTimeTrackerReporter.groovy: -------------------------------------------------------------------------------- 1 | package net.rdrei.android.buildtimetracker.reporters 2 | 3 | import net.rdrei.android.buildtimetracker.Timing 4 | import org.gradle.BuildResult 5 | import org.gradle.api.logging.Logger 6 | 7 | abstract class AbstractBuildTimeTrackerReporter { 8 | Map options 9 | Logger logger 10 | 11 | AbstractBuildTimeTrackerReporter(Map options, Logger logger) { 12 | this.options = options 13 | this.logger = logger 14 | } 15 | 16 | abstract run(List timings) 17 | 18 | String getOption(String name, String defaultVal) { 19 | options[name] == null ? defaultVal : options[name] 20 | } 21 | 22 | void onBuildResult(BuildResult result) {} 23 | } 24 | -------------------------------------------------------------------------------- /src/main/groovy/net/rdrei/android/buildtimetracker/BuildAndTaskExecutionListenerAdapter.groovy: -------------------------------------------------------------------------------- 1 | package net.rdrei.android.buildtimetracker 2 | 3 | import org.gradle.api.Task 4 | import org.gradle.api.execution.TaskExecutionListener 5 | 6 | import org.gradle.BuildListener 7 | import org.gradle.BuildResult 8 | import org.gradle.api.initialization.Settings 9 | import org.gradle.api.invocation.Gradle 10 | import org.gradle.api.tasks.TaskState 11 | 12 | class BuildAndTaskExecutionListenerAdapter implements BuildListener, TaskExecutionListener { 13 | @Override 14 | void buildStarted(Gradle gradle) { } 15 | 16 | @Override 17 | void settingsEvaluated(Settings settings) { } 18 | 19 | @Override 20 | void projectsLoaded(Gradle gradle) { } 21 | 22 | @Override 23 | void projectsEvaluated(Gradle gradle) { } 24 | 25 | @Override 26 | void buildFinished(BuildResult buildResult) { } 27 | 28 | @Override 29 | void beforeExecute(Task task) {} 30 | 31 | @Override 32 | void afterExecute(Task task, TaskState taskState) {} 33 | } 34 | -------------------------------------------------------------------------------- /src/main/groovy/net/rdrei/android/buildtimetracker/reporters/FormattingUtils.groovy: -------------------------------------------------------------------------------- 1 | package net.rdrei.android.buildtimetracker.reporters 2 | 3 | import java.util.concurrent.TimeUnit 4 | 5 | class FormattingUtils { 6 | private FormattingUtils() {} 7 | 8 | static String formatDuration(long ms) { 9 | def hours = TimeUnit.MILLISECONDS.toHours(ms) 10 | def minutes = TimeUnit.MILLISECONDS.toMinutes(ms) - TimeUnit.HOURS.toMinutes(hours) 11 | def seconds = TimeUnit.MILLISECONDS.toSeconds(ms) - TimeUnit.MINUTES.toSeconds(minutes) - TimeUnit.HOURS.toSeconds(hours) 12 | def millis = ms - TimeUnit.MINUTES.toMillis(minutes) - TimeUnit.SECONDS.toMillis(seconds) - TimeUnit.HOURS.toMillis(hours) 13 | if (hours > 0) { 14 | String.format("%d:%02d:%02d", 15 | hours, 16 | minutes, 17 | seconds 18 | ) 19 | } else { 20 | String.format("%d:%02d.%03d", 21 | minutes, 22 | seconds, 23 | millis 24 | ) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/groovy/net/rdrei/android/buildtimetracker/reporters/SysInfo.groovy: -------------------------------------------------------------------------------- 1 | package net.rdrei.android.buildtimetracker.reporters; 2 | 3 | public class SysInfo { 4 | String getOSIdentifier() { 5 | ["os.name", "os.version", "os.arch"].collect { System.getProperty(it) }.join(" ") 6 | } 7 | 8 | long getMaxMemory() { 9 | MemoryUtil.physicalMemoryAvailable 10 | } 11 | 12 | String getCPUIdentifier() { 13 | def os = System.getProperty("os.name") 14 | if (os.equalsIgnoreCase("mac os x")) { 15 | def proc = ["sysctl", "-n", "machdep.cpu.brand_string"].execute() 16 | proc.waitFor() 17 | 18 | if (proc.exitValue() == 0) { 19 | return proc.in.text.trim() 20 | } 21 | } else if (os.equalsIgnoreCase("linux")) { 22 | def osName = "" 23 | new File("/proc/cpuinfo").eachLine { 24 | if (!osName.isEmpty()) return 25 | 26 | if (it.startsWith("model name")) { 27 | osName = it.split(": ")[1] 28 | } 29 | } 30 | return osName 31 | } 32 | 33 | return "" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/groovy/net/rdrei/android/buildtimetracker/reporters/TerminalInfo.groovy: -------------------------------------------------------------------------------- 1 | package net.rdrei.android.buildtimetracker.reporters 2 | 3 | import groovy.transform.Memoized 4 | 5 | /** 6 | * Singleton info class for determining the width of the terminal we run in. 7 | */ 8 | class TerminalInfo { 9 | @Memoized 10 | public int getWidth(int fallback) { 11 | // Start by trying to get the `COLUMNS` env variable, which is swallowed by Gradle most of the time. 12 | def cols = System.getenv("COLUMNS") 13 | if (cols != null) { 14 | return Integer.parseInt(cols, 10) 15 | } 16 | 17 | // tput requires $TERM to be set, otherwise it's going to print an error. 18 | // This unfortunately means this doesn't work in daemon mode. 19 | if (System.getenv("TERM") == null) { 20 | return fallback 21 | } 22 | 23 | // Totally unportable way of detecting the terminal width on POSIX and OS X. 24 | try { 25 | Process p = Runtime.getRuntime().exec([ "bash", "-c", "tput cols 2> /dev/tty" ] as String[]) 26 | p.waitFor() 27 | def reader = new BufferedReader(new InputStreamReader(p.getInputStream())) 28 | def line = reader.readLine()?.trim() 29 | if (line != null) Integer.valueOf(line) else fallback 30 | } catch (IOException ignored) { 31 | fallback 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/groovy/net/rdrei/android/buildtimetracker/reporters/ReporterConfigurationError.groovy: -------------------------------------------------------------------------------- 1 | package net.rdrei.android.buildtimetracker.reporters 2 | 3 | class ReporterConfigurationError extends Exception { 4 | String reporterName 5 | String optionName 6 | ErrorType errorType 7 | String details 8 | 9 | static enum ErrorType { 10 | REQUIRED, 11 | INVALID, 12 | OTHER 13 | } 14 | 15 | ReporterConfigurationError(ErrorType errorType, String reporterName, String optionName) { 16 | this(errorType, reporterName, optionName, null) 17 | } 18 | 19 | ReporterConfigurationError(ErrorType errorType, String reporterName, String optionName, 20 | String details) { 21 | super(generateMessage(errorType, reporterName, optionName, details)) 22 | this.reporterName = reporterName 23 | this.optionName = optionName 24 | this.errorType = errorType 25 | this.details = details 26 | } 27 | 28 | static String generateMessage(ErrorType errorType, String reporterName, String optionName, 29 | String details) { 30 | def msg 31 | 32 | switch (errorType) { 33 | case ErrorType.REQUIRED: 34 | msg = "$reporterName requires option $optionName to be set" 35 | if (details != null) msg += ": $details" 36 | break 37 | case ErrorType.INVALID: 38 | msg = "Option $optionName set for $reporterName is invalid" 39 | if (details != null) msg += ": $details" 40 | break 41 | default: 42 | msg = details ?: "Unknown error. Well, fuck." 43 | } 44 | 45 | msg 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/groovy/net/rdrei/android/buildtimetracker/TimingRecorder.groovy: -------------------------------------------------------------------------------- 1 | package net.rdrei.android.buildtimetracker 2 | 3 | import net.rdrei.android.buildtimetracker.util.Clock 4 | import org.gradle.BuildResult 5 | import org.gradle.api.Task 6 | import org.gradle.api.execution.TaskExecutionListener 7 | import org.gradle.api.tasks.TaskState 8 | 9 | class Timing { 10 | long ms 11 | String path 12 | boolean success 13 | boolean didWork 14 | boolean skipped 15 | 16 | Timing(long ms, String path, boolean success, boolean didWork, boolean skipped) { 17 | this.ms = ms 18 | this.path = path 19 | this.success = success 20 | this.didWork = didWork 21 | this.skipped = skipped 22 | } 23 | } 24 | 25 | class TimingRecorder extends BuildAndTaskExecutionListenerAdapter implements TaskExecutionListener { 26 | private Clock clock 27 | private List timings = [] 28 | private BuildTimeTrackerPlugin plugin 29 | 30 | TimingRecorder(BuildTimeTrackerPlugin plugin) { 31 | this.plugin = plugin 32 | } 33 | 34 | @Override 35 | void beforeExecute(Task task) { 36 | clock = new Clock() 37 | } 38 | 39 | @Override 40 | void afterExecute(Task task, TaskState taskState) { 41 | timings << new Timing( 42 | clock.getTimeInMs(), 43 | task.getPath(), 44 | taskState.getFailure() == null, 45 | taskState.getDidWork(), 46 | taskState.getSkipped() 47 | ) 48 | } 49 | 50 | @Override 51 | void buildFinished(BuildResult result) { 52 | plugin.reporters.each { it.run timings; it.onBuildResult result } 53 | } 54 | 55 | List getTasks() { 56 | timings*.path 57 | } 58 | 59 | Timing getTiming(String path) { 60 | timings.find { it.path == path } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/test/groovy/net/rdrei/android/buildtimetracker/TaskStateBuilder.groovy: -------------------------------------------------------------------------------- 1 | package net.rdrei.android.buildtimetracker 2 | 3 | import org.gradle.api.tasks.TaskState 4 | 5 | class TestTaskState implements TaskState { 6 | boolean executed = false 7 | Throwable failure = null 8 | boolean didWork = false 9 | boolean skipped = false 10 | boolean upToDate = false 11 | boolean noSource = false 12 | String skipMessage = null 13 | 14 | @Override 15 | boolean getExecuted() { 16 | executed 17 | } 18 | 19 | @Override 20 | Throwable getFailure() { 21 | failure 22 | } 23 | 24 | @Override 25 | void rethrowFailure() { 26 | throw failure 27 | } 28 | 29 | @Override 30 | boolean getDidWork() { 31 | didWork 32 | } 33 | 34 | @Override 35 | boolean getSkipped() { 36 | skipped 37 | } 38 | 39 | @Override 40 | String getSkipMessage() { 41 | skipMessage 42 | } 43 | 44 | @Override 45 | boolean getUpToDate() { 46 | upToDate 47 | } 48 | 49 | @Override 50 | boolean getNoSource() { 51 | noSource 52 | } 53 | } 54 | 55 | class TaskStateBuilder { 56 | TestTaskState state = new TestTaskState() 57 | 58 | TaskStateBuilder withExecuted(boolean executed) { 59 | state.executed = executed 60 | this 61 | } 62 | 63 | TaskStateBuilder withFailure(Throwable failure) { 64 | state.failure = failure 65 | this 66 | } 67 | 68 | TaskStateBuilder withDidWork(boolean didWork) { 69 | state.didWork = didWork 70 | this 71 | } 72 | 73 | TaskStateBuilder withSkipped(boolean skipped) { 74 | state.skipped = skipped 75 | this 76 | } 77 | 78 | TaskStateBuilder withSkipMessage(boolean skipMessage) { 79 | state.skipMessage = skipMessage 80 | this 81 | } 82 | 83 | TaskState withUpToDate(boolean upToDate) { 84 | state.upToDate = upToDate 85 | this 86 | } 87 | 88 | TaskState build() { 89 | state 90 | } 91 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Change Log 2 | ========== 3 | 4 | Version 0.11 5 | ------------ 6 | 7 | * Compatibility with Gradle 4.2. 8 | * Change logging levels. 9 | 10 | Version 0.10 11 | ------------ 12 | 13 | * Update to Gradle 3.5. 14 | 15 | 16 | Version 0.9 17 | ----------- 18 | 19 | * Add JSON reporter. [#45](https://github.com/passy/build-time-tracker-plugin/pull/72) 20 | * Provide forward-compatibility with Gradle (3.3+) through own 21 | TrueTimeProvider. [#75](https://github.com/passy/build-time-tracker-plugin/issues/75) 22 | 23 | Version 0.7 24 | ----------- 25 | 26 | * Upgrade to Gradle 2.13 27 | * Add the ability to toggle the shortening of long task names via 28 | `shortenTaskNames` [#64](https://github.com/passy/build-time-tracker-plugin/pull/64) 29 | * Up build time summary log level to info 30 | [#63](https://github.com/passy/build-time-tracker-plugin/pull/63) 31 | 32 | Version 0.6 33 | ----------- 34 | 35 | * Upgrade to Gradle 2.10 36 | 37 | Version 0.5 38 | ----------- 39 | 40 | * Upgrade to Gradle 2.6 41 | [#52](https://github.com/passy/build-time-tracker-plugin/pull/52) 42 | 43 | Version 0.4.3 44 | ------------- 45 | 46 | * Another workaround for bash -echo problems 47 | [#44](https://github.com/passy/build-time-tracker-plugin/issues/44) 48 | 49 | Version 0.4.2 50 | ------------- 51 | 52 | * Resurface build success status in SummaryReporter 53 | [#40](https://github.com/passy/build-time-tracker-plugin/issues/40) 54 | 55 | Version 0.4.1 56 | ------------- 57 | 58 | * Downgrade to JLine 2.11 to avoid terminal problems. 59 | [#41](https://github.com/passy/build-time-tracker-plugin/pull/41) 60 | 61 | Version 0.4.0 62 | ------------- 63 | 64 | * Upgrade to Gradle 2.1. ABI-compatible with pre Gradle 2.0. 65 | [#38](https://github.com/passy/build-time-tracker-plugin/pull/38) 66 | * Add `barstyle` option to SummaryReporter 67 | [#39](https://github.com/passy/build-time-tracker-plugin/pull/39) 68 | 69 | Version 0.3.0 70 | ------------- 71 | 72 | * Possibly breaking change: CSV now also logs information about CPU, memory and 73 | operating system 74 | 75 | Version 0.2.0 76 | ------------- 77 | 78 | * New: Summary reporter gives you daily and all-time stats 79 | -------------------------------------------------------------------------------- /src/main/groovy/net/rdrei/android/buildtimetracker/reporters/JSONReporter.groovy: -------------------------------------------------------------------------------- 1 | package net.rdrei.android.buildtimetracker.reporters 2 | 3 | import groovy.json.JsonBuilder 4 | import net.rdrei.android.buildtimetracker.Timing 5 | import org.gradle.api.logging.Logger 6 | 7 | import java.text.DateFormat 8 | import java.text.SimpleDateFormat 9 | 10 | class JSONReporter extends AbstractBuildTimeTrackerReporter { 11 | JSONReporter(Map options, Logger logger) { 12 | super(options, logger) 13 | } 14 | 15 | @Override 16 | def run(List timings) { 17 | long timestamp = new TrueTimeProvider().getCurrentTime() 18 | String output = getOption("output", "") 19 | boolean append = getOption("append", "false").toBoolean() 20 | TimeZone tz = TimeZone.getTimeZone("UTC") 21 | DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss,SSS'Z'") 22 | df.setTimeZone(tz) 23 | 24 | File file = new File(output) 25 | file.getParentFile()?.mkdirs() 26 | 27 | def info = new SysInfo() 28 | def osId = info.getOSIdentifier() 29 | def cpuId = info.getCPUIdentifier() 30 | def maxMem = info.getMaxMemory() 31 | def measurements = [] 32 | 33 | timings.eachWithIndex { it, index -> 34 | measurements << [ 35 | timestamp: timestamp, 36 | order: index, 37 | task: it.path, 38 | success: it.success, 39 | did_work: it.didWork, 40 | skipped: it.skipped, 41 | ms: it.ms, 42 | date: df.format(new Date(timestamp)), 43 | cpu: cpuId, 44 | memory: maxMem, 45 | os: osId, 46 | ] 47 | } 48 | 49 | def data = [ 50 | success: timings.every { it.success }, 51 | count: timings.size(), 52 | measurements: measurements, 53 | ] 54 | 55 | FileWriter writer = new FileWriter(file, append) 56 | try { 57 | writer.write(new JsonBuilder(data).toPrettyString()) 58 | writer.flush() 59 | } finally { 60 | writer.close() 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/test/resources/timesWithHeaders.csv: -------------------------------------------------------------------------------- 1 | "timestamp","order","task","success","did_work","skipped","ms","date","cpu","memory","os" 2 | "1485893856288","0",":clean","true","true","false","115","2017-01-31T20:17:36,288Z","Intel(R) Core(TM) i7-4980HQ CPU @ 2.80GHz","17179869184","Mac OS X 10.11.5 x86_64" 3 | "1485893856288","1",":compileJava","true","true","false","3061","2017-01-31T20:17:36,288Z","Intel(R) Core(TM) i7-4980HQ CPU @ 2.80GHz","17179869184","Mac OS X 10.11.5 x86_64" 4 | "1485893856288","2",":compileGroovy","true","true","false","8487","2017-01-31T20:17:36,288Z","Intel(R) Core(TM) i7-4980HQ CPU @ 2.80GHz","17179869184","Mac OS X 10.11.5 x86_64" 5 | "1485893856288","3",":modules:processResources","true","true","false","2746","2017-01-31T20:17:36,288Z","Intel(R) Core(TM) i7-4980HQ CPU @ 2.80GHz","17179869184","Mac OS X 10.11.5 x86_64" 6 | "1485893856288","4",":processTestResources","true","true","false","2375","2017-01-31T20:17:36,288Z","Intel(R) Core(TM) i7-4980HQ CPU @ 2.80GHz","17179869184","Mac OS X 10.11.5 x86_64" 7 | "1485893856288","5",":test","true","true","false","10606","2017-01-31T20:17:36,288Z","Intel(R) Core(TM) i7-4980HQ CPU @ 2.80GHz","17179869184","Mac OS X 10.11.5 x86_64" 8 | "timestamp","order","task","success","did_work","skipped","ms","date","cpu","memory","os" 9 | "1485894430576","0",":clean","true","true","false","104","2017-01-31T20:27:10,576Z","Intel(R) Core(TM) i7-4980HQ CPU @ 2.80GHz","17179869184","Mac OS X 10.11.5 x86_64" 10 | "1485894430576","1",":compileJava","true","true","false","2701","2017-01-31T20:27:10,576Z","Intel(R) Core(TM) i7-4980HQ CPU @ 2.80GHz","17179869184","Mac OS X 10.11.5 x86_64" 11 | "1485894430576","2",":compileGroovy","true","true","false","7361","2017-01-31T20:27:10,576Z","Intel(R) Core(TM) i7-4980HQ CPU @ 2.80GHz","17179869184","Mac OS X 10.11.5 x86_64" 12 | "1485894430576","3",":modules:processResources","true","true","false","2483","2017-01-31T20:27:10,576Z","Intel(R) Core(TM) i7-4980HQ CPU @ 2.80GHz","17179869184","Mac OS X 10.11.5 x86_64" 13 | "1485894430576","4",":test","true","true","false","2098","2017-01-31T20:27:10,576Z","Intel(R) Core(TM) i7-4980HQ CPU @ 2.80GHz","17179869184","Mac OS X 10.11.5 x86_64" 14 | "1485894430576","5",":modules:integration","true","true","false","8968","2017-01-31T20:27:10,576Z","Intel(R) Core(TM) i7-4980HQ CPU @ 2.80GHz","17179869184","Mac OS X 10.11.5 x86_64" -------------------------------------------------------------------------------- /src/main/groovy/net/rdrei/android/buildtimetracker/BuildTimeTrackerPlugin.groovy: -------------------------------------------------------------------------------- 1 | package net.rdrei.android.buildtimetracker 2 | 3 | import net.rdrei.android.buildtimetracker.reporters.AbstractBuildTimeTrackerReporter 4 | import net.rdrei.android.buildtimetracker.reporters.CSVSummaryReporter 5 | import net.rdrei.android.buildtimetracker.reporters.JSONReporter 6 | import net.rdrei.android.buildtimetracker.reporters.SummaryReporter 7 | import net.rdrei.android.buildtimetracker.reporters.CSVReporter 8 | import org.gradle.api.NamedDomainObjectCollection 9 | import org.gradle.api.Plugin 10 | import org.gradle.api.Project 11 | import org.gradle.api.logging.Logger 12 | 13 | class BuildTimeTrackerPlugin implements Plugin { 14 | def REPORTERS = [ 15 | summary: SummaryReporter, 16 | csv: CSVReporter, 17 | csvSummary: CSVSummaryReporter, 18 | json: JSONReporter 19 | ] 20 | Logger logger 21 | 22 | NamedDomainObjectCollection reporterExtensions 23 | 24 | @Override 25 | void apply(Project project) { 26 | this.logger = project.logger 27 | project.extensions.create("buildtimetracker", BuildTimeTrackerExtension) 28 | reporterExtensions = project.buildtimetracker.extensions.reporters = project.container(ReporterExtension) 29 | project.gradle.addBuildListener(new TimingRecorder(this)) 30 | } 31 | 32 | List getReporters() { 33 | reporterExtensions.collect { ext -> 34 | if (REPORTERS.containsKey(ext.name)) { 35 | return REPORTERS.get(ext.name).newInstance(ext.options, logger) 36 | } 37 | }.findAll { ext -> ext != null } 38 | } 39 | } 40 | 41 | class BuildTimeTrackerExtension { 42 | // Not in use at the moment. 43 | } 44 | 45 | class ReporterExtension { 46 | final String name 47 | final Map options = [:] 48 | 49 | ReporterExtension(String name) { 50 | this.name = name 51 | } 52 | 53 | @Override 54 | String toString() { 55 | return name 56 | } 57 | 58 | def methodMissing(String name, args) { 59 | // I'm feeling really, really naughty. 60 | if (args.length == 1) { 61 | options[name] = args[0].toString() 62 | } else { 63 | throw new MissingMethodException(name, this.class, args) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/groovy/net/rdrei/android/buildtimetracker/reporters/CSVReporter.groovy: -------------------------------------------------------------------------------- 1 | package net.rdrei.android.buildtimetracker.reporters 2 | 3 | import au.com.bytecode.opencsv.CSVWriter 4 | import net.rdrei.android.buildtimetracker.Timing 5 | import org.gradle.api.logging.Logger 6 | 7 | import java.text.DateFormat 8 | import java.text.SimpleDateFormat 9 | 10 | class CSVReporter extends AbstractBuildTimeTrackerReporter { 11 | CSVReporter(Map options, Logger logger) { 12 | super(options, logger) 13 | } 14 | 15 | @Override 16 | def run(List timings) { 17 | long timestamp = new TrueTimeProvider().getCurrentTime() 18 | String output = getOption("output", "") 19 | boolean append = getOption("append", "false").toBoolean() 20 | TimeZone tz = TimeZone.getTimeZone("UTC") 21 | DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss,SSS'Z'") 22 | df.setTimeZone(tz) 23 | 24 | File file = new File(output) 25 | file.getParentFile()?.mkdirs() 26 | 27 | CSVWriter writer = new CSVWriter(new BufferedWriter(new FileWriter(file, append))) 28 | 29 | try { 30 | if (getOption("header", "true").toBoolean()) { 31 | String[] headers = ["timestamp", "order", "task", "success", "did_work", "skipped", "ms", "date", 32 | "cpu", "memory", "os"] 33 | writer.writeNext(headers) 34 | } 35 | 36 | def info = new SysInfo() 37 | def osId = info.getOSIdentifier() 38 | def cpuId = info.getCPUIdentifier() 39 | def maxMem = info.getMaxMemory() 40 | 41 | timings.eachWithIndex { timing, idx -> 42 | String[] line = [ 43 | timestamp.toString(), 44 | idx.toString(), 45 | timing.path, 46 | timing.success.toString(), 47 | timing.didWork.toString(), 48 | timing.skipped.toString(), 49 | timing.ms.toString(), 50 | df.format(new Date(timestamp)), 51 | cpuId, 52 | maxMem, 53 | osId 54 | ].toArray() 55 | writer.writeNext(line) 56 | } 57 | } finally { 58 | writer.close() 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/groovy/net/rdrei/android/buildtimetracker/PluginTest.groovy: -------------------------------------------------------------------------------- 1 | package net.rdrei.android.buildtimetracker 2 | 3 | import groovy.mock.interceptor.MockFor 4 | import net.rdrei.android.buildtimetracker.reporters.CSVReporter 5 | import net.rdrei.android.buildtimetracker.reporters.SummaryReporter 6 | import net.rdrei.android.buildtimetracker.util.Clock 7 | import org.gradle.api.Project 8 | import org.gradle.testfixtures.ProjectBuilder 9 | import org.junit.Before 10 | import org.junit.Test 11 | 12 | class PluginTest { 13 | Project project 14 | 15 | MockFor mockClock(int ms) { 16 | def mockClock = new MockFor(Clock) 17 | mockClock.demand.getTimeInMs(0..1) { ms } 18 | 19 | mockClock 20 | } 21 | 22 | @Before 23 | void setUp() { 24 | project = ProjectBuilder.builder().build() 25 | } 26 | 27 | @Test 28 | void testSummaryInvocation() { 29 | def mockSummaryReporter = new MockFor(SummaryReporter) 30 | mockSummaryReporter.demand.run { timings -> 31 | assertEquals 1, timings.size 32 | assertEquals "test", timings.get(0).path 33 | assertEquals 123, timings.get(0).ms 34 | } 35 | 36 | mockClock(123).use { 37 | // TODO: This is broken until mockSummaryReporter.use verified 38 | project.apply plugin: 'build-time-tracker' 39 | project.buildtimetracker { 40 | reporters { 41 | summary {} 42 | } 43 | } 44 | } 45 | } 46 | 47 | @Test 48 | void testCSVInvocation() { 49 | def mockCSVReporter = new MockFor(CSVReporter) 50 | // TODO: Demand the constructor is called with output and header options 51 | mockCSVReporter.demand.run { timings -> 52 | assertEquals 1, timings.size 53 | assertEquals "test", timings.get(0).path 54 | assertEquals 123, timings.get(0).ms 55 | } 56 | 57 | mockClock(123).use { 58 | // TODO: This is broken until mockCSVReporter.use verified 59 | project.apply plugin: 'build-time-tracker' 60 | project.buildtimetracker { 61 | reporters { 62 | csv { 63 | output "buildtime/output.csv" 64 | header false 65 | append true 66 | } 67 | } 68 | } 69 | } 70 | } 71 | 72 | @Test 73 | void testCSVInvocationWithOutputDateVariableExpansion() { 74 | // TBD 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/groovy/net/rdrei/android/buildtimetracker/reporters/CSVSummaryReporter.groovy: -------------------------------------------------------------------------------- 1 | package net.rdrei.android.buildtimetracker.reporters 2 | 3 | import au.com.bytecode.opencsv.CSVReader 4 | import net.rdrei.android.buildtimetracker.Timing 5 | import org.gradle.api.logging.Logger 6 | import org.ocpsoft.prettytime.PrettyTime 7 | 8 | class CSVSummaryReporter extends AbstractBuildTimeTrackerReporter { 9 | DateUtils dateUtils 10 | 11 | CSVSummaryReporter(Map options, Logger logger) { 12 | super(options, logger) 13 | dateUtils = new DateUtils() 14 | } 15 | 16 | @Override 17 | def run(List timings) { 18 | def csv = getOption("csv", "") 19 | def csvFile = new File(csv) 20 | 21 | if (csv.isEmpty()) { 22 | throw new ReporterConfigurationError( 23 | ReporterConfigurationError.ErrorType.REQUIRED, 24 | this.getClass().getSimpleName(), 25 | "csv" 26 | ) 27 | } 28 | 29 | if (!csvFile.exists() || !csvFile.isFile()) { 30 | throw new ReporterConfigurationError( 31 | ReporterConfigurationError.ErrorType.INVALID, 32 | this.getClass().getSimpleName(), 33 | "csv", 34 | "$csv either doesn't exist or is not a valid file" 35 | ) 36 | } 37 | 38 | printReport(new CSVReader(new BufferedReader(new FileReader(csvFile)))) 39 | } 40 | 41 | void printReport(CSVReader reader) { 42 | try { 43 | def lines = reader.readAll() 44 | if (lines.size() == 0) return 45 | 46 | logger.lifecycle "== CSV Build Time Summary ==" 47 | 48 | Map times = lines.findAll { it[0] != 'timestamp' }.groupBy { 49 | it[0] 50 | }.collectEntries { 51 | k, v -> [Long.valueOf(k), v.collect { Long.valueOf(it[6]) }.sum()] 52 | } 53 | 54 | printToday(times) 55 | printTotal(times) 56 | } finally { 57 | reader.close() 58 | } 59 | } 60 | 61 | void printTotal(Map times) { 62 | long total = times.collect { it.value }.sum() 63 | def prettyTime = new PrettyTime() 64 | def first = new Date((Long) times.keySet().min()) 65 | logger.lifecycle "Total build time: " + FormattingUtils.formatDuration(total) 66 | logger.lifecycle "(measured since " + prettyTime.format(first) + ")" 67 | } 68 | 69 | void printToday(Map times) { 70 | def midnight = dateUtils.localMidnightUTCTimestamp 71 | long today = times.collect { it.key >= midnight ? it.value : 0 }.sum() 72 | logger.lifecycle "Build time today: " + FormattingUtils.formatDuration(today) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/groovy/net/rdrei/android/buildtimetracker/reporters/SummaryReporter.groovy: -------------------------------------------------------------------------------- 1 | package net.rdrei.android.buildtimetracker.reporters 2 | 3 | import net.rdrei.android.buildtimetracker.Timing 4 | import org.gradle.BuildResult 5 | import org.gradle.api.logging.Logger 6 | 7 | class SummaryReporter extends AbstractBuildTimeTrackerReporter { 8 | def static final UNICODE_SQUARE = "▇" 9 | def static final ASCII_SQUARE = "▒" 10 | def static final FILL = " " 11 | 12 | String barStyle 13 | boolean successOutput 14 | boolean shortenTaskNames 15 | 16 | SummaryReporter(Map options, Logger logger) { 17 | super(options, logger) 18 | 19 | barStyle = getOption("barstyle", "unicode") 20 | successOutput = Boolean.parseBoolean(getOption("successOutput", "true")) 21 | shortenTaskNames = Boolean.parseBoolean(getOption("shortenTaskNames", "true")) 22 | } 23 | 24 | @Override 25 | def run(List timings) { 26 | if (timings.size() == 0) return 27 | 28 | def threshold = getOption("threshold", "50").toInteger() 29 | 30 | if (getOption("ordered", "false").toBoolean()) { 31 | timings = timings.sort(false, { it.ms }) 32 | } 33 | 34 | logger.lifecycle("== Build Time Summary ==") 35 | formatTable(timings, threshold) 36 | } 37 | 38 | @Override 39 | void onBuildResult(BuildResult result) { 40 | if (!successOutput) { 41 | return; 42 | } 43 | 44 | // Separate from previous output with a new line 45 | logger.lifecycle("") 46 | 47 | if (result.failure != null) { 48 | logger.error("== BUILD FAILED ==") 49 | } else { 50 | logger.lifecycle("== BUILD SUCCESSFUL ==") 51 | } 52 | } 53 | 54 | // Thanks to @sindresorhus for the logic. https://github.com/sindresorhus/time-grunt 55 | def formatTable(List timings, int threshold) { 56 | def total = timings.sum { t -> t.ms } 57 | def longestTaskName = timings.collect { it.path.length() }.max() 58 | def longestTiming = timings*.ms.max() 59 | def maxColumns = (new TerminalInfo()).getWidth(80) 60 | 61 | def maxBarWidth 62 | if (longestTaskName > maxColumns / 2) { 63 | maxBarWidth = (maxColumns - 20) / 2 64 | } else { 65 | maxBarWidth = maxColumns - (longestTaskName + 20) 66 | } 67 | 68 | for (timing in timings) { 69 | if (timing.ms >= threshold) { 70 | logger.lifecycle(sprintf("%s %s (%s)", 71 | createBar(timing.ms / total, timing.ms / longestTiming, maxBarWidth), 72 | shortenTaskNames ? shortenTaskName(timing.path, maxBarWidth) : timing.path, 73 | FormattingUtils.formatDuration(timing.ms))) 74 | } 75 | } 76 | } 77 | 78 | def static shortenTaskName(String taskName, def max) { 79 | if (taskName.length() < max) { return taskName } 80 | 81 | int partLength = Math.floor((max - 3) / 2) as int 82 | def start = taskName.substring(0, partLength + 1) 83 | def end = taskName.substring(taskName.length() - partLength) 84 | 85 | start.trim() + '…' + end.trim() 86 | } 87 | 88 | def createBar(def fracOfTotal, def fracOfMax, def max) { 89 | def symbol = barStyle == "ascii" ? ASCII_SQUARE : UNICODE_SQUARE 90 | 91 | def roundedTotal = Math.round(fracOfTotal * 100) 92 | def barLength = Math.ceil(max * fracOfMax) 93 | def bar = FILL * (max - barLength) + symbol * (barLength - 1) 94 | def formatted = (roundedTotal < 10 ? " " : "") + roundedTotal 95 | return (barStyle != "none" ? (bar + " ") : "") + formatted + '%' 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /report.Rmd: -------------------------------------------------------------------------------- 1 | ## Build Times 2 | 3 | ```{r preliminaries, include=FALSE} 4 | library(ggplot2) 5 | library(plyr) 6 | 7 | options(width=120) 8 | options(scipen=999) 9 | ``` 10 | 11 | ```{r load, include=FALSE} 12 | # Load data frame and prep. 13 | df <- read.csv("times.csv", header=FALSE) 14 | colnames(df) <- c("timestamp", "order", "task", "success", "did_work", "skipped", "milliseconds") 15 | 16 | # Correct mis-factoring of task and boolean fields 17 | df$task <- as.character(df$task) 18 | df$success <- as.logical(as.character(df$success)) 19 | df$did_work <- as.logical(as.character(df$did_work)) 20 | df$skipped <- as.logical(as.character(df$skipped)) 21 | 22 | # Create date objects from timestamp field for display 23 | df$date <- as.POSIXct(df$timestamp/1000, origin="1970-01-01") 24 | 25 | # Add seconds fields 26 | df$seconds <- floor(df$milliseconds / 1000) 27 | 28 | # Sort by timestamp and order 29 | df <- df[with(df, order(timestamp, order)),] 30 | row.names(df) <- 1:nrow(df) 31 | ``` 32 | 33 | 34 | ```{r echo=FALSE} 35 | total <- ddply(df, .(date), summarise, milliseconds = sum(milliseconds)) 36 | total$seconds = total$milliseconds/1000 37 | total$minutes = total$milliseconds/1000/60 38 | total.ecdf <- ecdf(total$milliseconds) 39 | 40 | df.required_work <- df[!df$skipped & df$did_work,] 41 | ``` 42 | 43 | Data collected on `r length(unique(df$timestamp))` Gradle builds running for 44 | `r format(sum(df$milliseconds)/1000/60/60, digits=1)` hours in total. 45 | 46 | * The median build time was `r format(median(total$seconds), digits=1)` seconds. 47 | * `r format(100 - total.ecdf(10*1000) * 100, digits=1)`% of builds took longer 48 | than ten seconds to complete. 49 | * `r format(100 - total.ecdf(60*1000) * 100, digits=1)`% of builds took longer 50 | than one minute to complete. 51 | * `r format(100 - length(df.required_work$timestamp)/length(df$timestamp)*100, digits=1)`% 52 | of subtasks were skipped or required no work. 53 | 54 | 55 | ```{r echo=FALSE, fig.width=10, fig.height=7} 56 | ggplot(total, aes(x = date, y = minutes)) + 57 | geom_bar(stat="identity", colour = "black") + 58 | theme(axis.title.x = element_blank()) + 59 | ylab("Build Time (minutes)") + 60 | ggtitle("Build History") + 61 | theme(legend.title = element_blank()) 62 | ``` 63 | 64 | ```{r echo=FALSE, fig.width=10, fig.height=7} 65 | ggplot(total, aes(x = minutes)) + 66 | geom_histogram(binwidth = .25, colour = "black") + 67 | xlab("Build Time (minutes)") + 68 | theme(axis.title.y = element_blank()) + 69 | ggtitle("Build Time Distribution") + 70 | theme(legend.title = element_blank()) 71 | ``` 72 | 73 | The slowest ten subtargets requiring work ordered by aggregate total time were: 74 | 75 | ```{r echo=FALSE, comment=NA} 76 | task <- ddply(df.required_work, .(task), summarise, 77 | executions = length(date), 78 | median_seconds = median(seconds), 79 | sd_seconds = sd(seconds), 80 | max_seconds = max(seconds), 81 | total_seconds = sum(seconds), 82 | mean_seconds = mean(seconds)) 83 | 84 | # Sort by total time 85 | task.total_seconds <- task[with(task, order(-total_seconds)),] 86 | row.names(task.total_seconds) <- 1:nrow(task.total_seconds) 87 | 88 | # Display top ten by median build time 89 | task.total_seconds[1:10,c('task', 'executions', 'total_seconds', 'median_seconds')] 90 | ``` 91 | 92 | The slowest ten subtargets requiring work ordered by median time were: 93 | 94 | ```{r echo=FALSE, comment=NA} 95 | # Sort by median time 96 | task.median_seconds <- task[with(task, order(-median_seconds)),] 97 | row.names(task.median_seconds) <- 1:nrow(task.median_seconds) 98 | 99 | # Display top ten by median build time 100 | task.median_seconds[1:10,c('task', 'executions', 'median_seconds')] 101 | ``` 102 | -------------------------------------------------------------------------------- /src/test/groovy/net/rdrei/android/buildtimetracker/TimingRecorderTest.groovy: -------------------------------------------------------------------------------- 1 | package net.rdrei.android.buildtimetracker 2 | 3 | import groovy.mock.interceptor.MockFor 4 | import net.rdrei.android.buildtimetracker.reporters.AbstractBuildTimeTrackerReporter 5 | import net.rdrei.android.buildtimetracker.util.Clock 6 | import org.gradle.api.Task 7 | import org.gradle.api.logging.Logger 8 | import org.gradle.api.tasks.TaskState 9 | import org.junit.Test 10 | 11 | import static org.junit.Assert.assertEquals 12 | import static org.junit.Assert.assertNull 13 | 14 | class TimingRecorderTest { 15 | 16 | Task mockTask(String path) { 17 | def mockTask = new MockFor(Task) 18 | mockTask.demand.getPath { path } 19 | 20 | mockTask.proxyInstance() 21 | } 22 | 23 | MockFor mockClock(int ms) { 24 | def mockClock = new MockFor(Clock) 25 | 26 | mockClock.demand.getTimeInMs { ms } 27 | 28 | mockClock 29 | } 30 | 31 | BuildTimeTrackerPlugin buildPlugin() { 32 | new BuildTimeTrackerPlugin() 33 | } 34 | 35 | @Test 36 | void recordsTaskPaths() { 37 | mockClock(0).use { 38 | def plugin = buildPlugin() 39 | TimingRecorder listener = new TimingRecorder(plugin) 40 | Task task = mockTask "test" 41 | TaskState state = new TaskStateBuilder().build() 42 | 43 | listener.buildStarted null 44 | listener.beforeExecute task 45 | listener.afterExecute task, state 46 | 47 | assertEquals(["test"], listener.getTasks()) 48 | } 49 | } 50 | 51 | @Test 52 | void recordsTaskTiming() { 53 | mockClock(123).use { 54 | TimingRecorder listener = new TimingRecorder() 55 | Task task = mockTask "test" 56 | TaskState state = new TaskStateBuilder().build() 57 | 58 | listener.buildStarted null 59 | listener.beforeExecute task 60 | listener.afterExecute task, state 61 | 62 | Timing timing = listener.getTiming "test" 63 | assertEquals 123, timing.ms 64 | } 65 | } 66 | 67 | @Test 68 | void buildFinishes() { 69 | mockClock(0).use { 70 | def plugin = buildPlugin() 71 | 72 | TimingRecorder listener = new TimingRecorder(plugin) 73 | Task task = mockTask "test" 74 | TaskState state = new TaskStateBuilder().build() 75 | 76 | listener.buildStarted null 77 | listener.beforeExecute task 78 | listener.afterExecute task, state 79 | listener.buildFinished null 80 | } 81 | } 82 | 83 | @Test 84 | void callsReportersOnBuildFinished() { 85 | def mockReporter = new MockFor(AbstractBuildTimeTrackerReporter) 86 | def mockLogger = new MockFor(Logger) 87 | mockReporter.demand.run { timings -> 88 | assertEquals 1, timings.size 89 | assertEquals "test", timings.get(0).path 90 | assertEquals 123, timings.get(0).ms 91 | } 92 | mockReporter.demand.onBuildResult { result -> 93 | assertNull result 94 | } 95 | def proxyReporter = mockReporter.proxyInstance([[:], mockLogger.proxyInstance()] as Object[]) 96 | 97 | def mockPlugin = new MockFor(BuildTimeTrackerPlugin) 98 | mockPlugin.demand.getReporters { [ proxyReporter ] } 99 | def proxyPlugin = mockPlugin.proxyInstance() 100 | 101 | mockClock(123).use { 102 | TimingRecorder listener = new TimingRecorder(proxyPlugin) 103 | Task task = mockTask "test" 104 | TaskState state = new TaskStateBuilder().build() 105 | 106 | listener.buildStarted null 107 | listener.beforeExecute task 108 | listener.afterExecute task, state 109 | listener.buildFinished null 110 | 111 | mockReporter.verify proxyReporter 112 | mockPlugin.verify proxyPlugin 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/test/groovy/net/rdrei/android/buildtimetracker/reporters/CSVSummaryReporterTest.groovy: -------------------------------------------------------------------------------- 1 | package net.rdrei.android.buildtimetracker.reporters 2 | 3 | import groovy.mock.interceptor.MockFor 4 | import org.gradle.api.logging.Logger 5 | import org.junit.Rule 6 | import org.junit.Test 7 | import org.junit.rules.TemporaryFolder 8 | import org.ocpsoft.prettytime.PrettyTime 9 | 10 | import static org.junit.Assert.* 11 | 12 | class CSVSummaryReporterTest { 13 | 14 | @Rule 15 | public TemporaryFolder folder = new TemporaryFolder() 16 | 17 | def getFixture(String name) { 18 | new File(getClass().getClassLoader().getResource(name).getPath()) 19 | } 20 | 21 | @Test 22 | void testThrowsErrorWithoutCSV() { 23 | Logger logger = new MockFor(Logger).proxyInstance() 24 | def reporter = new CSVSummaryReporter([:], logger) 25 | def err = null 26 | try { 27 | reporter.run([]) 28 | } catch (ReporterConfigurationError e) { 29 | err = e 30 | } 31 | 32 | assertNotNull err 33 | assertEquals ReporterConfigurationError.ErrorType.REQUIRED, err.errorType 34 | assertEquals "csv", err.optionName 35 | } 36 | 37 | @Test 38 | void testThrowsErrorWithInvalidFile() { 39 | Logger logger = new MockFor(Logger).proxyInstance() 40 | def reporter = new CSVSummaryReporter([csv: "/invalid/file"], logger) 41 | def err = null 42 | 43 | try { 44 | reporter.run([]) 45 | } catch (ReporterConfigurationError e) { 46 | err = e 47 | } 48 | 49 | assertNotNull err 50 | assertEquals ReporterConfigurationError.ErrorType.INVALID, err.errorType 51 | assertEquals "csv", err.optionName 52 | } 53 | 54 | @Test 55 | void testRunsWithValidEmptyFile() { 56 | def mockLogger = new MockFor(Logger) 57 | def reporter = new CSVSummaryReporter([csv: getFixture("empty.csv")], mockLogger.proxyInstance()) 58 | reporter.run([]) 59 | // Expect no calls to the logger. 60 | } 61 | 62 | @Test 63 | void testReportsTotalSummary() { 64 | def mockPrettyTime = new MockFor(PrettyTime) 65 | def mockLogger = new MockFor(Logger) 66 | def lines = [] 67 | mockLogger.demand.lifecycle(4) { l -> lines << l } 68 | mockPrettyTime.demand.format { "2 weeks ago" } 69 | 70 | mockPrettyTime.use { 71 | def reporter = new CSVSummaryReporter([csv: getFixture("times.csv")], mockLogger.proxyInstance()) 72 | reporter.run([]) 73 | } 74 | 75 | assertEquals "Total build time: 1:57.006", lines[2].trim() 76 | assertEquals "(measured since 2 weeks ago)", lines[3].trim() 77 | } 78 | 79 | @Test 80 | void testReportsTotalSummaryWithHeaders() { 81 | def mockPrettyTime = new MockFor(PrettyTime) 82 | def mockLogger = new MockFor(Logger) 83 | def lines = [] 84 | def err = null 85 | mockLogger.demand.lifecycle(4) { l -> lines << l } 86 | mockPrettyTime.demand.format { "2 weeks ago" } 87 | 88 | mockPrettyTime.use { 89 | def reporter = new CSVSummaryReporter([csv: getFixture("timesWithHeaders.csv")], mockLogger.proxyInstance()) 90 | 91 | try { 92 | reporter.run([]) 93 | } catch (NumberFormatException e) { 94 | err = e 95 | } 96 | } 97 | assertNull(err) 98 | assertTrue(lines[2].toString().contains("Total build time:")) 99 | 100 | } 101 | 102 | 103 | @Test 104 | void testReportsDailySummary() { 105 | def mockLogger = new MockFor(Logger) 106 | def mockDateUtils = new MockFor(DateUtils) 107 | def lines = [] 108 | mockLogger.demand.lifecycle(4) { l -> lines << l } 109 | mockDateUtils.demand.getLocalMidnightUTCTimestamp { 1407188121286L } 110 | 111 | mockDateUtils.use { 112 | def reporter = new CSVSummaryReporter([csv: getFixture("times.csv")], mockLogger.proxyInstance()) 113 | reporter.run([]) 114 | } 115 | 116 | assertEquals "Build time today: 0:46.069", lines[1].trim() 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/test/groovy/net/rdrei/android/buildtimetracker/reporters/JSONReporterTest.groovy: -------------------------------------------------------------------------------- 1 | package net.rdrei.android.buildtimetracker.reporters 2 | 3 | import groovy.json.JsonSlurper 4 | import groovy.mock.interceptor.MockFor 5 | import net.rdrei.android.buildtimetracker.Timing 6 | import org.gradle.api.logging.Logger 7 | import org.junit.Rule 8 | import org.junit.Test 9 | import org.junit.rules.TemporaryFolder 10 | 11 | import static org.junit.Assert.assertEquals 12 | import static org.junit.Assert.assertNotNull; 13 | import static org.junit.Assert.assertTrue 14 | import static org.junit.Assert.assertFalse 15 | 16 | class JSONReporterTest { 17 | 18 | @Rule 19 | public TemporaryFolder folder = new TemporaryFolder() 20 | 21 | Logger mockLogger = new MockFor(Logger).proxyInstance() 22 | 23 | File mkTemporaryFile(String name) { 24 | File file = folder.newFile name 25 | if (file.exists()) { 26 | file.delete() 27 | } 28 | 29 | file 30 | } 31 | 32 | @Test 33 | void createsOutputJSON() { 34 | File file = mkTemporaryFile "test.json" 35 | assertFalse("Output JSON file exists.", file.exists()) 36 | 37 | JSONReporter reporter = new JSONReporter([ output: file.getPath() ], mockLogger) 38 | 39 | reporter.run([ 40 | new Timing(100, "task1", true, false, true), 41 | new Timing(200, "task2", false, true, false) 42 | ]) 43 | 44 | assertTrue "Output JSON does not exist.", file.exists() 45 | } 46 | 47 | 48 | @Test 49 | void writesTimingsToOutputJSON() { 50 | File file = mkTemporaryFile "test.json" 51 | JSONReporter reporter = new JSONReporter([ output: file.getPath() ], mockLogger) 52 | 53 | reporter.run([ 54 | new Timing(100, "task1", true, false, true), 55 | new Timing(200, "task2", false, true, false) 56 | ]) 57 | 58 | def jsonSlurper = new JsonSlurper() 59 | def jsonObject = jsonSlurper.parseText(new File(file.getPath()).text) 60 | 61 | def measurements = jsonObject.measurements.iterator() 62 | 63 | // Verify first task 64 | def line0 = measurements.next() 65 | assertNotNull line0 66 | assertEquals 11, line0.size() 67 | assertEquals "task1", line0.task 68 | assertEquals 100, line0.ms 69 | 70 | // Verify second task 71 | def line1 = measurements.next() 72 | assertNotNull line1 73 | assertEquals 11, line1.size() 74 | assertEquals "task2", line1.task 75 | assertEquals 200, line1.ms 76 | 77 | } 78 | 79 | @Test 80 | void writesTimingsSuccessTrue() { 81 | File file = mkTemporaryFile "test.json" 82 | JSONReporter reporter = new JSONReporter([ output: file.getPath() ], mockLogger) 83 | 84 | reporter.run([ 85 | new Timing(100, "task1", true, false, true), 86 | new Timing(200, "task2", true, true, false) 87 | ]) 88 | 89 | def jsonSlurper = new JsonSlurper() 90 | def jsonObject = jsonSlurper.parseText(new File(file.getPath()).text) 91 | def measurements = jsonObject.measurements.iterator() 92 | 93 | // Verify first task 94 | assertEquals measurements.next().success, true 95 | // Verify second task 96 | assertEquals measurements.next().success, true 97 | // Verify overall success 98 | assertEquals jsonObject.success, true 99 | } 100 | 101 | @Test 102 | void writesTimingsSuccessFalse() { 103 | File file = mkTemporaryFile "test.json" 104 | JSONReporter reporter = new JSONReporter([ output: file.getPath() ], mockLogger) 105 | 106 | reporter.run([ 107 | new Timing(100, "task1", true, false, true), 108 | new Timing(200, "task2", false, true, false) 109 | ]) 110 | 111 | def jsonSlurper = new JsonSlurper() 112 | def jsonObject = jsonSlurper.parseText(new File(file.getPath()).text) 113 | def measurements = jsonObject.measurements.iterator() 114 | 115 | // Verify first task 116 | assertEquals measurements.next().success, true 117 | // Verify second task 118 | assertEquals measurements.next().success, false 119 | // Verify overall success 120 | assertEquals jsonObject.success, false 121 | } 122 | 123 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # ![Build Time Tracker](https://cdn.rawgit.com/passy/build-time-tracker-plugin/cc3bd9dcbda61ae7b699e4048c3f425525352d54/assets/logo.svg) 2 | 3 | [![Build Status](https://travis-ci.org/passy/build-time-tracker-plugin.svg?branch=master)](https://travis-ci.org/passy/build-time-tracker-plugin) 4 | ![Maven Version](https://img.shields.io/maven-central/v/net.rdrei.android.buildtimetracker/gradle-plugin.svg?maxAge=2592000) 5 | [![Stories in Ready](https://img.shields.io/waffle/label/passy/build-time-tracker-plugin/ready.svg)](http://waffle.io/passy/build-time-tracker-plugin) 6 | 7 | How much time do you spend each day waiting for Gradle? Now you know! 8 | 9 | ## Features 10 | 11 | * Sortable bar chart summaries 12 | * CSV output 13 | * Daily and total summary 14 | 15 | ## Screenshot 16 | 17 | ![Screenshot](assets/screenshot.png) 18 | 19 | ## Usage 20 | 21 | Apply the plugin in your `build.gradle`. On Gradle >2.1 you can do this 22 | using the Plugin DSL Syntax: 23 | 24 | ```groovy 25 | plugins { 26 | id "net.rdrei.android.buildtimetracker" version "0.11.0" 27 | } 28 | ``` 29 | 30 | Otherwise, use it as `classpath` dependency: 31 | 32 | ```groovy 33 | buildscript { 34 | repositories { 35 | mavenCentral() 36 | } 37 | 38 | dependencies { 39 | classpath "net.rdrei.android.buildtimetracker:gradle-plugin:0.11.+" 40 | } 41 | } 42 | 43 | apply plugin: "build-time-tracker" 44 | ``` 45 | 46 | Configure the plugin: 47 | 48 | ```groovy 49 | buildtimetracker { 50 | reporters { 51 | csv { 52 | output "build/times.csv" 53 | append true 54 | header false 55 | } 56 | 57 | summary { 58 | ordered false 59 | threshold 50 60 | barstyle "unicode" 61 | } 62 | 63 | csvSummary { 64 | csv "build/times.csv" 65 | } 66 | } 67 | } 68 | ``` 69 | 70 | Using the `SNAPSHOT` release: 71 | 72 | ```groovy 73 | buildscript { 74 | repositories { 75 | maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } 76 | } 77 | 78 | dependencies { 79 | classpath "net.rdrei.android.buildtimetracker:gradle-plugin:0.12.0-SNAPSHOT" 80 | } 81 | } 82 | 83 | ``` 84 | 85 | ## Difference to `--profile` 86 | 87 | You may wonder why you would want to use this plugin when gradle has 88 | a built-in [build 89 | profiler](https://docs.gradle.org/current/userguide/tutorial_gradle_command_line.html#sec:profiling_build). 90 | The quick version is, that if you just want to quickly check what it is that's 91 | slowing down your build, `--profile` will be all you need. However, if you want 92 | to continuously monitor your build and find bottlenecks that develop over time, 93 | this plugin may be the right fit for you. `build-time-tracker` writes a 94 | continuous log that is monoidal and can be collected from various different 95 | machines to run statistical analyses. Importantly, the written files contain 96 | identifying information about the machine the build happened on so you can 97 | compare apples with apples. 98 | 99 | ## Reporters 100 | 101 | ### CSVReporter 102 | 103 | The `csv` reporter takes the following options: 104 | 105 | * `output`: CSV output file location relative to Gradle execution. 106 | * `append`: When set to `true` the CSV output file is not truncated. This is 107 | useful for collecting a series of build time profiles in a single CSV. 108 | * `header`: When set to `false` the CSV output does not include a prepended 109 | header row with column names. Is desirable in conjunction with `append`. 110 | 111 | A basic [R Markdown](http://rmarkdown.rstudio.com/) script, `report.Rmd` is 112 | included for ploting and analysing build times using CSV output. 113 | 114 | ### CSVSummaryReporter 115 | 116 | The `csvSummary` displays the accumulated total build time from a CSV file. 117 | The reporter takes the following option: 118 | 119 | * `csv`: Path (relative to the gradle file or absolute) to a CSV file created 120 | with the above reporter and the options `append = true` and `header = false`. 121 | 122 | ### SummaryReporter 123 | 124 | The `summary` reporter gives you an overview of your tasks at the end of the 125 | build. It has the following options: 126 | 127 | * `threshold`: (default: 50) Minimum time in milliseconds to display a task. 128 | * `ordered`: (default: false) Whether or not to sort the output in ascending 129 | order by time spent. 130 | * `barstyle`: (default: "unicode") Supports "unicode", "ascii" and "none" for 131 | displaying a bar chart of the relative times spent on each task. 132 | * `successOutput`: (default: "true") Redisplay build success or failure message 133 | so you don't miss it if the summary output is long. 134 | * `shortenTaskNames`: (default: "true") Shortens long tasks names. 135 | 136 | _Note_ This plugin only measures the task times that constitute a build. 137 | Specifically, it does not measure the time in configuration at the start 138 | of a Gradle run. This means that the time to execute a build with very fast 139 | tasks is not accurately represented in output because it is dominated by 140 | the time in configuration instead. 141 | 142 | ## Developing 143 | 144 | This project is built and tested by [Travis](https://travis-ci.org) at 145 | [passy/build-time-tracker-plugin](https://travis-ci.org/passy/build-time-tracker-plugin). 146 | 147 | ## Acknowledgements 148 | 149 | Thanks to [Sindre Sorhus](https://github.com/sindresorhus) for contributing the 150 | wonderful logo! 151 | 152 | ## License 153 | 154 | Copyright 2014 Pascal Hartig 155 | 156 | Licensed under the Apache License, Version 2.0 (the "License"); 157 | you may not use this file except in compliance with the License. 158 | You may obtain a copy of the License at 159 | 160 | http://www.apache.org/licenses/LICENSE-2.0 161 | 162 | Unless required by applicable law or agreed to in writing, software 163 | distributed under the License is distributed on an "AS IS" BASIS, 164 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 165 | See the License for the specific language governing permissions and 166 | limitations under the License. 167 | -------------------------------------------------------------------------------- /src/test/resources/times.csv: -------------------------------------------------------------------------------- 1 | "1406977879284","0",":clean","true","true","false","16" 2 | "1406977984495","0",":compileJava","true","false","true","3" 3 | "1406977984495","1",":compileGroovy","true","true","false","1958" 4 | "1406977984495","2",":processResources","true","true","false","50" 5 | "1406977984495","3",":classes","true","false","false","1" 6 | "1406977984495","4",":compileTestJava","true","false","true","1" 7 | "1406977984495","5",":compileTestGroovy","true","true","false","982" 8 | "1406977984495","6",":processTestResources","true","false","true","1" 9 | "1406977984495","7",":testClasses","true","false","false","0" 10 | "1406977984495","8",":test","false","true","false","6502" 11 | "1406978084185","0",":compileJava","true","false","true","3" 12 | "1406978084185","1",":compileGroovy","true","true","false","1763" 13 | "1406978084185","2",":processResources","true","false","true","20" 14 | "1406978084185","3",":classes","true","false","false","1" 15 | "1406978084185","4",":compileTestJava","true","false","true","1" 16 | "1406978084185","5",":compileTestGroovy","true","true","false","914" 17 | "1406978084185","6",":processTestResources","true","false","true","1" 18 | "1406978084185","7",":testClasses","true","false","false","1" 19 | "1406978084185","8",":test","true","true","false","7161" 20 | "1407187408134","0",":compileJava","true","false","true","4" 21 | "1407187408134","1",":compileGroovy","true","true","false","1991" 22 | "1407187408134","2",":processResources","true","false","true","20" 23 | "1407187408134","3",":classes","true","false","false","1" 24 | "1407187408134","4",":compileTestJava","true","false","true","1" 25 | "1407187408134","5",":compileTestGroovy","true","true","false","1031" 26 | "1407187408134","6",":processTestResources","true","false","true","1" 27 | "1407187408134","7",":testClasses","true","false","false","0" 28 | "1407187408134","8",":test","false","true","false","14103" 29 | "1407187707046","0",":compileJava","true","false","true","3" 30 | "1407187707046","1",":compileGroovy","true","false","true","179" 31 | "1407187707046","2",":processResources","true","false","true","19" 32 | "1407187707046","3",":classes","true","false","true","3" 33 | "1407187707046","4",":compileTestJava","true","false","true","1" 34 | "1407187707046","5",":compileTestGroovy","false","true","false","2530" 35 | "1407187748118","0",":compileJava","true","false","true","5" 36 | "1407187748118","1",":compileGroovy","true","false","true","242" 37 | "1407187748118","2",":processResources","true","false","true","28" 38 | "1407187748118","3",":classes","true","false","true","2" 39 | "1407187748118","4",":compileTestJava","true","false","true","1" 40 | "1407187748118","5",":compileTestGroovy","true","true","false","2599" 41 | "1407187748118","6",":processTestResources","true","false","true","1" 42 | "1407187748118","7",":testClasses","true","false","false","1" 43 | "1407187748118","8",":test","false","true","false","15068" 44 | "1407187801581","0",":compileJava","true","false","true","5" 45 | "1407187801581","1",":compileGroovy","true","false","true","187" 46 | "1407187801581","2",":processResources","true","false","true","19" 47 | "1407187801581","3",":classes","true","false","true","2" 48 | "1407187801581","4",":compileTestJava","true","false","true","1" 49 | "1407187801581","5",":compileTestGroovy","true","true","false","2702" 50 | "1407187801581","6",":processTestResources","true","false","true","1" 51 | "1407187801581","7",":testClasses","true","false","false","1" 52 | "1407187801581","8",":test","false","true","false","10806" 53 | "1407188121286","0",":compileJava","true","false","true","3" 54 | "1407188121286","1",":compileGroovy","true","false","true","163" 55 | "1407188121286","2",":processResources","true","false","true","18" 56 | "1407188121286","3",":classes","true","false","true","2" 57 | "1407188121286","4",":compileTestJava","true","false","true","1" 58 | "1407188121286","5",":compileTestGroovy","true","true","false","2255" 59 | "1407188121286","6",":processTestResources","true","false","true","1" 60 | "1407188121286","7",":testClasses","true","false","false","0" 61 | "1407188121286","8",":test","false","true","false","8653" 62 | "1407188143321","0",":compileJava","true","false","true","3" 63 | "1407188143321","1",":compileGroovy","true","false","true","173" 64 | "1407188143321","2",":processResources","true","false","true","20" 65 | "1407188143321","3",":classes","true","false","true","2" 66 | "1407188143321","4",":compileTestJava","true","false","true","0" 67 | "1407188143321","5",":compileTestGroovy","true","true","false","2366" 68 | "1407188143321","6",":processTestResources","true","false","true","2" 69 | "1407188143321","7",":testClasses","true","false","false","1" 70 | "1407188143321","8",":test","true","true","false","7549" 71 | "1407188143321","9",":check","true","false","false","1" 72 | "1407363170474","0",":compileJava","true","false","true","3" 73 | "1407363170474","1",":compileGroovy","true","true","false","2549" 74 | "1407363170474","2",":processResources","true","false","true","32" 75 | "1407363170474","3",":classes","true","false","false","2" 76 | "1407363170474","4",":jar","true","true","false","171" 77 | "1407363170474","5",":groovydoc","true","true","false","4239" 78 | "1407363170474","6",":javadocJar","true","true","false","42" 79 | "1407363170474","7",":sourcesJar","true","true","false","26" 80 | "1407363170474","8",":signArchives","true","false","true","8" 81 | "1407363170474","9",":uploadArchives","true","true","false","8799" 82 | "1407363357829","0",":compileJava","true","false","true","1" 83 | "1407363357829","1",":compileGroovy","true","false","true","60" 84 | "1407363357829","2",":processResources","true","false","true","9" 85 | "1407363357829","3",":classes","true","false","true","1" 86 | "1407363357829","4",":jar","true","false","true","9" 87 | "1407363357829","5",":groovydoc","true","false","true","26" 88 | "1407363357829","6",":javadocJar","true","false","true","6" 89 | "1407363357829","7",":sourcesJar","true","false","true","6" 90 | "1407363357829","8",":signArchives","true","false","true","1" 91 | "1407363357829","9",":assemble","true","false","true","1" 92 | "1407363357829","10",":compileTestJava","true","false","true","1" 93 | "1407363357829","11",":compileTestGroovy","true","true","false","1819" 94 | "1407363357829","12",":processTestResources","true","false","true","1" 95 | "1407363357829","13",":testClasses","true","false","false","0" 96 | "1407363357829","14",":test","true","true","false","7043" 97 | "1407363357829","15",":check","true","false","false","0" 98 | "1407363357829","16",":build","true","false","false","1" 99 | -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/test/groovy/net/rdrei/android/buildtimetracker/reporters/SummaryReporterTest.groovy: -------------------------------------------------------------------------------- 1 | package net.rdrei.android.buildtimetracker.reporters 2 | 3 | import groovy.mock.interceptor.MockFor 4 | import net.rdrei.android.buildtimetracker.Timing 5 | import org.gradle.BuildResult 6 | import org.gradle.api.logging.Logger 7 | 8 | import static org.junit.Assert.assertEquals 9 | import static org.junit.Assert.assertTrue 10 | import static org.junit.Assert.assertFalse 11 | 12 | import org.junit.Test 13 | 14 | class SummaryReporterTest { 15 | @Test 16 | void testLinesCountMatchesTimings() { 17 | def mockLogger = new MockFor(Logger) 18 | mockLogger.demand.lifecycle(3) {} 19 | 20 | def reporter = new SummaryReporter([:], mockLogger.proxyInstance()) 21 | reporter.run([ 22 | new Timing(100, "task1", true, false, true), 23 | new Timing(200, "task2", false, true, false) 24 | ]) 25 | } 26 | 27 | @Test 28 | void testIncludesSummaryHeader() { 29 | def mockLogger = new MockFor(Logger) 30 | def lines = [] 31 | mockLogger.demand.lifecycle(3) { l -> lines << l } 32 | 33 | def reporter = new SummaryReporter([:], mockLogger.proxyInstance()) 34 | reporter.run([ 35 | new Timing(100, "task1", true, false, true), 36 | new Timing(200, "task2", false, true, false) 37 | ]) 38 | 39 | assertEquals lines[0], "== Build Time Summary ==" 40 | } 41 | 42 | @Test 43 | void testExcludesBelowTreshold() { 44 | def mockLogger = new MockFor(Logger) 45 | mockLogger.demand.lifecycle(2) {} 46 | 47 | def reporter = new SummaryReporter([threshold: 150], mockLogger.proxyInstance()) 48 | reporter.run([ 49 | new Timing(100, "task1", true, false, true), 50 | new Timing(200, "task2", false, true, false) 51 | ]) 52 | } 53 | 54 | @Test 55 | void testDoesntOrderWithoutOptionEnabled() { 56 | def mockLogger = new MockFor(Logger) 57 | def lines = [] 58 | mockLogger.demand.lifecycle(4) { l -> lines << l } 59 | 60 | def reporter = new SummaryReporter([ordered: false], mockLogger.proxyInstance()) 61 | reporter.run([ 62 | new Timing(300, "task1", true, false, true), 63 | new Timing(100, "task3", true, false, true), 64 | new Timing(200, "task2", false, true, false) 65 | ]) 66 | 67 | // Don't hard code the exact format, don't unit test design 68 | assertTrue lines[1].contains("0:00.300") 69 | assertTrue lines[2].contains("0:00.100") 70 | assertTrue lines[3].contains("0:00.200") 71 | } 72 | 73 | @Test 74 | void testDoesntOrderWithOptionEnabled() { 75 | def mockLogger = new MockFor(Logger) 76 | def lines = [] 77 | mockLogger.demand.lifecycle(4) { l -> lines << l } 78 | 79 | def reporter = new SummaryReporter([ordered: true], mockLogger.proxyInstance()) 80 | reporter.run([ 81 | new Timing(300, "task1", true, false, true), 82 | new Timing(100, "task3", true, false, true), 83 | new Timing(200, "task2", false, true, false) 84 | ]) 85 | 86 | assertTrue lines[1].contains("0:00.100") 87 | assertTrue lines[2].contains("0:00.200") 88 | assertTrue lines[3].contains("0:00.300") 89 | } 90 | 91 | @Test 92 | void testOutputIncludesTaskName() { 93 | def mockLogger = new MockFor(Logger) 94 | def lines = [] 95 | mockLogger.demand.lifecycle(4) { l -> lines << l } 96 | 97 | def reporter = new SummaryReporter([:], mockLogger.proxyInstance()) 98 | reporter.run([ 99 | new Timing(300, "task1", true, false, true), 100 | new Timing(100, "task3", true, false, true), 101 | new Timing(200, "task2", false, true, false) 102 | ]) 103 | 104 | assertTrue lines[1].contains("task1") 105 | assertTrue lines[2].contains("task3") 106 | assertTrue lines[3].contains("task2") 107 | } 108 | 109 | @Test 110 | void testOutputIncludesShortenedLongTaskName() { 111 | def mockLogger = new MockFor(Logger) 112 | def mockTerminal = new MockFor(TerminalInfo) 113 | def lines = [] 114 | mockLogger.demand.lifecycle(2) { l -> lines << l } 115 | mockTerminal.demand.getWidth(1) { 80 } 116 | 117 | mockTerminal.use { 118 | def reporter = new SummaryReporter([:], mockLogger.proxyInstance()) 119 | reporter.run([ 120 | new Timing(300, "thisIsAReallyLongNameJustForTask1", true, false, true) 121 | ]) 122 | } 123 | 124 | assert lines[1].contains("thisIsAReally…JustForTask1") 125 | } 126 | 127 | @Test 128 | void testOutputIncludesLongTaskName() { 129 | def mockLogger = new MockFor(Logger) 130 | def mockTerminal = new MockFor(TerminalInfo) 131 | def lines = [] 132 | mockLogger.demand.lifecycle(2) { l -> lines << l } 133 | mockTerminal.demand.getWidth(1) { 80 } 134 | 135 | mockTerminal.use { 136 | def reporter = new SummaryReporter([shortenTaskNames: false], mockLogger.proxyInstance()) 137 | reporter.run([ 138 | new Timing(300, "thisIsAReallyLongNameJustForTask1", true, false, true) 139 | ]) 140 | } 141 | 142 | assert lines[1].contains("thisIsAReallyLongNameJustForTask1") 143 | } 144 | 145 | @Test 146 | void testEmptyTaskList() { 147 | def mockLogger = new MockFor(Logger) 148 | mockLogger.demand.lifecycle(0) {} 149 | 150 | def reporter = new SummaryReporter([:], mockLogger.proxyInstance()) 151 | reporter.run([ 152 | ]) 153 | } 154 | 155 | @Test 156 | void testOutputIncludesUnicodeBars() { 157 | def mockLogger = new MockFor(Logger) 158 | def lines = [] 159 | mockLogger.demand.lifecycle(4) { l -> lines << l } 160 | 161 | def reporter = new SummaryReporter([:], mockLogger.proxyInstance()) 162 | reporter.run([ 163 | new Timing(300, "task1", true, false, true), 164 | new Timing(100, "task3", true, false, true), 165 | ]) 166 | 167 | assertTrue lines[1].contains(SummaryReporter.UNICODE_SQUARE) 168 | assertFalse lines[1].contains(SummaryReporter.ASCII_SQUARE) 169 | assertTrue lines[2].contains(SummaryReporter.UNICODE_SQUARE) 170 | assertFalse lines[2].contains(SummaryReporter.ASCII_SQUARE) 171 | } 172 | 173 | @Test 174 | void testOutputIncludesASCIIBars() { 175 | def mockLogger = new MockFor(Logger) 176 | def lines = [] 177 | mockLogger.demand.lifecycle(4) { l -> lines << l } 178 | 179 | def reporter = new SummaryReporter([barstyle: "ascii"], mockLogger.proxyInstance()) 180 | reporter.run([ 181 | new Timing(300, "task1", true, false, true), 182 | new Timing(100, "task3", true, false, true), 183 | ]) 184 | 185 | assertTrue lines[1].contains(SummaryReporter.ASCII_SQUARE) 186 | assertFalse lines[1].contains(SummaryReporter.UNICODE_SQUARE) 187 | assertTrue lines[2].contains(SummaryReporter.ASCII_SQUARE) 188 | assertFalse lines[2].contains(SummaryReporter.UNICODE_SQUARE) 189 | } 190 | 191 | @Test 192 | void testOutputIncludesNoBars() { 193 | def mockLogger = new MockFor(Logger) 194 | def lines = [] 195 | mockLogger.demand.lifecycle(4) { l -> lines << l } 196 | 197 | def reporter = new SummaryReporter([barstyle: "none"], mockLogger.proxyInstance()) 198 | reporter.run([ 199 | new Timing(300, "task1", true, false, true), 200 | new Timing(100, "task3", true, false, true), 201 | ]) 202 | 203 | assertFalse lines[1].contains(SummaryReporter.ASCII_SQUARE) 204 | assertFalse lines[1].contains(SummaryReporter.UNICODE_SQUARE) 205 | assertFalse lines[2].contains(SummaryReporter.ASCII_SQUARE) 206 | assertFalse lines[2].contains(SummaryReporter.UNICODE_SQUARE) 207 | } 208 | 209 | @Test 210 | void testOutputIncludesPercentagesEvenWithoutBars() { 211 | def mockLogger = new MockFor(Logger) 212 | def lines = [] 213 | mockLogger.demand.lifecycle(4) { l -> lines << l } 214 | 215 | def reporter = new SummaryReporter([barstyle: "none"], mockLogger.proxyInstance()) 216 | reporter.run([ 217 | new Timing(300, "task1", true, false, true), 218 | new Timing(100, "task3", true, false, true), 219 | ]) 220 | 221 | assertTrue lines[1].contains("%") 222 | assertTrue lines[2].contains("%") 223 | } 224 | 225 | @Test 226 | void testOutputContainsStatusSuccessMessage() { 227 | def mockLogger = new MockFor(Logger) 228 | def mockBuildResult = new BuildResult(null, null) 229 | def lines = [] 230 | mockLogger.ignore.lifecycle { l -> lines << l } 231 | 232 | def reporter = new SummaryReporter([:], mockLogger.proxyInstance()) 233 | reporter.run([ 234 | new Timing(300, "task1", true, false, true), 235 | new Timing(100, "task3", true, false, true), 236 | ]) 237 | 238 | reporter.onBuildResult(mockBuildResult) 239 | assertTrue lines[4].contains("BUILD SUCCESSFUL") 240 | } 241 | 242 | @Test 243 | void testOutputContainsStatusFailureMessage() { 244 | def mockLogger = new MockFor(Logger) 245 | def mockBuildResult = new BuildResult(null, new Throwable()) 246 | def lines = [] 247 | mockLogger.ignore.lifecycle { l -> lines << l } 248 | mockLogger.ignore.error { l -> lines << l } 249 | 250 | def reporter = new SummaryReporter([:], mockLogger.proxyInstance()) 251 | reporter.run([ 252 | new Timing(300, "task1", false, false, true), 253 | new Timing(100, "task3", true, false, true), 254 | ]) 255 | 256 | reporter.onBuildResult(mockBuildResult) 257 | assertTrue lines[4].contains("BUILD FAILED") 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /src/test/groovy/net/rdrei/android/buildtimetracker/reporters/CSVReporterTest.groovy: -------------------------------------------------------------------------------- 1 | package net.rdrei.android.buildtimetracker.reporters 2 | 3 | import au.com.bytecode.opencsv.CSVReader 4 | import groovy.mock.interceptor.MockFor 5 | import groovy.mock.interceptor.StubFor 6 | import org.gradle.api.logging.Logger 7 | import net.rdrei.android.buildtimetracker.Timing 8 | import org.junit.Rule 9 | import org.junit.Test 10 | import org.junit.rules.TemporaryFolder 11 | 12 | import static org.junit.Assert.assertEquals 13 | import static org.junit.Assert.assertNotEquals 14 | import static org.junit.Assert.assertNotNull 15 | import static org.junit.Assert.assertTrue 16 | import static org.junit.Assert.assertFalse 17 | 18 | class CSVReporterTest { 19 | 20 | @Rule 21 | public TemporaryFolder folder = new TemporaryFolder() 22 | 23 | Logger mockLogger = new MockFor(Logger).proxyInstance() 24 | 25 | File mkTemporaryFile(String name) { 26 | File file = folder.newFile name 27 | if (file.exists()) { 28 | file.delete() 29 | } 30 | 31 | file 32 | } 33 | 34 | @Test 35 | void createsOutputCSV() { 36 | File file = mkTemporaryFile "test.csv" 37 | assertFalse("Output CSV exists.", file.exists()) 38 | 39 | CSVReporter reporter = new CSVReporter([ output: file.getPath() ], mockLogger) 40 | 41 | reporter.run([ 42 | new Timing(100, "task1", true, false, true), 43 | new Timing(200, "task2", false, true, false) 44 | ]) 45 | 46 | assertTrue "Output CSV does not exist.", file.exists() 47 | } 48 | 49 | @Test 50 | void writesHeaderToOutputCSV() { 51 | File file = mkTemporaryFile "test.csv" 52 | CSVReporter reporter = new CSVReporter([ output: file.getPath() ], mockLogger) 53 | 54 | reporter.run([ 55 | new Timing(100, "task1", true, false, true), 56 | new Timing(200, "task2", false, true, false) 57 | ]) 58 | 59 | CSVReader reader = new CSVReader(new FileReader(file)) 60 | 61 | String[] header = reader.readNext() 62 | assertNotNull header 63 | assertEquals 11, header.length 64 | assertEquals "timestamp", header[0] 65 | assertEquals "order", header[1] 66 | assertEquals "task", header[2] 67 | assertEquals "success", header[3] 68 | assertEquals "did_work", header[4] 69 | assertEquals "skipped", header[5] 70 | assertEquals "ms", header[6] 71 | assertEquals "date", header[7] 72 | assertEquals "cpu", header[8] 73 | assertEquals "memory", header[9] 74 | assertEquals "os", header[10] 75 | 76 | reader.close() 77 | } 78 | 79 | @Test 80 | void doesNotWritesHeaderToOutputCSVWhenHeaderOptionFalse() { 81 | File file = mkTemporaryFile "test.csv" 82 | CSVReporter reporter = new CSVReporter([ 83 | output: file.getPath(), 84 | header: "false" 85 | ], mockLogger) 86 | 87 | reporter.run([ 88 | new Timing(100, "task1", true, false, true), 89 | new Timing(200, "task2", false, true, false) 90 | ]) 91 | 92 | CSVReader reader = new CSVReader(new FileReader(file)) 93 | 94 | String[] line = reader.readNext() 95 | assertNotNull line 96 | assertEquals 11, line.length 97 | assertNotEquals "timestamp", line[0] 98 | assertNotEquals "order", line[1] 99 | assertNotEquals "task", line[2] 100 | assertNotEquals "success", line[3] 101 | assertNotEquals "did_work", line[4] 102 | assertNotEquals "skipped", line[5] 103 | assertNotEquals "ms", line[6] 104 | assertNotEquals "date", line[7] 105 | assertNotEquals "cpu", line[8] 106 | assertNotEquals "memory", line[9] 107 | assertNotEquals "os", line[10] 108 | 109 | reader.close() 110 | } 111 | 112 | @Test 113 | void writesTimingsToOutputCSV() { 114 | File file = mkTemporaryFile "test.csv" 115 | CSVReporter reporter = new CSVReporter([ output: file.getPath() ], mockLogger) 116 | 117 | reporter.run([ 118 | new Timing(100, "task1", true, false, true), 119 | new Timing(200, "task2", false, true, false) 120 | ]) 121 | 122 | CSVReader reader = new CSVReader(new FileReader(file)) 123 | 124 | Iterator lines = reader.readAll().iterator() 125 | 126 | // Skip the header 127 | lines.next() 128 | 129 | // Verify first task 130 | String[] line = lines.next() 131 | assertNotNull line 132 | assertEquals 11, line.length 133 | assertEquals "task1", line[2] 134 | assertEquals "100", line[6] 135 | 136 | // Verify second task 137 | line = lines.next() 138 | assertNotNull line 139 | assertEquals 11, line.length 140 | assertEquals "task2", line[2] 141 | assertEquals "200", line[6] 142 | 143 | reader.close() 144 | } 145 | 146 | @Test 147 | void includesBuildTimestampInOutputCSVRows() { 148 | def mockTimeProvider = new MockFor(TrueTimeProvider) 149 | mockTimeProvider.demand.getCurrentTime { 1234 } 150 | 151 | mockTimeProvider.use { 152 | File file = mkTemporaryFile "test.csv" 153 | CSVReporter reporter = new CSVReporter([ output: file.getPath() ], mockLogger) 154 | 155 | reporter.run([ 156 | new Timing(100, "task1", true, false, true), 157 | new Timing(200, "task2", false, true, false) 158 | ]) 159 | 160 | CSVReader reader = new CSVReader(new FileReader(file)) 161 | 162 | Iterator lines = reader.readAll().iterator() 163 | 164 | // Skip the header 165 | lines.next() 166 | 167 | // Verify first task 168 | String[] line = lines.next() 169 | assertNotNull line 170 | assertEquals 11, line.length 171 | assertEquals "1234", line[0] 172 | 173 | // Verify second task 174 | line = lines.next() 175 | assertNotNull line 176 | assertEquals 11, line.length 177 | assertEquals "1234", line[0] 178 | 179 | reader.close() 180 | } 181 | } 182 | 183 | @Test 184 | void includesTaskOrderInOutputCSVRows() { 185 | File file = mkTemporaryFile "test.csv" 186 | CSVReporter reporter = new CSVReporter([ output: file.getPath() ], mockLogger) 187 | 188 | reporter.run([ 189 | new Timing(100, "task1", true, false, true), 190 | new Timing(200, "task2", false, true, false) 191 | ]) 192 | 193 | CSVReader reader = new CSVReader(new FileReader(file)) 194 | 195 | Iterator lines = reader.readAll().iterator() 196 | 197 | // Skip the header 198 | lines.next() 199 | 200 | // Verify first task 201 | String[] line = lines.next() 202 | assertNotNull line 203 | assertEquals 11, line.length 204 | assertEquals "0", line[1] 205 | 206 | // Verify second task 207 | line = lines.next() 208 | assertNotNull line 209 | assertEquals 11, line.length 210 | assertEquals "1", line[1] 211 | 212 | reader.close() 213 | } 214 | 215 | @Test 216 | void includesTaskSuccessInOutputCSVRows() { 217 | File file = mkTemporaryFile "test.csv" 218 | CSVReporter reporter = new CSVReporter([ output: file.getPath() ], mockLogger) 219 | 220 | reporter.run([ 221 | new Timing(100, "task1", true, false, true), 222 | new Timing(200, "task2", false, true, false) 223 | ]) 224 | 225 | CSVReader reader = new CSVReader(new FileReader(file)) 226 | 227 | Iterator lines = reader.readAll().iterator() 228 | 229 | // Skip the header 230 | lines.next() 231 | 232 | // Verify first task 233 | String[] line = lines.next() 234 | assertNotNull line 235 | assertEquals 11, line.length 236 | assertEquals "true", line[3] 237 | 238 | // Verify second task 239 | line = lines.next() 240 | assertNotNull line 241 | assertEquals 11, line.length 242 | assertEquals "false", line[3] 243 | 244 | reader.close() 245 | } 246 | 247 | @Test 248 | void includesTaskDidWorkInOutputCSVRows() { 249 | File file = mkTemporaryFile "test.csv" 250 | CSVReporter reporter = new CSVReporter([ output: file.getPath() ], mockLogger) 251 | 252 | reporter.run([ 253 | new Timing(100, "task1", true, false, true), 254 | new Timing(200, "task2", false, true, false) 255 | ]) 256 | 257 | CSVReader reader = new CSVReader(new FileReader(file)) 258 | 259 | Iterator lines = reader.readAll().iterator() 260 | 261 | // Skip the header 262 | lines.next() 263 | 264 | // Verify first task 265 | String[] line = lines.next() 266 | assertNotNull line 267 | assertEquals 11, line.length 268 | assertEquals "false", line[4] 269 | 270 | // Verify second task 271 | line = lines.next() 272 | assertNotNull line 273 | assertEquals 11, line.length 274 | assertEquals "true", line[4] 275 | 276 | reader.close() 277 | } 278 | 279 | @Test 280 | void includesTaskSkippedInOutputCSVRows() { 281 | File file = mkTemporaryFile "test.csv" 282 | CSVReporter reporter = new CSVReporter([ output: file.getPath() ], mockLogger) 283 | 284 | reporter.run([ 285 | new Timing(100, "task1", true, false, true), 286 | new Timing(200, "task2", false, true, false) 287 | ]) 288 | 289 | CSVReader reader = new CSVReader(new FileReader(file)) 290 | 291 | Iterator lines = reader.readAll().iterator() 292 | 293 | // Skip the header 294 | lines.next() 295 | 296 | // Verify first task 297 | String[] line = lines.next() 298 | assertNotNull line 299 | assertEquals 11, line.length 300 | assertEquals "true", line[5] 301 | 302 | // Verify second task 303 | line = lines.next() 304 | assertNotNull line 305 | assertEquals 11, line.length 306 | assertEquals "false", line[5] 307 | 308 | reader.close() 309 | } 310 | 311 | @Test 312 | void appendsCSV() { 313 | File file = mkTemporaryFile "test.csv" 314 | file.write "existing content\n" 315 | 316 | CSVReporter reporter = new CSVReporter([ 317 | output: file.getPath(), 318 | append: "true", 319 | header: "false" 320 | ], mockLogger) 321 | 322 | reporter.run([ 323 | new Timing(100, "task1", true, false, true), 324 | new Timing(200, "task2", false, true, false) 325 | ]) 326 | 327 | CSVReader reader = new CSVReader(new FileReader(file)) 328 | 329 | Iterator lines = reader.readAll().iterator() 330 | 331 | // Verify existing content 332 | String[] line = lines.next() 333 | assertNotNull line 334 | assertEquals 1, line.length 335 | assertEquals "existing content", line[0] 336 | 337 | // Verify first task 338 | line = lines.next() 339 | assertNotNull line 340 | assertEquals 11, line.length 341 | assertEquals "task1", line[2] 342 | assertEquals "100", line[6] 343 | 344 | // Verify second task 345 | line = lines.next() 346 | assertNotNull line 347 | assertEquals 11, line.length 348 | assertEquals "task2", line[2] 349 | assertEquals "200", line[6] 350 | 351 | reader.close() 352 | } 353 | 354 | @Test 355 | void includesISO8601InOutputCSVRows() { 356 | def mockTimeProvider = new MockFor(TrueTimeProvider) 357 | mockTimeProvider.demand.getCurrentTime { 617007600 * 1000 } 358 | 359 | mockTimeProvider.use { 360 | File file = mkTemporaryFile "test.csv" 361 | CSVReporter reporter = new CSVReporter([ output: file.getPath() ], mockLogger) 362 | 363 | reporter.run([ 364 | new Timing(100, "task1", true, false, true), 365 | new Timing(200, "task2", false, true, false) 366 | ]) 367 | 368 | CSVReader reader = new CSVReader(new FileReader(file)) 369 | 370 | Iterator lines = reader.readAll().iterator() 371 | 372 | // Skip the header 373 | lines.next() 374 | 375 | // Verify first task 376 | String[] line = lines.next() 377 | assertNotNull line 378 | assertEquals 11, line.length 379 | assertEquals "1969-12-15T00:18:29,376Z", line[7] 380 | 381 | // Verify second task 382 | line = lines.next() 383 | assertNotNull line 384 | assertEquals 11, line.length 385 | assertEquals "1969-12-15T00:18:29,376Z", line[7] 386 | 387 | reader.close() 388 | } 389 | } 390 | 391 | @Test 392 | void includesOSInfo() { 393 | def mockSystem = new StubFor(System) 394 | mockSystem.demand.getProperty(4) { key -> 395 | switch (key) { 396 | case "os.name": 397 | return "DaithiOS" 398 | case "os.version": 399 | return "10.0" 400 | case "os.arch": 401 | return "power" 402 | } 403 | } 404 | def mockTimeProvider = new MockFor(TrueTimeProvider) 405 | mockTimeProvider.demand.getCurrentTime { 0 } 406 | 407 | mockSystem.use { 408 | File file = mkTemporaryFile "test.csv" 409 | CSVReporter reporter = new CSVReporter([ output: file.getPath() ], mockLogger) 410 | 411 | mockTimeProvider.use { 412 | reporter.run([ 413 | new Timing(100, "task1", true, false, true), 414 | new Timing(200, "task2", false, true, false) 415 | ]) 416 | } 417 | 418 | CSVReader reader = new CSVReader(new FileReader(file)) 419 | 420 | Iterator lines = reader.readAll().iterator() 421 | 422 | // Skip the header 423 | lines.next() 424 | 425 | // Verify first task 426 | String[] line = lines.next() 427 | assertNotNull line 428 | assertEquals 11, line.length 429 | assertEquals "DaithiOS 10.0 power", line[10] 430 | 431 | // Verify second task 432 | line = lines.next() 433 | assertNotNull line 434 | assertEquals 11, line.length 435 | assertEquals "DaithiOS 10.0 power", line[10] 436 | 437 | reader.close() 438 | } 439 | } 440 | 441 | @Test 442 | void extractsMemory() { 443 | def mockSysInfo = new StubFor(SysInfo) 444 | // We can't mock native methods, which getMaxMemory is by the looks of it 445 | // so we have to mock the entire wrapper instead. 446 | mockSysInfo.demand.getMaxMemory(1) { 1337 } 447 | mockSysInfo.ignore('getOSIdentifier') 448 | mockSysInfo.ignore('getCPUIdentifier') 449 | 450 | mockSysInfo.use { 451 | File file = mkTemporaryFile "test.csv" 452 | CSVReporter reporter = new CSVReporter([ output: file.getPath() ], mockLogger) 453 | 454 | reporter.run([ 455 | new Timing(100, "task1", true, false, true), 456 | new Timing(200, "task2", false, true, false) 457 | ]) 458 | 459 | CSVReader reader = new CSVReader(new FileReader(file)) 460 | 461 | Iterator lines = reader.readAll().iterator() 462 | 463 | // Skip the header 464 | lines.next() 465 | 466 | // Verify first task 467 | String[] line = lines.next() 468 | assertNotNull line 469 | assertEquals 11, line.length 470 | assertEquals "1337", line[9] 471 | 472 | // Verify second task 473 | line = lines.next() 474 | assertNotNull line 475 | assertEquals 11, line.length 476 | assertEquals "1337", line[9] 477 | 478 | reader.close() 479 | } 480 | } 481 | 482 | @Test 483 | void extractsCPUInfo() { 484 | def mockSysInfo = new StubFor(SysInfo) 485 | def cpuId = "Batman i9 PPC 5Ghz TurboPascal" 486 | mockSysInfo.demand.getCPUIdentifier() { cpuId } 487 | mockSysInfo.ignore('getMaxMemory') 488 | mockSysInfo.ignore('getOSIdentifier') 489 | 490 | mockSysInfo.use { 491 | File file = mkTemporaryFile "test.csv" 492 | CSVReporter reporter = new CSVReporter([ output: file.getPath() ], mockLogger) 493 | 494 | reporter.run([ 495 | new Timing(100, "task1", true, false, true), 496 | new Timing(200, "task2", false, true, false) 497 | ]) 498 | 499 | CSVReader reader = new CSVReader(new FileReader(file)) 500 | 501 | Iterator lines = reader.readAll().iterator() 502 | 503 | // Skip the header 504 | lines.next() 505 | 506 | // Verify first task 507 | String[] line = lines.next() 508 | assertNotNull line 509 | assertEquals 11, line.length 510 | assertEquals cpuId, line[8] 511 | 512 | // Verify second task 513 | line = lines.next() 514 | assertNotNull line 515 | assertEquals 11, line.length 516 | assertEquals cpuId, line[8] 517 | 518 | reader.close() 519 | } 520 | } 521 | } 522 | --------------------------------------------------------------------------------