Merge branch 'main' of github.com:android/nowinandroid into rpf-update-firebase-ktx

# Conflicts:
#	gradle/libs.versions.toml
pull/1006/head
rosariopf 2 years ago
commit 5a42e1fe19

@ -47,7 +47,7 @@ jobs:
path: '**/build/outputs/apk/**/*.apk' path: '**/build/outputs/apk/**/*.apk'
- name: Run local tests - name: Run local tests
run: ./gradlew testDemoDebug testProdDebug run: ./gradlew testDemoDebug testProdDebug :lint:test
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -125,6 +125,9 @@ jobs:
name: lint-reports name: lint-reports
path: '**/build/reports/lint-results-*.html' path: '**/build/reports/lint-results-*.html'
- name: Check badging
run: ./gradlew :app:checkProdReleaseBadging
androidTest: androidTest:
needs: build needs: build
runs-on: macOS-latest # enables hardware acceleration in the virtual machine runs-on: macOS-latest # enables hardware acceleration in the virtual machine

@ -0,0 +1,121 @@
package: name='com.google.samples.apps.nowinandroid' versionCode='8' versionName='0.1.2' platformBuildVersionName='14' platformBuildVersionCode='34' compileSdkVersion='34' compileSdkVersionCodename='14'
sdkVersion:'21'
targetSdkVersion:'34'
uses-permission: name='android.permission.INTERNET'
uses-permission: name='android.permission.ACCESS_NETWORK_STATE'
uses-permission: name='android.permission.POST_NOTIFICATIONS'
uses-permission: name='android.permission.WAKE_LOCK'
uses-permission: name='com.google.android.c2dm.permission.RECEIVE'
uses-permission: name='com.google.android.finsky.permission.BIND_GET_INSTALL_REFERRER_SERVICE'
uses-permission: name='android.permission.RECEIVE_BOOT_COMPLETED'
uses-permission: name='android.permission.FOREGROUND_SERVICE'
uses-permission: name='com.google.samples.apps.nowinandroid.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION'
application-label:'Now in Android'
application-label-af:'Now in Android'
application-label-am:'Now in Android'
application-label-ar:'Now in Android'
application-label-as:'Now in Android'
application-label-az:'Now in Android'
application-label-be:'Now in Android'
application-label-bg:'Now in Android'
application-label-bn:'Now in Android'
application-label-bs:'Now in Android'
application-label-ca:'Now in Android'
application-label-cs:'Now in Android'
application-label-da:'Now in Android'
application-label-de:'Now in Android'
application-label-el:'Now in Android'
application-label-en-AU:'Now in Android'
application-label-en-CA:'Now in Android'
application-label-en-GB:'Now in Android'
application-label-en-IN:'Now in Android'
application-label-en-XC:'Now in Android'
application-label-es:'Now in Android'
application-label-es-US:'Now in Android'
application-label-et:'Now in Android'
application-label-eu:'Now in Android'
application-label-fa:'Now in Android'
application-label-fi:'Now in Android'
application-label-fr:'Now in Android'
application-label-fr-CA:'Now in Android'
application-label-gl:'Now in Android'
application-label-gu:'Now in Android'
application-label-hi:'Now in Android'
application-label-hr:'Now in Android'
application-label-hu:'Now in Android'
application-label-hy:'Now in Android'
application-label-in:'Now in Android'
application-label-is:'Now in Android'
application-label-it:'Now in Android'
application-label-iw:'Now in Android'
application-label-ja:'Now in Android'
application-label-ka:'Now in Android'
application-label-kk:'Now in Android'
application-label-km:'Now in Android'
application-label-kn:'Now in Android'
application-label-ko:'Now in Android'
application-label-ky:'Now in Android'
application-label-lo:'Now in Android'
application-label-lt:'Now in Android'
application-label-lv:'Now in Android'
application-label-mk:'Now in Android'
application-label-ml:'Now in Android'
application-label-mn:'Now in Android'
application-label-mr:'Now in Android'
application-label-ms:'Now in Android'
application-label-my:'Now in Android'
application-label-nb:'Now in Android'
application-label-ne:'Now in Android'
application-label-nl:'Now in Android'
application-label-or:'Now in Android'
application-label-pa:'Now in Android'
application-label-pl:'Now in Android'
application-label-pt:'Now in Android'
application-label-pt-BR:'Now in Android'
application-label-pt-PT:'Now in Android'
application-label-ro:'Now in Android'
application-label-ru:'Now in Android'
application-label-si:'Now in Android'
application-label-sk:'Now in Android'
application-label-sl:'Now in Android'
application-label-sq:'Now in Android'
application-label-sr:'Now in Android'
application-label-sr-Latn:'Now in Android'
application-label-sv:'Now in Android'
application-label-sw:'Now in Android'
application-label-ta:'Now in Android'
application-label-te:'Now in Android'
application-label-th:'Now in Android'
application-label-tl:'Now in Android'
application-label-tr:'Now in Android'
application-label-uk:'Now in Android'
application-label-ur:'Now in Android'
application-label-uz:'Now in Android'
application-label-vi:'Now in Android'
application-label-zh-CN:'Now in Android'
application-label-zh-HK:'Now in Android'
application-label-zh-TW:'Now in Android'
application-label-zu:'Now in Android'
application-icon-120:'res/mipmap-anydpi-v26/ic_launcher.xml'
application-icon-160:'res/mipmap-anydpi-v26/ic_launcher.xml'
application-icon-240:'res/mipmap-anydpi-v26/ic_launcher.xml'
application-icon-320:'res/mipmap-anydpi-v26/ic_launcher.xml'
application-icon-480:'res/mipmap-anydpi-v26/ic_launcher.xml'
application-icon-640:'res/mipmap-anydpi-v26/ic_launcher.xml'
application-icon-65534:'res/mipmap-anydpi-v26/ic_launcher.xml'
application: label='Now in Android' icon='res/mipmap-anydpi-v26/ic_launcher.xml'
launchable-activity: name='com.google.samples.apps.nowinandroid.MainActivity' label='' icon=''
uses-library-not-required:'androidx.window.extensions'
uses-library-not-required:'androidx.window.sidecar'
uses-library-not-required:'android.ext.adservices'
feature-group: label=''
uses-feature: name='android.hardware.faketouch'
uses-implied-feature: name='android.hardware.faketouch' reason='default feature for all apps'
main
other-activities
other-receivers
other-services
supports-screens: 'small' 'normal' 'large' 'xlarge'
supports-any-density: 'true'
locales: '--_--' 'af' 'am' 'ar' 'as' 'az' 'be' 'bg' 'bn' 'bs' 'ca' 'cs' 'da' 'de' 'el' 'en-AU' 'en-CA' 'en-GB' 'en-IN' 'en-XC' 'es' 'es-US' 'et' 'eu' 'fa' 'fi' 'fr' 'fr-CA' 'gl' 'gu' 'hi' 'hr' 'hu' 'hy' 'in' 'is' 'it' 'iw' 'ja' 'ka' 'kk' 'km' 'kn' 'ko' 'ky' 'lo' 'lt' 'lv' 'mk' 'ml' 'mn' 'mr' 'ms' 'my' 'nb' 'ne' 'nl' 'or' 'pa' 'pl' 'pt' 'pt-BR' 'pt-PT' 'ro' 'ru' 'si' 'sk' 'sl' 'sq' 'sr' 'sr-Latn' 'sv' 'sw' 'ta' 'te' 'th' 'tl' 'tr' 'uk' 'ur' 'uz' 'vi' 'zh-CN' 'zh-HK' 'zh-TW' 'zu'
densities: '120' '160' '240' '320' '480' '640' '65534'

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2023 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<color name="ic_launcher_background_tint">#FFFFFF</color>
<color name="ic_launcher_foreground_tint">#FF006780</color>
</resources>

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2023 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<color name="ic_launcher_background_tint">#000000</color>
<color name="ic_launcher_foreground_tint">#FF006780</color>
</resources>

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2023 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<color name="ic_launcher_background_tint">#FFFFFF</color>
<color name="ic_launcher_foreground_tint">#FFA23F16</color>
</resources>

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2023 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<color name="ic_launcher_background_tint">#000000</color>
<color name="ic_launcher_foreground_tint">#FFA23F16</color>
</resources>

