diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 176a29478e6..29bb9122419 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -46,6 +46,11 @@ jobs: with: name: testDebugUnitTest path: ./**/build/reports/tests/testDebugUnitTest + - uses: ./.github/actions/setup-ruby + - name: Sonar + run: bundle exec fastlane run_sonar_analysis + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} size_check_xml: name: Size Check XML diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 580ebf4e5e4..18468b627c7 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -7,6 +7,8 @@ env: BUILD_CACHE_AWS_BUCKET: ${{ secrets.BUILD_CACHE_AWS_BUCKET }} BUILD_CACHE_AWS_ACCESS_KEY_ID: ${{ secrets.BUILD_CACHE_AWS_ACCESS_KEY_ID }} BUILD_CACHE_AWS_SECRET_KEY: ${{ secrets.BUILD_CACHE_AWS_SECRET_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_PR_NUM: ${{ github.event.pull_request.number }} concurrency: group: ${{ github.head_ref }} @@ -73,6 +75,11 @@ jobs: with: name: testDebugUnitTest path: ./**/build/reports/tests/testDebugUnitTest + - uses: ./.github/actions/setup-ruby + - name: Sonar + run: bundle exec fastlane run_sonar_analysis + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} rubocop: name: Rubocop diff --git a/.github/workflows/snapshot-test.yaml b/.github/workflows/snapshot-test.yaml index ae712b88caa..99c019a8ac6 100644 --- a/.github/workflows/snapshot-test.yaml +++ b/.github/workflows/snapshot-test.yaml @@ -11,6 +11,10 @@ on: required: false default: false +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_PR_NUM: ${{ github.event.pull_request.number }} + jobs: run_snapshot_tests: name: Run diff --git a/build.gradle b/build.gradle index 5c1369038ff..c5e4e108fa3 100644 --- a/build.gradle +++ b/build.gradle @@ -11,6 +11,7 @@ apply plugin: "com.github.ben-manes.versions" apply plugin: 'io.github.gradle-nexus.publish-plugin' apply plugin: 'binary-compatibility-validator' apply plugin: 'org.jetbrains.dokka' +apply from: "${rootDir}/scripts/sonar.gradle" apply from: "${rootDir}/scripts/sample-app-versioner.gradle" apply plugin: UnitTestsPlugin apply plugin: ReleasePlugin @@ -73,15 +74,21 @@ buildscript { classpath Dependencies.spotlessGradlePlugin classpath Dependencies.ksp classpath Dependencies.detektPlugin + classpath Dependencies.sonarQubePlugin + + // Workaround for integrate sonarqube plugin with AGP + // It looks like will be fixed after AGP 8.9.0-alpha04 is released + // https://issuetracker.google.com/issues/380600747?pli=1 + classpath("org.bouncycastle:bcutil-jdk18on:1.79") } } subprojects { - if (it.name != 'stream-chat-android-docs' - && it.buildFile.exists()) { + if (it.name != 'stream-chat-android-docs' && it.buildFile.exists()) { apply from: "${rootDir}/spotless/spotless.gradle" } apply plugin: "io.gitlab.arturbosch.detekt" + apply from: "${rootDir}/scripts/coverage.gradle" } versionPrint { @@ -134,4 +141,4 @@ apiValidation { ] } -apply from: "${rootDir}/scripts/publish-root.gradle" +apply from: "${rootDir}/scripts/publish-root.gradle" \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/io/getstream/chat/android/Dependencies.kt b/buildSrc/src/main/kotlin/io/getstream/chat/android/Dependencies.kt index 011b32d6f93..ac70b939fc1 100644 --- a/buildSrc/src/main/kotlin/io/getstream/chat/android/Dependencies.kt +++ b/buildSrc/src/main/kotlin/io/getstream/chat/android/Dependencies.kt @@ -79,6 +79,7 @@ object Versions { internal const val SHIMMER = "0.5.0" internal const val SHIMMER_COMPOSE = "1.3.1" internal const val SHOT = "6.1.0" + internal const val SONARQUBE_PLUGIN = "6.0.1.5171" internal const val SPOTLESS = "6.20.0" internal const val STREAM_LOG = "1.3.1" internal const val STREAM_PUSH = "1.1.9" @@ -252,6 +253,7 @@ object Dependencies { const val allureKotlinCommons = "io.qameta.allure:allure-kotlin-commons:${Versions.ALLURE_KOTLIN}" const val allureKotlinJunit = "io.qameta.allure:allure-kotlin-junit4:${Versions.ALLURE_KOTLIN}" const val allureKotlinAndroid = "io.qameta.allure:allure-kotlin-android:${Versions.ALLURE_KOTLIN}" + const val sonarQubePlugin = "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:${Versions.SONARQUBE_PLUGIN}" @JvmStatic fun isNonStable(version: String): Boolean = isStable(version).not() diff --git a/buildSrc/src/main/kotlin/io/getstream/chat/android/command/unittest/filter/UnitTestsFilter.kt b/buildSrc/src/main/kotlin/io/getstream/chat/android/command/unittest/filter/UnitTestsFilter.kt index dd90e2841fe..c788615a9ba 100644 --- a/buildSrc/src/main/kotlin/io/getstream/chat/android/command/unittest/filter/UnitTestsFilter.kt +++ b/buildSrc/src/main/kotlin/io/getstream/chat/android/command/unittest/filter/UnitTestsFilter.kt @@ -10,7 +10,9 @@ fun List.selectedUnitTestCommand(rootProject: Project): String { return filterUnitTestableModules(rootProject) .filter { (testableModule, _) -> modulesWithTest.contains(testableModule) } - .generateGradleCommand { (module, testType) -> "$module:${testType.testCommand}" } + .generateGradleCommand { (module, testType) -> + "$module:${testType.testCommand} $module:${TestType.JACOCO_TEST_COVERAGE.testCommand}" + } } private fun List.filterModulesWithTests(): List { @@ -24,7 +26,7 @@ private fun List.filterUnitTestableModules(rootProject: Project): List

project.hasUnitTest() && this.contains(project.name) } .map { project -> val testType = when { - project.tasks.any { task -> task.name == "testDebugUnitTest" } -> { + project.tasks.any { task -> task.name == TestType.ANDROID_LIBRARY_TEST.testCommand } -> { TestType.ANDROID_LIBRARY_TEST } @@ -38,5 +40,5 @@ private fun List.filterUnitTestableModules(rootProject: Project): List

- task.name == "testDebugUnitTest" || task.name == "test" + task.name == TestType.ANDROID_LIBRARY_TEST.testCommand || task.name == TestType.JAVA_LIBRARY_TEST.testCommand } diff --git a/buildSrc/src/main/kotlin/io/getstream/chat/android/command/unittest/model/TestType.kt b/buildSrc/src/main/kotlin/io/getstream/chat/android/command/unittest/model/TestType.kt index 66401036bc6..4430252e0cd 100644 --- a/buildSrc/src/main/kotlin/io/getstream/chat/android/command/unittest/model/TestType.kt +++ b/buildSrc/src/main/kotlin/io/getstream/chat/android/command/unittest/model/TestType.kt @@ -3,4 +3,5 @@ package io.getstream.chat.android.command.unittest.model enum class TestType(val testCommand: String) { JAVA_LIBRARY_TEST("test"), ANDROID_LIBRARY_TEST("testDebugUnitTest"), + JACOCO_TEST_COVERAGE("testCoverage"), } diff --git a/fastlane/Fastfile b/fastlane/Fastfile index bc3cad6c907..1ee83114066 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -34,6 +34,12 @@ lane :stop_mock_server do Net::HTTP.get_response(URI("http://localhost:#{mock_server_driver_port}/stop")) rescue nil end +lane :run_sonar_analysis do |options| + next unless is_check_required(sources: sources_matrix[:sonar], force_check: @force_check) + + Dir.chdir('..') { sh('./gradlew sonar') } +end + lane :run_snapshot_test do |options| next unless is_check_required(sources: sources_matrix[:ui], force_check: @force_check) @@ -128,8 +134,10 @@ end private_lane :sources_matrix do { + unit: ['stream-chat-android-test', 'stream-chat-android-client', 'stream-chat-android-compose/', 'stream-chat-android-core', 'stream-chat-android-markdown-transformer', 'stream-chat-android-offline', 'stream-chat-android-state', 'stream-chat-android-ui-common', 'stream-chat-android-ui-components/', 'stream-chat-android-ui-utils/'], ui: ['stream-chat-android-ui', '.github/workflows/snapshot-test'], e2e: ['buildSrc', 'stream-chat-android', '.github/workflows/e2e-test'], + sonar: ['stream-chat-android-client/', 'stream-chat-android-compose/', 'stream-chat-android-core/', 'stream-chat-android-markdown-transformer/', 'stream-chat-android-offline/', 'stream-chat-android-state/', 'stream-chat-android-ui-common/', 'stream-chat-android-ui-components/', 'stream-chat-android-ui-utils/'], ruby: ['fastlane', 'Gemfile', 'Gemfile.lock'] } end diff --git a/scripts/coverage.gradle b/scripts/coverage.gradle new file mode 100644 index 00000000000..c70b644ee53 --- /dev/null +++ b/scripts/coverage.gradle @@ -0,0 +1,61 @@ +if (!rootProject.ext.sonar.ignoreModules.contains(name)) { + apply plugin: 'jacoco' + apply plugin: "org.sonarqube" + + def isCore = (name == 'stream-chat-android-core') + def testTask = isCore ? "test" : "testDebugUnitTest" + def jacocoResults = "${buildDir}/reports/jacoco/report.xml" + + if (hasProperty('android')) { + android { + buildTypes { + debug { + testCoverageEnabled = true + enableUnitTestCoverage = true + enableAndroidTestCoverage true + } + } + } + } + + afterEvaluate { + tasks.withType(Test).configureEach { + jacoco.includeNoLocationClasses = true + jacoco.excludes = ['jdk.internal.*'] + } + + tasks.register("testCoverage", JacocoReport) { + dependsOn testTask + + reports { + xml.required.set(true) + xml.outputLocation.set(file(jacocoResults)) + } + + executionData.setFrom(fileTree(dir: buildDir, includes: [ + "outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec", + "jacoco/test.exec" + ])) + + def sources = isCore ? sourceSets.main.java.srcDirs : android.sourceSets.main.java.srcDirs + sourceDirectories.setFrom(files(sources)) + classDirectories.setFrom(files([ + fileTree( + dir: "${buildDir}/tmp/kotlin-classes/debug", + excludes: rootProject.ext.sonar.excludeFilter + ), + fileTree( + dir: "${buildDir}/classes/kotlin/main", + excludes: rootProject.ext.sonar.excludeFilter + ) + ])) + } + } + + sonarqube { + properties { + property "sonar.junit.reportPaths", "${buildDir}/test-results/${testTask}" + property "sonar.coverage.jacoco.xmlReportPaths", jacocoResults + } + } +} diff --git a/scripts/sonar.gradle b/scripts/sonar.gradle new file mode 100644 index 00000000000..aab33b5dd5e --- /dev/null +++ b/scripts/sonar.gradle @@ -0,0 +1,45 @@ +apply plugin: "org.sonarqube" + +ext.sonar = [ + ignoreModules : [ + 'stream-chat-android-ai-assistant', + 'stream-chat-android-previewdata', + 'stream-chat-android-docs', + 'stream-chat-android-benchmark', + 'stream-chat-android-compose-sample', + 'stream-chat-android-e2e-test', + 'stream-chat-android-test', + 'stream-chat-android-ui-guides', + 'stream-chat-android-ui-components-sample', + 'stream-chat-android-client-test', + 'stream-chat-android-ui-uitests' + ], + excludeFilter : [ + '**/test/**', + '**/androidTest/**', + '**/R.class', + '**/R2.class', + '**/R$*.class', + '**/BuildConfig.*', + '**/Manifest*.*', + '**/*Test*.*' + ] +] + +ext.sonar.ignoreModules.each { + ext.sonar.excludeFilter << "**/${it}/**" +} + +sonarqube { + properties { + property("sonar.host.url", "https://sonarcloud.io") + property("sonar.token", "${System.getenv("SONAR_TOKEN")}") + property("sonar.organization", "getstream") + property("sonar.projectKey", "GetStream_stream-chat-android") + property("sonar.projectName", "stream-chat-android") + property "sonar.java.coveragePlugin", "jacoco" + property "sonar.sourceEncoding", "UTF-8" + property "sonar.java.binaries", "${rootDir}/**/build/tmp/kotlin-classes/debug,${rootDir}/**/build/classes/kotlin/main" + property "sonar.coverage.exclusions", rootProject.ext.sonar.excludeFilter + } +} diff --git a/stream-chat-android-client-test/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/stream-chat-android-client-test/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker deleted file mode 100644 index ca6ee9cea8e..00000000000 --- a/stream-chat-android-client-test/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker +++ /dev/null @@ -1 +0,0 @@ -mock-maker-inline \ No newline at end of file diff --git a/stream-chat-android-client-test/src/test/resources/robolectric.properties b/stream-chat-android-client-test/src/test/resources/robolectric.properties deleted file mode 100644 index 89a6c8b4c2e..00000000000 --- a/stream-chat-android-client-test/src/test/resources/robolectric.properties +++ /dev/null @@ -1 +0,0 @@ -sdk=28 \ No newline at end of file diff --git a/stream-chat-android-client/build.gradle b/stream-chat-android-client/build.gradle index d802c186668..58d9d368eac 100644 --- a/stream-chat-android-client/build.gradle +++ b/stream-chat-android-client/build.gradle @@ -30,7 +30,6 @@ android { consumerProguardFiles 'consumer-proguard-rules.pro' } debug { - testCoverageEnabled false consumerProguardFiles 'consumer-proguard-rules.pro' } } diff --git a/stream-chat-android-offline/build.gradle b/stream-chat-android-offline/build.gradle index 9377cd70e6a..3491461ae7b 100644 --- a/stream-chat-android-offline/build.gradle +++ b/stream-chat-android-offline/build.gradle @@ -40,9 +40,6 @@ android { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } - debug { - testCoverageEnabled false - } } }