Browse Source
* Update Gradle used in tooling subprojects * Update Kotlin in Compose Gradle plugin * Decrease verbosity of Gradle plugin tests * Disable mac sign test * Add workflow to test Gradle plugin * Fix custom jdk tests on Linux * Make Compose Gradle plugin build compatible with Configuration cache * Print tests summary * Remove unused code * Refactor tests configuration * Turn off parallel execution * Try adding windows runner * Turn off fail fast * Fix Windows test issues #2368 * Adjust default proguard rules The following rule is needed to fix tests on Windows: ``` -dontwarn org.graalvm.compiler.core.aarch64.AArch64NodeMatchRules_MatchStatementSet* ``` Other rules are just to make builds less noisy. Kotlin's `*.internal` packages often contain bytecode, which triggers ProGuard's notes. However, these notes are not actionable for most users, so we can ignore notes by default. #2393pull/2521/head
Alexey Tsvetkov
2 years ago
committed by
GitHub
23 changed files with 448 additions and 128 deletions
@ -0,0 +1,35 @@
|
||||
name: Test Gradle plugin |
||||
on: |
||||
pull_request: |
||||
paths: |
||||
- 'gradle-plugins/**' |
||||
- '.github/workflows/gradle-plugin.yml' |
||||
jobs: |
||||
test-gradle-plugin: |
||||
strategy: |
||||
fail-fast: false |
||||
matrix: |
||||
os: [ubuntu-20.04, macos-12, windows-2022] |
||||
runs-on: ${{ matrix.os }} |
||||
steps: |
||||
- uses: actions/checkout@v3 |
||||
- uses: actions/setup-java@v3 |
||||
with: |
||||
distribution: 'corretto' |
||||
java-version: '16' |
||||
- name: Test Gradle plugin |
||||
shell: bash |
||||
run: | |
||||
cd gradle-plugins |
||||
./gradlew assemble |
||||
./gradlew :compose:check --continue |
||||
- name: Print summary |
||||
shell: bash |
||||
if: always() |
||||
run: | |
||||
cd gradle-plugins/compose/build/test-summary |
||||
for SUMMARY_FILE in `find . -name "*.md"`; do |
||||
FILE_NAME=`basename $SUMMARY_FILE` |
||||
echo "## $FILE_NAME" >> $GITHUB_STEP_SUMMARY |
||||
cat $SUMMARY_FILE >> $GITHUB_STEP_SUMMARY |
||||
done |
@ -0,0 +1,65 @@
|
||||
/* |
||||
* Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. |
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. |
||||
*/ |
||||
|
||||
import org.gradle.api.DefaultTask |
||||
import org.gradle.api.file.RegularFile |
||||
import org.gradle.api.model.ObjectFactory |
||||
import org.gradle.api.provider.Property |
||||
import org.gradle.api.provider.SetProperty |
||||
import org.gradle.api.tasks.Input |
||||
import org.gradle.api.tasks.InputFile |
||||
import org.gradle.api.tasks.TaskAction |
||||
import java.util.* |
||||
import java.util.zip.ZipFile |
||||
import javax.inject.Inject |
||||
|
||||
/** |
||||
* Checks that every class in a [jarFile] matches one of [allowedPackagePrefixes] |
||||
*/ |
||||
abstract class CheckJarPackagesTask @Inject constructor( |
||||
objects: ObjectFactory |
||||
) : DefaultTask() { |
||||
@get:InputFile |
||||
val jarFile: Property<RegularFile> = objects.fileProperty() |
||||
|
||||
@get:Input |
||||
val allowedPackagePrefixes: SetProperty<String> = objects.setProperty(String::class.java) |
||||
|
||||
@TaskAction |
||||
fun run() { |
||||
ZipFile(jarFile.get().asFile).use { zip -> |
||||
checkJarContainsExpectedPackages(zip) |
||||
} |
||||
} |
||||
|
||||
private fun checkJarContainsExpectedPackages(jar: ZipFile) { |
||||
val unexpectedClasses = arrayListOf<String>() |
||||
val allowedPrefixes = allowedPackagePrefixes.get().map { it.replace(".", "/") } |
||||
|
||||
for (entry in jar.entries()) { |
||||
if (entry.isDirectory || !entry.name.endsWith(".class")) continue |
||||
|
||||
if (allowedPrefixes.none { prefix -> entry.name.startsWith(prefix) }) { |
||||
unexpectedClasses.add(entry.name) |
||||
} |
||||
} |
||||
|
||||
if (unexpectedClasses.any()) { |
||||
error(buildString { |
||||
appendLine("All classes in ${jar.name} must match allowed prefixes:") |
||||
allowedPrefixes.forEach { |
||||
appendLine(" * $it") |
||||
} |
||||
appendLine("Non-valid classes:") |
||||
val unexpectedGroups = unexpectedClasses |
||||
.groupByTo(TreeMap()) { it.substringBeforeLast("/") } |
||||
for ((_, classes) in unexpectedGroups) { |
||||
appendLine(" * ${classes.first()}") |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
} |
||||
|
@ -0,0 +1,32 @@
|
||||
/* |
||||
* Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. |
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. |
||||
*/ |
||||
|
||||
import org.gradle.api.DefaultTask |
||||
import org.gradle.api.file.ConfigurableFileCollection |
||||
import org.gradle.api.file.RegularFileProperty |
||||
import org.gradle.api.model.ObjectFactory |
||||
import org.gradle.api.tasks.InputFiles |
||||
import org.gradle.api.tasks.OutputFile |
||||
import org.gradle.api.tasks.TaskAction |
||||
import java.io.File |
||||
import javax.inject.Inject |
||||
|
||||
abstract class SerializeClasspathTask @Inject constructor( |
||||
objects: ObjectFactory |
||||
) : DefaultTask() { |
||||
@get:InputFiles |
||||
val classpathFileCollection: ConfigurableFileCollection = objects.fileCollection() |
||||
|
||||
@get:OutputFile |
||||
val outputFile: RegularFileProperty = objects.fileProperty() |
||||
|
||||
@TaskAction |
||||
fun run() { |
||||
val classpath = classpathFileCollection.files.joinToString(File.pathSeparator) { it.absolutePath } |
||||
val outputFile = outputFile.get().asFile |
||||
outputFile.parentFile.mkdirs() |
||||
outputFile.writeText(classpath) |
||||
} |
||||
} |
@ -0,0 +1,146 @@
|
||||
/* |
||||
* Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. |
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. |
||||
*/ |
||||
|
||||
package org.jetbrains.compose.test.utils |
||||
|
||||
import org.junit.platform.engine.TestExecutionResult |
||||
import org.junit.platform.engine.support.descriptor.MethodSource |
||||
import org.junit.platform.launcher.TestExecutionListener |
||||
import org.junit.platform.launcher.TestIdentifier |
||||
import org.junit.platform.launcher.TestPlan |
||||
import java.io.File |
||||
import java.io.Writer |
||||
|
||||
class ComposeTestSummary : TestExecutionListener { |
||||
private val summaryFile = TestProperties.summaryFile |
||||
private val isEnabled = summaryFile != null |
||||
private val startNanoTime = hashMapOf<TestIdentifier, Long>() |
||||
private val results = arrayListOf<TestResult>() |
||||
|
||||
override fun executionStarted(testIdentifier: TestIdentifier) { |
||||
if (isEnabled && testIdentifier.isTest) { |
||||
startNanoTime[testIdentifier] = System.nanoTime() |
||||
} |
||||
} |
||||
|
||||
override fun executionSkipped(testIdentifier: TestIdentifier, reason: String?) { |
||||
if (isEnabled && testIdentifier.isTest) { |
||||
addTestResult(testIdentifier, TestResult.Status.Skipped, durationMs = null) |
||||
} |
||||
} |
||||
|
||||
override fun executionFinished(testIdentifier: TestIdentifier, testExecutionResult: TestExecutionResult) { |
||||
if (isEnabled && testIdentifier.isTest) { |
||||
val durationMs = (System.nanoTime() - startNanoTime[testIdentifier]!!) / 1_000_000 |
||||
val status = when (testExecutionResult.status!!) { |
||||
TestExecutionResult.Status.SUCCESSFUL -> TestResult.Status.Successful |
||||
TestExecutionResult.Status.ABORTED -> TestResult.Status.Aborted |
||||
TestExecutionResult.Status.FAILED -> |
||||
TestResult.Status.Failed( |
||||
testExecutionResult.throwable.orElse(null) |
||||
) |
||||
} |
||||
addTestResult(testIdentifier, status, durationMs = durationMs) |
||||
} |
||||
} |
||||
|
||||
override fun testPlanExecutionFinished(testPlan: TestPlan) { |
||||
if (isEnabled) { |
||||
MarkdownSummary.write(results, summaryFile!!) |
||||
} |
||||
} |
||||
|
||||
private fun addTestResult( |
||||
identifier: TestIdentifier, |
||||
status: TestResult.Status, |
||||
durationMs: Long? |
||||
) { |
||||
val result = TestResult( |
||||
testCase = identifier.displayName, |
||||
testClass = (identifier.source.get() as? MethodSource)?.className ?: "", |
||||
status = status, |
||||
durationMs = durationMs |
||||
) |
||||
results.add(result) |
||||
} |
||||
} |
||||
|
||||
internal data class TestResult( |
||||
val testCase: String, |
||||
val testClass: String, |
||||
val durationMs: Long?, |
||||
val status: Status |
||||
) { |
||||
sealed class Status { |
||||
object Successful : Status() |
||||
object Aborted : Status() |
||||
object Skipped : Status() |
||||
class Failed(val exception: Throwable?) : Status() |
||||
} |
||||
|
||||
val displayName: String |
||||
get() = "${testClass.substringAfterLast(".")}.$testCase" |
||||
} |
||||
|
||||
internal object MarkdownSummary { |
||||
fun write(testResults: List<TestResult>, file: File) { |
||||
file.parentFile.mkdirs() |
||||
file.bufferedWriter().use { writer -> |
||||
writer.writeSummary(testResults) |
||||
} |
||||
} |
||||
|
||||
private fun Writer.writeSummary(testResults: List<TestResult>) { |
||||
writeLn() |
||||
writeLn("|Status|Test case|Duration|") |
||||
writeLn("|---|---|---:|") |
||||
|
||||
for (result in testResults) { |
||||
val status = when (result.status) { |
||||
is TestResult.Status.Successful -> ":white_check_mark:" |
||||
is TestResult.Status.Aborted -> ":fast_forward:" |
||||
is TestResult.Status.Failed -> ":x:" |
||||
is TestResult.Status.Skipped -> ":fast_forward:" |
||||
} |
||||
writeLn("|$status|${result.displayName}|${result.durationMs ?: 0} ms|") |
||||
} |
||||
|
||||
val failedTests = testResults.filter { it.status is TestResult.Status.Failed } |
||||
if (failedTests.isEmpty()) return |
||||
|
||||
writeLn("#### ${failedTests.size} failed tests") |
||||
for (failedTest in failedTests) { |
||||
withDetails(failedTest.displayName) { |
||||
withHtmlTag("samp") { |
||||
val exception = (failedTest.status as TestResult.Status.Failed).exception |
||||
val stacktrace = exception?.stackTraceToString() ?: "" |
||||
write(stacktrace.replace("\n", "<br/>")) |
||||
} |
||||
} |
||||
writeLn() |
||||
} |
||||
} |
||||
|
||||
private inline fun Writer.withDetails(summary: String, details: Writer.() -> Unit) { |
||||
withHtmlTag("details") { |
||||
withHtmlTag("summary") { |
||||
write(summary) |
||||
} |
||||
|
||||
details() |
||||
} |
||||
} |
||||
|
||||
private inline fun Writer.withHtmlTag(tag: String, fn: Writer.() -> Unit) { |
||||
writeLn("<$tag>") |
||||
fn() |
||||
writeLn("</$tag>") |
||||
} |
||||
|
||||
private fun Writer.writeLn(str: String = "") { |
||||
write(str) |
||||
write("\n") |
||||
} |
||||
} |
@ -0,0 +1 @@
|
||||
org.jetbrains.compose.test.utils.ComposeTestSummary |
@ -1,3 +1,4 @@
|
||||
-keep public class Main { |
||||
public void keptByKeepRule(...); |
||||
} |
||||
} |
||||
-dontnote |
@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME |
||||
distributionPath=wrapper/dists |
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip |
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip |
||||
zipStoreBase=GRADLE_USER_HOME |
||||
zipStorePath=wrapper/dists |
||||
|
@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME |
||||
distributionPath=wrapper/dists |
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip |
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip |
||||
zipStoreBase=GRADLE_USER_HOME |
||||
zipStorePath=wrapper/dists |
||||
|
@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME |
||||
distributionPath=wrapper/dists |
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip |
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip |
||||
zipStoreBase=GRADLE_USER_HOME |
||||
zipStorePath=wrapper/dists |
||||
|
Loading…
Reference in new issue