Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Konsist] Integration & Custom Tests #13824

Draft
wants to merge 4 commits into
base: trunk
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .buildkite/commands/run-unit-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ bundle exec fastlane run configure_apply

echo "--- 🧪 Testing"
set +e
./gradlew testJalapenoDebugUnitTest testDebugUnitTest
./gradlew testJalapenoDebugUnitTest testDebugUnitTest -x libs:konsist-test:testDebugUnitTest
TESTS_EXIT_STATUS=$?
set -e

Expand Down
7 changes: 7 additions & 0 deletions .buildkite/pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ steps:
agents:
queue: "linter"

- label: "konsist"
command: ./gradlew konsist
plugins: [$CI_TOOLKIT]
artifact_paths:
- "**/build/reports/tests/**/*"
- "**/build/test-results/*/*.xml"

- label: "detekt"
command: ./gradlew detektAll
plugins: [$CI_TOOLKIT]
Expand Down
13 changes: 13 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ allprojects {
}
}

/* DETEKT */

tasks.register("detektAll", Detekt) {
description = "Custom DETEKT build for all modules"
parallel = true
Expand All @@ -70,6 +72,17 @@ dependencies {
detektPlugins project(':libs:detektrules')
}

/* KONSIST */

tasks.register("konsist") {
group = "verification"
description = "Runs Konsist static code analysis."

dependsOn(':libs:konsist-tests:testDebugUnitTest')
}

/* OTHER */

tasks.register('clean', Delete) {
delete rootProject.layout.buildDirectory
}
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ jna = '5.5.0@aar'
json-path = '2.9.0'
junit = '4.13.2'
lottie = '5.2.0'
konsist = '0.17.3'
kotlin = '2.1.10'
kotlinx-coroutines = '1.8.1'
ksp = '2.1.10-1.0.29'
Expand Down Expand Up @@ -242,6 +243,7 @@ jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
json-path = { group = "com.jayway.jsonpath", name = "json-path", version.ref = "json-path" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
lottie-compose = { group = "com.airbnb.android", name = "lottie-compose", version.ref = "lottie" }
konsist = { module = "com.lemonappdev:konsist", version.ref = "konsist" }
kotlin-test-junit = { group = "org.jetbrains.kotlin", name = "kotlin-test-junit", version.ref = "kotlin" }
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
Expand Down
47 changes: 47 additions & 0 deletions libs/konsist-tests/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.dependency.analysis)
}

kotlin {
sourceCompatibility = JvmTarget.fromTarget(libs.versions.java.get()).target
targetCompatibility = JvmTarget.fromTarget(libs.versions.java.get()).target
}

android {
namespace 'com.woocommerce.android.konsist.tests'

compileOptions {
sourceCompatibility libs.versions.java.get()
targetCompatibility libs.versions.java.get()
}

defaultConfig {
minSdkVersion gradle.ext.minSdkVersion
targetSdkVersion gradle.ext.targetSdkVersion
compileSdk gradle.ext.compileSdkVersion
}
}

dependencies {
implementation(libs.androidx.lifecycle.viewmodel.savedstate)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.fragment.ktx)
implementation(libs.google.dagger.compiler)
implementation(libs.google.dagger.hilt.compiler)
implementation(libs.google.dagger.hilt.android.main)
implementation(libs.google.dagger.hilt.android.testing)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui.tooling.main)

testImplementation(libs.junit)
testImplementation(libs.konsist)
}

