Skip to content

Commit

Permalink
Detekt the api coverage in Integration tests
Browse files Browse the repository at this point in the history
  • Loading branch information
xgouchet committed Oct 3, 2024
1 parent 21025fd commit 01bbfec
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 24 deletions.
17 changes: 16 additions & 1 deletion .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,22 @@ test-pyramid:legacy-integration-instrumented-median-api:
- !reference [.snippets, install-android-sdk]
- !reference [.snippets, run-legacy-integration-instrumented]

test-pyramid:detekt-api-coverage:
tags: [ "arch:amd64" ]
image: $CI_IMAGE_DOCKER
stage: test-pyramid
timeout: 1h
script:
- mkdir -p ./config/
- aws ssm get-parameter --region us-east-1 --name ci.dd-sdk-android.gradle-properties --with-decryption --query "Parameter.Value" --out text >> ./gradle.properties
- GRADLE_OPTS="-Xmx4096M" ./gradlew assembleLibrariesDebug --stacktrace --no-daemon
- GRADLE_OPTS="-Xmx4096M" ./gradlew printSdkDebugRuntimeClasspath --stacktrace --no-daemon
- GRADLE_OPTS="-Xmx4096M" ./gradlew :tools:detekt:jar --stacktrace --no-daemon
- curl -sSLO https://github.com/detekt/detekt/releases/download/v1.23.4/detekt-cli-1.23.4-all.jar
- java -jar detekt-cli-1.23.4-all.jar --config detekt_test_pyramid.yml --plugins tools/detekt/build/libs/detekt.jar -ex "**/*.kts" --jvm-target 11 -cp $(cat sdk_classpath)
# For now we just print the uncovered apis, eventually we will fail if it's not empty
- grep -v -f apiUsage.log apiSurface.log

test-pyramid:publish-e2e-synthetics:
tags: [ "arch:amd64" ]
image: $CI_IMAGE_DOCKER
Expand Down Expand Up @@ -424,7 +440,6 @@ test-pyramid:publish-webview-synthetics:
paths:
- sample/kotlin/build/outputs/apk/us1/release/kotlin-us1-release.apk