@ -20,6 +20,7 @@ import androidx.benchmark.macro.MacrobenchmarkScope
import androidx.test.uiautomator.By import androidx.test.uiautomator.By
import androidx.test.uiautomator.Until import androidx.test.uiautomator.Until
import com.google.samples.apps.nowinandroid.flingElementDownUp import com.google.samples.apps.nowinandroid.flingElementDownUp
import com.google.samples.apps.nowinandroid.waitAndFindObject
fun MacrobenchmarkScope.goToInterestsScreen() { fun MacrobenchmarkScope.goToInterestsScreen() {
device.findObject(By.text("Interests")).click() device.findObject(By.text("Interests")).click()
@ -34,7 +35,7 @@ fun MacrobenchmarkScope.goToInterestsScreen() {
} }
fun MacrobenchmarkScope.interestsScrollTopicsDownUp() { fun MacrobenchmarkScope.interestsScrollTopicsDownUp() {
val topicsList = device.wait(Until.findObject(By.res("interests:topics")), 2_000) val topicsList = device.waitAndFindObject(By.res("interests:topics"), 2_000)
device.flingElementDownUp(topicsList) device.flingElementDownUp(topicsList)
} }

@ -17,11 +17,14 @@
import com.android.build.api.dsl.ApplicationExtension import com.android.build.api.dsl.ApplicationExtension
import com.google.samples.apps.nowinandroid.configureGradleManagedDevices import com.google.samples.apps.nowinandroid.configureGradleManagedDevices
import com.android.build.api.variant.ApplicationAndroidComponentsExtension import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.gradle.BaseExtension
import com.google.samples.apps.nowinandroid.configureBadgingTasks
import com.google.samples.apps.nowinandroid.configureKotlinAndroid import com.google.samples.apps.nowinandroid.configureKotlinAndroid
import com.google.samples.apps.nowinandroid.configurePrintApksTask import com.google.samples.apps.nowinandroid.configurePrintApksTask
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.getByType
class AndroidApplicationConventionPlugin : Plugin<Project> { class AndroidApplicationConventionPlugin : Plugin<Project> {
override fun apply(target: Project) { override fun apply(target: Project) {
@ -39,6 +42,7 @@ class AndroidApplicationConventionPlugin : Plugin<Project> {
} }
extensions.configure<ApplicationAndroidComponentsExtension> { extensions.configure<ApplicationAndroidComponentsExtension> {
configurePrintApksTask(this) configurePrintApksTask(this)
configureBadgingTasks(extensions.getByType<BaseExtension>(), this)
} }
} }
} }

