diff --git a/gradle.properties b/gradle.properties index 6489cc8..387791e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,6 +9,6 @@ org.gradle.jvmargs=-Xmx4096m # Project properties config.group = xyz.marinkovic.milos config.artifact = codestats -config.version = 0.6.0 +config.version = 0.7.0 config.gitHubRepoOwner = milosmns config.gitHubRepoName = code-stats diff --git a/src/commonMain/kotlin/calculator/CodeReviewChangeLinesAddedCalculator.kt b/src/commonMain/kotlin/calculator/CodeReviewChangeLinesAddedCalculator.kt index 17e8440..1c7ee74 100644 --- a/src/commonMain/kotlin/calculator/CodeReviewChangeLinesAddedCalculator.kt +++ b/src/commonMain/kotlin/calculator/CodeReviewChangeLinesAddedCalculator.kt @@ -2,8 +2,10 @@ package calculator import components.data.Repository import components.metrics.CodeReviewChangeLinesAdded +import history.filter.transform.RepositoryFinishedAtTransform +import kotlinx.datetime.LocalDate -class CodeReviewChangeLinesAddedCalculator : GenericLongMetricCalculator { +class CodeReviewChangeLinesAddedCalculator : GenericCountMetricCalculator { override fun calculate(repositories: List): CodeReviewChangeLinesAdded { val perUser = repositories @@ -42,4 +44,6 @@ class CodeReviewChangeLinesAddedCalculator : GenericLongMetricCalculator { +class CodeReviewChangeLinesDeletedCalculator : GenericCountMetricCalculator { override fun calculate(repositories: List): CodeReviewChangeLinesDeleted { val perUser = repositories @@ -42,4 +44,6 @@ class CodeReviewChangeLinesDeletedCalculator : GenericLongMetricCalculator { +class CodeReviewChangeLinesTotalCalculator : GenericCountMetricCalculator { override fun calculate(repositories: List): CodeReviewChangeLinesTotal { val perUser = repositories @@ -42,4 +44,6 @@ class CodeReviewChangeLinesTotalCalculator : GenericLongMetricCalculator { +class CodeReviewChangesAddedCalculator : GenericCountMetricCalculator { override fun calculate(repositories: List): CodeReviewChangesAdded { val perUser = repositories @@ -42,4 +44,6 @@ class CodeReviewChangesAddedCalculator : GenericLongMetricCalculator { +class CodeReviewChangesModifiedCalculator : GenericCountMetricCalculator { override fun calculate(repositories: List): CodeReviewChangesModified { val perUser = repositories @@ -42,4 +44,6 @@ class CodeReviewChangesModifiedCalculator : GenericLongMetricCalculator { +class CodeReviewChangesRemovedCalculator : GenericCountMetricCalculator { override fun calculate(repositories: List): CodeReviewChangesRemoved { val perUser = repositories @@ -42,4 +44,6 @@ class CodeReviewChangesRemovedCalculator : GenericLongMetricCalculator { +class CodeReviewChangesTotalCalculator : GenericCountMetricCalculator { override fun calculate(repositories: List): CodeReviewChangesTotal { val perUser = repositories @@ -42,4 +44,6 @@ class CodeReviewChangesTotalCalculator : GenericLongMetricCalculator { +class CodeReviewCommentsAuthoredCalculator : GenericCountMetricCalculator { override fun calculate(repositories: List): CodeReviewCommentsAuthored { val perUser = repositories @@ -43,4 +45,10 @@ class CodeReviewCommentsAuthoredCalculator : GenericLongMetricCalculator { +class CodeReviewCommentsReceivedCalculator : GenericCountMetricCalculator { override fun calculate(repositories: List): CodeReviewCommentsReceived { @Suppress("DuplicatedCode") // false positive from DiscussionCommentsReceivedCalculator @@ -26,4 +28,10 @@ class CodeReviewCommentsReceivedCalculator : GenericLongMetricCalculator { +class CodeReviewFeedbacksApprovedCalculator : GenericCountMetricCalculator { override fun calculate(repositories: List): CodeReviewFeedbacksApproved { val perUser = repositories @@ -37,4 +39,10 @@ class CodeReviewFeedbacksApprovedCalculator : GenericLongMetricCalculator { +class CodeReviewFeedbacksPostponedCalculator : GenericCountMetricCalculator { override fun calculate(repositories: List): CodeReviewFeedbacksPostponed { val perUser = repositories @@ -37,4 +39,10 @@ class CodeReviewFeedbacksPostponedCalculator : GenericLongMetricCalculator { +class CodeReviewFeedbacksRejectedCalculator : GenericCountMetricCalculator { override fun calculate(repositories: List): CodeReviewFeedbacksRejected { val perUser = repositories @@ -37,4 +39,10 @@ class CodeReviewFeedbacksRejectedCalculator : GenericLongMetricCalculator { +class CodeReviewFeedbacksTotalCalculator : GenericCountMetricCalculator { override fun calculate(repositories: List): CodeReviewFeedbacksTotal { val perUser = repositories @@ -37,4 +39,10 @@ class CodeReviewFeedbacksTotalCalculator : GenericLongMetricCalculator { +class CodeReviewsCalculator : GenericCountMetricCalculator { override fun calculate(repositories: List): CodeReviews { val perUser = repositories @@ -32,4 +34,10 @@ class CodeReviewsCalculator : GenericLongMetricCalculator { ) } + override fun getTimeSeriesTransform(date: LocalDate) = RepositoryCreatedAtTransform( + createdAt = date, + applyToCommentsAndFeedbacks = false, + applyToCodeReviewsAndDiscussions = true, + ) + } diff --git a/src/commonMain/kotlin/calculator/CycleTimeCalculator.kt b/src/commonMain/kotlin/calculator/CycleTimeCalculator.kt index ec71b04..da67cf8 100644 --- a/src/commonMain/kotlin/calculator/CycleTimeCalculator.kt +++ b/src/commonMain/kotlin/calculator/CycleTimeCalculator.kt @@ -2,9 +2,11 @@ package calculator import components.data.Repository import components.metrics.CycleTime +import history.filter.transform.RepositoryFinishedAtTransform import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDate -class CycleTimeCalculator(private val now: Instant) : GenericLongMetricCalculator { +class CycleTimeCalculator(private val now: Instant) : GenericCountMetricCalculator { override fun calculate(repositories: List): CycleTime { val perUser = repositories @@ -42,4 +44,6 @@ class CycleTimeCalculator(private val now: Instant) : GenericLongMetricCalculato ) } + override fun getTimeSeriesTransform(date: LocalDate) = RepositoryFinishedAtTransform(date) + } diff --git a/src/commonMain/kotlin/calculator/DiscussionCommentsAuthoredCalculator.kt b/src/commonMain/kotlin/calculator/DiscussionCommentsAuthoredCalculator.kt index ae7b956..283715c 100644 --- a/src/commonMain/kotlin/calculator/DiscussionCommentsAuthoredCalculator.kt +++ b/src/commonMain/kotlin/calculator/DiscussionCommentsAuthoredCalculator.kt @@ -2,8 +2,10 @@ package calculator import components.data.Repository import components.metrics.DiscussionCommentsAuthored +import history.filter.transform.RepositoryCreatedAtTransform +import kotlinx.datetime.LocalDate -class DiscussionCommentsAuthoredCalculator : GenericLongMetricCalculator { +class DiscussionCommentsAuthoredCalculator : GenericCountMetricCalculator { override fun calculate(repositories: List): DiscussionCommentsAuthored { val perUser = repositories @@ -47,4 +49,10 @@ class DiscussionCommentsAuthoredCalculator : GenericLongMetricCalculator { +class DiscussionCommentsReceivedCalculator : GenericCountMetricCalculator { override fun calculate(repositories: List): DiscussionCommentsReceived { @Suppress("DuplicatedCode") // false positive from CodeReviewCommentsReceivedCalculator @@ -26,4 +28,10 @@ class DiscussionCommentsReceivedCalculator : GenericLongMetricCalculator { +class DiscussionsCalculator : GenericCountMetricCalculator { override fun calculate(repositories: List): Discussions { val perUser = repositories @@ -42,4 +44,10 @@ class DiscussionsCalculator : GenericLongMetricCalculator { ) } + override fun getTimeSeriesTransform(date: LocalDate) = RepositoryCreatedAtTransform( + createdAt = date, + applyToCommentsAndFeedbacks = false, + applyToCodeReviewsAndDiscussions = true, + ) + } diff --git a/src/commonMain/kotlin/calculator/GenericCountMetricCalculator.kt b/src/commonMain/kotlin/calculator/GenericCountMetricCalculator.kt new file mode 100644 index 0000000..d81892f --- /dev/null +++ b/src/commonMain/kotlin/calculator/GenericCountMetricCalculator.kt @@ -0,0 +1,29 @@ +package calculator + +import components.data.Repository +import components.metrics.GenericCountMetric +import kotlinx.datetime.LocalDate + +interface GenericCountMetricCalculator { + + fun calculate(repositories: List): T + + fun getTimeSeriesTransform(date: LocalDate): (Repository) -> Repository + + fun asTimeSeries( + startDate: LocalDate, + endDate: LocalDate, + repositories: List, + ): Map { + val results = mutableMapOf() + for (date in startDate..endDate step 1) { + val scopedDownRepos = repositories + .map(getTimeSeriesTransform(date)) + .filterNot(Repository::isDead) + if (scopedDownRepos.isEmpty()) continue + results[date] = calculate(scopedDownRepos) + } + return results + } + +} diff --git a/src/commonMain/kotlin/calculator/GenericLongMetricCalculator.kt b/src/commonMain/kotlin/calculator/GenericLongMetricCalculator.kt deleted file mode 100644 index 6c7d6d2..0000000 --- a/src/commonMain/kotlin/calculator/GenericLongMetricCalculator.kt +++ /dev/null @@ -1,10 +0,0 @@ -package calculator - -import components.data.Repository -import components.metrics.GenericCountMetric - -interface GenericLongMetricCalculator { - - fun calculate(repositories: List): T - -} diff --git a/src/commonMain/kotlin/calculator/LocalDateUtils.kt b/src/commonMain/kotlin/calculator/LocalDateUtils.kt new file mode 100644 index 0000000..c137048 --- /dev/null +++ b/src/commonMain/kotlin/calculator/LocalDateUtils.kt @@ -0,0 +1,37 @@ +package calculator + +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.LocalDate +import kotlinx.datetime.plus + +class LocalDateDayIterator( + startDate: LocalDate, + private val endDateInclusive: LocalDate, + private val increment: Long, // in days +) : Iterator { + + private var currentDate = startDate + + override fun hasNext() = currentDate <= endDateInclusive + + override fun next(): LocalDate { + val next = currentDate + currentDate = currentDate.plus(increment, DateTimeUnit.DAY) + return next + } + +} + +class LocalDateDayIterableRange( + override val start: LocalDate, + override val endInclusive: LocalDate, + private val increment: Long = 1, // in days +) : Iterable, ClosedRange { + + override fun iterator() = LocalDateDayIterator(start, endInclusive, increment) + + infix fun step(increment: Long) = LocalDateDayIterableRange(start, endInclusive, increment) + +} + +operator fun LocalDate.rangeTo(other: LocalDate) = LocalDateDayIterableRange(this, other) diff --git a/src/commonMain/kotlin/calculator/di/ManualModule.kt b/src/commonMain/kotlin/calculator/di/ManualModule.kt index 76b34d2..5757ec7 100644 --- a/src/commonMain/kotlin/calculator/di/ManualModule.kt +++ b/src/commonMain/kotlin/calculator/di/ManualModule.kt @@ -57,7 +57,7 @@ fun provideDiscussionsCalculator() = DiscussionsCalculator() fun provideCycleTimeCalculator(now: Instant = Clock.System.now()) = CycleTimeCalculator(now) -fun provideGenericLongMetricCalculators() = listOf( +fun provideGenericCountMetricCalculators() = listOf( provideCodeReviewChangeLinesAddedCalculator(), provideCodeReviewChangeLinesDeletedCalculator(), provideCodeReviewChangeLinesTotalCalculator(), diff --git a/src/commonMain/kotlin/commands/cli/PrintCommand.kt b/src/commonMain/kotlin/commands/cli/PrintCommand.kt index a23d3a5..a72b35e 100644 --- a/src/commonMain/kotlin/commands/cli/PrintCommand.kt +++ b/src/commonMain/kotlin/commands/cli/PrintCommand.kt @@ -1,8 +1,8 @@ package commands.cli -import calculator.di.provideGenericLongMetricCalculators +import calculator.di.provideGenericCountMetricCalculators import components.data.TeamHistoryConfig -import history.filter.transform.RepositoryDateTransform +import history.filter.transform.RepositoryDateBetweenTransform import history.storage.StoredHistory import kotlinx.coroutines.Runnable @@ -23,14 +23,14 @@ class PrintCommand( includeDiscussions = true, ) } - val transform = RepositoryDateTransform(teamHistoryConfig.startDate, teamHistoryConfig.endDate) + val transform = RepositoryDateBetweenTransform(teamHistoryConfig.startDate, teamHistoryConfig.endDate) val filteredRepos = storedRepos.map(transform) .filter { repo -> repo.codeReviews.isNotEmpty() || repo.discussions.isNotEmpty() } print("Print details with date filtering applied? (y/n) ") val dataSet = if (readln().lowercase().trim() == "y") filteredRepos else storedRepos println("OK.\n") - provideGenericLongMetricCalculators().forEach { calculator -> + provideGenericCountMetricCalculators().forEach { calculator -> val metric = calculator.calculate(dataSet) println(metric.simpleFormat) println("-- ${metric.name} --\n") diff --git a/src/commonMain/kotlin/commands/cli/ReportCommand.kt b/src/commonMain/kotlin/commands/cli/ReportCommand.kt index 59f98f0..7c5bd6e 100644 --- a/src/commonMain/kotlin/commands/cli/ReportCommand.kt +++ b/src/commonMain/kotlin/commands/cli/ReportCommand.kt @@ -1,7 +1,7 @@ package commands.cli import components.data.TeamHistoryConfig -import history.filter.transform.RepositoryDateTransform +import history.filter.transform.RepositoryDateBetweenTransform import history.storage.StoredHistory import kotlinx.coroutines.Runnable @@ -30,7 +30,7 @@ class ReportCommand( includeDiscussions = true, ) } - val transform = RepositoryDateTransform(teamHistoryConfig.startDate, teamHistoryConfig.endDate) + val transform = RepositoryDateBetweenTransform(teamHistoryConfig.startDate, teamHistoryConfig.endDate) val filteredRepos = storedReposDeep.map(transform) .filter { repo -> repo.codeReviews.isNotEmpty() || repo.discussions.isNotEmpty() } if (filteredRepos.isNotEmpty()) { diff --git a/src/commonMain/kotlin/commands/cli/ServeCommand.kt b/src/commonMain/kotlin/commands/cli/ServeCommand.kt index 22d3e6b..b04ad73 100644 --- a/src/commonMain/kotlin/commands/cli/ServeCommand.kt +++ b/src/commonMain/kotlin/commands/cli/ServeCommand.kt @@ -1,11 +1,13 @@ package commands.cli +import calculator.di.provideGenericCountMetricCalculators import components.data.Repository import components.data.TeamHistoryConfig +import components.metrics.SerializableGenericCountMetric +import history.filter.transform.RepositoryDateBetweenTransform import history.storage.StoredHistory import io.ktor.serialization.kotlinx.json.json import io.ktor.server.application.Application -import io.ktor.server.application.ApplicationCall import io.ktor.server.application.call import io.ktor.server.application.install import io.ktor.server.cio.CIO @@ -15,11 +17,11 @@ import io.ktor.server.plugins.contentnegotiation.ContentNegotiation import io.ktor.server.plugins.cors.routing.CORS import io.ktor.server.plugins.forwardedheaders.ForwardedHeaders import io.ktor.server.response.respond -import io.ktor.server.response.respondText import io.ktor.server.routing.get import io.ktor.server.routing.routing -import io.ktor.util.pipeline.PipelineContext import kotlinx.coroutines.Runnable +import kotlinx.datetime.LocalDate +import kotlinx.serialization.Serializable import server.config.ServerConfig class ServeCommand( @@ -28,25 +30,47 @@ class ServeCommand( private val serverConfig: ServerConfig, ) : Runnable { + @Serializable + private data class MessageResponse(val message: String) + private lateinit var server: BaseApplicationEngine private lateinit var storedRepos: List + private val metricsByName = mutableMapOf() + private val metricsByNameTimeSeries = mutableMapOf>() override fun run() { println("== File configuration ==") println(teamHistoryConfig.simpleFormat) println("\n== Serving ==") - storedRepos = storedHistory.fetchAllRepositories().map { - storedHistory.fetchRepository( - name = it.name, - includeCodeReviews = true, - includeDiscussions = true, - ) - } + storedRepos = storedHistory.fetchAllRepositories() + .map { + storedHistory.fetchRepository( + name = it.name, + includeCodeReviews = true, + includeDiscussions = true, + ) + } + .map(RepositoryDateBetweenTransform(teamHistoryConfig.startDate, teamHistoryConfig.endDate)) + if (storedRepos.isEmpty()) { println("Aborted. No repositories found in the local storage.") return } + + val calculators = provideGenericCountMetricCalculators() + calculators.forEach { calculator -> + // store the metric for the whole time period + val metric = calculator.calculate(storedRepos) + metricsByName[metric.name] = metric.serializable + + // store the metric as a time series + val timeSeries = calculator.asTimeSeries(teamHistoryConfig.startDate, teamHistoryConfig.endDate, storedRepos) + if (timeSeries.values.isNotEmpty()) { + metricsByNameTimeSeries[metric.name] = timeSeries.mapValues { (_, metric) -> metric.serializable } + } + } + println("Now booting up a server...") server = embeddedServer( factory = CIO, @@ -62,18 +86,15 @@ class ServeCommand( install(ContentNegotiation) { json() } routing { - get("/") { respondToRoot() } - get("/shutdown") { respondToShutdown() } + get("/") { call.respond(MessageResponse("Yep, it runs…")) } + get("/repos") { call.respond(storedRepos) } + get("/metrics") { call.respond(metricsByName) } + get("/time-series") { call.respond(metricsByNameTimeSeries) } + get("/shutdown") { + call.respond(MessageResponse("☠\uFE0F The server is dead now.")) + server.stop() + } } } - private suspend fun PipelineContext.respondToRoot() { - call.respond(storedRepos) - } - - private suspend fun PipelineContext.respondToShutdown() { - call.respondText("☠\uFE0F The server is dead now.") - server.stop() - } - } diff --git a/src/commonMain/kotlin/components/data/CodeReview.kt b/src/commonMain/kotlin/components/data/CodeReview.kt index 84a8f0c..ec690dd 100644 --- a/src/commonMain/kotlin/components/data/CodeReview.kt +++ b/src/commonMain/kotlin/components/data/CodeReview.kt @@ -29,6 +29,8 @@ data class CodeReview( @Serializable enum class State { OPEN, CLOSED } + val isDead: Boolean by lazy { comments.isEmpty() && feedbacks.isEmpty() } + private val lifetime = when { mergedAt != null -> mergedAt.epochMillisecondsUtc - createdAt.epochMillisecondsUtc closedAt != null -> closedAt.epochMillisecondsUtc - createdAt.epochMillisecondsUtc diff --git a/src/commonMain/kotlin/components/data/Discussion.kt b/src/commonMain/kotlin/components/data/Discussion.kt index 6e24799..0502c22 100644 --- a/src/commonMain/kotlin/components/data/Discussion.kt +++ b/src/commonMain/kotlin/components/data/Discussion.kt @@ -20,6 +20,8 @@ data class Discussion( val author: User, ) : HasSimpleFormat { + val isDead: Boolean by lazy { comments.isEmpty() } + private val lifetime = when { closedAt != null -> closedAt.epochMillisecondsUtc - createdAt.epochMillisecondsUtc else -> Clock.System.now().toEpochMilliseconds() - createdAt.epochMillisecondsUtc diff --git a/src/commonMain/kotlin/components/data/Repository.kt b/src/commonMain/kotlin/components/data/Repository.kt index 292ddac..dff2ebf 100644 --- a/src/commonMain/kotlin/components/data/Repository.kt +++ b/src/commonMain/kotlin/components/data/Repository.kt @@ -15,6 +15,8 @@ data class Repository( val fullName: String get() = "$owner/$name" + val isDead: Boolean by lazy { codeReviews.all(CodeReview::isDead) && discussions.all(Discussion::isDead) } + override val simpleFormat = """ |Repository $fullName | · ${codeReviews.size} code reviews diff --git a/src/commonMain/kotlin/components/metrics/GenericCountMetric.kt b/src/commonMain/kotlin/components/metrics/GenericCountMetric.kt index 3b1691a..ba65d16 100644 --- a/src/commonMain/kotlin/components/metrics/GenericCountMetric.kt +++ b/src/commonMain/kotlin/components/metrics/GenericCountMetric.kt @@ -43,6 +43,26 @@ interface GenericCountMetric : HasSimpleFormat { val averagePerRepository: Float get() = totalForAllRepositories.toFloat() / perRepository.size + val serializable: SerializableGenericCountMetric + get() = SerializableGenericCountMetric( + name = name, + perAuthor = perAuthor.mapKeys { it.key.login }, + perReviewer = perReviewer.mapKeys { it.key.login }, + perCodeReview = perCodeReview.mapKeys { "#${it.key.number}" }, + perDiscussion = perDiscussion.mapKeys { "#${it.key.number}" }, + perRepository = perRepository.mapKeys { it.key.fullName }, + totalForAllAuthors = totalForAllAuthors, + averagePerAuthor = averagePerAuthor, + totalForAllReviewers = totalForAllReviewers, + averagePerReviewer = averagePerReviewer, + totalForAllCodeReviews = totalForAllCodeReviews, + averagePerCodeReview = averagePerCodeReview, + totalForAllDiscussions = totalForAllDiscussions, + averagePerDiscussion = averagePerDiscussion, + totalForAllRepositories = totalForAllRepositories, + averagePerRepository = averagePerRepository, + ) + override val simpleFormat get() = buildString { appendLine(name) diff --git a/src/commonMain/kotlin/components/metrics/SerializableGenericCountMetric.kt b/src/commonMain/kotlin/components/metrics/SerializableGenericCountMetric.kt new file mode 100644 index 0000000..117d60a --- /dev/null +++ b/src/commonMain/kotlin/components/metrics/SerializableGenericCountMetric.kt @@ -0,0 +1,23 @@ +package components.metrics + +import kotlinx.serialization.Serializable + +@Serializable +data class SerializableGenericCountMetric( + val name: String, + val perAuthor: Map, + val perReviewer: Map, + val perCodeReview: Map, + val perDiscussion: Map, + val perRepository: Map, + val totalForAllAuthors: Long, + val averagePerAuthor: Float, + val totalForAllReviewers: Long, + val averagePerReviewer: Float, + val totalForAllCodeReviews: Long, + val averagePerCodeReview: Float, + val totalForAllDiscussions: Long, + val averagePerDiscussion: Float, + val totalForAllRepositories: Long, + val averagePerRepository: Float, +) diff --git a/src/commonMain/kotlin/history/filter/predicate/CodeReviewCommentIsCreatedAt.kt b/src/commonMain/kotlin/history/filter/predicate/CodeReviewCommentIsCreatedAt.kt new file mode 100644 index 0000000..d3041f2 --- /dev/null +++ b/src/commonMain/kotlin/history/filter/predicate/CodeReviewCommentIsCreatedAt.kt @@ -0,0 +1,10 @@ +package history.filter.predicate + +import components.data.CodeReviewComment +import kotlinx.datetime.LocalDate + +class CodeReviewCommentIsCreatedAt( + private val createdAt: LocalDate, +) : (CodeReviewComment) -> Boolean { + override fun invoke(subject: CodeReviewComment) = subject.createdAt.date == createdAt +} diff --git a/src/commonMain/kotlin/history/filter/predicate/CodeReviewFeedbackIsSubmittedAt.kt b/src/commonMain/kotlin/history/filter/predicate/CodeReviewFeedbackIsSubmittedAt.kt new file mode 100644 index 0000000..430a0be --- /dev/null +++ b/src/commonMain/kotlin/history/filter/predicate/CodeReviewFeedbackIsSubmittedAt.kt @@ -0,0 +1,10 @@ +package history.filter.predicate + +import components.data.CodeReviewFeedback +import kotlinx.datetime.LocalDate + +class CodeReviewFeedbackIsSubmittedAt( + private val createdAt: LocalDate, +) : (CodeReviewFeedback) -> Boolean { + override fun invoke(subject: CodeReviewFeedback) = subject.submittedAt?.date == createdAt +} diff --git a/src/commonMain/kotlin/history/filter/predicate/CodeReviewIsCreatedAt.kt b/src/commonMain/kotlin/history/filter/predicate/CodeReviewIsCreatedAt.kt new file mode 100644 index 0000000..407013e --- /dev/null +++ b/src/commonMain/kotlin/history/filter/predicate/CodeReviewIsCreatedAt.kt @@ -0,0 +1,10 @@ +package history.filter.predicate + +import components.data.CodeReview +import kotlinx.datetime.LocalDate + +class CodeReviewIsCreatedAt( + private val createdAt: LocalDate, +) : (CodeReview) -> Boolean { + override fun invoke(subject: CodeReview): Boolean = subject.createdAt.date == createdAt +} diff --git a/src/commonMain/kotlin/history/filter/predicate/CodeReviewIsFinishedAt.kt b/src/commonMain/kotlin/history/filter/predicate/CodeReviewIsFinishedAt.kt new file mode 100644 index 0000000..4424ef0 --- /dev/null +++ b/src/commonMain/kotlin/history/filter/predicate/CodeReviewIsFinishedAt.kt @@ -0,0 +1,11 @@ +package history.filter.predicate + +import components.data.CodeReview +import kotlinx.datetime.LocalDate + +class CodeReviewIsFinishedAt( + private val createdAt: LocalDate, +) : (CodeReview) -> Boolean { + override fun invoke(subject: CodeReview): Boolean = + subject.closedAt?.date == createdAt || subject.mergedAt?.date == createdAt +} diff --git a/src/commonMain/kotlin/history/filter/predicate/DiscussionCommentIsCreatedAt.kt b/src/commonMain/kotlin/history/filter/predicate/DiscussionCommentIsCreatedAt.kt new file mode 100644 index 0000000..a0231ea --- /dev/null +++ b/src/commonMain/kotlin/history/filter/predicate/DiscussionCommentIsCreatedAt.kt @@ -0,0 +1,10 @@ +package history.filter.predicate + +import components.data.DiscussionComment +import kotlinx.datetime.LocalDate + +class DiscussionCommentIsCreatedAt( + private val createdAt: LocalDate, +) : (DiscussionComment) -> Boolean { + override fun invoke(subject: DiscussionComment) = subject.createdAt.date == createdAt +} diff --git a/src/commonMain/kotlin/history/filter/predicate/DiscussionIsCreatedAt.kt b/src/commonMain/kotlin/history/filter/predicate/DiscussionIsCreatedAt.kt new file mode 100644 index 0000000..b403e81 --- /dev/null +++ b/src/commonMain/kotlin/history/filter/predicate/DiscussionIsCreatedAt.kt @@ -0,0 +1,10 @@ +package history.filter.predicate + +import components.data.Discussion +import kotlinx.datetime.LocalDate + +class DiscussionIsCreatedAt( + private val createdAt: LocalDate, +) : (Discussion) -> Boolean { + override fun invoke(subject: Discussion): Boolean = subject.createdAt.date == createdAt +} diff --git a/src/commonMain/kotlin/history/filter/predicate/DiscussionIsFinishedAt.kt b/src/commonMain/kotlin/history/filter/predicate/DiscussionIsFinishedAt.kt new file mode 100644 index 0000000..90eef17 --- /dev/null +++ b/src/commonMain/kotlin/history/filter/predicate/DiscussionIsFinishedAt.kt @@ -0,0 +1,10 @@ +package history.filter.predicate + +import components.data.Discussion +import kotlinx.datetime.LocalDate + +class DiscussionIsFinishedAt( + private val createdAt: LocalDate, +) : (Discussion) -> Boolean { + override fun invoke(subject: Discussion): Boolean = subject.closedAt?.date == createdAt +} diff --git a/src/commonMain/kotlin/history/filter/transform/CodeReviewCreatedAtTransform.kt b/src/commonMain/kotlin/history/filter/transform/CodeReviewCreatedAtTransform.kt new file mode 100644 index 0000000..1aeaeb2 --- /dev/null +++ b/src/commonMain/kotlin/history/filter/transform/CodeReviewCreatedAtTransform.kt @@ -0,0 +1,15 @@ +package history.filter.transform + +import components.data.CodeReview +import history.filter.predicate.CodeReviewCommentIsCreatedAt +import history.filter.predicate.CodeReviewFeedbackIsSubmittedAt +import kotlinx.datetime.LocalDate + +class CodeReviewCreatedAtTransform( + private val createdAt: LocalDate, +) : (CodeReview) -> CodeReview { + override fun invoke(subject: CodeReview) = subject.copy( + comments = subject.comments.filter(CodeReviewCommentIsCreatedAt(createdAt)), + feedbacks = subject.feedbacks.filter(CodeReviewFeedbackIsSubmittedAt(createdAt)), + ) +} diff --git a/src/commonMain/kotlin/history/filter/transform/CodeReviewDateTransform.kt b/src/commonMain/kotlin/history/filter/transform/CodeReviewDateBetweenTransform.kt similarity index 94% rename from src/commonMain/kotlin/history/filter/transform/CodeReviewDateTransform.kt rename to src/commonMain/kotlin/history/filter/transform/CodeReviewDateBetweenTransform.kt index 579ad2c..c9c5e26 100644 --- a/src/commonMain/kotlin/history/filter/transform/CodeReviewDateTransform.kt +++ b/src/commonMain/kotlin/history/filter/transform/CodeReviewDateBetweenTransform.kt @@ -5,7 +5,7 @@ import history.filter.predicate.CodeReviewCommentIsBetween import history.filter.predicate.CodeReviewFeedbackIsBetween import kotlinx.datetime.LocalDate -class CodeReviewDateTransform( +class CodeReviewDateBetweenTransform( private val openDateInclusive: LocalDate, private val closeDateInclusive: LocalDate? = null, ) : (CodeReview) -> CodeReview { diff --git a/src/commonMain/kotlin/history/filter/transform/DiscussionCreatedAtTransform.kt b/src/commonMain/kotlin/history/filter/transform/DiscussionCreatedAtTransform.kt new file mode 100644 index 0000000..5b5be29 --- /dev/null +++ b/src/commonMain/kotlin/history/filter/transform/DiscussionCreatedAtTransform.kt @@ -0,0 +1,13 @@ +package history.filter.transform + +import components.data.Discussion +import history.filter.predicate.DiscussionCommentIsCreatedAt +import kotlinx.datetime.LocalDate + +class DiscussionCreatedAtTransform( + private val createdAt: LocalDate, +) : (Discussion) -> Discussion { + override fun invoke(subject: Discussion) = subject.copy( + comments = subject.comments.filter(DiscussionCommentIsCreatedAt(createdAt)), + ) +} diff --git a/src/commonMain/kotlin/history/filter/transform/DiscussionDateTransform.kt b/src/commonMain/kotlin/history/filter/transform/DiscussionDateBetweenTransform.kt similarity index 92% rename from src/commonMain/kotlin/history/filter/transform/DiscussionDateTransform.kt rename to src/commonMain/kotlin/history/filter/transform/DiscussionDateBetweenTransform.kt index 1a87686..48724c0 100644 --- a/src/commonMain/kotlin/history/filter/transform/DiscussionDateTransform.kt +++ b/src/commonMain/kotlin/history/filter/transform/DiscussionDateBetweenTransform.kt @@ -4,7 +4,7 @@ import components.data.Discussion import history.filter.predicate.DiscussionCommentIsBetween import kotlinx.datetime.LocalDate -class DiscussionDateTransform( +class DiscussionDateBetweenTransform( private val openDateInclusive: LocalDate, private val closeDateInclusive: LocalDate? = null, ) : (Discussion) -> Discussion { diff --git a/src/commonMain/kotlin/history/filter/transform/RepositoryCreatedAtTransform.kt b/src/commonMain/kotlin/history/filter/transform/RepositoryCreatedAtTransform.kt new file mode 100644 index 0000000..b883340 --- /dev/null +++ b/src/commonMain/kotlin/history/filter/transform/RepositoryCreatedAtTransform.kt @@ -0,0 +1,23 @@ +package history.filter.transform + +import components.data.Repository +import history.filter.predicate.CodeReviewIsCreatedAt +import history.filter.predicate.DiscussionIsCreatedAt +import kotlinx.datetime.LocalDate +import utils.filterIf +import utils.mapIf + +class RepositoryCreatedAtTransform( + private val createdAt: LocalDate, + private val applyToCommentsAndFeedbacks: Boolean, + private val applyToCodeReviewsAndDiscussions: Boolean, +) : (Repository) -> Repository { + override fun invoke(subject: Repository) = subject.copy( + codeReviews = subject.codeReviews + .mapIf(applyToCommentsAndFeedbacks, CodeReviewCreatedAtTransform(createdAt)) + .filterIf(applyToCodeReviewsAndDiscussions, CodeReviewIsCreatedAt(createdAt)), + discussions = subject.discussions + .mapIf(applyToCommentsAndFeedbacks, DiscussionCreatedAtTransform(createdAt)) + .filterIf(applyToCodeReviewsAndDiscussions, DiscussionIsCreatedAt(createdAt)), + ) +} diff --git a/src/commonMain/kotlin/history/filter/transform/RepositoryDateTransform.kt b/src/commonMain/kotlin/history/filter/transform/RepositoryDateBetweenTransform.kt similarity index 75% rename from src/commonMain/kotlin/history/filter/transform/RepositoryDateTransform.kt rename to src/commonMain/kotlin/history/filter/transform/RepositoryDateBetweenTransform.kt index 4eb746f..484ad0d 100644 --- a/src/commonMain/kotlin/history/filter/transform/RepositoryDateTransform.kt +++ b/src/commonMain/kotlin/history/filter/transform/RepositoryDateBetweenTransform.kt @@ -5,16 +5,16 @@ import history.filter.predicate.CodeReviewIsBetween import history.filter.predicate.DiscussionIsBetween import kotlinx.datetime.LocalDate -class RepositoryDateTransform( +class RepositoryDateBetweenTransform( private val openDateInclusive: LocalDate, private val closeDateInclusive: LocalDate? = null, ) : (Repository) -> Repository { override fun invoke(subject: Repository) = subject.copy( codeReviews = subject.codeReviews .filter(CodeReviewIsBetween(openDateInclusive, closeDateInclusive)) - .map(CodeReviewDateTransform(openDateInclusive, closeDateInclusive)), + .map(CodeReviewDateBetweenTransform(openDateInclusive, closeDateInclusive)), discussions = subject.discussions .filter(DiscussionIsBetween(openDateInclusive, closeDateInclusive)) - .map(DiscussionDateTransform(openDateInclusive, closeDateInclusive)), + .map(DiscussionDateBetweenTransform(openDateInclusive, closeDateInclusive)), ) } diff --git a/src/commonMain/kotlin/history/filter/transform/RepositoryFinishedAtTransform.kt b/src/commonMain/kotlin/history/filter/transform/RepositoryFinishedAtTransform.kt new file mode 100644 index 0000000..f544e9e --- /dev/null +++ b/src/commonMain/kotlin/history/filter/transform/RepositoryFinishedAtTransform.kt @@ -0,0 +1,17 @@ +package history.filter.transform + +import components.data.Repository +import history.filter.predicate.CodeReviewIsFinishedAt +import history.filter.predicate.DiscussionIsFinishedAt +import kotlinx.datetime.LocalDate + +class RepositoryFinishedAtTransform( + private val finishedAt: LocalDate, +) : (Repository) -> Repository { + override fun invoke(subject: Repository) = subject.copy( + codeReviews = subject.codeReviews + .filter(CodeReviewIsFinishedAt(finishedAt)), + discussions = subject.discussions + .filter(DiscussionIsFinishedAt(finishedAt)), + ) +} diff --git a/src/commonMain/kotlin/utils/Utils.kt b/src/commonMain/kotlin/utils/Utils.kt index ec7a41f..66d519d 100644 --- a/src/commonMain/kotlin/utils/Utils.kt +++ b/src/commonMain/kotlin/utils/Utils.kt @@ -26,11 +26,17 @@ expect fun readEnvVar(name: String): String? suspend inline fun Iterable.parallelMap( crossinline mapper: suspend (Input) -> Output, ): List = coroutineScope { - map { - async { mapper(it) } + map { input -> + async { mapper(input) } }.awaitAll() } +inline fun List.mapIf(condition: Boolean, transform: (Item) -> Item) = + if (condition) map(transform) else this + +inline fun List.filterIf(condition: Boolean, predicate: (Item) -> Boolean) = + if (condition) filter(predicate) else this + fun > Map.getTop(n: Int = 1): List> = toList() .sortedByDescending { (_, value) -> value } diff --git a/src/commonTest/kotlin/components/metrics/GenericLongMetricTest.kt b/src/commonTest/kotlin/components/metrics/GenericCountMetricTest.kt similarity index 68% rename from src/commonTest/kotlin/components/metrics/GenericLongMetricTest.kt rename to src/commonTest/kotlin/components/metrics/GenericCountMetricTest.kt index 8c89486..a71a922 100644 --- a/src/commonTest/kotlin/components/metrics/GenericLongMetricTest.kt +++ b/src/commonTest/kotlin/components/metrics/GenericCountMetricTest.kt @@ -5,73 +5,73 @@ import assertk.assertions.isEqualTo import kotlin.test.Test import stubs.Stubs -class GenericLongMetricTest { +class GenericCountMetricTest { @Test fun `long metric total for all users is correct`() { - val result = Stubs.metrics.genericLong.totalForAllAuthors + val result = Stubs.metrics.genericCount.totalForAllAuthors val expected = 6L assertThat(result).isEqualTo(expected) } @Test fun `long metric average per user is correct`() { - val result = Stubs.metrics.genericLong.averagePerAuthor + val result = Stubs.metrics.genericCount.averagePerAuthor val expected = 2.0F assertThat(result).isEqualTo(expected) } @Test fun `long metric total for all reviewers is correct`() { - val result = Stubs.metrics.genericLong.totalForAllReviewers + val result = Stubs.metrics.genericCount.totalForAllReviewers val expected = 60L assertThat(result).isEqualTo(expected) } @Test fun `long metric average per reviewer is correct`() { - val result = Stubs.metrics.genericLong.averagePerReviewer + val result = Stubs.metrics.genericCount.averagePerReviewer val expected = 20f assertThat(result).isEqualTo(expected) } @Test fun `long metric total for all code reviews is correct`() { - val result = Stubs.metrics.genericLong.totalForAllCodeReviews + val result = Stubs.metrics.genericCount.totalForAllCodeReviews val expected = 600L assertThat(result).isEqualTo(expected) } @Test fun `long metric average per code review is correct`() { - val result = Stubs.metrics.genericLong.averagePerCodeReview + val result = Stubs.metrics.genericCount.averagePerCodeReview val expected = 200f assertThat(result).isEqualTo(expected) } @Test fun `long metric total for all discussions is correct`() { - val result = Stubs.metrics.genericLong.totalForAllDiscussions + val result = Stubs.metrics.genericCount.totalForAllDiscussions val expected = 6000L assertThat(result).isEqualTo(expected) } @Test fun `long metric average per discussion is correct`() { - val result = Stubs.metrics.genericLong.averagePerDiscussion + val result = Stubs.metrics.genericCount.averagePerDiscussion val expected = 2000f assertThat(result).isEqualTo(expected) } @Test fun `long metric total for all repositories is correct`() { - val result = Stubs.metrics.genericLong.totalForAllRepositories + val result = Stubs.metrics.genericCount.totalForAllRepositories val expected = 60000L assertThat(result).isEqualTo(expected) } @Test fun `long metric average per repository is correct`() { - val result = Stubs.metrics.genericLong.averagePerRepository + val result = Stubs.metrics.genericCount.averagePerRepository val expected = 20000f assertThat(result).isEqualTo(expected) diff --git a/src/commonTest/kotlin/history/filter/predicate/CodeReviewCommentIsCreatedAtTest.kt b/src/commonTest/kotlin/history/filter/predicate/CodeReviewCommentIsCreatedAtTest.kt new file mode 100644 index 0000000..aa41457 --- /dev/null +++ b/src/commonTest/kotlin/history/filter/predicate/CodeReviewCommentIsCreatedAtTest.kt @@ -0,0 +1,32 @@ +package history.filter.predicate + +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import stubs.Stubs +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class CodeReviewCommentIsCreatedAtTest { + + @Test fun `code review comment is created at returns false when days mismatch`() { + val comment = Stubs.generic.codeReviewComment.copy( + createdAt = LocalDateTime(2023, 2, 28, 10, 40, 20), + ) + + val check = CodeReviewCommentIsCreatedAt(LocalDate(2023, 2, 27)) + + assertFalse(check(comment)) + } + + @Test fun `code review comment is created at returns true when days match`() { + val comment = Stubs.generic.codeReviewComment.copy( + createdAt = LocalDateTime(2023, 2, 28, 10, 40, 20), + ) + + val check = CodeReviewCommentIsCreatedAt(LocalDate(2023, 2, 28)) + + assertTrue(check(comment)) + } + +} diff --git a/src/commonTest/kotlin/history/filter/predicate/CodeReviewFeedbackIsSubmittedAtTest.kt b/src/commonTest/kotlin/history/filter/predicate/CodeReviewFeedbackIsSubmittedAtTest.kt new file mode 100644 index 0000000..5cacfc3 --- /dev/null +++ b/src/commonTest/kotlin/history/filter/predicate/CodeReviewFeedbackIsSubmittedAtTest.kt @@ -0,0 +1,32 @@ +package history.filter.predicate + +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import stubs.Stubs + +class CodeReviewFeedbackIsSubmittedAtTest { + + @Test fun `code review feedback is submitted at returns false when days mismatch`() { + val feedback = Stubs.generic.codeReviewFeedback.copy( + submittedAt = LocalDateTime(2023, 2, 28, 10, 40, 20), + ) + + val check = CodeReviewFeedbackIsSubmittedAt(LocalDate(2023, 2, 27)) + + assertFalse(check(feedback)) + } + + @Test fun `code review feedback is submitted at returns true when days match`() { + val feedback = Stubs.generic.codeReviewFeedback.copy( + submittedAt = LocalDateTime(2023, 2, 28, 10, 40, 20), + ) + + val check = CodeReviewFeedbackIsSubmittedAt(LocalDate(2023, 2, 28)) + + assertTrue(check(feedback)) + } + +} diff --git a/src/commonTest/kotlin/history/filter/predicate/CodeReviewIsCreatedAtTest.kt b/src/commonTest/kotlin/history/filter/predicate/CodeReviewIsCreatedAtTest.kt new file mode 100644 index 0000000..fdd9a6c --- /dev/null +++ b/src/commonTest/kotlin/history/filter/predicate/CodeReviewIsCreatedAtTest.kt @@ -0,0 +1,32 @@ +package history.filter.predicate + +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import stubs.Stubs + +class CodeReviewIsCreatedAtTest { + + @Test fun `code review is created at returns false when days mismatch`() { + val codeReview = Stubs.generic.codeReview.copy( + createdAt = LocalDateTime(2023, 2, 28, 10, 40, 20), + ) + + val check = CodeReviewIsCreatedAt(LocalDate(2023, 2, 27)) + + assertFalse(check(codeReview)) + } + + @Test fun `code review is created at returns true when days match`() { + val codeReview = Stubs.generic.codeReview.copy( + createdAt = LocalDateTime(2023, 2, 28, 10, 40, 20), + ) + + val check = CodeReviewIsCreatedAt(LocalDate(2023, 2, 28)) + + assertTrue(check(codeReview)) + } + +} diff --git a/src/commonTest/kotlin/history/filter/predicate/CodeReviewIsFinishedAtTest.kt b/src/commonTest/kotlin/history/filter/predicate/CodeReviewIsFinishedAtTest.kt new file mode 100644 index 0000000..0f6fd10 --- /dev/null +++ b/src/commonTest/kotlin/history/filter/predicate/CodeReviewIsFinishedAtTest.kt @@ -0,0 +1,56 @@ +package history.filter.predicate + +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import stubs.Stubs + +class CodeReviewIsFinishedAtTest { + + @Test fun `code review is finished at returns false when days mismatch`() { + val codeReview = Stubs.generic.codeReview.copy( + closedAt = LocalDateTime(2023, 2, 28, 10, 40, 20), + mergedAt = LocalDateTime(2023, 2, 28, 10, 40, 20), + ) + + val check = CodeReviewIsFinishedAt(LocalDate(2023, 2, 27)) + + assertFalse(check(codeReview)) + } + + @Test fun `code review is finished at returns true when closed days match`() { + val codeReview = Stubs.generic.codeReview.copy( + closedAt = LocalDateTime(2023, 2, 28, 10, 40, 20), + mergedAt = LocalDateTime(2023, 2, 27, 10, 40, 20), + ) + + val check = CodeReviewIsFinishedAt(LocalDate(2023, 2, 28)) + + assertTrue(check(codeReview)) + } + + @Test fun `code review is finished at returns true when merged days match`() { + val codeReview = Stubs.generic.codeReview.copy( + closedAt = LocalDateTime(2023, 2, 27, 10, 40, 20), + mergedAt = LocalDateTime(2023, 2, 28, 10, 40, 20), + ) + + val check = CodeReviewIsFinishedAt(LocalDate(2023, 2, 28)) + + assertTrue(check(codeReview)) + } + + @Test fun `code review is finished at returns true when both days match`() { + val codeReview = Stubs.generic.codeReview.copy( + closedAt = LocalDateTime(2023, 2, 28, 10, 40, 20), + mergedAt = LocalDateTime(2023, 2, 28, 10, 40, 20), + ) + + val check = CodeReviewIsFinishedAt(LocalDate(2023, 2, 28)) + + assertTrue(check(codeReview)) + } + +} diff --git a/src/commonTest/kotlin/history/filter/predicate/DiscussionCommentIsCreatedAtTest.kt b/src/commonTest/kotlin/history/filter/predicate/DiscussionCommentIsCreatedAtTest.kt new file mode 100644 index 0000000..f71f49f --- /dev/null +++ b/src/commonTest/kotlin/history/filter/predicate/DiscussionCommentIsCreatedAtTest.kt @@ -0,0 +1,32 @@ +package history.filter.predicate + +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import stubs.Stubs + +class DiscussionCommentIsCreatedAtTest { + + @Test fun `discussion comment is created at returns false when days mismatch`() { + val comment = Stubs.generic.discussionComment.copy( + createdAt = LocalDateTime(2023, 2, 28, 10, 40, 20), + ) + + val check = DiscussionCommentIsCreatedAt(LocalDate(2023, 2, 27)) + + assertFalse(check(comment)) + } + + @Test fun `discussion comment is created at returns true when days match`() { + val comment = Stubs.generic.discussionComment.copy( + createdAt = LocalDateTime(2023, 2, 28, 10, 40, 20), + ) + + val check = DiscussionCommentIsCreatedAt(LocalDate(2023, 2, 28)) + + assertTrue(check(comment)) + } + +} diff --git a/src/commonTest/kotlin/history/filter/predicate/DiscussionIsCreatedAtTest.kt b/src/commonTest/kotlin/history/filter/predicate/DiscussionIsCreatedAtTest.kt new file mode 100644 index 0000000..ecb37b1 --- /dev/null +++ b/src/commonTest/kotlin/history/filter/predicate/DiscussionIsCreatedAtTest.kt @@ -0,0 +1,32 @@ +package history.filter.predicate + +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import stubs.Stubs + +class DiscussionIsCreatedAtTest { + + @Test fun `discussion is created at returns false when days mismatch`() { + val discussion = Stubs.generic.discussion.copy( + createdAt = LocalDateTime(2023, 2, 28, 10, 40, 20), + ) + + val check = DiscussionIsCreatedAt(LocalDate(2023, 2, 27)) + + assertFalse(check(discussion)) + } + + @Test fun `discussion is created at returns true when days match`() { + val discussion = Stubs.generic.discussion.copy( + createdAt = LocalDateTime(2023, 2, 28, 10, 40, 20), + ) + + val check = DiscussionIsCreatedAt(LocalDate(2023, 2, 28)) + + assertTrue(check(discussion)) + } + +} diff --git a/src/commonTest/kotlin/history/filter/predicate/DiscussionIsFinishedAtTest.kt b/src/commonTest/kotlin/history/filter/predicate/DiscussionIsFinishedAtTest.kt new file mode 100644 index 0000000..3fb33a1 --- /dev/null +++ b/src/commonTest/kotlin/history/filter/predicate/DiscussionIsFinishedAtTest.kt @@ -0,0 +1,32 @@ +package history.filter.predicate + +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import stubs.Stubs + +class DiscussionIsFinishedAtTest { + + @Test fun `discussion is finished at returns false when days mismatch`() { + val discussion = Stubs.generic.discussion.copy( + closedAt = LocalDateTime(2023, 2, 28, 10, 40, 20), + ) + + val check = DiscussionIsFinishedAt(LocalDate(2023, 2, 27)) + + assertFalse(check(discussion)) + } + + @Test fun `discussion is finished at returns true when days match`() { + val discussion = Stubs.generic.discussion.copy( + closedAt = LocalDateTime(2023, 2, 28, 10, 40, 20), + ) + + val check = DiscussionIsFinishedAt(LocalDate(2023, 2, 28)) + + assertTrue(check(discussion)) + } + +} diff --git a/src/commonTest/kotlin/history/filter/transform/CodeReviewCreatedAtTransformTest.kt b/src/commonTest/kotlin/history/filter/transform/CodeReviewCreatedAtTransformTest.kt new file mode 100644 index 0000000..349b556 --- /dev/null +++ b/src/commonTest/kotlin/history/filter/transform/CodeReviewCreatedAtTransformTest.kt @@ -0,0 +1,32 @@ +package history.filter.transform + +import assertk.assertThat +import assertk.assertions.isEqualTo +import kotlin.test.Test +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import stubs.Stubs + +class CodeReviewCreatedAtTransformTest { + + @Test fun `transform is applied correctly`() { + val earlyComment = Stubs.generic.codeReviewComment.copy(createdAt = LocalDateTime(2023, 2, 28, 10, 40, 20)) + val validComment = Stubs.generic.codeReviewComment.copy(createdAt = LocalDateTime(2023, 3, 1, 10, 40, 20)) + val lateComment = Stubs.generic.codeReviewComment.copy(createdAt = LocalDateTime(2023, 3, 2, 10, 40, 20)) + val comments = listOf(earlyComment, validComment, lateComment) + + val earlyFeedback = Stubs.generic.codeReviewFeedback.copy(submittedAt = LocalDateTime(2023, 2, 28, 10, 40, 20)) + val validFeedback = Stubs.generic.codeReviewFeedback.copy(submittedAt = LocalDateTime(2023, 3, 1, 10, 40, 20)) + val lateFeedback = Stubs.generic.codeReviewFeedback.copy(submittedAt = LocalDateTime(2023, 3, 2, 10, 40, 20)) + val feedbacks = listOf(earlyFeedback, validFeedback, lateFeedback) + + val codeReview = Stubs.generic.codeReview.copy(comments = comments, feedbacks = feedbacks) + val expected = Stubs.generic.codeReview.copy(comments = listOf(validComment), feedbacks = listOf(validFeedback)) + val transform = CodeReviewCreatedAtTransform( + createdAt = LocalDate(2023, 3, 1), + ) + + assertThat(transform(codeReview)).isEqualTo(expected) + } + +} diff --git a/src/commonTest/kotlin/history/filter/transform/CodeReviewDateTransformTest.kt b/src/commonTest/kotlin/history/filter/transform/CodeReviewDateBetweenTransformTest.kt similarity index 93% rename from src/commonTest/kotlin/history/filter/transform/CodeReviewDateTransformTest.kt rename to src/commonTest/kotlin/history/filter/transform/CodeReviewDateBetweenTransformTest.kt index 3b180e7..3950f15 100644 --- a/src/commonTest/kotlin/history/filter/transform/CodeReviewDateTransformTest.kt +++ b/src/commonTest/kotlin/history/filter/transform/CodeReviewDateBetweenTransformTest.kt @@ -7,7 +7,7 @@ import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime import stubs.Stubs -class CodeReviewDateTransformTest { +class CodeReviewDateBetweenTransformTest { @Test fun `transform is applied correctly`() { val earlyComment = Stubs.generic.codeReviewComment.copy(createdAt = LocalDateTime(2023, 2, 28, 10, 40, 20)) @@ -22,7 +22,7 @@ class CodeReviewDateTransformTest { val codeReview = Stubs.generic.codeReview.copy(comments = comments, feedbacks = feedbacks) val expected = Stubs.generic.codeReview.copy(comments = listOf(validComment), feedbacks = listOf(validFeedback)) - val transform = CodeReviewDateTransform( + val transform = CodeReviewDateBetweenTransform( openDateInclusive = LocalDate(2023, 3, 1), closeDateInclusive = LocalDate(2023, 3, 1), ) diff --git a/src/commonTest/kotlin/history/filter/transform/DiscussionCreatedAtTransformTest.kt b/src/commonTest/kotlin/history/filter/transform/DiscussionCreatedAtTransformTest.kt new file mode 100644 index 0000000..bc75152 --- /dev/null +++ b/src/commonTest/kotlin/history/filter/transform/DiscussionCreatedAtTransformTest.kt @@ -0,0 +1,27 @@ +package history.filter.transform + +import assertk.assertThat +import assertk.assertions.isEqualTo +import kotlin.test.Test +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import stubs.Stubs + +class DiscussionCreatedAtTransformTest { + + @Test fun `transform is applied correctly`() { + val earlyComment = Stubs.generic.discussionComment.copy(createdAt = LocalDateTime(2023, 2, 28, 10, 40, 20)) + val validComment = Stubs.generic.discussionComment.copy(createdAt = LocalDateTime(2023, 3, 1, 10, 40, 20)) + val lateComment = Stubs.generic.discussionComment.copy(createdAt = LocalDateTime(2023, 3, 2, 10, 40, 20)) + val comments = listOf(earlyComment, validComment, lateComment) + + val discussion = Stubs.generic.discussion.copy(comments = comments) + val expected = Stubs.generic.discussion.copy(comments = listOf(validComment)) + val transform = DiscussionCreatedAtTransform( + createdAt = LocalDate(2023, 3, 1), + ) + + assertThat(transform(discussion)).isEqualTo(expected) + } + +} diff --git a/src/commonTest/kotlin/history/filter/transform/DiscussionDateTransformTest.kt b/src/commonTest/kotlin/history/filter/transform/DiscussionDateBetweenTransformTest.kt similarity index 91% rename from src/commonTest/kotlin/history/filter/transform/DiscussionDateTransformTest.kt rename to src/commonTest/kotlin/history/filter/transform/DiscussionDateBetweenTransformTest.kt index 759bf7e..2bd449b 100644 --- a/src/commonTest/kotlin/history/filter/transform/DiscussionDateTransformTest.kt +++ b/src/commonTest/kotlin/history/filter/transform/DiscussionDateBetweenTransformTest.kt @@ -7,7 +7,7 @@ import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime import stubs.Stubs -class DiscussionDateTransformTest { +class DiscussionDateBetweenTransformTest { @Test fun `transform is applied correctly`() { val earlyComment = Stubs.generic.discussionComment.copy(createdAt = LocalDateTime(2023, 2, 28, 10, 40, 20)) @@ -17,7 +17,7 @@ class DiscussionDateTransformTest { val discussion = Stubs.generic.discussion.copy(comments = comments) val expected = Stubs.generic.discussion.copy(comments = listOf(validComment)) - val transform = DiscussionDateTransform( + val transform = DiscussionDateBetweenTransform( openDateInclusive = LocalDate(2023, 3, 1), closeDateInclusive = LocalDate(2023, 3, 1), ) diff --git a/src/commonTest/kotlin/history/filter/transform/RepositoryCreatedAtTransformTest.kt b/src/commonTest/kotlin/history/filter/transform/RepositoryCreatedAtTransformTest.kt new file mode 100644 index 0000000..c5aa1fc --- /dev/null +++ b/src/commonTest/kotlin/history/filter/transform/RepositoryCreatedAtTransformTest.kt @@ -0,0 +1,199 @@ +package history.filter.transform + +import assertk.assertThat +import assertk.assertions.isEqualTo +import kotlin.test.Test +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import stubs.Stubs + +class RepositoryCreatedAtTransformTest { + + // region Code Review components + private val earlyCodeReviewComment = Stubs.generic.codeReviewComment.copy( + createdAt = LocalDateTime(2023, 2, 28, 10, 40, 20), + ) + private val validCodeReviewComment = Stubs.generic.codeReviewComment.copy( + createdAt = LocalDateTime(2023, 3, 1, 10, 40, 20), + ) + private val lateCodeReviewComment = Stubs.generic.codeReviewComment.copy( + createdAt = LocalDateTime(2023, 3, 2, 10, 40, 20), + ) + private val codeReviewComments = listOf(earlyCodeReviewComment, validCodeReviewComment, lateCodeReviewComment) + + private val earlyCodeReviewFeedback = Stubs.generic.codeReviewFeedback.copy( + submittedAt = LocalDateTime(2023, 2, 28, 10, 40, 20), + ) + private val validCodeReviewFeedback = Stubs.generic.codeReviewFeedback.copy( + submittedAt = LocalDateTime(2023, 3, 1, 10, 40, 20), + ) + private val lateCodeReviewFeedback = Stubs.generic.codeReviewFeedback.copy( + submittedAt = LocalDateTime(2023, 3, 2, 10, 40, 20), + ) + private val codeReviewFeedbacks = listOf(earlyCodeReviewFeedback, validCodeReviewFeedback, lateCodeReviewFeedback) + // endregion Code Review components + + // region Discussion components + private val earlyDiscussionComment = Stubs.generic.discussionComment.copy( + createdAt = LocalDateTime(2023, 2, 28, 10, 40, 20), + ) + private val validDiscussionComment = Stubs.generic.discussionComment.copy( + createdAt = LocalDateTime(2023, 3, 1, 10, 40, 20), + ) + private val lateDiscussionComment = Stubs.generic.discussionComment.copy( + createdAt = LocalDateTime(2023, 3, 2, 10, 40, 20), + ) + private val discussionComments = listOf(earlyDiscussionComment, validDiscussionComment, lateDiscussionComment) + // endregion Discussion components + + // region Repository components + private val earlyCodeReview = Stubs.generic.codeReview.copy( + createdAt = LocalDateTime(2023, 2, 28, 10, 40, 20), + mergedAt = LocalDateTime(2023, 2, 28, 10, 45, 20), + closedAt = LocalDateTime(2023, 2, 28, 10, 50, 20), + comments = codeReviewComments, + feedbacks = codeReviewFeedbacks, + ) + private val validCodeReview = Stubs.generic.codeReview.copy( + createdAt = LocalDateTime(2023, 3, 1, 10, 40, 20), + mergedAt = LocalDateTime(2023, 3, 1, 10, 45, 20), + closedAt = LocalDateTime(2023, 3, 1, 10, 50, 20), + comments = codeReviewComments, + feedbacks = codeReviewFeedbacks, + ) + private val lateCodeReview = Stubs.generic.codeReview.copy( + createdAt = LocalDateTime(2023, 3, 2, 10, 40, 20), + mergedAt = LocalDateTime(2023, 3, 2, 10, 45, 20), + closedAt = LocalDateTime(2023, 3, 2, 10, 50, 20), + comments = codeReviewComments, + feedbacks = codeReviewFeedbacks, + ) + val codeReviews = listOf(earlyCodeReview, validCodeReview, lateCodeReview) + + private val earlyDiscussion = Stubs.generic.discussion.copy( + createdAt = LocalDateTime(2023, 2, 28, 10, 40, 20), + closedAt = LocalDateTime(2023, 2, 28, 10, 45, 20), + comments = discussionComments, + ) + private val validDiscussion = Stubs.generic.discussion.copy( + createdAt = LocalDateTime(2023, 3, 1, 10, 40, 20), + closedAt = LocalDateTime(2023, 3, 1, 10, 45, 20), + comments = discussionComments, + ) + private val lateDiscussion = Stubs.generic.discussion.copy( + createdAt = LocalDateTime(2023, 3, 2, 10, 40, 20), + closedAt = LocalDateTime(2023, 3, 2, 10, 45, 20), + comments = discussionComments, + ) + private val discussions = listOf(earlyDiscussion, validDiscussion, lateDiscussion) + // endregion Repository components + + val repository = Stubs.generic.repository.copy( + codeReviews = codeReviews, + discussions = discussions, + ) + + @Test fun `transform is applied correctly - apply to all`() { + val expectedCodeReviewComments = listOf(validCodeReviewComment) + val expectedCodeReviewFeedbacks = listOf(validCodeReviewFeedback) + val expectedCodeReviews = listOf( + validCodeReview.copy( + comments = expectedCodeReviewComments, + feedbacks = expectedCodeReviewFeedbacks, + ), + ) + + val expectedDiscussionComments = listOf(validDiscussionComment) + val expectedDiscussions = listOf( + validDiscussion.copy( + comments = expectedDiscussionComments, + ), + ) + + val expectedRepository = Stubs.generic.repository.copy( + codeReviews = expectedCodeReviews, + discussions = expectedDiscussions, + ) + + val transform = RepositoryCreatedAtTransform( + createdAt = LocalDate(2023, 3, 1), + applyToCommentsAndFeedbacks = true, + applyToCodeReviewsAndDiscussions = true, + ) + + assertThat(transform(repository)).isEqualTo(expectedRepository) + } + + @Test fun `transform is applied correctly - apply to code reviews and discussions only`() { + val expectedCodeReviewComments = codeReviewComments + val expectedCodeReviewFeedbacks = codeReviewFeedbacks + val expectedCodeReviews = listOf( + validCodeReview.copy( + comments = expectedCodeReviewComments, + feedbacks = expectedCodeReviewFeedbacks, + ), + ) + + val expectedDiscussionComments = discussionComments + val expectedDiscussions = listOf( + validDiscussion.copy( + comments = expectedDiscussionComments, + ), + ) + + val expectedRepository = Stubs.generic.repository.copy( + codeReviews = expectedCodeReviews, + discussions = expectedDiscussions, + ) + + val transform = RepositoryCreatedAtTransform( + createdAt = LocalDate(2023, 3, 1), + applyToCommentsAndFeedbacks = false, + applyToCodeReviewsAndDiscussions = true, + ) + + assertThat(transform(repository)).isEqualTo(expectedRepository) + } + + @Test fun `transform is applied correctly - apply to comments and feedbacks only`() { + val expectedCodeReviewComments = listOf(validCodeReviewComment) + val expectedCodeReviewFeedbacks = listOf(validCodeReviewFeedback) + val expectedCodeReviews = codeReviews.map { + it.copy( + comments = expectedCodeReviewComments, + feedbacks = expectedCodeReviewFeedbacks, + ) + } + + val expectedDiscussionComments = listOf(validDiscussionComment) + val expectedDiscussions = discussions.map { + it.copy( + comments = expectedDiscussionComments, + ) + } + + val expectedRepository = Stubs.generic.repository.copy( + codeReviews = expectedCodeReviews, + discussions = expectedDiscussions, + ) + + val transform = RepositoryCreatedAtTransform( + createdAt = LocalDate(2023, 3, 1), + applyToCommentsAndFeedbacks = true, + applyToCodeReviewsAndDiscussions = false, + ) + + assertThat(transform(repository)).isEqualTo(expectedRepository) + } + + @Test fun `transform is applied correctly - apply to none`() { + val transform = RepositoryCreatedAtTransform( + createdAt = LocalDate(2023, 3, 1), + applyToCommentsAndFeedbacks = false, + applyToCodeReviewsAndDiscussions = false, + ) + + assertThat(transform(repository)).isEqualTo(repository) + } + +} diff --git a/src/commonTest/kotlin/history/filter/transform/RepositoryDateTransformTest.kt b/src/commonTest/kotlin/history/filter/transform/RepositoryDateBetweenTransformTest.kt similarity index 97% rename from src/commonTest/kotlin/history/filter/transform/RepositoryDateTransformTest.kt rename to src/commonTest/kotlin/history/filter/transform/RepositoryDateBetweenTransformTest.kt index 9d5c35d..117869e 100644 --- a/src/commonTest/kotlin/history/filter/transform/RepositoryDateTransformTest.kt +++ b/src/commonTest/kotlin/history/filter/transform/RepositoryDateBetweenTransformTest.kt @@ -7,7 +7,7 @@ import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime import stubs.Stubs -class RepositoryDateTransformTest { +class RepositoryDateBetweenTransformTest { @Test fun `transform is applied correctly`() { // Code Review components @@ -113,7 +113,7 @@ class RepositoryDateTransformTest { discussions = expectedDiscussions, ) - val transform = RepositoryDateTransform( + val transform = RepositoryDateBetweenTransform( openDateInclusive = LocalDate(2023, 3, 1), closeDateInclusive = LocalDate(2023, 3, 1), ) diff --git a/src/commonTest/kotlin/history/filter/transform/RepositoryFinishedAtTransformTest.kt b/src/commonTest/kotlin/history/filter/transform/RepositoryFinishedAtTransformTest.kt new file mode 100644 index 0000000..09fa2a3 --- /dev/null +++ b/src/commonTest/kotlin/history/filter/transform/RepositoryFinishedAtTransformTest.kt @@ -0,0 +1,109 @@ +package history.filter.transform + +import assertk.assertThat +import assertk.assertions.isEqualTo +import kotlin.test.Test +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import stubs.Stubs + +class RepositoryFinishedAtTransformTest { + + @Test fun `transform is applied correctly`() { + // Code Review components + val earlyCodeReviewComment = Stubs.generic.codeReviewComment.copy( + createdAt = LocalDateTime(2023, 2, 28, 10, 40, 20), + ) + val validCodeReviewComment = Stubs.generic.codeReviewComment.copy( + createdAt = LocalDateTime(2023, 3, 1, 10, 40, 20), + ) + val lateCodeReviewComment = Stubs.generic.codeReviewComment.copy( + createdAt = LocalDateTime(2023, 3, 2, 10, 40, 20), + ) + val codeReviewComments = listOf(earlyCodeReviewComment, validCodeReviewComment, lateCodeReviewComment) + + val earlyCodeReviewFeedback = Stubs.generic.codeReviewFeedback.copy( + submittedAt = LocalDateTime(2023, 2, 28, 10, 40, 20), + ) + val validCodeReviewFeedback = Stubs.generic.codeReviewFeedback.copy( + submittedAt = LocalDateTime(2023, 3, 1, 10, 40, 20), + ) + val lateCodeReviewFeedback = Stubs.generic.codeReviewFeedback.copy( + submittedAt = LocalDateTime(2023, 3, 2, 10, 40, 20), + ) + val codeReviewFeedbacks = listOf(earlyCodeReviewFeedback, validCodeReviewFeedback, lateCodeReviewFeedback) + + // Discussion components + val earlyDiscussionComment = Stubs.generic.discussionComment.copy( + createdAt = LocalDateTime(2023, 2, 28, 10, 40, 20), + ) + val validDiscussionComment = Stubs.generic.discussionComment.copy( + createdAt = LocalDateTime(2023, 3, 1, 10, 40, 20), + ) + val lateDiscussionComment = Stubs.generic.discussionComment.copy( + createdAt = LocalDateTime(2023, 3, 2, 10, 40, 20), + ) + val discussionComments = listOf(earlyDiscussionComment, validDiscussionComment, lateDiscussionComment) + + // Repository components + val earlyCodeReview = Stubs.generic.codeReview.copy( + createdAt = LocalDateTime(2023, 2, 28, 10, 40, 20), + mergedAt = LocalDateTime(2023, 2, 28, 10, 45, 20), + closedAt = LocalDateTime(2023, 2, 28, 10, 50, 20), + comments = codeReviewComments, + feedbacks = codeReviewFeedbacks, + ) + val validCodeReview = Stubs.generic.codeReview.copy( + createdAt = LocalDateTime(2023, 3, 1, 10, 40, 20), + mergedAt = LocalDateTime(2023, 3, 1, 10, 45, 20), + closedAt = LocalDateTime(2023, 3, 1, 10, 50, 20), + comments = codeReviewComments, + feedbacks = codeReviewFeedbacks, + ) + val lateCodeReview = Stubs.generic.codeReview.copy( + createdAt = LocalDateTime(2023, 3, 2, 10, 40, 20), + mergedAt = LocalDateTime(2023, 3, 2, 10, 45, 20), + closedAt = LocalDateTime(2023, 3, 2, 10, 50, 20), + comments = codeReviewComments, + feedbacks = codeReviewFeedbacks, + ) + val codeReviews = listOf(earlyCodeReview, validCodeReview, lateCodeReview) + + val earlyDiscussion = Stubs.generic.discussion.copy( + createdAt = LocalDateTime(2023, 2, 28, 10, 40, 20), + closedAt = LocalDateTime(2023, 2, 28, 10, 45, 20), + comments = discussionComments, + ) + val validDiscussion = Stubs.generic.discussion.copy( + createdAt = LocalDateTime(2023, 3, 1, 10, 40, 20), + closedAt = LocalDateTime(2023, 3, 1, 10, 45, 20), + comments = discussionComments, + ) + val lateDiscussion = Stubs.generic.discussion.copy( + createdAt = LocalDateTime(2023, 3, 2, 10, 40, 20), + closedAt = LocalDateTime(2023, 3, 2, 10, 45, 20), + comments = discussionComments, + ) + val discussions = listOf(earlyDiscussion, validDiscussion, lateDiscussion) + + // Evaluation + val repository = Stubs.generic.repository.copy( + codeReviews = codeReviews, + discussions = discussions, + ) + + val expectedCodeReviews = listOf(validCodeReview) + val expectedDiscussions = listOf(validDiscussion) + val expectedRepository = Stubs.generic.repository.copy( + codeReviews = expectedCodeReviews, + discussions = expectedDiscussions, + ) + + val transform = RepositoryFinishedAtTransform( + finishedAt = LocalDate(2023, 3, 1), + ) + + assertThat(transform(repository)).isEqualTo(expectedRepository) + } + +} diff --git a/src/commonTest/kotlin/stubs/MetricsStubs.kt b/src/commonTest/kotlin/stubs/MetricsStubs.kt index 91bec8e..dc1bebc 100644 --- a/src/commonTest/kotlin/stubs/MetricsStubs.kt +++ b/src/commonTest/kotlin/stubs/MetricsStubs.kt @@ -20,7 +20,7 @@ object MetricsStubs { val user4 = User("Four") // region Metrics - val genericLong = object : GenericCountMetric { + val genericCount = object : GenericCountMetric { override val name = "Generic Long" override val perAuthor = mapOf( user1 to 1L,