// See: https://docs.konsist.lemonappdev.com/advanced/isolate-konsist-tests#solution-1-module-is-always-out-of-date
tasks.withType(Test).configureEach {
outputs.upToDateWhen { false }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.woocommerce.android.konsist.test

import android.app.Application
import androidx.fragment.app.Fragment
import com.lemonappdev.konsist.api.Konsist
import com.lemonappdev.konsist.api.ext.list.properties
import com.lemonappdev.konsist.api.ext.list.withoutAllParentsNamed
import com.lemonappdev.konsist.api.ext.list.withoutAllParentsOf
import com.lemonappdev.konsist.api.ext.list.withoutAnnotationOf
import com.lemonappdev.konsist.api.ext.list.withoutName
import com.lemonappdev.konsist.api.ext.provider.hasAnnotationOf
import com.lemonappdev.konsist.api.verify.assertFalse
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.HiltAndroidApp
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
import javax.inject.Inject

class KonsistAnnotationsTest {
@Test // Or suppress 'AppInitializer' class: @Suppress("konsist.no class should use field injection")
fun `no class should use field injection`() {
Konsist.scopeFromProject()
.classes()
.withoutAnnotationOf(HiltAndroidApp::class, AndroidEntryPoint::class, HiltAndroidTest::class)
.withoutAllParentsOf(Application::class)
.withoutAllParentsOf(Fragment::class)
.withoutAllParentsNamed("BaseFragment")
.withoutName("AppInitializer")
.properties()
.assertFalse { it.hasAnnotationOf<Inject>() }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.woocommerce.android.konsist.test

import androidx.compose.ui.tooling.preview.Preview
import com.lemonappdev.konsist.api.Konsist
import com.lemonappdev.konsist.api.ext.list.withAnnotationOf
import com.lemonappdev.konsist.api.verify.assertTrue
import org.junit.Test

class KonsistComposeTest {
@Test
fun `all jetpack compose previews contain 'preview' in method name`() {
Konsist.scopeFromProject()
.functions()
.withAnnotationOf(Preview::class)
.assertTrue { it.hasNameContaining("Preview") }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.woocommerce.android.konsist.test

import com.lemonappdev.konsist.api.Konsist
import com.lemonappdev.konsist.api.ext.list.properties
import com.lemonappdev.konsist.api.verify.assertFalse
import org.junit.Test

class KonsistFieldsTest {
@Test
fun `no field should have 'm' prefix`() {
Konsist.scopeFromProject()
.classes()
.properties()
.assertFalse {
val secondCharacterIsUppercase = it.name.getOrNull(1)?.isUpperCase() ?: false
it.name.startsWith('m') && secondCharacterIsUppercase
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.woocommerce.android.konsist.test

import com.lemonappdev.konsist.api.Konsist
import com.lemonappdev.konsist.api.verify.assertFalse
import org.junit.Test

class KonsistFilesTest {
@Test
fun `no empty files allowed`() {
Konsist.scopeFromProject()
.files
.assertFalse { it.text.isEmpty() }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.woocommerce.android.konsist.test

import com.lemonappdev.konsist.api.Konsist
import com.lemonappdev.konsist.api.ext.list.functions
import com.lemonappdev.konsist.api.ext.list.returnTypes
import com.lemonappdev.konsist.api.ext.list.withoutSourceSet
import com.lemonappdev.konsist.api.verify.assertFalse
import org.junit.Test

class KonsistFunctionsTest {
@Test // Or suppress all files: @file:Suppress("konsist.return type of all functions are immutable")
fun `return type of all functions - expect in a test - are immutable`() {
Konsist.scopeFromProject()
.files
.filter { file ->
!file.path.contains("FlowExt.kt") &&
!file.path.contains("LiveDataExt.kt") &&
!file.path.contains("SavedStateFlow.kt")
}
.functions()
.returnTypes
.withoutSourceSet("test")
.assertFalse { it.isMutableType }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.woocommerce.android.konsist.test

import com.lemonappdev.konsist.api.Konsist
import com.lemonappdev.konsist.api.ext.list.declaration.flatten
import com.lemonappdev.konsist.api.ext.list.types
import com.lemonappdev.konsist.api.verify.assertFalse
import org.junit.Test

class KonsistGenericsTest {
@Test
fun `property generic type does not contains star projection`() {
Konsist.scopeFromProduction()
.properties()
.types
.assertFalse { type ->
type.typeArguments
?.flatten()
?.any { it.isStarProjection }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.woocommerce.android.konsist.test

import com.lemonappdev.konsist.api.Konsist
import com.lemonappdev.konsist.api.verify.assertFalse
import org.junit.Test

class KonsistImportsTest {
@Test // Detekt: WildcardImport (https://detekt.dev/docs/rules/style/#wildcardimport)
fun `no wildcard imports allowed`() {
Konsist.scopeFromProject()
.imports
.assertFalse { it.isWildcard }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.woocommerce.android.konsist.test

import com.lemonappdev.konsist.api.Konsist
import com.lemonappdev.konsist.api.ext.list.withoutSourceSet
import com.lemonappdev.konsist.api.verify.assertFalse
import org.junit.Test

class KonsistLogsTest {
@Test
fun `no class should use java util logging`() {
Konsist.scopeFromProject()
.files
.assertFalse { it.hasImport { import -> import.name == "java.util.logging.." } }
}

@Test // Or suppress 'WooLog' file: @file:Suppress("konsist.return type of all functions are immutable")
fun `no class should use android util logging`() {
Konsist.scopeFromProject()
.files
.filter { file ->
!file.path.contains("WooLog.kt")
}
.withoutSourceSet("androidTest")
.assertFalse { it.hasImport { import -> import.name == "android.util.Log" } }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.woocommerce.android.konsist.test

import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import com.lemonappdev.konsist.api.Konsist
import com.lemonappdev.konsist.api.ext.list.withAllParentsNamed
import com.lemonappdev.konsist.api.ext.list.withAllParentsOf
import com.lemonappdev.konsist.api.verify.assertTrue
import org.junit.Test

class KonsistNamingTest {
@Test
fun `android activity class name ends with 'activity'`() {
Konsist.scopeFromProject()
.classes()
.withAllParentsOf(AppCompatActivity::class)
.assertTrue { it.name.endsWith("Activity") }
}

@Test
fun `android fragment class name ends with 'fragment'`() {
Konsist.scopeFromProject()
.classes()
.withAllParentsOf(Fragment::class)
.plus(
Konsist.scopeFromProject()
.classes()
.withAllParentsNamed("BaseFragment")
)
.assertTrue { it.name.endsWith("Fragment") }
}

@Test
fun `android view model class name ends with 'viewmodel'`() {
Konsist.scopeFromProject()
.classes()
.withAllParentsOf(ViewModel::class)
.plus(
Konsist.scopeFromProject()
.classes()
.withAllParentsNamed("ScopedViewModel")
)
.assertTrue { it.name.endsWith("ViewModel") }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.woocommerce.android.konsist.test

import com.lemonappdev.konsist.api.Konsist
import com.lemonappdev.konsist.api.ext.list.withNameEndingWith
import com.lemonappdev.konsist.api.ext.list.withPackage
import com.lemonappdev.konsist.api.ext.list.withoutName
import com.lemonappdev.konsist.api.ext.list.withoutSourceSet
import com.lemonappdev.konsist.api.verify.assertTrue
import org.junit.Test

class KonsistPackagesTest {
@Test
fun `files in 'extensions' package must have name ending with 'ext'`() {
Konsist.scopeFromProject()
.files
.withPackage("..extensions..")
.withoutSourceSet("test")
.assertTrue { it.hasNameEndingWith("Ext") }
}

@Test // Detekt: InvalidPackageDeclaration (https://detekt.dev/docs/rules/naming/#invalidpackagedeclaration)
fun `package name must match file path`() {
Konsist.scopeFromProject()
.packages
.withoutName("org.wordpress.android.util.config")
.assertTrue { it.hasMatchingPath }
}

@Test
fun `classes with 'usecase' suffix should reside in 'usecases' package`() {
Konsist.scopeFromProject()
.classes()
.withNameEndingWith("UseCase")
.assertTrue { it.resideInPackage("..usecases..") }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.woocommerce.android.konsist.test

import com.lemonappdev.konsist.api.Konsist
import com.lemonappdev.konsist.api.ext.list.modifierprovider.withValueModifier
import com.lemonappdev.konsist.api.ext.list.primaryConstructors
import com.lemonappdev.konsist.api.verify.assertTrue
import org.junit.Test

class KonsistParametersTest {
@Test
fun `every value class has parameter named 'value'`() {
Konsist.scopeFromProject()
.classes()
.withValueModifier()
.primaryConstructors
.assertTrue { it.hasParameterWithName("value") }
}
}
Loading