@ -0,0 +1,143 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid
import com.android.build.api.artifact.SingleArtifact
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.gradle.BaseExtension
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.Copy
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.register
import org.gradle.language.base.plugins.LifecycleBasePlugin
import org.gradle.process.ExecOperations
import java.io.File
import java.nio.file.Files
import javax.inject.Inject
abstract class GenerateBadgingTask : DefaultTask() {
@get:OutputFile
abstract val badging: RegularFileProperty
@get:InputFile
abstract val apk: RegularFileProperty
@get:InputFile
abstract val aapt2Executable: RegularFileProperty
@get:Inject
abstract val execOperations: ExecOperations
@TaskAction
fun taskAction() {
execOperations.exec {
commandLine(
aapt2Executable.get().asFile.absolutePath,
"dump",
"badging",
apk.get().asFile.absolutePath
)
standardOutput = badging.asFile.get().outputStream()
}
}
}
abstract class CheckBadgingTask : DefaultTask() {
// In order for the task to be up-to-date when the inputs have not changed,
// the task must declare an output, even if it's not used. Tasks with no
// output are always run regardless of whether the inputs changed
@get:OutputDirectory
abstract val output: DirectoryProperty
@get:InputFile
abstract val goldenBadging: RegularFileProperty
@get:InputFile
abstract val generatedBadging: RegularFileProperty
override fun getGroup(): String = LifecycleBasePlugin.VERIFICATION_GROUP
@TaskAction
fun taskAction() {
if (
Files.mismatch(
goldenBadging.get().asFile.toPath(),
generatedBadging.get().asFile.toPath()
) != -1L
) {
throw GradleException(
"Generated badging is different from golden badging! " +
"If this change is intended, run ./gradlew updateBadging"
)
}
}
}
fun Project.configureBadgingTasks(
baseExtension: BaseExtension,
componentsExtension: ApplicationAndroidComponentsExtension
) {
// Registers a callback to be called, when a new variant is configured
componentsExtension.onVariants { variant ->
// Registers a new task to verify the app bundle.
val generateBadging = tasks.register<GenerateBadgingTask>("generate${variant.name}Badging") {
apk.set(
variant.artifacts.get(SingleArtifact.APK_FROM_BUNDLE)
)
aapt2Executable.set(
File(
baseExtension.sdkDirectory,
"build-tools/${baseExtension.buildToolsVersion}/aapt2"
)
)
badging.set(
project.layout.buildDirectory.file(
"outputs/apk_from_bundle/${variant.name}/${variant.name}-badging.txt"
)
)
}
tasks.register<Copy>("update${variant.name}Badging") {
from(generateBadging.get().badging)
into(project.layout.projectDirectory)
}
val checkBadgingTaskName = "check${variant.name}Badging"
tasks.register<CheckBadgingTask>(checkBadgingTaskName) {
goldenBadging.set(
project.layout.projectDirectory.file("${variant.name}-badging.txt"))
generatedBadging.set(
generateBadging.get().badging
)
output.set(
project.layout.buildDirectory.dir("intermediates/$checkBadgingTaskName")
)
}
}
}

