develop #2

Merged
swordsteel merged 3 commits from develop into master 2025-09-08 15:42:48 +00:00
9 changed files with 474 additions and 1 deletions

View File

@@ -2,6 +2,18 @@
## Extension
### Extension Git
The GitExtension enhances versioning by dynamically appending the Git hash before "snapshot" in the version string. For example, 0.0.0-SNAPSHOT becomes 0.0.0.0a2b3c4d-SNAPSHOT, ensuring each build reflects its commit origin, prevents overwriting existing versions. This feature aids developers during development by providing clear version identification.
### Extension Info
The InfoExtension provides information for name and version, vendor name, and UTC timestamp.
### Extension Config
The ConfigExtension provides a find or findOrDefault for getting a property or environment.
## Publish version catalog locally.
```shell

View File

@@ -17,7 +17,12 @@ plugins {
}
dependencies {
implementation(aa.jgit)
testImplementation(aa.assertj)
testImplementation(aa.junit.jupiter.params)
testImplementation(aa.kotlin.junit5)
testImplementation(aa.mockk)
testRuntimeOnly(aa.junit.platform.launcher)
}
@@ -110,6 +115,8 @@ tasks {
}
}
withType<Test> {
// Set TEST_ENV environment variable for test execution
environment["TEST_ENV"] = "lulz"
useJUnitPlatform()
}
}

View File

@@ -1,7 +1,13 @@
package ltd.lulz.plugin
import ltd.lulz.plugin.extension.ConfigExtension
import ltd.lulz.plugin.extension.GitExtension
import ltd.lulz.plugin.extension.InfoExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import ltd.lulz.plugin.extension.ConfigExtension.Companion.PLUGIN_NAME as CONFIG_PLUGIN_NAME
import ltd.lulz.plugin.extension.GitExtension.Companion.PLUGIN_NAME as GIT_PLUGIN_NAME
import ltd.lulz.plugin.extension.InfoExtension.Companion.PLUGIN_NAME as INFO_PLUGIN_NAME
@Suppress("unused")
class CorePlugin : Plugin<Project> {
@@ -12,6 +18,23 @@ class CorePlugin : Plugin<Project> {
override fun apply(
project: Project,
) {
TODO("Not yet implemented")
gitExtension(project)
infoExtension(project)
configExtension(project)
}
private fun configExtension(
project: Project,
): ConfigExtension = project.extensions
.create(CONFIG_PLUGIN_NAME, ConfigExtension::class.java, project)
private fun infoExtension(
project: Project,
): InfoExtension = project.extensions
.create(INFO_PLUGIN_NAME, InfoExtension::class.java, project)
private fun gitExtension(
project: Project,
): GitExtension = project.extensions
.create(GIT_PLUGIN_NAME, GitExtension::class.java, project)
}

View File

@@ -0,0 +1,31 @@
package ltd.lulz.plugin.extension
import java.lang.System.getenv
import org.gradle.api.Project
abstract class ConfigExtension(private val project: Project) {
companion object {
const val PLUGIN_NAME = "config"
const val EMPTY = ""
}
fun find(
property: String,
environment: String,
): String? = findProperty(property) ?: findEnvironment(environment)
fun findOrDefault(
property: String,
environment: String,
default: String = EMPTY,
): String = findProperty(property) ?: findEnvironment(environment) ?: default
private fun findProperty(
property: String,
) = project.findProperty(property)?.toString()
private fun findEnvironment(
environment: String,
): String? = getenv(environment)
}

View File

@@ -0,0 +1,37 @@
package ltd.lulz.plugin.extension
import org.eclipse.jgit.api.Git.open
import org.eclipse.jgit.lib.Constants.HEAD
import org.gradle.api.Project
abstract class GitExtension(private val project: Project) {
companion object {
const val PLUGIN_NAME = "git"
private const val HASH_LENGTH = 8
private const val SNAPSHOT = "-SNAPSHOT"
private const val UNAVAILABLE = "n/a"
private val PRIMARY_BRANCHES = setOf("master", "develop")
}
fun version(): String = when {
isHead() || currentBranch() in PRIMARY_BRANCHES -> project.version.toString()
else -> makeVersion(project.version.toString(), currentShortHash())
}
fun currentShortHash(): String = open(project.projectDir)
.use { it.repository.exactRef(HEAD)?.objectId?.name?.take(HASH_LENGTH) ?: UNAVAILABLE }
fun currentBranch(): String = open(project.projectDir)
.use { it.repository.branch ?: UNAVAILABLE }
fun isHead(): Boolean = open(project.projectDir)
.use { it.repository.exactRef(HEAD)?.target?.name == HEAD }
private fun makeVersion(version: String, shortHash: String): String = when {
shortHash == UNAVAILABLE -> version.also { println("Failed to get data from GIT") }
version.endsWith(SNAPSHOT) -> version.replace(SNAPSHOT, ".$shortHash$SNAPSHOT")
else -> version.also { println("Failed version missing suffix $SNAPSHOT") }
}
}

View File

@@ -0,0 +1,16 @@
package ltd.lulz.plugin.extension
import java.time.OffsetDateTime.now
import java.time.ZoneId.of
import java.time.format.DateTimeFormatter.ofPattern
import org.gradle.api.Project
abstract class InfoExtension(private val project: Project) {
companion object {
const val PLUGIN_NAME = "info"
}
val nameVersion get() = "Project Name: ${project.name} Version: ${project.version}"
val utcTimestamp = now().atZoneSameInstant(of("UTC")).format(ofPattern("yyyy-MM-dd HH:mm:ss z")).toString()
val vendorName = "Lulz Ltd"
}

View File

@@ -0,0 +1,112 @@
package ltd.lulz.plugin.extension
import org.assertj.core.api.Assertions.assertThat
import org.gradle.api.Project
import org.gradle.testfixtures.ProjectBuilder
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
/**
* This test uses TEST_ENV set in gradle test task, and project extension to pretend to be a project property.
*/
class ConfigExtensionTest {
companion object {
const val EXTENSION = "config"
const val PLUGIN_ID = "ltd.lulz.plugin.core-plugin"
const val PROJECT_NAME = "test-project"
const val SNAPSHOT_VERSION = "0.0.0-SNAPSHOT"
}
lateinit var project: Project
lateinit var configExtension: ConfigExtension
@BeforeEach
fun beforeEach() {
project = ProjectBuilder.builder()
.withName(PROJECT_NAME)
.build()
project.version = SNAPSHOT_VERSION
project.pluginManager.apply(PLUGIN_ID)
project.extensions.add(
"testProperty",
object {
override fun toString(): String = "lulz"
},
)
configExtension = project.extensions.getByName(EXTENSION) as ConfigExtension
}
@Nested
inner class FindTest {
@Test
fun `property or environment find property`() {
// when
val result = configExtension.find("testProperty", "TEST_PROPERTY")
// then
assertThat(result).isEqualTo("lulz")
}
@Test
fun `property or environment find environment`() {
// when
val result = configExtension.find("test_env", "TEST_ENV")
// then
assertThat(result).isEqualTo("lulz")
}
@Test
fun `property or environment find nothing`() {
// when
val result = configExtension.find("test", "TEST")
// then
assertThat(result).isNull()
}
}
@Nested
inner class FindOrDefaultTest {
@Test
fun `property, environment, or default find property`() {
// when
val result = configExtension.findOrDefault("testProperty", "TEST_PROPERTY")
// then
assertThat(result).isEqualTo("lulz")
}
@Test
fun `property, environment, or default find environment`() {
// when
val result = configExtension.findOrDefault("test_env", "TEST_ENV")
// then
assertThat(result).isEqualTo("lulz")
}
@Test
fun `property, environment, or default find empty`() {
// when
val result = configExtension.findOrDefault("test", "TEST")
// then
assertThat(result).isEmpty()
}
@Test
fun `property, environment, or default find defined response`() {
// when
val result = configExtension.findOrDefault("test", "TEST", "value")
// then
assertThat(result).isEqualTo("value")
}
}
}

View File

@@ -0,0 +1,162 @@
package ltd.lulz.plugin.extension
import io.mockk.every
import io.mockk.justRun
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.ObjectId
import org.eclipse.jgit.lib.Ref
import org.eclipse.jgit.lib.Repository
import org.gradle.api.Project
import org.gradle.testfixtures.ProjectBuilder
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
class GitExtensionTest {
companion object {
const val GIT_HASH_SHA_1 = "0a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t"
const val SHORT_GIT_HASH = "0a1b2c3d"
const val SNAPSHOT_HASH_VERSION = "0.0.0.0a1b2c3d-SNAPSHOT"
const val SNAPSHOT_VERSION = "0.0.0-SNAPSHOT"
const val VERSION = "0.0.0"
const val BRANCH_FEATURE = "feature/ABC-123"
const val BRANCH_MASTER = "master"
const val EXTENSION = "git"
const val PLUGIN_ID = "ltd.lulz.plugin.core-plugin"
const val REF_HEAD = "HEAD"
const val UNAVAILABLE = "n/a"
}
private val gitMock: Git = mockk()
private val refMock: Ref = mockk()
private val repositoryMock: Repository = mockk()
private val objectIdMock: ObjectId = mockk()
lateinit var name: String
lateinit var project: Project
@BeforeEach
fun buildUp() {
project = ProjectBuilder.builder().build()
project.version = SNAPSHOT_VERSION
project.pluginManager.apply(PLUGIN_ID)
mockkStatic(Git::class)
every { Git.open(any()) } returns gitMock
every { gitMock.repository } returns repositoryMock
every { repositoryMock.branch } returns null
every { repositoryMock.exactRef(any()) } returns refMock
every { refMock.target } returns refMock
every { refMock.name } returns null
every { refMock.objectId } returns objectIdMock
every { objectIdMock.name } returns null
justRun { gitMock.close() }
}
@AfterEach
fun tearDown() {
unmockkStatic(Git::class)
}
@ParameterizedTest
@CsvSource(
"$REF_HEAD, true",
"$BRANCH_FEATURE, false",
)
fun `current ref is head`(ref: String, expected: Boolean) {
// given
every { refMock.name } returns ref
// when
val extension = project.extensions.getByName(EXTENSION) as GitExtension
// then
assertEquals(expected, extension.isHead())
}
@ParameterizedTest
@CsvSource(
", $UNAVAILABLE",
"$BRANCH_FEATURE, $BRANCH_FEATURE",
)
fun `get current branch`(branch: String?, expected: String) {
// given
every { repositoryMock.branch } returns branch
// when
val extension = project.extensions.getByName(EXTENSION) as GitExtension
// then
assertEquals(expected, extension.currentBranch())
}
@ParameterizedTest
@CsvSource(
", $UNAVAILABLE",
"$GIT_HASH_SHA_1, $SHORT_GIT_HASH",
)
fun `get current short hash`(hash: String?, expected: String) {
// given
every { objectIdMock.name } returns hash
// when
val extension = project.extensions.getByName(EXTENSION) as GitExtension
// then
assertEquals(expected, extension.currentShortHash())
}
@Test
fun `get version - version without snapshot`() {
// given
project.version = VERSION
every { objectIdMock.name } returns GIT_HASH_SHA_1
// when
val extension = project.extensions.getByName(EXTENSION) as GitExtension
// then
assertEquals(VERSION, extension.version())
}
@Test
fun `get version - short hash is blank`() {
// when
val extension = project.extensions.getByName(EXTENSION) as GitExtension
// then
assertEquals(SNAPSHOT_VERSION, extension.version())
}
@ParameterizedTest
@CsvSource(
", , $SNAPSHOT_HASH_VERSION",
", $REF_HEAD, $SNAPSHOT_VERSION",
"$BRANCH_MASTER, , $SNAPSHOT_VERSION",
"$BRANCH_MASTER, , $SNAPSHOT_VERSION",
"$BRANCH_FEATURE, , $SNAPSHOT_HASH_VERSION",
"$BRANCH_MASTER, $REF_HEAD, $SNAPSHOT_VERSION",
"$BRANCH_MASTER, $REF_HEAD, $SNAPSHOT_VERSION",
"$BRANCH_FEATURE, $REF_HEAD, $SNAPSHOT_VERSION",
)
fun `get version - different branches`(branch: String?, ref: String?, expected: String) {
// given
every { repositoryMock.branch } returns branch
every { objectIdMock.name } returns GIT_HASH_SHA_1
every { refMock.name } returns ref
// when
val extension = project.extensions.getByName(EXTENSION) as GitExtension
// then
assertEquals(expected, extension.version())
}
}

View File

@@ -0,0 +1,73 @@
package ltd.lulz.plugin.extension
import io.mockk.every
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import java.time.OffsetDateTime
import org.gradle.api.Project
import org.gradle.testfixtures.ProjectBuilder
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
class InfoExtensionTest {
companion object {
const val BASIC_TIMESTAMP = "2002-02-20T02:10:11+01:00"
const val EXTENSION = "info"
const val PLUGIN_ID = "ltd.lulz.plugin.core-plugin"
const val PROJECT_NAME = "test-project"
const val SNAPSHOT_VERSION = "0.0.0-SNAPSHOT"
@JvmStatic
@BeforeAll
fun buildUp() {
mockkStatic(OffsetDateTime::class)
every { OffsetDateTime.now() } returns OffsetDateTime.parse(BASIC_TIMESTAMP)
}
@JvmStatic
@AfterAll
fun tearDown() {
unmockkStatic(OffsetDateTime::class)
}
}
lateinit var project: Project
@BeforeEach
fun beforeEach() {
project = ProjectBuilder.builder().withName(PROJECT_NAME).build()
project.version = SNAPSHOT_VERSION
project.pluginManager.apply(PLUGIN_ID)
}
@Test
fun `get vendor`() {
// when
val extension = project.extensions.getByName(EXTENSION) as InfoExtension
// then
assertEquals("Lulz Ltd", extension.vendorName)
}
@Test
fun `get timestamp`() {
// when
val extension = project.extensions.getByName(EXTENSION) as InfoExtension
// then
assertEquals("2002-02-20 01:10:11 UTC", extension.utcTimestamp)
}
@Test
fun `get build`() {
// when
val extension = project.extensions.getByName(EXTENSION) as InfoExtension
// then
assertEquals("Project Name: test-project Version: 0.0.0-SNAPSHOT", extension.nameVersion)
}
}