Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,14 @@ abstract class AgentPreviewExtension(

abstract val includeUnmergedSemantics: Property<Boolean>
abstract val previewNameFilter: ListProperty<String>
abstract val viewportNameFilter: ListProperty<String>
abstract val maxPreviewParameterValues: Property<Int>

init {
includeUnmergedSemantics.convention(false)
previewNameFilter.convention(emptyList())
viewportNameFilter.convention(emptyList())
maxPreviewParameterValues.convention(50)
}

fun android(action: Action<AndroidPreviewConfig>) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ class AgentPreviewPlugin : Plugin<Project> {
},
)
it.previewNameFilter.set(extension.previewNameFilter)
it.previewNameFilter.addAll(csvGradleProperty(project, "agentPreview.previewNameFilter"))
it.maxPreviewParameterValues.set(extension.maxPreviewParameterValues)
it.cliMaxPreviewParameterValues.set(project.providers.gradleProperty("agentPreview.maxPreviewParameterValues"))
it.previewClassesDirs.from(extension.previewClassesDirs)
it.previewRuntimeClasspath.from(extension.previewRuntimeClasspath)
it.robolectricSdk.set(extension.android.robolectricSdk)
Expand All @@ -66,6 +69,11 @@ class AgentPreviewPlugin : Plugin<Project> {
it.outputDirectory.set(extension.outputDirectory)
it.includeUnmergedSemantics.set(extension.includeUnmergedSemantics)
it.previewNameFilter.set(extension.previewNameFilter)
it.previewNameFilter.addAll(csvGradleProperty(project, "agentPreview.previewNameFilter"))
it.viewportNameFilter.set(extension.viewportNameFilter)
it.viewportNameFilter.addAll(csvGradleProperty(project, "agentPreview.viewportFilter"))
it.maxPreviewParameterValues.set(extension.maxPreviewParameterValues)
it.cliMaxPreviewParameterValues.set(project.providers.gradleProperty("agentPreview.maxPreviewParameterValues"))
it.previewClassesDirs.from(extension.previewClassesDirs)
it.previewRuntimeClasspath.from(extension.previewRuntimeClasspath)
it.androidViewportsJson.set(
Expand All @@ -92,4 +100,17 @@ class AgentPreviewPlugin : Plugin<Project> {
.gradleProperty("agentPreview.javaMajorVersion")
.map(String::toInt)
.orElse(Runtime.version().feature())

private fun csvGradleProperty(
project: Project,
name: String,
) = project.providers
.gradleProperty(name)
.map { raw -> raw.splitCsv() }
.orElse(emptyList())

private fun String.splitCsv(): List<String> =
split(',')
.map(String::trim)
.filter(String::isNotEmpty)
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ interface PreviewParameterCountResolver {
}

class PreviewParameterExpander(
private val resolver: PreviewParameterCountResolver,
private val resolver: PreviewParameterCountResolver? = null,
private val defaultCap: Int = Int.MAX_VALUE,
private val requestedIndexes: Set<Int> = emptySet(),
) {
fun expand(previews: List<PreviewDescriptor>): PreviewParameterExpansionResult {
val expanded = previews.map(::expand)
Expand All @@ -38,13 +40,17 @@ class PreviewParameterExpander(
val parameter = preview.previewParameter ?: return PreviewParameterExpansionResult(listOf(preview))
if (parameter.index != null) return PreviewParameterExpansionResult(listOf(preview))

val count = resolver.count(parameter)
val diagnostics = count.diagnostics.map { message -> warning(message) }
if (count.count <= 0) return PreviewParameterExpansionResult(previews = emptyList(), diagnostics = diagnostics)
val resolvedCount = resolver?.count(parameter)
if (resolvedCount == null && parameter.limit == null && requestedIndexes.isEmpty()) {
return PreviewParameterExpansionResult(listOf(preview))
}
val diagnostics = resolvedCount?.diagnostics.orEmpty().map { message -> warning(message) }
val indexes = indexesToExpand(parameter, resolvedCount?.count)
if (indexes.isEmpty()) return PreviewParameterExpansionResult(previews = emptyList(), diagnostics = diagnostics)

return PreviewParameterExpansionResult(
previews =
(0 until count.count).map { index ->
indexes.map { index ->
preview.copy(
id = "${preview.id}:previewParam-$index",
previewParameter = parameter.copy(index = index),
Expand All @@ -54,6 +60,25 @@ class PreviewParameterExpander(
)
}

private fun indexesToExpand(
parameter: PreviewParameterDescriptor,
resolvedCount: Int?,
): List<Int> {
val effectiveCount =
when {
resolvedCount != null -> resolvedCount
parameter.limit != null -> parameter.limit.coerceAtMost(defaultCap)
requestedIndexes.isNotEmpty() -> defaultCap
else -> 0
}
if (effectiveCount <= 0) return emptyList()

if (requestedIndexes.isNotEmpty()) {
return requestedIndexes.filter { index -> index in 0 until effectiveCount }.sorted()
}
return (0 until effectiveCount).toList()
}

private fun warning(message: String): PreviewScanDiagnostic =
PreviewScanDiagnostic(
severity = PreviewScanDiagnostic.Severity.WARNING,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import dev.staticvar.agentpreview.discovery.JsonIndexPreviewDiscovery
import dev.staticvar.agentpreview.discovery.PreviewDiscovery
import dev.staticvar.agentpreview.discovery.PreviewDiscoveryResult
import dev.staticvar.agentpreview.discovery.PreviewParameterExpander
import dev.staticvar.agentpreview.discovery.PreviewParameterExpansionResult
import dev.staticvar.agentpreview.export.SnapshotExporter
import dev.staticvar.agentpreview.model.CURRENT_SNAPSHOT_SCHEMA_VERSION
import dev.staticvar.agentpreview.model.PreviewDescriptor
Expand All @@ -34,12 +33,11 @@ import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Classpath
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import java.io.File

private val PREVIEW_PARAMETER_ID_SUFFIX_REGEX = Regex(":previewParam-\\d+$")

abstract class CaptureComposePreviewsTask : DefaultTask() {
@get:Input
abstract val previewIndexFilePath: Property<String>
Expand All @@ -56,6 +54,16 @@ abstract class CaptureComposePreviewsTask : DefaultTask() {
@get:Input
abstract val previewNameFilter: ListProperty<String>

@get:Input
abstract val viewportNameFilter: ListProperty<String>

@get:Input
abstract val maxPreviewParameterValues: Property<Int>

@get:Input
@get:Optional
abstract val cliMaxPreviewParameterValues: Property<String>

@get:Input
abstract val fakeRenderer: Property<Boolean>

Expand All @@ -78,20 +86,38 @@ abstract class CaptureComposePreviewsTask : DefaultTask() {
fun capture() {
val indexFile = File(previewIndexFilePath.get())
warnIfConfigurationIsIncompatible()
val maxPreviewParameterValues = effectiveMaxPreviewParameterValues()
val filters = previewNameFilter.get().toSet()
val viewportFilters = viewportNameFilter.get().toSet()
val discoveryResult = discoverPreviews(indexFile)
val expansionResult = expandPreviewParameters(discoveryResult.previews)
val expansionCandidates =
discoveryResult.previews
.filter { preview -> filters.isEmpty() || preview.matchesBeforePreviewParameterExpansion(filters) }
val expansionResult = expandPreviewParameters(expansionCandidates, maxPreviewParameterValues)
logDiagnostics(discoveryResult.diagnostics + expansionResult.diagnostics)
val previews =
expansionResult.previews
.filter { preview -> filters.isEmpty() || preview.matches(filters) }
.filter { preview -> filters.isEmpty() || preview.matchesAfterPreviewParameterExpansion(filters) }
val previewFilterSkipped =
(discoveryResult.previews.size - expansionCandidates.size) +
(expansionResult.previews.size - previews.size)
val outputRoot = outputDirectory.get().asFile
if (outputRoot.exists()) {
outputRoot.deleteRecursively()
}

if (previews.isEmpty()) {
logger.lifecycle("No Compose previews discovered.")
logger.lifecycle(
captureSummary(
discoveredCount = discoveryResult.previews.size,
expandedCount = expansionResult.previews.size,
selectedPreviewCount = 0,
capturedViewportCount = 0,
previewFilterSkipped = previewFilterSkipped,
viewportFilterSkipped = 0,
),
)
logger.lifecycle("No Compose previews selected for capture.")
return
}

Expand All @@ -115,8 +141,13 @@ abstract class CaptureComposePreviewsTask : DefaultTask() {
val renderedSemanticsExtractor = RenderedSemanticsExtractor()
val exporter = SnapshotExporter()

var capturedViewportCount = 0
var viewportFilterSkipped = 0
previews.forEach { preview ->
viewportsFor(preview).forEach { viewport ->
val resolvedViewports = viewportsFor(preview)
val selectedViewports = resolvedViewports.filter { viewport -> viewportFilters.isEmpty() || viewport.matches(viewportFilters) }
viewportFilterSkipped += resolvedViewports.size - selectedViewports.size
selectedViewports.forEach { viewport ->
val renderResult =
if (useFakeRenderer) {
fakePreviewRenderer.render(preview, viewport, renderOutput)
Expand Down Expand Up @@ -153,9 +184,20 @@ abstract class CaptureComposePreviewsTask : DefaultTask() {
outputRoot = outputRoot,
viewport = viewport,
)
capturedViewportCount++
logger.lifecycle("Captured ${preview.id} (${viewport.platform}-${viewport.name}) via ${renderResult.renderMode.logLabel}")
}
}
logger.lifecycle(
captureSummary(
discoveredCount = discoveryResult.previews.size,
expandedCount = expansionResult.previews.size,
selectedPreviewCount = previews.size,
capturedViewportCount = capturedViewportCount,
previewFilterSkipped = previewFilterSkipped,
viewportFilterSkipped = viewportFilterSkipped,
),
)
}

private fun viewportsFor(preview: PreviewDescriptor): List<Viewport> =
Expand All @@ -181,13 +223,10 @@ abstract class CaptureComposePreviewsTask : DefaultTask() {
}
}

private fun PreviewDescriptor.matches(filters: Set<String>): Boolean = id in filters || parentPreviewId() in filters || name in filters

private fun PreviewDescriptor.parentPreviewId(): String =
if (previewParameter?.index == null) {
id
} else {
id.replace(PREVIEW_PARAMETER_ID_SUFFIX_REGEX, "")
private fun Viewport.matches(filters: Set<String>): Boolean =
filters.any { filter ->
name.matchesPreviewFilter(filter) ||
"$platform-$name" == filter
}

private fun PreviewDescriptor.hasExplicitWidth(): Boolean = widthDp != null && widthDp > 0
Expand All @@ -211,14 +250,46 @@ abstract class CaptureComposePreviewsTask : DefaultTask() {
private fun previewClasspath(): List<File> =
(previewClassesDirs.files + previewRuntimeClasspath.files + rendererRuntimeClasspathIfAndroidBacked()).toList()

private fun expandPreviewParameters(previews: List<PreviewDescriptor>) =
if (previewClassesDirs.files.isEmpty()) {
PreviewParameterExpansionResult(previews = previews)
} else {
PreviewParameterExpander(
IsolatedPreviewParameterCountResolver(previewClasspath()),
).expand(previews)
}
private fun expandPreviewParameters(
previews: List<PreviewDescriptor>,
maxPreviewParameterValues: Int,
) = PreviewParameterExpander(
resolver =
if (previewClassesDirs.files.isEmpty()) {
null
} else {
IsolatedPreviewParameterCountResolver(
previewClasspath = previewClasspath(),
defaultCap = maxPreviewParameterValues,
)
},
defaultCap = maxPreviewParameterValues,
requestedIndexes = previewNameFilter.get().toSet().previewParameterFilterIndexes(),
).expand(previews)

private fun effectiveMaxPreviewParameterValues(): Int {
val raw = cliMaxPreviewParameterValues.orNull
val value = raw?.toIntOrNull() ?: maxPreviewParameterValues.get()
require(raw == null || raw.toIntOrNull() != null) { maxPreviewParameterValuesError() }
require(value > 0) { maxPreviewParameterValuesError() }
return value
}

private fun maxPreviewParameterValuesError(): String =
"agentPreview.maxPreviewParameterValues must be a positive integer. " +
"Configure agentPreview { maxPreviewParameterValues.set(n) } or pass -PagentPreview.maxPreviewParameterValues=n."

private fun captureSummary(
discoveredCount: Int,
expandedCount: Int,
selectedPreviewCount: Int,
capturedViewportCount: Int,
previewFilterSkipped: Int,
viewportFilterSkipped: Int,
): String =
"AgentPreview capture: discovered $discoveredCount, expanded $expandedCount, selected $selectedPreviewCount, " +
"captured $capturedViewportCount viewport(s), skipped preview filters $previewFilterSkipped, " +
"viewport filters $viewportFilterSkipped."

private fun rendererRuntimeClasspathIfAndroidBacked(): Set<File> =
if (previewClassesDirs.files.isEmpty()) {
Expand Down
Loading
Loading