@ -87,6 +87,6 @@ data class UserEditableSettings(
) )
sealed interface SettingsUiState { sealed interface SettingsUiState {
object Loading : SettingsUiState data object Loading : SettingsUiState
data class Success(val settings: UserEditableSettings) : SettingsUiState data class Success(val settings: UserEditableSettings) : SettingsUiState
} }

@ -177,12 +177,12 @@ private fun newsUiState(
sealed interface TopicUiState { sealed interface TopicUiState {
data class Success(val followableTopic: FollowableTopic) : TopicUiState data class Success(val followableTopic: FollowableTopic) : TopicUiState
object Error : TopicUiState data object Error : TopicUiState
object Loading : TopicUiState data object Loading : TopicUiState
} }
sealed interface NewsUiState { sealed interface NewsUiState {
data class Success(val news: List<UserNewsResource>) : NewsUiState data class Success(val news: List<UserNewsResource>) : NewsUiState
object Error : NewsUiState data object Error : NewsUiState
object Loading : NewsUiState data object Loading : NewsUiState
} }

@ -5,7 +5,7 @@ androidGradlePlugin = "8.1.2"
androidxActivity = "1.8.0" androidxActivity = "1.8.0"
androidxAppCompat = "1.6.1" androidxAppCompat = "1.6.1"
androidxBrowser = "1.6.0" androidxBrowser = "1.6.0"
androidxComposeBom = "2023.10.00" androidxComposeBom = "2023.10.01"
androidxComposeCompiler = "1.5.3" androidxComposeCompiler = "1.5.3"
androidxComposeRuntimeTracing = "1.0.0-alpha03" androidxComposeRuntimeTracing = "1.0.0-alpha03"
androidxCore = "1.12.0" androidxCore = "1.12.0"
@ -14,7 +14,7 @@ androidxDataStore = "1.0.0"
androidxEspresso = "3.5.1" androidxEspresso = "3.5.1"
androidxHiltNavigationCompose = "1.0.0" androidxHiltNavigationCompose = "1.0.0"
androidxLifecycle = "2.6.2" androidxLifecycle = "2.6.2"
androidxMacroBenchmark = "1.2.0-rc02" androidxMacroBenchmark = "1.2.0"
androidxMetrics = "1.0.0-alpha04" androidxMetrics = "1.0.0-alpha04"
androidxNavigation = "2.7.4" androidxNavigation = "2.7.4"
androidxProfileinstaller = "1.3.1" androidxProfileinstaller = "1.3.1"
@ -26,7 +26,7 @@ androidxTestRunner = "1.5.2"
androidxTracing = "1.1.0" androidxTracing = "1.1.0"
androidxUiAutomator = "2.2.0" androidxUiAutomator = "2.2.0"
androidxWindowManager = "1.1.0" androidxWindowManager = "1.1.0"
androidxWork = "2.9.0-beta01" androidxWork = "2.9.0-rc01"
coil = "2.4.0" coil = "2.4.0"
firebaseBom = "32.5.0" firebaseBom = "32.5.0"
firebaseCrashlyticsPlugin = "2.9.9" firebaseCrashlyticsPlugin = "2.9.9"
@ -44,14 +44,14 @@ kotlinxDatetime = "0.4.1"
kotlinxSerializationJson = "1.6.0" kotlinxSerializationJson = "1.6.0"
ksp = "1.9.10-1.0.13" ksp = "1.9.10-1.0.13"
lint = "31.1.2" lint = "31.1.2"
okhttp = "4.11.0" okhttp = "4.12.0"
protobuf = "3.24.4" protobuf = "3.24.4"
protobufPlugin = "0.9.4" protobufPlugin = "0.9.4"
retrofit = "2.9.0" retrofit = "2.9.0"
retrofitKotlinxSerializationJson = "1.0.0" retrofitKotlinxSerializationJson = "1.0.0"
robolectric = "4.10.3" robolectric = "4.10.3"
roborazzi = "1.5.0" roborazzi = "1.6.0"
room = "2.5.2" room = "2.6.0"
secrets = "2.0.1" secrets = "2.0.1"
turbine = "1.0.0" turbine = "1.0.0"
@ -122,6 +122,8 @@ kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-cor
kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinxDatetime" } kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinxDatetime" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
lint-api = { group = "com.android.tools.lint", name = "lint-api", version.ref = "lint" } lint-api = { group = "com.android.tools.lint", name = "lint-api", version.ref = "lint" }
lint-checks = { group = "com.android.tools.lint", name = "lint-checks", version.ref = "lint" }
lint-tests = { group = "com.android.tools.lint", name = "lint-tests", version.ref = "lint" }
okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
protobuf-kotlin-lite = { group = "com.google.protobuf", name = "protobuf-kotlin-lite", version.ref = "protobuf" } protobuf-kotlin-lite = { group = "com.google.protobuf", name = "protobuf-kotlin-lite", version.ref = "protobuf" }
protobuf-protoc = { group = "com.google.protobuf", name = "protoc", version.ref = "protobuf" } protobuf-protoc = { group = "com.google.protobuf", name = "protoc", version.ref = "protobuf" }

@ -38,4 +38,7 @@ tasks.withType<KotlinCompile>().configureEach {
dependencies { dependencies {
compileOnly(libs.kotlin.stdlib) compileOnly(libs.kotlin.stdlib)
compileOnly(libs.lint.api) compileOnly(libs.lint.api)
testImplementation(libs.lint.checks)
testImplementation(libs.lint.tests)
testImplementation(kotlin("test"))
} }

@ -14,18 +14,20 @@
* limitations under the License. * limitations under the License.
*/ */
package com.google.samples.apps.nowinandroid.lint.designsystem package com.google.samples.apps.nowinandroid.lint
import com.android.tools.lint.client.api.IssueRegistry import com.android.tools.lint.client.api.IssueRegistry
import com.android.tools.lint.client.api.Vendor import com.android.tools.lint.client.api.Vendor
import com.android.tools.lint.detector.api.CURRENT_API import com.android.tools.lint.detector.api.CURRENT_API
import com.google.samples.apps.nowinandroid.lint.designsystem.DesignSystemDetector
/** class NiaIssueRegistry : IssueRegistry() {
* An issue registry that checks for incorrect usages of Compose Material APIs over equivalents in
* the Now in Android design system module. override val issues = listOf(
*/ DesignSystemDetector.ISSUE,
class DesignSystemIssueRegistry : IssueRegistry() { TestMethodNameDetector.FORMAT,
override val issues = listOf(DesignSystemDetector.ISSUE) TestMethodNameDetector.PREFIX,
)
override val api: Int = CURRENT_API override val api: Int = CURRENT_API

@ -0,0 +1,126 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.lint
import com.android.tools.lint.detector.api.AnnotationInfo
import com.android.tools.lint.detector.api.AnnotationUsageInfo
import com.android.tools.lint.detector.api.Category.Companion.TESTING
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Implementation
import com.android.tools.lint.detector.api.Issue
import com.android.tools.lint.detector.api.JavaContext
import com.android.tools.lint.detector.api.LintFix
import com.android.tools.lint.detector.api.Scope.JAVA_FILE
import com.android.tools.lint.detector.api.Scope.TEST_SOURCES
import com.android.tools.lint.detector.api.Severity.WARNING
import com.android.tools.lint.detector.api.SourceCodeScanner
import com.android.tools.lint.detector.api.TextFormat.RAW
import com.intellij.psi.PsiMethod
import org.jetbrains.uast.UElement
import java.util.EnumSet
import kotlin.io.path.Path
/**
* A detector that checks for common patterns in naming the test methods:
* - [detectPrefix] removes unnecessary "test" prefix in all unit test.
* - [detectFormat] Checks the `given_when_then` format of Android instrumented tests (backticks are not supported).
*/
class TestMethodNameDetector : Detector(), SourceCodeScanner {
override fun applicableAnnotations() = listOf("org.junit.Test")
override fun visitAnnotationUsage(
context: JavaContext,
element: UElement,
annotationInfo: AnnotationInfo,
usageInfo: AnnotationUsageInfo,
) {
val method = usageInfo.referenced as? PsiMethod ?: return
method.detectPrefix(context, usageInfo)
method.detectFormat(context, usageInfo)
}
private fun JavaContext.isAndroidTest() = Path("androidTest") in file.toPath()
private fun PsiMethod.detectPrefix(
context: JavaContext,
usageInfo: AnnotationUsageInfo,
) {
if (!name.startsWith("test")) return
context.report(
issue = PREFIX,
scope = usageInfo.usage,
location = context.getNameLocation(this),
message = PREFIX.getBriefDescription(RAW),
quickfixData = LintFix.create()
.name("Remove prefix")
.replace().pattern("""test[\s_]*""")
.with("")
.autoFix()
.build(),
)
}
private fun PsiMethod.detectFormat(
context: JavaContext,
usageInfo: AnnotationUsageInfo,
) {
if (!context.isAndroidTest()) return
if ("""[^\W_]+(_[^\W_]+){1,2}""".toRegex().matches(name)) return
context.report(
issue = FORMAT,
scope = usageInfo.usage,
location = context.getNameLocation(this),
message = FORMAT.getBriefDescription(RAW),
)
}
companion object {
private fun issue(
id: String,
briefDescription: String,
explanation: String,
): Issue = Issue.create(
id = id,
briefDescription = briefDescription,
explanation = explanation,
category = TESTING,
priority = 5,
severity = WARNING,
implementation = Implementation(
TestMethodNameDetector::class.java,
EnumSet.of(JAVA_FILE, TEST_SOURCES),
),
)
@JvmField
val PREFIX: Issue = issue(
id = "TestMethodPrefix",
briefDescription = "Test method starts with `test`",
explanation = "Test method should not start with `test`.",
)
@JvmField
val FORMAT: Issue = issue(
id = "TestMethodFormat",
briefDescription = "Test method does not follow the `given_when_then` or `when_then` format",
explanation = "Test method should follow the `given_when_then` or `when_then` format.",
)
}
}

@ -14,4 +14,4 @@
# limitations under the License. # limitations under the License.
# #
com.google.samples.apps.nowinandroid.lint.designsystem.DesignSystemIssueRegistry com.google.samples.apps.nowinandroid.lint.NiaIssueRegistry

@ -0,0 +1,123 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.lint
import com.android.tools.lint.checks.infrastructure.TestFile
import com.android.tools.lint.checks.infrastructure.TestFiles.kotlin
import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
import com.google.samples.apps.nowinandroid.lint.TestMethodNameDetector.Companion.FORMAT
import com.google.samples.apps.nowinandroid.lint.TestMethodNameDetector.Companion.PREFIX
import org.junit.Test
class TestMethodNameDetectorTest {
@Test
fun `detect prefix`() {
lint().issues(PREFIX)
.files(
JUNIT_TEST_STUB,
kotlin(
"""
import org.junit.Test
class Test {
@Test
fun foo() = Unit
@Test
fun test_foo() = Unit
@Test
fun `test foo`() = Unit
}
""",
).indented(),
)
.run()
.expect(
"""
src/Test.kt:6: Warning: Test method starts with test [TestMethodPrefix]
fun test_foo() = Unit
~~~~~~~~
src/Test.kt:8: Warning: Test method starts with test [TestMethodPrefix]
fun `test foo`() = Unit
~~~~~~~~~~
0 errors, 2 warnings
""".trimIndent(),
)
.expectFixDiffs(
"""
Autofix for src/Test.kt line 6: Remove prefix:
@@ -6 +6
- fun test_foo() = Unit
+ fun foo() = Unit
Autofix for src/Test.kt line 8: Remove prefix:
@@ -8 +8
- fun `test foo`() = Unit
+ fun `foo`() = Unit
""".trimIndent(),
)
}
@Test
fun `detect format`() {
lint().issues(FORMAT)
.files(
JUNIT_TEST_STUB,
kotlin(
"src/androidTest/com/example/Test.kt",
"""
import org.junit.Test
class Test {
@Test
fun when_then() = Unit
@Test
fun given_when_then() = Unit
@Test
fun foo() = Unit
@Test
fun foo_bar_baz_qux() = Unit
@Test
fun `foo bar baz`() = Unit
}
""",
).indented(),
)
.run()
.expect(
"""
src/androidTest/com/example/Test.kt:9: Warning: Test method does not follow the given_when_then or when_then format [TestMethodFormat]
fun foo() = Unit
~~~
src/androidTest/com/example/Test.kt:11: Warning: Test method does not follow the given_when_then or when_then format [TestMethodFormat]
fun foo_bar_baz_qux() = Unit
~~~~~~~~~~~~~~~
src/androidTest/com/example/Test.kt:13: Warning: Test method does not follow the given_when_then or when_then format [TestMethodFormat]
fun `foo bar baz`() = Unit
~~~~~~~~~~~~~
0 errors, 3 warnings
""".trimIndent(),
)
}
private companion object {
private val JUNIT_TEST_STUB: TestFile = kotlin(
"""
package org.junit
annotation class Test
""",
).indented()
}
}
Loading…
Cancel
Save