test-pyramid:publish-staging-synthetics:
tags: [ "arch:amd64" ]
image: $CI_IMAGE_DOCKER
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ fun Project.registerSubModuleAggregationTask(
) {
tasks.register(taskName) {
project.subprojects.forEach { subProject ->
println("SubProject ${subProject.name} / ${subProject.path}")
if (!exceptions.contains(subProject.name) &&
subProject.name.startsWith(subModuleNamePrefix) &&
subProject.path.startsWith(subModulePathPrefix)
Expand Down
24 changes: 20 additions & 4 deletions detekt_test_pyramid.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,25 @@ datadog-test-pyramid:
active: true
ApiUsage:
active: true
includes: ['**/reliability/**']
includes: [ '**/reliability/**' ]
ApiSurface:
active: true
includes: ['**/dd-sdk-android-*/**']
excludes: ['**/build/**', '**/test/**', '**/testDebug/**','**/testRelease/**', '**/androidTest/**', '**/testFixtures/**', '**/buildSrc/**', '**/*.kts', '**/instrumented/**', '**/sample/**', '**/tools/**']

includes: [ '**/dd-sdk-android-*/**' ]
excludes:
- '**/build/**'
- '**/test/**'
- '**/testDebug/**'
- '**/testRelease/**'
- '**/androidTest/**'
- '**/testFixtures/**'
- '**/buildSrc/**'
- '**/*.kts'
- '**/instrumented/**'
- '**/sample/**'
- '**/tools/**'
- '**/dd-sdk-android-internal/**'
ignoredAnnotations:
- "com.datadog.android.lint.InternalApi"
ignoredClasses:
- "com.datadog.android._InternalProxy"
- "com.datadog.android.rum._RumInternalProxy"
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,20 @@ import io.gitlab.arturbosch.detekt.api.Issue
import io.gitlab.arturbosch.detekt.api.Severity
import io.gitlab.arturbosch.detekt.api.config
import io.gitlab.arturbosch.detekt.api.internal.RequiresTypeResolution
import io.gitlab.arturbosch.detekt.rules.isOverride
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtFunctionType
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtNullableType
import org.jetbrains.kotlin.psi.KtObjectLiteralExpression
import org.jetbrains.kotlin.psi.KtObjectDeclaration
import org.jetbrains.kotlin.psi.KtParameter
import org.jetbrains.kotlin.psi.KtParameterList
import org.jetbrains.kotlin.psi.KtPrimaryConstructor
import org.jetbrains.kotlin.psi.KtTypeElement
import org.jetbrains.kotlin.psi.KtUserType
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
import org.jetbrains.kotlin.psi.psiUtil.getChildrenOfType
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
import org.jetbrains.kotlin.psi.psiUtil.isExtensionDeclaration
import java.io.File

Expand All @@ -40,6 +40,8 @@ class ApiSurface(

private val outputFileName: String by config(defaultValue = "apiSurface.log")
private val outputFile: File by lazy { File(outputFileName) }
private val ignoredAnnotations: List<String> by config(defaultValue = emptyList())
private val ignoredClasses: List<String> by config(defaultValue = emptyList())

// region Rule

Expand All @@ -50,20 +52,45 @@ class ApiSurface(
Debt.FIVE_MINS
)

override fun visitClass(klass: KtClass) {
if (klass.hasModifier(KtTokens.PRIVATE_KEYWORD) || klass.hasModifier(KtTokens.INTERNAL_KEYWORD)) {
override fun visitObjectDeclaration(declaration: KtObjectDeclaration) {
val hasIgnoredKeyword = ignoredKeywords.any { declaration.hasModifier(it) }
val isIgnoredClass = declaration.fqName?.asString() in ignoredClasses
val hasIgnoredAnnotation = declaration.annotationEntries.any {
it.shortName?.asString()?.resolveFullType() in ignoredAnnotations
}

@Suppress("ComplexCondition")
if (isIgnoredClass || hasIgnoredKeyword || hasIgnoredAnnotation) {
return
}
if (klass.isInterface()) {

super.visitObjectDeclaration(declaration)
}

override fun visitClass(klass: KtClass) {
val hasIgnoredKeyword = ignoredKeywords.any { klass.hasModifier(it) }
val isInterface = klass.isInterface()
val isEnum = klass.isEnum()
val isIgnoredClass = klass.fqName?.asString() in ignoredClasses
val hasIgnoredAnnotation = klass.annotationEntries.any {
it.shortName?.asString()?.resolveFullType() in ignoredAnnotations
}

@Suppress("ComplexCondition")
if (isIgnoredClass || hasIgnoredKeyword || isInterface || isEnum || hasIgnoredAnnotation) {
return
}

super.visitClass(klass)
}

override fun visitPrimaryConstructor(constructor: KtPrimaryConstructor) {
if (constructor.hasModifier(KtTokens.PRIVATE_KEYWORD) || constructor.hasModifier(KtTokens.INTERNAL_KEYWORD)) {
val hasIgnoredKeyword = ignoredKeywords.any { constructor.hasModifier(it) }

if (hasIgnoredKeyword) {
return
}

val parentName = constructor.containingClassOrObject?.fqName
?: constructor.containingKtFile.packageFqName
outputFile.appendText("$parentName.constructor(")
Expand All @@ -78,30 +105,34 @@ class ApiSurface(
outputFile.appendText(")\n")
}

@Suppress("ReturnCount")
override fun visitNamedFunction(function: KtNamedFunction) {
if (function.hasModifier(KtTokens.PRIVATE_KEYWORD) || function.hasModifier(KtTokens.INTERNAL_KEYWORD)) {
return
}
val parameterList = function.getChildrenOfType<KtParameterList>().firstOrNull()
if (function.name == "toString" && parameterList?.children.isNullOrEmpty()) {
return
val hasIgnoredKeyword = ignoredKeywords.any { function.hasModifier(it) }
val hasIgnoredName = function.name in ignoredFunctionNames
val hasIgnoredAnnotation = function.annotationEntries.any {
it.shortName?.asString()?.resolveFullType() in ignoredAnnotations
}
if (function.getStrictParentOfType<KtObjectLiteralExpression>() != null) {
// Function is overriding something in an anonymous object
// e.g.: val x = object : Interface { override fun foo() {} }
val isOverride = function.isOverride()

@Suppress("ComplexCondition")
if (hasIgnoredKeyword || hasIgnoredName || hasIgnoredAnnotation || isOverride) {
return
}

if (function.isExtensionDeclaration()) {
val target = function.receiverTypeReference?.nameForReceiverLabel()
val fqName = target?.resolveFullType()
val receiverTypeReference = function.receiverTypeReference
val target = receiverTypeReference?.nameForReceiverLabel()
val parentType = function.typeParameters.filter { it.name == target }
.map { it.extendsBound?.nameForReceiverLabel()?.resolveFullType() }
.firstOrNull()
val fqName = parentType ?: target?.resolveFullType()
outputFile.appendText("$fqName.${function.nameAsSafeName}(")
} else {
val parentName = function.containingClassOrObject?.fqName
?: function.containingKtFile.packageFqName
outputFile.appendText("$parentName.${function.nameAsSafeName}(")
}

val parameterList = function.getChildrenOfType<KtParameterList>().firstOrNull()
parameterList?.children?.filterIsInstance<KtParameter>()?.forEachIndexed { idx, p ->
val typeElement = p.typeReference?.typeElement
if (idx > 0) outputFile.appendText(", ")
Expand Down Expand Up @@ -137,4 +168,13 @@ class ApiSurface(
}

// endregion

companion object {
private val ignoredFunctionNames = setOf("toString", "equals", "hashCode")

private val ignoredKeywords = setOf(
KtTokens.PRIVATE_KEYWORD,
KtTokens.INTERNAL_KEYWORD
)
}
}

0 comments on commit 01bbfec

Please sign in to comment.