├── src
├── test
│ ├── resources
│ │ └── mockito-extensions
│ │ │ └── org.mockito.plugins.MockMaker
│ └── kotlin
│ │ └── com
│ │ └── dg
│ │ └── watcher
│ │ ├── PluginDescriptorTest.kt
│ │ ├── history
│ │ ├── HistoryTest.kt
│ │ └── graph
│ │ │ ├── HistoryGraphTooltipGeneratorTest.kt
│ │ │ ├── HistoryGraphUrlGeneratorTest.kt
│ │ │ └── HistoryGraphDataSetGenerationTest.kt
│ │ ├── base
│ │ └── extension
│ │ │ ├── FilePathExtensionTest.kt
│ │ │ ├── BuildExtensionTest.kt
│ │ │ └── ProjectExtensionTest.kt
│ │ ├── watching
│ │ ├── saving
│ │ │ └── SizeSavingTest.kt
│ │ ├── surveying
│ │ │ └── SizeSurveyingTest.kt
│ │ ├── loading
│ │ │ └── ApkLoadingTest.kt
│ │ └── ApkSizeWatchingTest.kt
│ │ ├── validation
│ │ └── InputValidationTest.kt
│ │ └── PluginTest.kt
└── main
│ ├── java
│ └── com
│ │ └── dg
│ │ └── watcher
│ │ ├── watching
│ │ ├── saving
│ │ │ ├── SizeEntry.kt
│ │ │ └── SizeSaving.kt
│ │ ├── surveying
│ │ │ ├── SizeSurveyingResult.kt
│ │ │ └── SizeSurveying.kt
│ │ ├── loading
│ │ │ └── ApkLoading.kt
│ │ └── ApkSizeWatching.kt
│ │ ├── base
│ │ ├── extension
│ │ │ ├── FilePathExtension.kt
│ │ │ ├── BuildExtension.kt
│ │ │ └── ProjectExtension.kt
│ │ ├── Alias.kt
│ │ ├── Log.kt
│ │ └── Const.kt
│ │ ├── history
│ │ ├── graph
│ │ │ ├── HistoryGraphUrlGenerator.kt
│ │ │ ├── HistoryGraphTooltipGenerator.kt
│ │ │ ├── HistoryGraphDataSetGeneration.kt
│ │ │ └── HistoryGraph.kt
│ │ └── History.kt
│ │ ├── validation
│ │ └── InputValidation.kt
│ │ ├── Plugin.kt
│ │ └── PluginDescriptor.java
│ └── resources
│ ├── index.jelly
│ └── com
│ └── dg
│ └── watcher
│ ├── Plugin
│ ├── help-thresholdInMb.html
│ ├── help-customPathToApk.html
│ └── config.jelly
│ └── history
│ └── History
│ └── index.jelly
├── .gitignore
├── README.md
├── LICENSE
└── pom.xml
/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker:
--------------------------------------------------------------------------------
1 | mock-maker-inline
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .classpath
2 | .project
3 | .settings/
4 | target/
5 | work/
6 | .idea
7 | *.iml
8 | release.properties
9 | pom.xml.releaseBackup
--------------------------------------------------------------------------------
/src/main/java/com/dg/watcher/watching/saving/SizeEntry.kt:
--------------------------------------------------------------------------------
1 | package com.dg.watcher.watching.saving
2 |
3 |
4 | data class SizeEntry(val buildName: String, val sizeInByte: Long)
--------------------------------------------------------------------------------
/src/main/java/com/dg/watcher/base/extension/FilePathExtension.kt:
--------------------------------------------------------------------------------
1 | package com.dg.watcher.base.extension
2 |
3 | import hudson.FilePath
4 |
5 |
6 | fun FilePath?.exists() = this?.exists() ?: false
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [Watch over the changing size of your .apk file and fail your build once the change exceeds your specified threshold.](https://wiki.jenkins.io/display/JENKINS/Android+Apk+Size+Watcher+Plugin)
--------------------------------------------------------------------------------
/src/main/resources/index.jelly:
--------------------------------------------------------------------------------
1 |
2 |
3 | Watch over the changing size of your .apk file and fail your build once the change exceeds your specified threshold.
4 |
--------------------------------------------------------------------------------
/src/main/java/com/dg/watcher/base/extension/BuildExtension.kt:
--------------------------------------------------------------------------------
1 | package com.dg.watcher.base.extension
2 |
3 | import com.dg.watcher.base.Build
4 |
5 |
6 | fun Build.getFileInWorkspace(path: String) = getWorkspace()?.child(path)
--------------------------------------------------------------------------------
/src/main/java/com/dg/watcher/watching/surveying/SizeSurveyingResult.kt:
--------------------------------------------------------------------------------
1 | package com.dg.watcher.watching.surveying
2 |
3 |
4 | enum class SizeSurveyingResult {
5 | SIZE_THRESHOLD_MET,
6 | SIZE_THRESHOLD_EXCEEDED
7 | }
--------------------------------------------------------------------------------
/src/main/java/com/dg/watcher/base/extension/ProjectExtension.kt:
--------------------------------------------------------------------------------
1 | package com.dg.watcher.base.extension
2 |
3 | import com.dg.watcher.base.Project
4 |
5 |
6 | fun Project.getFileInWorkspace(path: String) = getSomeWorkspace()?.child(path)
--------------------------------------------------------------------------------
/src/main/java/com/dg/watcher/base/Alias.kt:
--------------------------------------------------------------------------------
1 | package com.dg.watcher.base
2 |
3 | import hudson.model.AbstractBuild
4 | import hudson.model.AbstractProject
5 |
6 |
7 | typealias Build = AbstractBuild<*, *>
8 | typealias Project = AbstractProject<*, *>
--------------------------------------------------------------------------------
/src/main/resources/com/dg/watcher/Plugin/help-thresholdInMb.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Specify the allowed size difference between the last and the latest .apk File.
4 | Exceeding it will fail the build. The unit of the threshold is megabytes.
5 |
--------------------------------------------------------------------------------
/src/main/resources/com/dg/watcher/Plugin/help-customPathToApk.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Specify the path to the directory of your .apk file if you don't use the default one.
4 | Leaving this field empty will default the path to app/build/outputs/apk/.
5 |
--------------------------------------------------------------------------------
/src/main/resources/com/dg/watcher/Plugin/config.jelly:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/main/resources/com/dg/watcher/history/History/index.jelly:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/test/kotlin/com/dg/watcher/PluginDescriptorTest.kt:
--------------------------------------------------------------------------------
1 | package com.dg.watcher
2 |
3 | import org.hamcrest.Matchers.`is`
4 | import org.hamcrest.Matchers.equalTo
5 | import org.junit.Assert.assertThat
6 | import org.junit.Test
7 |
8 |
9 | class PluginDescriptorTest {
10 | @Test
11 | fun `Should specify the name of the post build action`() = assertThat(PluginDescriptor().displayName,
12 | `is`(equalTo("Watch over the changing size of your .apk file")))
13 | }
--------------------------------------------------------------------------------
/src/main/java/com/dg/watcher/base/Log.kt:
--------------------------------------------------------------------------------
1 | package com.dg.watcher.base
2 |
3 | import java.util.logging.Level
4 | import java.util.logging.Level.INFO
5 | import java.util.logging.Level.SEVERE
6 | import java.util.logging.Level.WARNING
7 | import java.util.logging.Logger.getLogger
8 |
9 |
10 | fun err(tag: String, msg: String) = log(SEVERE, tag, msg)
11 |
12 | fun warn(tag: String, msg: String) = log(WARNING, tag, msg)
13 |
14 | fun info(tag: String, msg: String) = log(INFO, tag, msg)
15 |
16 | private fun log(level: Level, tag: String, msg: String) = getLogger(tag).log(level, msg)
--------------------------------------------------------------------------------
/src/main/java/com/dg/watcher/history/graph/HistoryGraphUrlGenerator.kt:
--------------------------------------------------------------------------------
1 | package com.dg.watcher.history.graph
2 |
3 | import com.dg.watcher.base.Project
4 | import org.jfree.chart.urls.CategoryURLGenerator
5 | import org.jfree.data.category.CategoryDataset
6 |
7 |
8 | class HistoryGraphUrlGenerator(private val project: Project) : CategoryURLGenerator {
9 | override fun generateURL(categoryDataSet: CategoryDataset, series: Int, itemIndex: Int) =
10 | project.getAbsoluteUrl() + getBuildNumber(categoryDataSet, itemIndex)
11 |
12 | private fun getBuildNumber(categoryDataSet: CategoryDataset, itemIndex: Int) =
13 | (categoryDataSet.getColumnKey(itemIndex) as String).replaceFirst("#", "")
14 | }
--------------------------------------------------------------------------------
/src/test/kotlin/com/dg/watcher/history/HistoryTest.kt:
--------------------------------------------------------------------------------
1 | package com.dg.watcher.history
2 |
3 | import com.nhaarman.mockito_kotlin.mock
4 | import org.hamcrest.Matchers.`is`
5 | import org.hamcrest.Matchers.equalTo
6 | import org.junit.Assert.assertThat
7 | import org.junit.Test
8 |
9 |
10 | class HistoryTest {
11 | @Test
12 | fun `Should specify the shown icon`() = assertThat(history().iconFileName, `is`(equalTo("graph.gif")))
13 |
14 | @Test
15 | fun `Should specify the shown label`() = assertThat(history().displayName, `is`(equalTo("Apk Size History")))
16 |
17 | @Test
18 | fun `Should specify the used domain`() = assertThat(history().urlName, `is`(equalTo("apk_size_history")))
19 |
20 | private fun history() = History(mock())
21 | }
--------------------------------------------------------------------------------
/src/main/java/com/dg/watcher/history/graph/HistoryGraphTooltipGenerator.kt:
--------------------------------------------------------------------------------
1 | package com.dg.watcher.history.graph
2 |
3 | import org.jfree.chart.labels.CategoryToolTipGenerator
4 | import org.jfree.data.category.CategoryDataset
5 | import com.dg.watcher.base.GRAPH_TOOLTIP
6 | import java.lang.String.format
7 | import java.util.Locale.ENGLISH
8 |
9 |
10 | class HistoryGraphTooltipGenerator : CategoryToolTipGenerator {
11 | override fun generateToolTip(categoryDataset: CategoryDataset, series: Int, itemIndex: Int): String {
12 | val buildName = categoryDataset.getColumnKey(itemIndex).toString()
13 | val apkSizeInMb = categoryDataset.getValue(series, itemIndex).toFloat()
14 |
15 | return format(ENGLISH, GRAPH_TOOLTIP, buildName, apkSizeInMb)
16 | }
17 | }
--------------------------------------------------------------------------------
/src/test/kotlin/com/dg/watcher/base/extension/FilePathExtensionTest.kt:
--------------------------------------------------------------------------------
1 | package com.dg.watcher.base.extension
2 |
3 | import com.nhaarman.mockito_kotlin.mock
4 | import com.nhaarman.mockito_kotlin.verify
5 | import hudson.FilePath
6 | import org.junit.Assert.assertFalse
7 | import org.junit.Test
8 |
9 |
10 | class FilePathExtensionTest {
11 | @Test
12 | fun `A null file path doesn't exist per default`() {
13 | // GIVEN
14 | val filePath: FilePath? = null
15 |
16 | // THEN
17 | assertFalse(filePath.exists())
18 | }
19 |
20 | @Test
21 | fun `A non null file path checks for its existence`() {
22 | // GIVEN
23 | val filePath: FilePath? = mock()
24 |
25 | // WHEN
26 | filePath.exists()
27 |
28 | // THEN
29 | verify(filePath)?.exists()
30 | }
31 | }
--------------------------------------------------------------------------------
/src/test/kotlin/com/dg/watcher/base/extension/BuildExtensionTest.kt:
--------------------------------------------------------------------------------
1 | package com.dg.watcher.base.extension
2 |
3 | import com.dg.watcher.base.Build
4 | import com.nhaarman.mockito_kotlin.doReturn
5 | import com.nhaarman.mockito_kotlin.mock
6 | import hudson.FilePath
7 | import org.hamcrest.Matchers.`is`
8 | import org.hamcrest.Matchers.equalTo
9 | import org.junit.Assert.assertThat
10 | import org.junit.Test
11 |
12 |
13 | class BuildExtensionTest {
14 | @Test
15 | fun `Should provide a shortcut to get a file inside the builds's workspace`() {
16 | // GIVEN
17 | val file = mock()
18 | val workspace = mock { on { child("test/debug.apk") } doReturn file }
19 | val build = mock { on { getWorkspace() } doReturn workspace }
20 |
21 | // THEN
22 | assertThat(build.getFileInWorkspace("test/debug.apk"), `is`(equalTo(file)))
23 | }
24 | }
--------------------------------------------------------------------------------
/src/test/kotlin/com/dg/watcher/base/extension/ProjectExtensionTest.kt:
--------------------------------------------------------------------------------
1 | package com.dg.watcher.base.extension
2 |
3 | import com.dg.watcher.base.Project
4 | import com.nhaarman.mockito_kotlin.doReturn
5 | import com.nhaarman.mockito_kotlin.mock
6 | import hudson.FilePath
7 | import org.hamcrest.Matchers.`is`
8 | import org.hamcrest.Matchers.equalTo
9 | import org.junit.Assert.assertThat
10 | import org.junit.Test
11 |
12 |
13 | class ProjectExtensionTest {
14 | @Test
15 | fun `Should provide a shortcut to get a file inside the project's workspace`() {
16 | // GIVEN
17 | val file = mock()
18 | val workspace = mock { on { child("test/debug.apk") } doReturn file }
19 | val project = mock { on { getSomeWorkspace() } doReturn workspace }
20 |
21 | // THEN
22 | assertThat(project.getFileInWorkspace("test/debug.apk"), `is`(equalTo(file)))
23 | }
24 | }
--------------------------------------------------------------------------------
/src/main/java/com/dg/watcher/history/graph/HistoryGraphDataSetGeneration.kt:
--------------------------------------------------------------------------------
1 | package com.dg.watcher.history.graph
2 |
3 | import com.dg.watcher.base.CONVERSION_FACTOR_BYTE_TO_MEGABYTE
4 | import com.dg.watcher.base.GRAPH_LEGEND
5 | import com.dg.watcher.base.GRAPH_MAX_ENTRY_COUNT
6 | import com.dg.watcher.watching.saving.SizeEntry
7 | import org.jfree.data.category.DefaultCategoryDataset
8 |
9 |
10 | fun generateGraphDataSet(entries: List) = DefaultCategoryDataset().apply {
11 | for((buildName, sizeInByte) in cutToMaximumEntryCount(entries)) {
12 | val sizeInMegaByte = sizeInByte / CONVERSION_FACTOR_BYTE_TO_MEGABYTE
13 |
14 | addValue(sizeInMegaByte, GRAPH_LEGEND, buildName)
15 | }
16 | }
17 |
18 | private fun cutToMaximumEntryCount(entries: List): List {
19 | var indexOfFirstIncludedEntry = entries.size - GRAPH_MAX_ENTRY_COUNT
20 |
21 | if(indexOfFirstIncludedEntry < 0) indexOfFirstIncludedEntry = 0
22 |
23 | return entries.subList(indexOfFirstIncludedEntry, entries.size)
24 | }
--------------------------------------------------------------------------------
/src/test/kotlin/com/dg/watcher/history/graph/HistoryGraphTooltipGeneratorTest.kt:
--------------------------------------------------------------------------------
1 | package com.dg.watcher.history.graph
2 |
3 | import com.dg.watcher.base.GRAPH_LEGEND
4 | import hudson.util.DataSetBuilder
5 | import org.hamcrest.Matchers.`is`
6 | import org.hamcrest.Matchers.equalTo
7 | import org.junit.Assert.assertThat
8 | import org.junit.Test
9 |
10 |
11 | class HistoryGraphTooltipGeneratorTest {
12 | @Test
13 | fun `Should generate a tooltip consisting of the builds name and the size of the apk`() {
14 | // GIVEN
15 | val entry = createDataSetWithSizeEntry("#99", 1.5f)
16 |
17 | // WHEN
18 | val tooltip = HistoryGraphTooltipGenerator().generateToolTip(entry, 0, 0)
19 |
20 | // THEN
21 | assertThat(tooltip, `is`(equalTo("Build #99: 1.5 Megabytes")))
22 | }
23 |
24 | private fun createDataSetWithSizeEntry(buildName: String, apkSizeInMb: Float) = DataSetBuilder().run {
25 | add(apkSizeInMb, GRAPH_LEGEND, buildName)
26 |
27 | build()
28 | }
29 | }
--------------------------------------------------------------------------------
/src/main/java/com/dg/watcher/history/History.kt:
--------------------------------------------------------------------------------
1 | package com.dg.watcher.history
2 |
3 | import com.dg.watcher.base.Project
4 | import com.dg.watcher.history.graph.HistoryGraph
5 | import hudson.model.Action
6 | import org.kohsuke.stapler.StaplerRequest
7 | import org.kohsuke.stapler.StaplerResponse
8 |
9 |
10 | class History(val project: Project) : Action {
11 | private var graph: HistoryGraph? = null
12 |
13 | init {
14 | updateHistory()
15 | }
16 |
17 | override fun getIconFileName() = "graph.gif"
18 |
19 | override fun getDisplayName() = "Apk Size History"
20 |
21 | override fun getUrlName() = "apk_size_history"
22 |
23 | fun updateHistory() {
24 | graph = HistoryGraph(project)
25 | }
26 |
27 | // Used in index.jelly
28 | fun doHistory(request: StaplerRequest, response: StaplerResponse) {
29 | graph?.drawGraph(request, response)
30 | }
31 |
32 | // Used in index.jelly
33 | fun doHistoryTooltips(request: StaplerRequest, response: StaplerResponse) {
34 | graph?.drawGraphTooltips(request, response)
35 | }
36 | }
--------------------------------------------------------------------------------
/src/main/java/com/dg/watcher/watching/loading/ApkLoading.kt:
--------------------------------------------------------------------------------
1 | package com.dg.watcher.watching.loading
2 |
3 | import com.dg.watcher.base.APK_DEFAULT_DIR
4 | import com.dg.watcher.base.APK_FILE_FILTER
5 | import com.dg.watcher.base.Build
6 | import com.dg.watcher.base.extension.getFileInWorkspace
7 | import hudson.FilePath
8 |
9 |
10 | fun loadApk(build: Build, customPathToApk: String = ""): FilePath? {
11 | val apkDirectory = getApkDirectory(build, customPathToApk)
12 |
13 | return apkDirectory?.let { loadApkFiles(it).firstOrNull() }
14 | }
15 |
16 | private fun getApkDirectory(build: Build, customPathToApk: String) =
17 | build.getFileInWorkspace(decidePathToApk(customPathToApk))
18 |
19 | private fun decidePathToApk(customPathToApk: String) =
20 | if(customPathToApk.isNotBlank()) {
21 | customPathToApk
22 | }
23 | else {
24 | APK_DEFAULT_DIR
25 | }
26 |
27 | private fun loadApkFiles(apkDirectory: FilePath) =
28 | if(apkDirectory.exists()) {
29 | apkDirectory.list(APK_FILE_FILTER).toList()
30 | }
31 | else {
32 | emptyList()
33 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Daniel Gronau
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/test/kotlin/com/dg/watcher/history/graph/HistoryGraphUrlGeneratorTest.kt:
--------------------------------------------------------------------------------
1 | package com.dg.watcher.history.graph
2 |
3 | import com.dg.watcher.base.GRAPH_LEGEND
4 | import com.nhaarman.mockito_kotlin.doReturn
5 | import com.nhaarman.mockito_kotlin.mock
6 | import hudson.util.DataSetBuilder
7 | import org.hamcrest.Matchers.`is`
8 | import org.hamcrest.Matchers.equalTo
9 | import org.junit.Assert.assertThat
10 | import org.junit.Test
11 |
12 |
13 | class HistoryGraphUrlGeneratorTest {
14 | @Test
15 | fun `Should generate a url linking to the build of the size entry`() {
16 | // GIVEN
17 | val entry = createDataSetWithSizeEntry("#99")
18 |
19 | // WHEN
20 | val url = generator().generateURL(entry, 0, 0)
21 |
22 | // THEN
23 | assertThat(url, `is`(equalTo(".../jenkins/job/test/99")))
24 | }
25 |
26 | private fun generator() = HistoryGraphUrlGenerator(
27 | mock { on { getAbsoluteUrl() } doReturn ".../jenkins/job/test/" })
28 |
29 | private fun createDataSetWithSizeEntry(buildName: String) = DataSetBuilder().run {
30 | add(0f, GRAPH_LEGEND, buildName)
31 |
32 | build()
33 | }
34 | }
--------------------------------------------------------------------------------
/src/main/java/com/dg/watcher/validation/InputValidation.kt:
--------------------------------------------------------------------------------
1 | package com.dg.watcher.validation
2 |
3 | import com.dg.watcher.base.Project
4 | import com.dg.watcher.base.extension.exists
5 | import com.dg.watcher.base.extension.getFileInWorkspace
6 | import hudson.util.FormValidation
7 | import hudson.util.FormValidation.error
8 | import hudson.util.FormValidation.ok
9 |
10 |
11 | fun validateThresholdInMb(input: String): FormValidation =
12 | try {
13 | val thresholdInMb = input.toFloat()
14 |
15 | if(thresholdInMb >= 0f) {
16 | ok()
17 | }
18 | else {
19 | error("The threshold cannot be negative.")
20 | }
21 | }
22 | catch(e: Exception) {
23 | error("The threshold must be a floating point number.")
24 | }
25 |
26 | fun validateCustomPathToApk(input: String, project: Project): FormValidation =
27 | if(noPathSpecified(input) || validPathSpecified(input, project)) {
28 | ok()
29 | }
30 | else {
31 | error("The specified path does not exist.")
32 | }
33 |
34 | private fun noPathSpecified(path: String) = path.isBlank()
35 |
36 | private fun validPathSpecified(path: String, project: Project) =
37 | project.getFileInWorkspace(path).exists()
--------------------------------------------------------------------------------
/src/main/java/com/dg/watcher/Plugin.kt:
--------------------------------------------------------------------------------
1 | package com.dg.watcher
2 |
3 | import com.dg.watcher.base.Build
4 | import com.dg.watcher.base.Project
5 | import com.dg.watcher.history.History
6 | import com.dg.watcher.watching.watchApkSize
7 | import hudson.Launcher
8 | import hudson.model.BuildListener
9 | import hudson.tasks.BuildStepMonitor.NONE
10 | import hudson.tasks.Notifier
11 | import org.kohsuke.stapler.DataBoundConstructor
12 |
13 |
14 | class Plugin @DataBoundConstructor constructor(val thresholdInMb: Float, val customPathToApk: String) : Notifier() {
15 | override fun getDescriptor() = super.getDescriptor() as PluginDescriptor
16 |
17 | override fun getRequiredMonitorService() = NONE
18 |
19 | override fun getProjectActions(project: Project) = listOf(History(project))
20 |
21 | override fun perform(build: Build, launcher: Launcher, listener: BuildListener): Boolean {
22 | val buildPermitted = watchApkSize(build, listener.logger, thresholdInMb, customPathToApk)
23 |
24 | updateHistoryAction(build.getProject())
25 |
26 | return buildPermitted
27 | }
28 |
29 | private fun updateHistoryAction(project: Project) = getHistoryAction(project).updateHistory()
30 |
31 | private fun getHistoryAction(project: Project) = project.getAction(History::class.java)
32 | }
--------------------------------------------------------------------------------
/src/main/java/com/dg/watcher/watching/surveying/SizeSurveying.kt:
--------------------------------------------------------------------------------
1 | package com.dg.watcher.watching.surveying
2 |
3 | import com.dg.watcher.base.CONVERSION_FACTOR_BYTE_TO_MEGABYTE
4 | import com.dg.watcher.watching.saving.SizeEntry
5 | import com.dg.watcher.watching.surveying.SizeSurveyingResult.SIZE_THRESHOLD_EXCEEDED
6 | import com.dg.watcher.watching.surveying.SizeSurveyingResult.SIZE_THRESHOLD_MET
7 |
8 |
9 | fun surveySizes(sizes: List, thresholdInMb: Float): SizeSurveyingResult {
10 | if(atLeastTwoSizesRecorded(sizes)) {
11 | val latestSizeDifferenceInMb = calculateLatestSizeDifferenceInMb(sizes)
12 |
13 | if(sizeThresholdExceeded(latestSizeDifferenceInMb, thresholdInMb)) {
14 | return SIZE_THRESHOLD_EXCEEDED
15 | }
16 | }
17 |
18 | return SIZE_THRESHOLD_MET
19 | }
20 |
21 | private fun atLeastTwoSizesRecorded(sizes: List) = sizes.size > 1
22 |
23 | private fun calculateLatestSizeDifferenceInMb(sizes: List): Float {
24 | val sizeOfPreviousApk = sizes[sizes.lastIndex - 1].sizeInByte
25 | val sizeOfLatestApk = sizes[sizes.lastIndex].sizeInByte
26 |
27 | return (sizeOfLatestApk - sizeOfPreviousApk) / CONVERSION_FACTOR_BYTE_TO_MEGABYTE
28 | }
29 |
30 | private fun sizeThresholdExceeded(differenceInMb: Float, thresholdInMb: Float) = differenceInMb > thresholdInMb
--------------------------------------------------------------------------------
/src/main/java/com/dg/watcher/PluginDescriptor.java:
--------------------------------------------------------------------------------
1 | package com.dg.watcher;
2 |
3 | import hudson.Extension;
4 | import hudson.model.AbstractProject;
5 | import hudson.tasks.BuildStepDescriptor;
6 | import hudson.tasks.Publisher;
7 | import hudson.util.FormValidation;
8 | import org.kohsuke.stapler.AncestorInPath;
9 | import org.kohsuke.stapler.QueryParameter;
10 | import static com.dg.watcher.validation.InputValidationKt.validateCustomPathToApk;
11 | import static com.dg.watcher.validation.InputValidationKt.validateThresholdInMb;
12 |
13 |
14 | @Extension
15 | public class PluginDescriptor extends BuildStepDescriptor {
16 | public PluginDescriptor() {
17 | super(Plugin.class);
18 | }
19 |
20 | @Override
21 | public boolean isApplicable(Class type) {
22 | return true;
23 | }
24 |
25 | @Override
26 | public String getDisplayName() {
27 | return "Watch over the changing size of your .apk file";
28 | }
29 |
30 | // Used for validation in config.jelly
31 | public FormValidation doCheckThresholdInMb(@QueryParameter String value) {
32 | return validateThresholdInMb(value);
33 | }
34 |
35 | // Used for validation in config.jelly
36 | public FormValidation doCheckCustomPathToApk(@QueryParameter String value, @AncestorInPath AbstractProject, ?> project) {
37 | return validateCustomPathToApk(value, project);
38 | }
39 | }
--------------------------------------------------------------------------------
/src/main/java/com/dg/watcher/base/Const.kt:
--------------------------------------------------------------------------------
1 | package com.dg.watcher.base
2 |
3 | import org.jfree.chart.plot.DefaultDrawingSupplier
4 | import org.jfree.chart.plot.DefaultDrawingSupplier.*
5 | import java.awt.BasicStroke
6 | import java.awt.geom.Ellipse2D
7 | import java.io.File.separator
8 | import java.lang.System.getProperty
9 |
10 |
11 | val DB_FILE = separator + "android_apk_size_watcher_plugin" + separator + "database.csv"
12 | val DB_ENCODING = "UTF-8"
13 | val DB_ROW_SEPARATOR: String = getProperty("line.separator")
14 | val DB_COLUMN_SEPARATOR = ","
15 |
16 | val APK_FILE_FILTER = "*.apk"
17 | val APK_DEFAULT_DIR = "app" + separator + "build" + separator + "outputs" + separator + "apk"
18 |
19 | val BUILD_ALLOWED = true
20 | val BUILD_FORBIDDEN = false
21 |
22 | val GRAPH_WIDTH = 800
23 | val GRAPH_HEIGHT = 600
24 | val GRAPH_MAX_ENTRY_COUNT = 20
25 | val GRAPH_TITLE = "Apk Size History"
26 | val GRAPH_LEGEND = "Debug Apk File"
27 | val GRAPH_X_AXIS = "Build"
28 | val GRAPH_Y_AXIS = "Megabytes"
29 | val GRAPH_TOOLTIP = "Build %s: %.1f Megabytes"
30 | val GRAPH_LINE = BasicStroke(1.5f)
31 | val GRAPH_LINE_DOT = DefaultDrawingSupplier(
32 | DEFAULT_PAINT_SEQUENCE,
33 | DEFAULT_OUTLINE_PAINT_SEQUENCE,
34 | DEFAULT_STROKE_SEQUENCE,
35 | DEFAULT_OUTLINE_STROKE_SEQUENCE,
36 | arrayOf(Ellipse2D.Float(-4f, -4f, 8f, 8f)))
37 |
38 | val CONVERSION_FACTOR_BYTE_TO_MEGABYTE = 1024f * 1024f
--------------------------------------------------------------------------------
/src/test/kotlin/com/dg/watcher/watching/saving/SizeSavingTest.kt:
--------------------------------------------------------------------------------
1 | package com.dg.watcher.watching.saving
2 |
3 | import com.dg.watcher.base.Build
4 | import com.dg.watcher.base.Project
5 | import com.nhaarman.mockito_kotlin.mock
6 | import com.nhaarman.mockito_kotlin.whenever
7 | import org.hamcrest.Matchers.*
8 | import org.junit.Assert.assertThat
9 | import org.junit.Rule
10 | import org.junit.Test
11 | import org.junit.rules.TemporaryFolder
12 | import hudson.FilePath
13 |
14 |
15 | class SizeSavingTest {
16 | @Rule @JvmField
17 | val tempDir = TemporaryFolder()
18 |
19 |
20 | @Test
21 | fun `Should load the saved sizes`() {
22 | // GIVEN
23 | saveApkSize(mockApk(sizeInByte = 10000000L), mockBuild(name = "#1"))
24 | saveApkSize(mockApk(sizeInByte = 11000000L), mockBuild(name = "#2"))
25 |
26 | // WHEN
27 | val sizes = loadApkSizes(mockProject())
28 |
29 | // THEN
30 | assertThat(sizes, hasSize(2))
31 | assertThat(sizes.first().buildName, `is`(equalTo("#1")))
32 | assertThat(sizes.first().sizeInByte, `is`(equalTo(10000000L)))
33 | assertThat(sizes.last().buildName, `is`(equalTo("#2")))
34 | assertThat(sizes.last().sizeInByte, `is`(equalTo(11000000L)))
35 | }
36 |
37 | @Test
38 | fun `Should not load any sizes when there are none saved`() =
39 | assertThat(loadApkSizes(mockProject()), hasSize(0))
40 |
41 | private fun mockBuild(name: String) = mockBuild().apply {
42 | whenever(getDisplayName()).thenReturn(name)
43 | }
44 |
45 | private fun mockBuild() = mock().apply {
46 | val project = mockProject()
47 |
48 | whenever(getProject()).thenReturn(project)
49 | }
50 |
51 | private fun mockProject() = mock().apply {
52 | whenever(getRootDir()).thenReturn(tempDir.root)
53 | }
54 |
55 | private fun mockApk(sizeInByte: Long) = mock().apply {
56 | whenever(length()).thenReturn(sizeInByte)
57 | }
58 | }
--------------------------------------------------------------------------------
/src/main/java/com/dg/watcher/watching/saving/SizeSaving.kt:
--------------------------------------------------------------------------------
1 | package com.dg.watcher.watching.saving
2 |
3 | import com.dg.watcher.base.*
4 | import hudson.FilePath
5 | import org.apache.commons.io.FileUtils.readFileToString
6 | import org.apache.commons.io.FileUtils.writeStringToFile
7 | import java.io.File
8 | import java.io.IOException
9 |
10 |
11 | fun saveApkSize(apk: FilePath, build: Build) {
12 | try {
13 | insertApkSize(apk, build)
14 | }
15 | catch(e: IOException) {
16 | e.printStackTrace()
17 | }
18 | }
19 |
20 | fun loadApkSizes(project: Project) =
21 | try {
22 | val sizes = arrayListOf()
23 |
24 | loadRowsFromDatabase(loadDatabase(project)).forEach {
25 | sizes += createSizeEntryFromRow(it)
26 | }
27 |
28 | sizes.toList()
29 | }
30 | catch(e: IOException) {
31 | e.printStackTrace()
32 |
33 | emptyList()
34 | }
35 |
36 | private fun insertApkSize(apk: FilePath, build: Build) {
37 | val db = loadDatabase(build.getProject())
38 |
39 | writeStringToFile(db, createDatabaseRow(apk, build, db), DB_ENCODING, true)
40 | }
41 |
42 | private fun loadDatabase(project: Project) = File(project.getRootDir().absolutePath + DB_FILE)
43 |
44 | private fun createDatabaseRow(apk: FilePath, build: Build, db: File) = createRowSeparator(db) + createRowData(apk, build)
45 |
46 | private fun createRowSeparator(database: File) = if(database.exists()) DB_ROW_SEPARATOR else ""
47 |
48 | private fun createRowData(apk: FilePath, build: Build) = build.getDisplayName() + DB_COLUMN_SEPARATOR + apk.length()
49 |
50 | private fun loadRowsFromDatabase(database: File) =
51 | if(database.exists()) {
52 | readFileToString(database, DB_ENCODING).split(DB_ROW_SEPARATOR)
53 | }
54 | else {
55 | emptyList()
56 | }
57 |
58 | private fun createSizeEntryFromRow(entryRow: String): SizeEntry {
59 | val (name, size) = entryRow.split(DB_COLUMN_SEPARATOR)
60 |
61 | return SizeEntry(name, size.toLong())
62 | }
--------------------------------------------------------------------------------
/src/test/kotlin/com/dg/watcher/validation/InputValidationTest.kt:
--------------------------------------------------------------------------------
1 | package com.dg.watcher.validation
2 |
3 | import com.dg.watcher.base.Project
4 | import com.nhaarman.mockito_kotlin.doReturn
5 | import com.nhaarman.mockito_kotlin.mock
6 | import hudson.FilePath
7 | import hudson.util.FormValidation.ok
8 | import org.hamcrest.Matchers.`is`
9 | import org.hamcrest.Matchers.equalTo
10 | import org.junit.Assert.assertThat
11 | import org.junit.Before
12 | import org.junit.Rule
13 | import org.junit.Test
14 | import org.junit.rules.TemporaryFolder
15 |
16 |
17 | class InputValidationTest {
18 | @Rule @JvmField
19 | val tempDir = TemporaryFolder()
20 |
21 |
22 | @Before
23 | fun setUp() {
24 | createTempApkFolder()
25 | }
26 |
27 | @Test
28 | fun `Should allow a positive threshold`() =
29 | assertThat(validateThresholdInMb("2.5"), `is`(equalTo(ok())))
30 |
31 | @Test
32 | fun `Should indicate a negative threshold with an error message`() =
33 | assertThat(validateThresholdInMb("-2.5").message, `is`(equalTo("The threshold cannot be negative.")))
34 |
35 | @Test
36 | fun `Should indicate a non integral threshold with an error message`() =
37 | assertThat(validateThresholdInMb("abc").message, `is`(equalTo("The threshold must be a floating point number.")))
38 |
39 | @Test
40 | fun `Should allow an empty custom path to the apk`() =
41 | assertThat(validateCustomPathToApk("", mockProject()), `is`(equalTo(ok())))
42 |
43 | @Test
44 | fun `Should allow an existing custom path to the apk`() =
45 | assertThat(validateCustomPathToApk("temp_apk_folder", mockProject()), `is`(equalTo(ok())))
46 |
47 | @Test
48 | fun `Should indicate a invalid custom path to the apk with an error message`() =
49 | assertThat(validateCustomPathToApk("not/existing/path", mockProject()).message,
50 | `is`(equalTo("The specified path does not exist.")))
51 |
52 | private fun createTempApkFolder() = tempDir.newFolder("temp_apk_folder")
53 |
54 | private fun mockProject() = mock {
55 | on { getSomeWorkspace() } doReturn FilePath(tempDir.root)
56 | }
57 | }
--------------------------------------------------------------------------------
/src/main/java/com/dg/watcher/watching/ApkSizeWatching.kt:
--------------------------------------------------------------------------------
1 | package com.dg.watcher.watching
2 |
3 | import com.dg.watcher.base.BUILD_ALLOWED
4 | import com.dg.watcher.base.BUILD_FORBIDDEN
5 | import com.dg.watcher.base.Build
6 | import com.dg.watcher.watching.loading.loadApk
7 | import com.dg.watcher.watching.saving.loadApkSizes
8 | import com.dg.watcher.watching.saving.saveApkSize
9 | import com.dg.watcher.watching.surveying.SizeSurveyingResult.SIZE_THRESHOLD_EXCEEDED
10 | import com.dg.watcher.watching.surveying.surveySizes
11 | import java.io.PrintStream
12 |
13 |
14 | fun watchApkSize(build: Build, logger: PrintStream, thresholdInMb: Float, customPathToApk: String = ""): Boolean {
15 | val apk = loadApk(build, customPathToApk)
16 |
17 | return if(apk != null) {
18 | saveApkSize(apk, build)
19 |
20 | evaluateSize(build, logger, thresholdInMb)
21 | }
22 | else {
23 | permitBuildWithoutApk(logger)
24 | }
25 | }
26 |
27 | private fun evaluateSize(build: Build, logger: PrintStream, thresholdInMb: Float) =
28 | if(surveySizes(loadApkSizes(build.getProject()), thresholdInMb) == SIZE_THRESHOLD_EXCEEDED) {
29 | cancelBuildWithApk(logger, thresholdInMb)
30 | }
31 | else {
32 | permitBuildWithApk(logger, thresholdInMb)
33 | }
34 |
35 | private fun cancelBuildWithApk(logger: PrintStream, thresholdInMb: Float) = BUILD_FORBIDDEN.also {
36 | logger.println("Android Apk Size Watcher Plugin: Build Failed")
37 | logger.println("Android Apk Size Watcher Plugin: The size difference between your " +
38 | "last and latest .apk file exceeded the specified threshold of $thresholdInMb megabytes.")
39 | }
40 |
41 | private fun permitBuildWithApk(logger: PrintStream, thresholdInMb: Float) = BUILD_ALLOWED.also {
42 | logger.println("Android Apk Size Watcher Plugin: Build Succeeded")
43 | logger.println("Android Apk Size Watcher Plugin: The size difference between your " +
44 | "last and latest .apk file met the specified threshold of $thresholdInMb megabytes.")
45 | }
46 |
47 | private fun permitBuildWithoutApk(logger: PrintStream) = BUILD_ALLOWED.also {
48 | logger.println("Android Apk Size Watcher Plugin: Couldn't detect a generated .apk file.")
49 | }
--------------------------------------------------------------------------------
/src/test/kotlin/com/dg/watcher/watching/surveying/SizeSurveyingTest.kt:
--------------------------------------------------------------------------------
1 | package com.dg.watcher.watching.surveying
2 |
3 | import com.dg.watcher.watching.saving.SizeEntry
4 | import com.dg.watcher.watching.surveying.SizeSurveyingResult.SIZE_THRESHOLD_EXCEEDED
5 | import com.dg.watcher.watching.surveying.SizeSurveyingResult.SIZE_THRESHOLD_MET
6 | import org.hamcrest.Matchers.`is`
7 | import org.hamcrest.Matchers.equalTo
8 | import org.junit.Assert.assertThat
9 | import org.junit.Test
10 |
11 |
12 | class SizeSurveyingTest {
13 | @Test
14 | fun `Should pass the size survey when there are no sizes recorded`() {
15 | // GIVEN
16 | val sizes = listOf()
17 |
18 | // THEN
19 | assertThat(surveySizes(sizes, thresholdInMb = 1f), `is`(equalTo(SIZE_THRESHOLD_MET)))
20 | }
21 |
22 | @Test
23 | fun `Should pass the size survey when only one size is recorded`() {
24 | // GIVEN
25 | val sizes = listOf(SizeEntry("#1", sizeInByte = 10000000L))
26 |
27 | // THEN
28 | assertThat(surveySizes(sizes, thresholdInMb = 1f), `is`(equalTo(SIZE_THRESHOLD_MET)))
29 | }
30 |
31 | @Test
32 | fun `Should pass the size survey when the size difference is below the threshold`() {
33 | // GIVEN
34 | val sizes = listOf(SizeEntry("#1", sizeInByte = 10000000L), SizeEntry("#2", sizeInByte = 11000000L))
35 |
36 | // THEN
37 | assertThat(surveySizes(sizes, thresholdInMb = 2f), `is`(equalTo(SIZE_THRESHOLD_MET)))
38 | }
39 |
40 | @Test
41 | fun `Should pass the size survey when the size difference is exactly the threshold`() {
42 | // GIVEN
43 | val sizes = listOf(SizeEntry("#1", sizeInByte = 10000000L), SizeEntry("#2", sizeInByte = 12000000L))
44 |
45 | // THEN
46 | assertThat(surveySizes(sizes, thresholdInMb = 2f), `is`(equalTo(SIZE_THRESHOLD_MET)))
47 | }
48 |
49 | @Test
50 | fun `Should fail the size survey when the size difference exceeds the threshold`() {
51 | // GIVEN
52 | val sizes = listOf(SizeEntry("#1", sizeInByte = 10000000L), SizeEntry("#2", sizeInByte = 14000000L))
53 |
54 | // THEN
55 | assertThat(surveySizes(sizes, thresholdInMb = 2f), `is`(equalTo(SIZE_THRESHOLD_EXCEEDED)))
56 | }
57 | }
--------------------------------------------------------------------------------
/src/test/kotlin/com/dg/watcher/history/graph/HistoryGraphDataSetGenerationTest.kt:
--------------------------------------------------------------------------------
1 | package com.dg.watcher.history.graph
2 |
3 | import com.dg.watcher.base.CONVERSION_FACTOR_BYTE_TO_MEGABYTE
4 | import com.dg.watcher.watching.saving.SizeEntry
5 | import org.hamcrest.Matchers.`is`
6 | import org.hamcrest.Matchers.equalTo
7 | import org.jfree.data.category.DefaultCategoryDataset
8 | import org.junit.Assert.assertThat
9 | import org.junit.Test
10 |
11 |
12 | class HistoryGraphDataSetGenerationTest {
13 | @Test
14 | fun `Should include all 20 entries in the generated data set`() {
15 | // GIVEN
16 | val entries = arrayListOf()
17 | repeat(20) { entries.add(SizeEntry("#" + it, 0L)) }
18 |
19 | // WHEN
20 | val dataSet = generateGraphDataSet(entries)
21 |
22 | // THEN
23 | assertThat(retrieveEntries(dataSet), `is`(equalTo(entries)))
24 | }
25 |
26 | @Test
27 | fun `Should include the last 20 entries in the generated data set`() {
28 | // GIVEN
29 | val entries = arrayListOf()
30 | repeat(35) { entries.add(SizeEntry("#" + it, 0L)) }
31 |
32 | // WHEN
33 | val dataSet = generateGraphDataSet(entries)
34 |
35 | // THEN
36 | assertThat(retrieveEntries(dataSet), `is`(equalTo(entries.subList(15, 35))))
37 | }
38 |
39 | @Test
40 | fun `Should convert a entries size to megabyte for its row value`() {
41 | // GIVEN
42 | val entries = arrayListOf(SizeEntry("#1", sizeInByte = 10000000L))
43 |
44 | // WHEN
45 | val dataSet = generateGraphDataSet(entries)
46 |
47 | // THEN
48 | val rowValue = dataSet.getValue(0, 0).toFloat()
49 |
50 | assertThat(rowValue, `is`(10000000L / CONVERSION_FACTOR_BYTE_TO_MEGABYTE))
51 | }
52 |
53 | @Test
54 | fun `Should use a entries build name as its column key`() {
55 | // GIVEN
56 | val entries = arrayListOf(SizeEntry("#1", 0L))
57 |
58 | // WHEN
59 | val dataSet = generateGraphDataSet(entries)
60 |
61 | // THEN
62 | val columnKey = dataSet.getColumnKey(0).toString()
63 |
64 | assertThat(columnKey, `is`(equalTo("#1")))
65 | }
66 |
67 | private fun retrieveEntries(dataSet: DefaultCategoryDataset) = arrayListOf().apply {
68 | for(i in 0 until dataSet.columnCount) {
69 | val buildName = dataSet.getColumnKey(i).toString()
70 |
71 | val apkSizeInMb = dataSet.getValue(0, i).toFloat()
72 | val apkSizeInByte = (apkSizeInMb * CONVERSION_FACTOR_BYTE_TO_MEGABYTE).toLong()
73 |
74 | add(SizeEntry(buildName, apkSizeInByte))
75 | }
76 | }
77 | }
--------------------------------------------------------------------------------
/src/test/kotlin/com/dg/watcher/PluginTest.kt:
--------------------------------------------------------------------------------
1 | package com.dg.watcher
2 |
3 | import com.dg.watcher.base.Build
4 | import com.dg.watcher.base.Project
5 | import com.dg.watcher.history.History
6 | import com.nhaarman.mockito_kotlin.mock
7 | import com.nhaarman.mockito_kotlin.whenever
8 | import hudson.FilePath
9 | import hudson.Launcher
10 | import hudson.model.BuildListener
11 | import hudson.tasks.BuildStepMonitor.NONE
12 | import org.hamcrest.Matchers.*
13 | import org.junit.Assert.assertThat
14 | import org.junit.Before
15 | import org.junit.Rule
16 | import org.junit.Test
17 | import org.junit.rules.TemporaryFolder
18 | import org.mockito.Mockito.verify
19 |
20 |
21 | class PluginTest {
22 | @Rule @JvmField
23 | val tempDir = TemporaryFolder()
24 |
25 |
26 | @Before
27 | fun setUp() {
28 | createApkFolder()
29 | createApkFile()
30 | }
31 |
32 | @Test
33 | fun `Should not require a monitor service`() =
34 | assertThat(plugin().requiredMonitorService, `is`(equalTo(NONE)))
35 |
36 | @Test
37 | fun `Should provide the history as the projects only action`() {
38 | // WHEN
39 | val actions = plugin().getProjectActions(mockProject())
40 |
41 | // THEN
42 | assertThat(actions, hasSize(1))
43 | assertThat(actions.first(), `is`(instanceOf(History::class.java)))
44 | }
45 |
46 | @Test
47 | fun `Should update the history action when a build is performed`() {
48 | // GIVEN
49 | val action = mockAction()
50 |
51 | // WHEN
52 | plugin().perform(mockBuildIncludesAction(action), mockLauncher(), mockListener())
53 |
54 | // THEN
55 | verify(action).updateHistory()
56 | }
57 |
58 | private fun createApkFolder() = tempDir.newFolder("temp_apk_folder")
59 |
60 | private fun createApkFile() = tempDir.newFile("temp_apk_folder/debug.txt")
61 |
62 | private fun plugin() = Plugin(0f, "temp_apk_folder")
63 |
64 | private fun mockProjectIncludesAction(action: History) = mockProject().apply {
65 | whenever(getAction(History::class.java)).thenReturn(action)
66 | }
67 |
68 | private fun mockProject() = mock().apply {
69 | whenever(getRootDir()).thenReturn(tempDir.root)
70 | }
71 |
72 | private fun mockBuildIncludesAction(action: History) = mockBuild().apply {
73 | val project = mockProjectIncludesAction(action)
74 |
75 | whenever(getProject()).thenReturn(project)
76 | }
77 |
78 | private fun mockBuild() = mock().apply {
79 | whenever(getWorkspace()).thenReturn(FilePath(tempDir.root))
80 | }
81 |
82 | private fun mockAction() = mock()
83 |
84 | private fun mockLauncher() = mock()
85 |
86 | private fun mockListener() = mock().apply {
87 | whenever(logger).thenReturn(mock())
88 | }
89 | }
--------------------------------------------------------------------------------
/src/test/kotlin/com/dg/watcher/watching/loading/ApkLoadingTest.kt:
--------------------------------------------------------------------------------
1 | package com.dg.watcher.watching.loading
2 |
3 | import com.dg.watcher.base.Build
4 | import com.nhaarman.mockito_kotlin.mock
5 | import com.nhaarman.mockito_kotlin.whenever
6 | import hudson.FilePath
7 | import org.junit.Assert.*
8 | import org.junit.Rule
9 | import org.junit.Test
10 | import org.junit.rules.TemporaryFolder
11 | import java.io.File.separator
12 |
13 |
14 | class ApkLoadingTest {
15 | @Rule @JvmField
16 | val tempDir = TemporaryFolder()
17 |
18 |
19 | @Test
20 | fun `Should return null when the workspace is non existent`() =
21 | assertNull(loadApk(mockBuild(workspace = null)))
22 |
23 | @Test
24 | fun `Should return null when the default folder is non existent`() =
25 | assertNull(loadApk(mockBuild()))
26 |
27 | @Test
28 | fun `Should return null when the specified folder is non existent`() =
29 | assertNull(loadApk(mockBuild(), "temp_apk_folder"))
30 |
31 | @Test
32 | fun `Should return null when the apk in the default folder is non existent`() {
33 | // GIVEN
34 | createApkFolder("app", "build", "outputs", "apk")
35 |
36 | // THEN
37 | assertNull(loadApk(mockBuild()))
38 | }
39 |
40 | @Test
41 | fun `Should return null when the apk in the specified folder is non existent`() {
42 | // GIVEN
43 | createApkFolder("temp_apk_folder")
44 |
45 | // THEN
46 | assertNull(loadApk(mockBuild(), "temp_apk_folder"))
47 | }
48 |
49 | @Test
50 | fun `Should load the apk from the default folder`() {
51 | // GIVEN
52 | createApkFolder("app", "build", "outputs", "apk")
53 | createApkFile("app/build/outputs/apk", "debug.apk")
54 |
55 | // THEN
56 | assertNotNull(loadApk(mockBuild()))
57 | }
58 |
59 | @Test
60 | fun `Should load the apk from the specified folder`() {
61 | // GIVEN
62 | createApkFolder("temp_apk_folder")
63 | createApkFile("temp_apk_folder", "debug.apk")
64 |
65 | // THEN
66 | assertNotNull(loadApk(mockBuild(), "temp_apk_folder"))
67 | }
68 |
69 | @Test
70 | fun `Should never load the apk from a nested folder`() {
71 | // GIVEN
72 | createApkFolder("apk_folder", "nested_folder")
73 | createApkFile("apk_folder/nested_folder", "debug.apk")
74 |
75 | // THEN
76 | assertNull(loadApk(mockBuild(), "apk_folder"))
77 | }
78 |
79 | private fun createApkFolder(vararg folders: String) = tempDir.newFolder(*folders)
80 |
81 | private fun createApkFile(folder: String, fileName: String) = tempDir.newFile("$folder$separator$fileName")
82 |
83 | private fun mockBuild(workspace: FilePath? = FilePath(tempDir.root)) = mock().apply {
84 | whenever(getWorkspace()).thenReturn(workspace)
85 | }
86 | }
--------------------------------------------------------------------------------
/src/main/java/com/dg/watcher/history/graph/HistoryGraph.kt:
--------------------------------------------------------------------------------
1 | package com.dg.watcher.history.graph
2 |
3 | import com.dg.watcher.base.*
4 | import com.dg.watcher.watching.saving.loadApkSizes
5 | import hudson.util.Graph
6 | import org.jfree.chart.ChartFactory.createLineChart
7 | import org.jfree.chart.JFreeChart
8 | import org.jfree.chart.axis.CategoryLabelPositions.UP_90
9 | import org.jfree.chart.axis.NumberAxis
10 | import org.jfree.chart.plot.CategoryPlot
11 | import org.jfree.chart.plot.PlotOrientation.VERTICAL
12 | import org.jfree.chart.renderer.category.LineAndShapeRenderer
13 | import org.kohsuke.stapler.StaplerRequest
14 | import org.kohsuke.stapler.StaplerResponse
15 | import java.awt.Color.RED
16 | import java.awt.Color.white
17 | import java.lang.System.currentTimeMillis
18 | import java.text.NumberFormat
19 |
20 |
21 | class HistoryGraph(private val project: Project) : Graph(currentTimeMillis(), GRAPH_WIDTH, GRAPH_HEIGHT) {
22 | fun drawGraph(request: StaplerRequest, response: StaplerResponse) = doPng(request, response)
23 |
24 | fun drawGraphTooltips(request: StaplerRequest, response: StaplerResponse) = doMap(request, response)
25 |
26 | override fun createGraph(): JFreeChart = createLineGraph().also {
27 | setupGraphBackground(it)
28 | setupGraphSizesAxis(it)
29 | setupGraphBuildAxis(it)
30 | setupGraphSizesLine(it)
31 | setupGraphTooltips(it)
32 | setupGraphLinks(it)
33 | }
34 |
35 | private fun createLineGraph() = createLineChart(GRAPH_TITLE, GRAPH_X_AXIS, GRAPH_Y_AXIS,
36 | createGraphData(), VERTICAL, true, false, false)
37 |
38 | private fun createGraphData() = generateGraphDataSet(loadApkSizes(project))
39 |
40 | private fun setupGraphBackground(graph: JFreeChart) {
41 | graph.backgroundPaint = white
42 | }
43 |
44 | private fun setupGraphSizesAxis(graph: JFreeChart) {
45 | val sizesAxis = getPlot(graph).rangeAxis as NumberAxis
46 |
47 | val sizeInMegabyteFormat = NumberFormat.getInstance().apply {
48 | minimumFractionDigits = 1
49 | maximumFractionDigits = 1
50 | }
51 |
52 | sizesAxis.numberFormatOverride = sizeInMegabyteFormat
53 | }
54 |
55 | private fun setupGraphBuildAxis(graph: JFreeChart) {
56 | val buildAxis = getPlot(graph).domainAxis
57 |
58 | buildAxis.categoryLabelPositions = UP_90
59 | }
60 |
61 | private fun setupGraphSizesLine(graph: JFreeChart) {
62 | getRenderer(graph).apply {
63 | baseShapesVisible = true
64 | baseStroke = GRAPH_LINE
65 | setSeriesPaint(0, RED)
66 | }
67 |
68 | getPlot(graph).drawingSupplier = GRAPH_LINE_DOT
69 | }
70 |
71 | private fun setupGraphTooltips(graph: JFreeChart) {
72 | getRenderer(graph).baseToolTipGenerator = HistoryGraphTooltipGenerator()
73 | }
74 |
75 | private fun setupGraphLinks(graph: JFreeChart) {
76 | getRenderer(graph).baseItemURLGenerator = HistoryGraphUrlGenerator(project)
77 | }
78 |
79 | private fun getRenderer(graph: JFreeChart) = getPlot(graph).renderer as LineAndShapeRenderer
80 |
81 | private fun getPlot(graph: JFreeChart) = graph.plot as CategoryPlot
82 | }
--------------------------------------------------------------------------------
/src/test/kotlin/com/dg/watcher/watching/ApkSizeWatchingTest.kt:
--------------------------------------------------------------------------------
1 | package com.dg.watcher.watching
2 |
3 | import com.dg.watcher.base.BUILD_ALLOWED
4 | import com.dg.watcher.base.BUILD_FORBIDDEN
5 | import com.dg.watcher.base.Build
6 | import com.dg.watcher.base.Project
7 | import com.nhaarman.mockito_kotlin.doReturn
8 | import com.nhaarman.mockito_kotlin.mock
9 | import com.nhaarman.mockito_kotlin.whenever
10 | import hudson.FilePath
11 | import org.apache.commons.io.FileUtils.cleanDirectory
12 | import org.apache.commons.io.FileUtils.writeByteArrayToFile
13 | import org.hamcrest.Matchers.equalTo
14 | import org.hamcrest.core.Is.`is`
15 | import org.junit.Assert.assertThat
16 | import org.junit.Before
17 | import org.junit.Rule
18 | import org.junit.Test
19 | import org.junit.rules.TemporaryFolder
20 | import java.io.File
21 |
22 |
23 | class ApkSizeWatchingTest {
24 | @Rule @JvmField
25 | val tempDir = TemporaryFolder()
26 |
27 |
28 | @Before
29 | fun setUp() {
30 | createApkFolder()
31 | }
32 |
33 | @Test
34 | fun `Should allow the initial build with an generated apk`() {
35 | // GIVEN
36 | val build = mockBuildWithAnApkOfSizeInByte(10000000L)
37 |
38 | // WHEN
39 | val result = watchApkSize(build, thresholdInMb = 1f)
40 |
41 | // THEN
42 | assertThat(result, `is`(equalTo(BUILD_ALLOWED)))
43 | }
44 |
45 | @Test
46 | fun `Should allow a build without an generated apk`() {
47 | // GIVEN
48 | val build = mockBuildWithoutAnApk()
49 |
50 | // WHEN
51 | val result = watchApkSize(build)
52 |
53 | // THEN
54 | assertThat(result, `is`(equalTo(BUILD_ALLOWED)))
55 | }
56 |
57 | @Test
58 | fun `Should allow the build when the generated apk meets the size threshold`() {
59 | // GIVEN
60 | watchApkSize(mockBuildWithAnApkOfSizeInByte(10000000L))
61 | val build = mockBuildWithAnApkOfSizeInByte(11000000L)
62 |
63 | // WHEN
64 | val result = watchApkSize(build, thresholdInMb = 1f)
65 |
66 | // THEN
67 | assertThat(result, `is`(equalTo(BUILD_ALLOWED)))
68 | }
69 |
70 | @Test
71 | fun `Should forbid the build when the generated apk exceeds the size threshold`() {
72 | // GIVEN
73 | watchApkSize(mockBuildWithAnApkOfSizeInByte(10000000L))
74 | val build = mockBuildWithAnApkOfSizeInByte(12000000L)
75 |
76 | // WHEN
77 | val result = watchApkSize(build, thresholdInMb = 1f)
78 |
79 | // THEN
80 | assertThat(result, `is`(equalTo(BUILD_FORBIDDEN)))
81 | }
82 |
83 | private fun watchApkSize(build: Build, thresholdInMb: Float = 0f) =
84 | watchApkSize(build, mock(), thresholdInMb)
85 |
86 | private fun mockBuild() = mock().apply {
87 | val project: Project = mock {
88 | on { getRootDir() } doReturn tempDir.root
89 | }
90 |
91 | whenever(getProject()).thenReturn(project)
92 | whenever(getWorkspace()).thenReturn(FilePath(tempDir.root))
93 | }
94 |
95 | private fun mockBuildWithoutAnApk() = mockBuild()
96 |
97 | private fun mockBuildWithAnApkOfSizeInByte(apkSizeInByte: Long) = mockBuild().also {
98 | createApkForBuild(apkSizeInByte)
99 | }
100 |
101 | private fun createApkFolder() = tempDir.newFolder("app", "build", "outputs", "apk")
102 |
103 | private fun createApkForBuild(apkSizeInByte: Long) {
104 | deleteOldApk()
105 |
106 | createNewApk(apkSizeInByte)
107 | }
108 |
109 | private fun deleteOldApk() = cleanDirectory(File(tempDir.root.absolutePath + "/app/build/outputs/apk"))
110 |
111 | private fun createNewApk(apkSizeInByte: Long) {
112 | val apk = tempDir.newFile("app/build/outputs/apk/debug.apk")
113 |
114 | writeByteArrayToFile(apk, ByteArray(apkSizeInByte.toInt()))
115 | }
116 | }
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 4.0.0
5 |
6 | android-apk-size-watcher
7 | hpi
8 | 1.1-SNAPSHOT
9 |
10 | Android Apk Size Watcher Plugin
11 | Watch over the changing size of your .apk file and fail your build once the change exceeds your specified threshold.
12 | https://wiki.jenkins.io/display/JENKINS/Android+Apk+Size+Watcher+Plugin
13 |
14 |
15 |
16 | xgleich1
17 | Daniel Gronau
18 | xgleich1@gmail.com
19 |
20 |
21 |
22 |
23 |
24 | The MIT License (MIT)
25 | http://opensource.org/licenses/MIT
26 | repo
27 |
28 |
29 |
30 |
31 | scm:git:ssh://github.com/jenkinsci/android-apk-size-watcher-plugin.git
32 | scm:git:ssh://git@github.com/jenkinsci/android-apk-size-watcher-plugin.git
33 | https://github.com/jenkinsci/android-apk-size-watcher-plugin
34 | HEAD
35 |
36 |
37 |
38 |
39 |
40 | org.jetbrains.kotlin
41 | kotlin-maven-plugin
42 | 1.1.4-3
43 |
44 |
45 |
46 | compile
47 |
48 |
49 | compile
50 |
51 |
52 |
53 |
54 | test-compile
55 |
56 |
57 | test-compile
58 |
59 |
60 |
61 |
62 | ${project.basedir}/src/test/kotlin
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | org.apache.maven.plugins
71 | maven-compiler-plugin
72 |
73 |
74 |
75 | default-compile
76 |
77 | none
78 |
79 |
80 |
81 | default-testCompile
82 |
83 | none
84 |
85 |
86 |
87 | java-compile
88 |
89 | compile
90 |
91 |
92 | compile
93 |
94 |
95 |
96 |
97 | java-test-compile
98 |
99 | test-compile
100 |
101 |
102 | testCompile
103 |
104 |
105 |
106 |
107 |
108 |
109 | org.apache.maven.plugins
110 | maven-release-plugin
111 | 2.5.3
112 |
113 |
114 |
115 | org.apache.maven.plugins
116 | maven-scm-plugin
117 | 1.9.5
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 | org.jenkins-ci.plugins
126 | plugin
127 | 2.33
128 |
129 |
130 |
131 |
132 | repo.jenkins-ci.org
133 | http://repo.jenkins-ci.org/public/
134 |
135 |
136 |
137 |
138 |
139 | repo.jenkins-ci.org
140 | http://repo.jenkins-ci.org/public/
141 |
142 |
143 |
144 |
145 |
146 | org.jetbrains.kotlin
147 | kotlin-stdlib-jre7
148 | 1.1.4-3
149 |
150 |
151 |
152 | commons-io
153 | commons-io
154 | 2.5
155 |
156 |
157 |
158 | junit
159 | junit
160 | 4.12
161 | test
162 |
163 |
164 |
165 | org.hamcrest
166 | hamcrest-all
167 | 1.3
168 | test
169 |
170 |
171 |
172 | com.nhaarman
173 | mockito-kotlin
174 | 1.5.0
175 |
176 |
177 |
178 |
--------------------------------------------------------------------------------