diff --git a/.editorconfig b/.editorconfig index 67bc95e35a..deed221db1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,3 +9,5 @@ end_of_line = lf [*.{kt, kts}] ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,kotlinx.**,^ + +ktlint_standard_argument-list-wrapping = disabled \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..966f8e3b2b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +* text=auto eol=lf + +*.bat text eol=crlf +*.jar binary +*.png binary diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml deleted file mode 100644 index 8a90ccaa93..0000000000 --- a/.github/dependabot.yaml +++ /dev/null @@ -1,7 +0,0 @@ -version: 2 - -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "daily" \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 280d57bfa1..d904308a31 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,13 @@ name: build -on: [push, pull_request] +on: + pull_request: {} + + push: + branches: + - '**' + tags-ignore: + - '**' env: GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4g -Dorg.gradle.daemon=false -Dkotlin.incremental=false" @@ -13,21 +20,15 @@ jobs: os: [ macOS-latest, windows-latest, - # Copied from SqlDelight: https://github.com/cashapp/sqldelight/blame/master/.github/workflows/PR.yml#L13-L18 - # TL;DR looks like libraries installed on ubuntu-latest conflicts, resulting in failed builds - # Also, see: https://github.com/touchlab/SQLiter/pull/38#issuecomment-867171789 - ubuntu-18.04 + ubuntu-latest ] - java-version: [11, 12, 16, 18] + java-version: [17, 18] runs-on: ${{matrix.os}} steps: - name: Checkout - uses: actions/checkout@v3.1.0 - - - name: Validate Gradle Wrapper - uses: gradle/wrapper-validation-action@v1.0.4 + uses: actions/checkout@v3 - name: Configure JDK uses: actions/setup-java@v3 @@ -38,10 +39,7 @@ jobs: - name: Setup Gradle uses: gradle/gradle-build-action@v2 - - name: Run Paparazzi Tests - run: ./gradlew -p paparazzi check - - - name: Run Sample Tests + - name: Run All Tests run: ./gradlew check - name: Upload Test Failures @@ -50,8 +48,8 @@ jobs: with: name: test-failures path: | - **/build/reports/tests/test/ - **/out/failures/ + **/build/reports/tests/*/ + **/build/paparazzi/failures/ paparazzi/paparazzi-gradle-plugin/src/test/projects/**/build/reports/paparazzi/images/ publish: @@ -62,16 +60,16 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3.1.0 + uses: actions/checkout@v3 - name: Configure JDK uses: actions/setup-java@v3 with: distribution: 'zulu' - java-version: 11 + java-version: 17 - name: Publish Artifacts - run: ./gradlew -p paparazzi publishMavenPublicationToMavenCentralRepository paparazzi-gradle-plugin:publishPluginMavenPublicationToMavenCentralRepository paparazzi-gradle-plugin:publishPaparazziPluginMarkerMavenPublicationToMavenCentralRepository --no-parallel + run: ./gradlew publishMavenPublicationToMavenCentralRepository paparazzi-gradle-plugin:publishAllPublicationsToMavenCentralRepository --no-parallel env: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} diff --git a/.github/workflows/gradle-wrapper.yml b/.github/workflows/gradle-wrapper.yml new file mode 100644 index 0000000000..673d0fd999 --- /dev/null +++ b/.github/workflows/gradle-wrapper.yml @@ -0,0 +1,18 @@ +name: gradle-wrapper + +on: + pull_request: + paths: + - 'gradlew' + - 'gradlew.bat' + - 'gradle/wrapper/' + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Validate Gradle Wrapper + uses: gradle/wrapper-validation-action@v1 diff --git a/.github/workflows/prepare_mkdocs.sh b/.github/workflows/prepare_mkdocs.sh index a9ce4dc15c..575d8e3318 100755 --- a/.github/workflows/prepare_mkdocs.sh +++ b/.github/workflows/prepare_mkdocs.sh @@ -9,7 +9,7 @@ set -ex # Generate the API docs -./gradlew -p paparazzi dokkaGfm +./gradlew dokkaGfm # Dokka filenames like `-http-url/index.md` don't work well with MkDocs tags. # Assign metadata to the file's first Markdown heading. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..94b86e7efb --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,63 @@ +name: release + +on: + push: + tags: + - '**' + +env: + GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4g -Dorg.gradle.daemon=false -Dkotlin.incremental=false" + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Configure JDK + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: 17 + + - name: Publish Artifacts + run: ./gradlew publishMavenPublicationToMavenCentralRepository paparazzi-gradle-plugin:publishAllPublicationsToMavenCentralRepository --no-parallel + env: + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.ARTIFACT_SIGNING_PRIVATE_KEY }} + + - name: Extract release notes + id: release_notes + uses: ffurrer2/extract-release-notes@v1 + + - name: Create release + uses: softprops/action-gh-release@v1 + with: + body: ${{ steps.release_notes.outputs.release_notes }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: 3.8 + + - name: Prep mkdocs + run: .github/workflows/prepare_mkdocs.sh + + - name: Build mkdocs + run: | + pip3 install -r .github/workflows/requirements.txt + mkdocs build + + - name: Deploy docs to website + if: success() + uses: JamesIves/github-pages-deploy-action@releases/v3 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BRANCH: gh-pages + FOLDER: site + CLEAN: true diff --git a/.github/workflows/requirements.txt b/.github/workflows/requirements.txt index b0c2ff4bb3..89a3ded780 100644 --- a/.github/workflows/requirements.txt +++ b/.github/workflows/requirements.txt @@ -1,19 +1,19 @@ -click==7.1.2 -future==0.18.2 -Jinja2==2.11.3 +click==8.1.5 +future==0.18.3 +Jinja2==3.1.2 livereload==2.6.3 -lunr==0.5.8 -Markdown==3.2.2 -MarkupSafe==1.1.1 -mkdocs==1.2.3 -mkdocs-macros-plugin==0.4.9 -mkdocs-material==5.5.7 -mkdocs-material-extensions==1.0 -Pygments==2.7.4 -pymdown-extensions==8.0 -python-dateutil==2.8.1 -PyYAML==5.4 +lunr==0.6.2 +Markdown<3.5 +MarkupSafe==2.1.3 +mkdocs==1.4.3 +mkdocs-macros-plugin==1.0.2 +mkdocs-material==9.1.18 +mkdocs-material-extensions==1.1.1 +Pygments==2.15.1 +pymdown-extensions==10.1 +python-dateutil==2.8.2 +PyYAML==6.0 repackage==0.7.3 -six==1.15.0 -termcolor==1.1.0 -tornado==6.0.4 \ No newline at end of file +six==1.16.0 +termcolor==2.3.0 +tornado==6.3.2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 414f054503..744c4a1d3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,86 @@ -Change Log -========== +# Change Log -## Version 1.1.0 -_2022-10-12_ +## [Unreleased] + +## [1.3.1] - 2023-07-18 + +### New +* Migrated to new resource and asset loading mechanisms. To explicitly opt-out and fall back to the +legacy mechanisms, add either/both of the following to your `gradle.properties`: +``` +app.cash.paparazzi.legacy.resource.loading=true +app.cash.paparazzi.legacy.asset.loading=true +``` + +* The Android system ui (status + navigation bar) is now hidden by default; to re-enable: +``` + @get:Rule + val paparazzi = Paparazzi( + showSystemUi = true + ) +``` + +* Relocate failure deltas from `PROJECT_ROOT/out/failures/` to `BUILD_DIR/paparazzi/failures/` +* Support for application and dynamic feature modules +* [Gradle Plugin] Gradle 8.2.1 + +### Fixed +* Fix accessibility labels when mergeDescendants is true +* Fixes compose alert dialogs not rendering when using RenderingMode.SHRINK + +Kudos to @kevinzheng-ap, @adamalyyan and others for contributions this release! + +## [1.3.0] - 2023-05-31 + +As of this release, consumers must build on Java 17+ environments. + +### New +* Migrate Paparazzi to layoutlib Flamingo 2022.2.1 +* Add accessibility support for Composables +* Add layout accessibility check support +* Compose 1.4.7 +* Kotlin 1.8.21 +* [Gradle Plugin] Gradle 8.1.1 +* [Gradle Plugin] Android Gradle Plugin 8.0.2 + +### Fixed +* Configure android.os.Build values via reflection +* Various bug fixes with AccessibilityRenderExtension +* Make sure changes to system properties actually affect test tasks +* Fix caching bug with preparePaparazziResources task +* Use Dispatchers.Main for delay functionality +* Recomposition does not happen unless lifecycle is RESUMED +* Fix NPE when unit test variant is disabled +* Fix incompatibility with androidx.savedstate:1.1.0 + +Kudos to @gamepro65, @geoff-powell, @TWiStErRob, @adamalyyan and others for contributions this release! + +## [1.2.0] - 2023-01-18 + +### New +* Migrate Paparazzi to layoutlib Electric Eel 2022.1.1 +* Add support for RenderingMode.SHRINK to allow view-only screenshots +* Expose flag to show/hide system ui +* Register a default OnBackPressedDispatcherOwner if its present in classpath +* Bump default compileSdk to API 33 +* Compose 1.3.1 +* Kotlin 1.7.20 +* [Gradle Plugin] Gradle 7.6 +* [Gradle Plugin] Android Gradle Plugin 7.4.0 + +### Fixed +* Flush errors on unsafeUpdateConfig +* Only apply wear circle shape to full device screenshots +* Synchronize access to Handler_Delegate.queue +* Apply compose hooks to all snapshot calls +* Register LifecycleOwner and SavedStateRegistryOwner to all views +* Execute Handler callbacks after snapshots to clean up Compose references +* Fix RecyclerView issue due to layoutlib Dolphin update +* Keep AGP and tools dependencies aligned + +Kudos to @gamepro65, @saket, @rharter and others for contributions this release! + +## [1.1.0] - 2022-10-12 ### New * Migrate Paparazzi to layoutlib Chipmunk 2021.2.1 @@ -15,7 +93,7 @@ _2022-10-12_ * Google Wear DeviceConfig support * Expose an API for offsetting frame capture time * Add InstantAnimationsRule to delay snapshot capture until the last frame. -* Compose 1.1.1 +* Compose 1.3.0 * Kotlin 1.7.10 * [Gradle Plugin] Gradle 7.5.1 @@ -31,8 +109,7 @@ _2022-10-12_ Kudos to @chris-horner, @swankjesse, @yschimke, @dniHze, @TWiStErRob, @gamepro65, @liutikas and others for contributions this release! -## Version 1.0.0 -_2022-06-03_ +## [1.0.0] - 2022-06-03 ### New * Support for Composable snapshots @@ -59,8 +136,7 @@ _2022-06-03_ Kudos to @luis-cortes, @nak5ive, @alexvanyo, @gamepro65 and others for contributions this release! -## Version 0.9.3 -_2022-01-20_ +## [0.9.3] - 2022-01-20 ### Fixed * Load the correct mac arm artifact on M1 machines @@ -69,12 +145,12 @@ _2022-01-20_ Kudos to @geoff-powell, @nicbell for their contributions this release! -## Version 0.9.2 (Please ignore this release) -_2022-01-20_ +## [0.9.2] - 2022-01-20 + +Please ignore this release -## Version 0.9.1 -_2022-01-14_ +## [0.9.1] - 2022-01-14 ### Fixed * Download mac arm artifact if on M1 machines @@ -87,8 +163,7 @@ _2022-01-14_ Kudos to @luis-cortes, @geoff-powell, @autonomousapps and @LuK1709 for their contributions this release! -## Version 0.9.0 -_2021-11-22_ +## [0.9.0] - 2021-11-22 ### New * Migrate Paparazzi to layoutlib Arctic Fox 2020.3.1, providing native support for M1 machines @@ -114,8 +189,7 @@ _2021-11-22_ Kudos to @luis-cortes, @geoff-powell and @TWiStErRob for their contributions this release! -## Version 0.8.0 -_2021-10-07_ +## [0.8.0] - 2021-10-07 ### New * Migrate Paparazzi to use native layoutlib for better rendering and API 30 support @@ -135,8 +209,7 @@ _2021-10-07_ * Don't swallow FileNotFoundExceptions when overridden platform dir doesn't exist * [Gradle Plugin] Fix remote caching bug by referencing relative, not absolute, paths in intermediate resources file. -## Version 0.7.1 -_2021-05-17_ +## [0.7.1] - 2021-05-17 ### New * [Gradle Plugin] Support the --tests option for record/verify tasks @@ -144,8 +217,7 @@ _2021-05-17_ ### Fixed * [Gradle Plugin] Defer task configuration until created -## Version 0.7.0 -_2021-02-26_ +## [0.7.0] - 2021-02-26 ### New * Kotlin 1.4.30 @@ -167,8 +239,7 @@ _2021-02-26_ * [Gradle Plugin] Force test re-runs when a resource or asset has changed * [Gradle Plugin] Force test re-runs if generated report or snapshot dirs are deleted -## Version 0.6.0 -_2020-10-02_ +## [0.6.0] - 2020-10-02 As of this release, consumers must build on Java 11 environments. @@ -177,19 +248,36 @@ As of this release, consumers must build on Java 11 environments. * Refactor Paparazzi to better support non-Gradle builds * Added device configs for Pixel 4 series -## Version 0.5.2 -_2020-09-17_ +## [0.5.2] - 2020-09-17 ### Fixed * [Gradle Plugin] Fixed record and verify tasks in multi-module projects. -## Version 0.5.1 -_2020-09-17_ +## [0.5.1] - 2020-09-17 ### Fixed * [Gradle Plugin] Fixed race condition in record and verify tasks. -## Version 0.5.0 -_2020-09-16_ +## [0.5.0] - 2020-09-16 * Initial release. + + + +[Unreleased]: https://github.com/cashapp/paparazzi/compare/1.3.1...HEAD +[1.3.1]: https://github.com/cashapp/paparazzi/releases/tag/1.3.1 +[1.3.0]: https://github.com/cashapp/paparazzi/releases/tag/1.3.0 +[1.2.0]: https://github.com/cashapp/paparazzi/releases/tag/1.2.0 +[1.1.0]: https://github.com/cashapp/paparazzi/releases/tag/1.1.0 +[1.0.0]: https://github.com/cashapp/paparazzi/releases/tag/1.0.0 +[0.9.3]: https://github.com/cashapp/paparazzi/releases/tag/0.9.3 +[0.9.2]: https://github.com/cashapp/paparazzi/releases/tag/0.9.2 +[0.9.1]: https://github.com/cashapp/paparazzi/releases/tag/0.9.1 +[0.9.0]: https://github.com/cashapp/paparazzi/releases/tag/0.9.0 +[0.8.0]: https://github.com/cashapp/paparazzi/releases/tag/0.8.0 +[0.7.1]: https://github.com/cashapp/paparazzi/releases/tag/0.7.1 +[0.7.0]: https://github.com/cashapp/paparazzi/releases/tag/0.7.0 +[0.6.0]: https://github.com/cashapp/paparazzi/releases/tag/0.6.0 +[0.5.2]: https://github.com/cashapp/paparazzi/releases/tag/0.5.2 +[0.5.1]: https://github.com/cashapp/paparazzi/releases/tag/0.5.1 +[0.5.0]: https://github.com/cashapp/paparazzi/releases/tag/0.5.0 diff --git a/README.md b/README.md index b7aea94ca8..7707897724 100644 --- a/README.md +++ b/README.md @@ -35,25 +35,26 @@ See the [project website][paparazzi] for documentation and APIs. Tasks ------- -``` -$ ./gradlew sample:testDebug + +```bash +./gradlew sample:testDebug ``` Runs tests and generates an HTML report at `sample/build/reports/paparazzi/` showing all test runs and snapshots. -``` -$ ./gradlew sample:recordPaparazziDebug +```bash +./gradlew sample:recordPaparazziDebug ``` Saves snapshots as golden values to a predefined source-controlled location (defaults to `src/test/snapshots`). -``` -$ ./gradlew sample:verifyPaparazziDebug +```bash +./gradlew sample:verifyPaparazziDebug ``` -Runs tests and verifies against previously-recorded golden values. Failures generate diffs at `sample/out/failures`. +Runs tests and verifies against previously-recorded golden values. Failures generate diffs at `sample/build/paparazzi/failures`. For more examples, check out the [sample][sample] project. @@ -62,11 +63,11 @@ Git LFS It is recommended you use [Git LFS][lfs] to store your snapshots. Here's a quick setup: ```bash -$ brew install git-lfs -$ git config core.hooksPath # optional, confirm where your git hooks will be installed -$ git lfs install --local -$ git lfs track "**/snapshots/**/*.png" -$ git add .gitattributes +brew install git-lfs +git config core.hooksPath # optional, confirm where your git hooks will be installed +git lfs install --local +git lfs track "**/snapshots/**/*.png" +git add .gitattributes ``` On CI, you might set up something like: @@ -99,10 +100,22 @@ Jetifier If using Jetifier to migrate off Support libraries, add the following to your `gradle.properties` to exclude bundled Android dependencies. -```text +```properties android.jetifier.ignorelist=android-base-common,common ``` +Lottie +-------- + +When taking screenshots of Lottie animations, you need to force Lottie to not run on a background thread, otherwise Paparazzi can throw exceptions [#494](https://github.com/cashapp/paparazzi/issues/494), [#630](https://github.com/cashapp/paparazzi/issues/630). + +```kotlin +@Before +fun setup() { + LottieTask.EXECUTOR = Executor(Runnable::run) +} +``` + Releases -------- @@ -116,7 +129,7 @@ buildscript { google() } dependencies { - classpath 'app.cash.paparazzi:paparazzi-gradle-plugin:1.1.0' + classpath 'app.cash.paparazzi:paparazzi-gradle-plugin:1.3.1' } } @@ -126,19 +139,19 @@ apply plugin: 'app.cash.paparazzi' Using the plugins DSL: ```groovy plugins { - id 'app.cash.paparazzi' version '1.1.0' + id 'app.cash.paparazzi' version '1.3.1' } ``` Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap]. ```groovy - repositories { - // ... - maven { - url 'https://oss.sonatype.org/content/repositories/snapshots/' - } - } +repositories { + // ... + maven { + url 'https://oss.sonatype.org/content/repositories/snapshots/' + } +} ``` License diff --git a/RELEASING.md b/RELEASING.md index d246c0997f..e3a156e624 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,26 +1,16 @@ -Releasing -======== +# Releasing - 1. Change the version in `gradle.properties` to a non-SNAPSHOT version. - 2. Update the `CHANGELOG.md` for the impending release. - 3. Update the `README.md` with the new version. + 1. Update `VERSION_NAME` in `gradle.properties` to the release (non-SNAPSHOT) version. + 2. Update `CHANGELOG.md` for the impending release. + 1. Change the `Unreleased` header to the version, appending today's date + 2. Add a new `Unreleased` section to the top. + 3. Add a link URL at the bottom to ensure the impending release header link works. + 4. Update the `Unreleased` link URL to compare this new version...HEAD + 3. Update `README.md` with the new version. 4. `git commit -am "Prepare version X.Y.Z"` (where X.Y.Z is the new version) - 5. `./gradlew -p paparazzi clean publishMavenPublicationToMavenCentralRepository paparazzi-gradle-plugin:publishPluginMavenPublicationToMavenCentralRepository paparazzi-gradle-plugin:publishPaparazziPluginMarkerMavenPublicationToMavenCentralRepository --no-parallel` - 6. Visit [Sonatype Nexus](https://oss.sonatype.org/) and promote the artifacts. - 7. `git tag -a X.Y.Z -m "X.Y.Z"` (where X.Y.Z is the new version) - 8. Update the `gradle.properties` to the next SNAPSHOT version. - 9. `git commit -am "Prepare next development version"` - 10. `git push && git push --tags` - 11. Update the sample app to the release version and send a PR. + 5. `git tag -a X.Y.Z -m "X.Y.Z"` (where X.Y.Z is the new version) + 6. Update `VERSION_NAME` in `gradle.properties` to the next SNAPSHOT version. + 7. `git commit -am "Prepare next development version"` + 8. `git push && git push --tags` - -If step 5 or 6 fails, drop the Sonatype repo, fix the problem, commit, and start again at step 4. - - -Prerequisites -------------- - -In `~/.gradle/gradle.properties`, set the following: - - * `mavenCentralUsername` - Sonatype username for releasing to `app.cash`. - * `mavenCentralPassword` - Sonatype password for releasing to `app.cash`. +This will trigger a GitHub Action workflow which will create a GitHub release and upload the release artifacts to Maven Central. diff --git a/build-logic/build.gradle b/build-logic/build.gradle new file mode 100644 index 0000000000..7db1e7ef78 --- /dev/null +++ b/build-logic/build.gradle @@ -0,0 +1,32 @@ +buildscript { + repositories { + mavenCentral() + google() + gradlePluginPortal() + } + + dependencies { + classpath libs.plugin.kotlin + classpath libs.plugin.android + classpath libs.plugin.buildConfig + classpath libs.grgit + } +} + +subprojects { + repositories { + mavenCentral() + google() + } + + tasks.withType(JavaCompile).configureEach { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + + tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile).configureEach { + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } + } +} diff --git a/build-logic/settings.gradle b/build-logic/settings.gradle new file mode 100644 index 0000000000..158ae302da --- /dev/null +++ b/build-logic/settings.gradle @@ -0,0 +1,12 @@ +rootProject.name = 'build-logic' + +include(':paparazzi-gradle-plugin') +project(':paparazzi-gradle-plugin').projectDir = new File('../paparazzi-gradle-plugin') + +dependencyResolutionManagement { + versionCatalogs { + libs { + from(files("../gradle/libs.versions.toml")) + } + } +} diff --git a/build.gradle b/build.gradle index d7b2ce9412..3f317f7320 100644 --- a/build.gradle +++ b/build.gradle @@ -11,14 +11,25 @@ buildscript { dependencies { classpath libs.plugin.kotlin classpath libs.plugin.android + classpath libs.plugin.mavenPublish + classpath libs.plugin.dokka classpath libs.plugin.versions classpath libs.plugin.spotless + classpath libs.plugin.buildConfig + classpath libs.plugin.ksp + classpath libs.grgit - classpath 'app.cash.paparazzi:paparazzi-gradle-plugin:1.1.0' + // Normally you would declare a version here, but we use dependency substitution in + // settings.gradle to use the version built from inside the repo. +// classpath 'app.cash.paparazzi:paparazzi-gradle-plugin' + + classpath libs.paparazzi.gradle.plugin } } subprojects { + version = property("VERSION_NAME") as String + repositories { mavenCentral() google() @@ -27,7 +38,7 @@ subprojects { tasks.withType(Test).configureEach { testLogging { - events "failed" + events "passed", "failed", "skipped", "standardOut", "standardError" exceptionFormat "full" showExceptions true showStackTraces true @@ -40,12 +51,45 @@ subprojects { targetCompatibility = libs.versions.javaTarget.get() } - tasks.withType(org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompile).configureEach { + tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile).configureEach { kotlinOptions { jvmTarget = libs.versions.javaTarget.get() } } + plugins.withId('com.vanniktech.maven.publish') { + publishing { + repositories { + maven { + name = "projectLocalMaven" + url = "${rootProject.buildDir}/localMaven" + } + /** + * Want to push to an internal repository for testing? + * Set the following properties in ~/.gradle/gradle.properties. + * + * internalUrl=YOUR_INTERNAL_URL + * internalUsername=YOUR_USERNAME + * internalPassword=YOUR_PASSWORD + */ + maven { + name = "internal" + url = providers.gradleProperty("internalUrl") + credentials(PasswordCredentials) + } + } + } + } + + tasks.register('emptySourcesJar', Jar) { + // TODO: fetch sources from the corresponding AOSP repos. + archiveClassifier = 'sources' + } + + tasks.register('emptyJavadocJar', Jar) { + archiveClassifier = 'javadoc' + } + apply plugin: 'com.diffplug.spotless' spotless { kotlin { @@ -57,7 +101,10 @@ subprojects { 'charset': 'utf-8', 'indent_size': '2', 'trim_trailing_whitespace': 'true', - 'ij_kotlin_imports_layout': '*,java.**,javax.**,kotlin.**,kotlinx.**,^' + 'ij_kotlin_imports_layout': '*,java.**,javax.**,kotlin.**,kotlinx.**,^', + 'ij_kotlin_allow_trailing_comma': 'false', + 'ij_kotlin_allow_trailing_comma_on_call_site': 'false', + 'ktlint_standard_argument-list-wrapping': 'disabled', ]) } } @@ -67,6 +114,20 @@ tasks.register("clean", Delete).configure { delete rootProject.buildDir } -tasks.named("wrapper").configure { - distributionType = Wrapper.DistributionType.ALL +allprojects { project -> + tasks.register("mavenLocalize").configure { task -> + def projectRootDir = project.projectDir + task.doFirst { + projectRootDir.eachFileRecurse(groovy.io.FileType.FILES) { file -> + if (file.name != 'build.gradle') { + return + } + def text = file.text + file.withWriter { w -> + // Intentional concatenation to prevent self-replacement + w << text.replace("//" + "mavenLocal()", "mavenLocal()") + } + } + } + } } diff --git a/gradle.properties b/gradle.properties index e2381e4651..d362cf4cf8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,28 @@ +GROUP=com.whoop.paparazzi +VERSION_NAME=1.102.0 + +POM_URL=https://github.com/WhoopInc/paparazzi/ +POM_SCM_URL=https://github.com/WhoopInc/paparazzi/ +POM_SCM_CONNECTION=scm:git:git://github.com/WhoopInc/paparazzi.git +POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/WhoopInc/paparazzi.git + +POM_LICENCE_NAME=The Apache Software License, Version 2.0 +POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt +POM_LICENCE_DIST=repo + +POM_DEVELOPER_ID=cashapp +POM_DEVELOPER_NAME=CashApp +POM_DEVELOPER_URL=https://github.com/WhoopInc/ + +SONATYPE_HOST=DEFAULT +RELEASE_SIGNING_ENABLED=false + org.gradle.caching=true org.gradle.parallel=true -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4g android.useAndroidX=true + +# Signals to our own plugin that we are building within the repo. +app.cash.paparazzi.internal=true + +#app.cash.paparazzi.legacy.resource.loading=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3f1c634806..c5f580542a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,39 +1,46 @@ [versions] -agp = "7.1.2" -bytebuddy = "1.12.10" -composeCompiler = "1.3.0" -javaTarget = "1.8" +agp = "8.0.2" +androidTools = "31.0.2" # == 23.0.0 + agp version +bytebuddy = "1.14.5" +composeCompiler = "1.4.7" +javaTarget = "11" jcodec = "0.2.5" -kotlin = "1.7.10" -ktlint = "0.46.1" -moshi = "1.13.0" +kotlin = "1.8.21" +ktlint = "0.49.1" +moshi = "1.15.0" minSdk = "25" -compileSdk = "31" +compileSdk = "33" -# Maps to this commit: https://android.googlesource.com/platform/prebuilts/studio/layoutlib/+/fa3aa65 +# Maps to this commit: https://android.googlesource.com/platform/prebuilts/studio/layoutlib/+/5128371 # which runs on JDK 11. -layoutlib = "2021.2.1-patch1-fa3aa65" -layoutlibPrebuiltSha = "fa3aa65" +layoutlib = "2022.2.1-5128371-2" +layoutlibPrebuiltSha = "5128371" +paparazziGradlePlugin = "1.3.1" [libraries] -androidx-annotations = { module = "androidx.annotation:annotation", version = "1.3.0" } -androidx-appcompat = { module = "androidx.appcompat:appcompat", version = "1.5.1" } -androidx-lifecycleCommon = { module = "androidx.lifecycle:lifecycle-common", version = "2.4.0" } +androidx-annotations = { module = "androidx.annotation:annotation", version = "1.6.0" } +androidx-appcompat = { module = "androidx.appcompat:appcompat", version = "1.6.1" } +androidx-lifecycleCommon = { module = "androidx.lifecycle:lifecycle-common", version = "2.6.1" } +androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version = "1.3.0" } bytebuddy-agent = { module = "net.bytebuddy:byte-buddy-agent", version.ref = "bytebuddy" } bytebuddy-core = { module = "net.bytebuddy:byte-buddy", version.ref = "bytebuddy" } compose-compiler = { module = "androidx.compose.compiler:compiler", version.ref = "composeCompiler" } -composeUi-material = { module = "androidx.compose.material:material", version = "1.2.1" } +composeUi-material = { module = "androidx.compose.material:material", version = "1.4.3" } +composeUi-uiTooling = { module = "androidx.compose.ui:ui-tooling" } -guava = { module = "com.google.guava:guava", version = "31.0.1-jre" } +grgit = { module = "org.ajoberstar.grgit:grgit-core", version = "5.2.0" } +guava = { module = "com.google.guava:guava", version = "32.0.1-jre" } jcodec-core = { module = "org.jcodec:jcodec", version.ref = "jcodec" } jcodec-javase = { module = "org.jcodec:jcodec-javase", version.ref = "jcodec" } kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "kotlin" } -kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version = "1.6.4" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version = "1.7.2" } + +ktlint = { module = "com.pinterest.ktlint:ktlint-rule-engine", version.ref = "ktlint" } kxml2 = { module = "kxml2:kxml2", version = "2.3.0" } @@ -47,24 +54,27 @@ moshi-adapters = { module = "com.squareup.moshi:moshi-adapters", version.ref = " moshi-core = { module = "com.squareup.moshi:moshi", version.ref = "moshi" } moshi-kotlinCodegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" } -okio = { module = "com.squareup.okio:okio", version = "3.1.0" } +okio = { module = "com.squareup.okio:okio", version = "3.4.0" } + +paparazzi-gradle-plugin = { module = "app.cash.paparazzi:paparazzi-gradle-plugin", version.ref = "paparazziGradlePlugin" } +tools-common = { module = "com.android.tools:common", version.ref = "androidTools" } +tools-layoutlib = { module = "com.android.tools.layoutlib:layoutlib-api", version = "31.0.2" } +tools-ninepatch = { module = "com.android.tools:ninepatch", version.ref = "androidTools" } +tools-sdkCommon = { module = "com.android.tools:sdk-common", version.ref = "androidTools" } -tools-common = { module = "com.android.tools:common", version = "27.1.2" } -tools-layoutlib = { module = "com.android.tools.layoutlib:layoutlib-api", version = "27.2.2" } -tools-ninepatch = { module = "com.android.tools:ninepatch", version = "30.1.2" } -tools-sdkCommon = { module = "com.android.tools:sdk-common", version = "26.6.4" } +trove4j = { module = "org.jetbrains.intellij.deps:trove4j", version = "1.0.20200330" } # Test libraries -assertj = { module = "org.assertj:assertj-core", version = "3.23.1" } junit = { module = "junit:junit", version = "4.13.2" } -testParameterInjector = { module = "com.google.testparameterinjector:test-parameter-injector", version = "1.8" } -truth = { module = "com.google.truth:truth", version = "1.1.3" } +testParameterInjector = { module = "com.google.testparameterinjector:test-parameter-injector", version = "1.12" } +truth = { module = "com.google.truth:truth", version = "1.1.5" } # Plugins plugin-android = { module = "com.android.tools.build:gradle", version.ref = "agp" } -plugin-dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "kotlin" } -plugin-grgit = { module = "org.ajoberstar.grgit:grgit-gradle", version = "5.0.0" } +plugin-buildConfig = { module = "com.github.gmazzo:gradle-buildconfig-plugin", version = "3.1.0" } +plugin-dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version = "1.8.20" } plugin-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } -plugin-mavenPublish = { module = "com.vanniktech:gradle-maven-publish-plugin", version = "0.21.0" } -plugin-spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version = "6.8.0" } -plugin-versions = { module = "com.github.ben-manes:gradle-versions-plugin", version = "0.42.0" } +plugin-ksp = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version = "1.8.21-1.0.11" } +plugin-mavenPublish = { module = "com.vanniktech:gradle-maven-publish-plugin", version = "0.25.3" } +plugin-spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version = "6.19.0" } +plugin-versions = { module = "com.github.ben-manes:gradle-versions-plugin", version = "0.47.0" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 249e5832f0..033e24c4cd 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8fad3f5a98..9f4197d5f4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index a69d9cb6c2..fcb6fca147 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,13 +80,10 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,22 +130,29 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,6 +197,10 @@ if "$cygwin" || "$msys" ; then done fi + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + # Collect all arguments for the java command; # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # shell script including quotes and variable substitutions, so put them in diff --git a/gradlew.bat b/gradlew.bat index 53a6b238d4..93e3f59f13 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,91 +1,92 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/libs/README.md b/libs/README.md new file mode 100644 index 0000000000..ee41e5d4b4 --- /dev/null +++ b/libs/README.md @@ -0,0 +1,54 @@ +Paparazzi Libraries +------------------- + +This project publishes artifacts from [Android Studio's][android_studio] UI renderer to Maven +Central so that we can consume them in Paparazzi. + +Note that layoutlib's version tracks [Android Studio Releases][studio_releases] and not Paparazzi. + +1. Find the corresponding [Android Studio version][studio_versions] you wish to update LayoutLib to. + ``` + Flamingo => 2022.2.1 + ``` +2. Find and click the [tag][prebuilt_refs] for the prebuilt corresponding to that version. +3. Copy the commit short sha from the resulting page + ``` + # Example: https://android.googlesource.com/platform/prebuilts/studio/layoutlib/+/refs/tags/studio-2022.2.1 + + 512837137ea60b9b86836cab7169fec5c635f422 => 5128371 + ``` +4. Update commit link, `layoutlib` and `layoutlibPrebuiltSha` in `libs.versions.toml` as expected. + ``` + https://android.googlesource.com/platform/prebuilts/studio/layoutlib/+/5128371 + ... + layoutlib = "2022.2.1-5128371" + layoutlibPrebuiltSha = "5128371" + ``` +5. Build and upload: + ``` + ./gradlew publishMavenNativeLibraryPublicationToMavenCentralRepository + ``` + + This may take a few minutes. It clones a large repo (2.4 GiB) and then uploads a large artifact + (30 MiB) to Maven Central. + +6. Visit [Sonatype Nexus][nexus] to promote the artifact. Or drop it if there is a problem! +7. Once deploy is live, continue with changeset from step 4 to update Paparazzi to consume this + latest version. Here's an [example PR][dolphin_bump]. + + +Prerequisites +------------- + +In `~/.gradle/gradle.properties`, set the following: + + * `mavenCentralUsername` - Sonatype username for releasing to `app.cash`. + * `mavenCentralPassword` - Sonatype password for releasing to `app.cash`. + + +[android_studio]: https://developer.android.com/studio +[studio_releases]: https://developer.android.com/studio/releases +[studio_versions]: https://developer.android.com/studio/releases#android_gradle_plugin_and_android_studio_compatibility +[nexus]: https://oss.sonatype.org/ +[prebuilt_refs]: https://android.googlesource.com/platform/prebuilts/studio/layoutlib/+refs +[dolphin_bump]: https://github.com/cashapp/paparazzi/pull/639 diff --git a/paparazzi/libs/build.gradle b/libs/build.gradle similarity index 100% rename from paparazzi/libs/build.gradle rename to libs/build.gradle diff --git a/paparazzi/libs/layoutlib/build.gradle b/libs/layoutlib/build.gradle similarity index 70% rename from paparazzi/libs/layoutlib/build.gradle rename to libs/layoutlib/build.gradle index c9ad60d5df..7575921eac 100644 --- a/paparazzi/libs/layoutlib/build.gradle +++ b/libs/layoutlib/build.gradle @@ -1,4 +1,5 @@ -apply plugin: 'org.ajoberstar.grgit' +import org.ajoberstar.grgit.Grgit + apply plugin: 'com.vanniktech.maven.publish' version = libs.versions.layoutlib.get() @@ -17,22 +18,26 @@ tasks.register('cloneLayoutlib') { if (repoDir.exists() && !repoDir.list()) { repoDir.delete() } + Grgit grgit if (!repoDir.exists()) { - logger.warn('Cloning prebuilt layoutlib: this make take a few minutes...') - grgit.clone { + logger.warn('Cloning prebuilt layoutlib: this may take a few minutes...') + grgit = Grgit.clone { dir = repoDir uri = "https://android.googlesource.com/platform/prebuilts/studio/layoutlib" } logger.warn('Cloned prebuilt layoutlib.') + } else { + logger.warn('Using existing prebuilt layoutlib clone.') + grgit = Grgit.open { + dir = repoDir + } } - - def repo = grgit.open { - dir = repoDir - } - - logger.warn("Checking out SHA ${libs.versions.layoutlibPrebuiltSha.get()}") - repo.checkout { - branch = libs.versions.layoutlibPrebuiltSha.get() + grgit.withCloseable { repo -> + repo.fetch() + logger.warn("Checking out SHA ${libs.versions.layoutlibPrebuiltSha.get()}") + repo.checkout { + branch = libs.versions.layoutlibPrebuiltSha.get() + } } } } diff --git a/paparazzi/libs/layoutlib/gradle.properties b/libs/layoutlib/gradle.properties similarity index 100% rename from paparazzi/libs/layoutlib/gradle.properties rename to libs/layoutlib/gradle.properties diff --git a/paparazzi/libs/native-linux/build.gradle b/libs/native-linux/build.gradle similarity index 93% rename from paparazzi/libs/native-linux/build.gradle rename to libs/native-linux/build.gradle index 752ebb6731..830763aed6 100644 --- a/paparazzi/libs/native-linux/build.gradle +++ b/libs/native-linux/build.gradle @@ -7,6 +7,7 @@ tasks.register('linuxJar', Jar) { include 'data/linux/**' include 'data/fonts/**' include 'data/icu/**' + include 'data/keyboards/**' exclude '**/BUILD' } dependsOn(':libs:layoutlib:cloneLayoutlib') diff --git a/paparazzi/libs/native-linux/gradle.properties b/libs/native-linux/gradle.properties similarity index 100% rename from paparazzi/libs/native-linux/gradle.properties rename to libs/native-linux/gradle.properties diff --git a/paparazzi/libs/native-macarm/build.gradle b/libs/native-macarm/build.gradle similarity index 93% rename from paparazzi/libs/native-macarm/build.gradle rename to libs/native-macarm/build.gradle index 47b86d2b10..b092d74c69 100644 --- a/paparazzi/libs/native-macarm/build.gradle +++ b/libs/native-macarm/build.gradle @@ -7,6 +7,7 @@ tasks.register('macArmJar', Jar) { include 'data/mac-arm/**' include 'data/fonts/**' include 'data/icu/**' + include 'data/keyboards/**' exclude '**/BUILD' } dependsOn(':libs:layoutlib:cloneLayoutlib') diff --git a/paparazzi/libs/native-macarm/gradle.properties b/libs/native-macarm/gradle.properties similarity index 100% rename from paparazzi/libs/native-macarm/gradle.properties rename to libs/native-macarm/gradle.properties diff --git a/paparazzi/libs/native-macosx/build.gradle b/libs/native-macosx/build.gradle similarity index 93% rename from paparazzi/libs/native-macosx/build.gradle rename to libs/native-macosx/build.gradle index 1781efd64e..a8c71a73d3 100644 --- a/paparazzi/libs/native-macosx/build.gradle +++ b/libs/native-macosx/build.gradle @@ -7,6 +7,7 @@ tasks.register('macJar', Jar) { include 'data/mac/**' include 'data/fonts/**' include 'data/icu/**' + include 'data/keyboards/**' exclude '**/BUILD' } dependsOn(':libs:layoutlib:cloneLayoutlib') diff --git a/paparazzi/libs/native-macosx/gradle.properties b/libs/native-macosx/gradle.properties similarity index 100% rename from paparazzi/libs/native-macosx/gradle.properties rename to libs/native-macosx/gradle.properties diff --git a/paparazzi/libs/native-win/build.gradle b/libs/native-win/build.gradle similarity index 93% rename from paparazzi/libs/native-win/build.gradle rename to libs/native-win/build.gradle index b367823da5..9bd9c895c8 100644 --- a/paparazzi/libs/native-win/build.gradle +++ b/libs/native-win/build.gradle @@ -7,6 +7,7 @@ tasks.register('winJar', Jar) { include 'data/win/**' include 'data/fonts/**' include 'data/icu/**' + include 'data/keyboards/**' exclude '**/BUILD' } dependsOn(':libs:layoutlib:cloneLayoutlib') diff --git a/paparazzi/libs/native-win/gradle.properties b/libs/native-win/gradle.properties similarity index 100% rename from paparazzi/libs/native-win/gradle.properties rename to libs/native-win/gradle.properties diff --git a/paparazzi/paparazzi-agent/build.gradle b/paparazzi-agent/build.gradle similarity index 85% rename from paparazzi/paparazzi-agent/build.gradle rename to paparazzi-agent/build.gradle index 1c22da5df4..12975104ae 100644 --- a/paparazzi/paparazzi-agent/build.gradle +++ b/paparazzi-agent/build.gradle @@ -6,5 +6,5 @@ dependencies { implementation libs.bytebuddy.agent api libs.junit - testImplementation libs.assertj + testImplementation libs.truth } diff --git a/paparazzi/paparazzi-agent/gradle.properties b/paparazzi-agent/gradle.properties similarity index 100% rename from paparazzi/paparazzi-agent/gradle.properties rename to paparazzi-agent/gradle.properties diff --git a/paparazzi/paparazzi-agent/src/main/java/app/cash/paparazzi/agent/AgentTestRule.kt b/paparazzi-agent/src/main/java/app/cash/paparazzi/agent/AgentTestRule.kt similarity index 100% rename from paparazzi/paparazzi-agent/src/main/java/app/cash/paparazzi/agent/AgentTestRule.kt rename to paparazzi-agent/src/main/java/app/cash/paparazzi/agent/AgentTestRule.kt diff --git a/paparazzi/paparazzi-agent/src/main/java/app/cash/paparazzi/agent/InterceptorRegistrar.kt b/paparazzi-agent/src/main/java/app/cash/paparazzi/agent/InterceptorRegistrar.kt similarity index 100% rename from paparazzi/paparazzi-agent/src/main/java/app/cash/paparazzi/agent/InterceptorRegistrar.kt rename to paparazzi-agent/src/main/java/app/cash/paparazzi/agent/InterceptorRegistrar.kt diff --git a/paparazzi/paparazzi-agent/src/test/java/app/cash/paparazzi/agent/InterceptorRegistrarTest.kt b/paparazzi-agent/src/test/java/app/cash/paparazzi/agent/InterceptorRegistrarTest.kt similarity index 95% rename from paparazzi/paparazzi-agent/src/test/java/app/cash/paparazzi/agent/InterceptorRegistrarTest.kt rename to paparazzi-agent/src/test/java/app/cash/paparazzi/agent/InterceptorRegistrarTest.kt index 1113073a8d..50ff218e10 100644 --- a/paparazzi/paparazzi-agent/src/test/java/app/cash/paparazzi/agent/InterceptorRegistrarTest.kt +++ b/paparazzi-agent/src/test/java/app/cash/paparazzi/agent/InterceptorRegistrarTest.kt @@ -1,7 +1,7 @@ package app.cash.paparazzi.agent +import com.google.common.truth.Truth.assertThat import net.bytebuddy.agent.ByteBuddyAgent -import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Before import org.junit.Test @@ -26,7 +26,7 @@ class InterceptorRegistrarTest { Utils.log1() Utils.log2() - assertThat(logs).containsExactly("intercept1", "intercept2") + assertThat(logs).containsExactly("intercept1", "intercept2").inOrder() } @After diff --git a/paparazzi/paparazzi-gradle-plugin/build.gradle b/paparazzi-gradle-plugin/build.gradle similarity index 60% rename from paparazzi/paparazzi-gradle-plugin/build.gradle rename to paparazzi-gradle-plugin/build.gradle index 68903b7cd5..596aaaa235 100644 --- a/paparazzi/paparazzi-gradle-plugin/build.gradle +++ b/paparazzi-gradle-plugin/build.gradle @@ -1,6 +1,15 @@ apply plugin: 'org.jetbrains.kotlin.jvm' apply plugin: 'java-gradle-plugin' -apply plugin: 'com.vanniktech.maven.publish' +apply plugin: 'com.github.gmazzo.buildconfig' + +// This module is included in two projects: +// - In the root project where it's released as one of our artifacts +// - In build-logic project where we can use it for the test-schema and samples. +// +// We only want to publish when it's being built in the root project. +if (rootProject.name == 'paparazzi-root') { + apply plugin: 'com.vanniktech.maven.publish' +} gradlePlugin { plugins { @@ -24,30 +33,15 @@ dependencies { testImplementation libs.truth } -sourceSets { - main.java.srcDir 'src/generated/kotlin' -} - -def generateVersion = tasks.register("pluginVersion") { - def outputDir = file('src/generated/kotlin') - - inputs.property 'version', version - inputs.property 'nativeLibVersion', libs.versions.layoutlib - outputs.dir outputDir - - doLast { - def versionFile = file("${outputDir}/app/cash/paparazzi/Version.kt") - versionFile.parentFile.mkdirs() - versionFile.text = """// Generated file. Do not edit! -package app.cash.paparazzi -const val VERSION = "${project.version}" -const val NATIVE_LIB_VERSION = "${libs.versions.layoutlib.get()}" -""" +buildConfig { + useKotlinOutput { + internalVisibility = true + topLevelConstants = true } -} -tasks.named('compileKotlin').configure { - it.dependsOn(generateVersion) + packageName('app.cash.paparazzi.gradle') + buildConfigField("String", "VERSION", "\"${project.version}\"") + buildConfigField("String", "NATIVE_LIB_VERSION", "\"${libs.versions.layoutlib.get()}\"") } tasks.withType(Test).configureEach { diff --git a/paparazzi/paparazzi-gradle-plugin/gradle.properties b/paparazzi-gradle-plugin/gradle.properties similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/gradle.properties rename to paparazzi-gradle-plugin/gradle.properties diff --git a/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/PaparazziPlugin.kt b/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/PaparazziPlugin.kt new file mode 100644 index 0000000000..12785fb157 --- /dev/null +++ b/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/PaparazziPlugin.kt @@ -0,0 +1,331 @@ +/* + * Copyright (C) 2019 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.gradle + +import app.cash.paparazzi.gradle.utils.artifactViewFor +import app.cash.paparazzi.gradle.utils.artifactsFor +import com.android.build.gradle.BaseExtension +import com.android.build.gradle.LibraryExtension +import com.android.build.gradle.TestedExtension +import com.android.build.gradle.api.BaseVariant +import com.android.build.gradle.internal.api.TestedVariant +import com.android.build.gradle.internal.dsl.BaseAppModuleExtension +import com.android.build.gradle.internal.dsl.DynamicFeatureExtension +import com.android.build.gradle.internal.publishing.AndroidArtifacts.ArtifactType +import com.android.build.gradle.tasks.MergeSourceSetFolders +import org.gradle.api.DefaultTask +import org.gradle.api.DomainObjectSet +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.artifacts.component.ProjectComponentIdentifier +import org.gradle.api.artifacts.type.ArtifactTypeDefinition +import org.gradle.api.artifacts.type.ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE +import org.gradle.api.file.Directory +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.FileCollection +import org.gradle.api.internal.artifacts.transform.UnzipTransform +import org.gradle.api.logging.LogLevel.LIFECYCLE +import org.gradle.api.plugins.JavaBasePlugin +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskProvider +import org.gradle.api.tasks.options.Option +import org.gradle.api.tasks.testing.Test +import org.gradle.internal.os.OperatingSystem +import org.gradle.language.base.plugins.LifecycleBasePlugin.VERIFICATION_GROUP +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinAndroidPluginWrapper +import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget +import java.util.Locale +import kotlin.io.path.invariantSeparatorsPathString + +@Suppress("unused") +class PaparazziPlugin : Plugin<Project> { + override fun apply(project: Project) { + val legacyResourceLoadingEnabled = (project.findProperty("app.cash.paparazzi.legacy.resource.loading") as? String)?.toBoolean() ?: false + + if (legacyResourceLoadingEnabled) { + project.afterEvaluate { + check(!project.plugins.hasPlugin("com.android.application")) { + error( + "Currently, Paparazzi only works in Android library -- not application -- modules. " + + "See https://github.com/cashapp/paparazzi/issues/107" + ) + } + + check(project.plugins.hasPlugin("com.android.library")) { + "The Android Gradle library plugin must be applied for Paparazzi to work properly." + } + } + + project.plugins.withId("com.android.library") { + setupPaparazzi(project, project.extensions.getByType(LibraryExtension::class.java).libraryVariants) + } + } else { + val supportedPlugins = listOf("com.android.application", "com.android.library", "com.android.dynamic-feature") + project.afterEvaluate { + check(supportedPlugins.any { project.plugins.hasPlugin(it) }) { + "One of ${supportedPlugins.joinToString(", ")} must be applied for Paparazzi to work properly." + } + } + + supportedPlugins.forEach { plugin -> + project.plugins.withId(plugin) { + val variants = when (val extension = project.extensions.getByType(TestedExtension::class.java)) { + is LibraryExtension -> extension.libraryVariants + is BaseAppModuleExtension -> extension.applicationVariants + is DynamicFeatureExtension -> extension.applicationVariants + // exhaustive to avoid potential breaking changes in future AGP releases + else -> throw IllegalStateException("${extension.javaClass.name} from $plugin is not supported in Paparazzi") + } + setupPaparazzi(project, variants) + } + } + } + } + + private fun <T> setupPaparazzi(project: Project, variants: DomainObjectSet<T>) where T : BaseVariant, T : TestedVariant { + project.addTestDependency() + val nativePlatformFileCollection = project.setupNativePlatformDependency() + + // Create anchor tasks for all variants. + val verifyVariants = project.tasks.register("verifyPaparazzi") { + it.group = VERIFICATION_GROUP + it.description = "Run screenshot tests for all variants" + } + val recordVariants = project.tasks.register("recordPaparazzi") { + it.group = VERIFICATION_GROUP + it.description = "Record golden images for all variants" + } + + variants.all { variant -> + val variantSlug = variant.name.capitalize(Locale.US) + val testVariant = variant.unitTestVariant ?: return@all + + val mergeResourcesOutputDir = variant.mergeResourcesProvider.flatMap { it.outputDir } + val mergeAssetsProvider = + project.tasks.named("merge${variantSlug}Assets") as TaskProvider<MergeSourceSetFolders> + val mergeAssetsOutputDir = mergeAssetsProvider.flatMap { it.outputDir } + val projectDirectory = project.layout.projectDirectory + val buildDirectory = project.layout.buildDirectory + val gradleUserHomeDir = project.gradle.gradleUserHomeDir + val reportOutputDir = buildDirectory.dir("reports/paparazzi") + val snapshotOutputDir = project.layout.projectDirectory.dir("src/test/snapshots") + + val localResourceDirs = project + .files(variant.sourceSets.flatMap { it.resDirectories }) + + // https://android.googlesource.com/platform/tools/base/+/96015063acd3455a76cdf1cc71b23b0828c0907f/build-system/gradle-core/src/main/java/com/android/build/gradle/tasks/MergeResources.kt#875 + + val moduleResourceDirs = variant.runtimeConfiguration + .artifactsFor(ArtifactType.ANDROID_RES.type) { it is ProjectComponentIdentifier } + .artifactFiles + + val aarExplodedDirs = variant.runtimeConfiguration + .artifactsFor(ArtifactType.ANDROID_RES.type) { it !is ProjectComponentIdentifier } + .artifactFiles + + val localAssetDirs = project + .files(variant.sourceSets.flatMap { it.assetsDirectories }) + + // https://android.googlesource.com/platform/tools/base/+/96015063acd3455a76cdf1cc71b23b0828c0907f/build-system/gradle-core/src/main/java/com/android/build/gradle/tasks/MergeResources.kt#875 + + val moduleAssetDirs = variant.runtimeConfiguration + .artifactsFor(ArtifactType.ASSETS.type) { it is ProjectComponentIdentifier } + .artifactFiles + + val aarAssetDirs = variant.runtimeConfiguration + .artifactsFor(ArtifactType.ASSETS.type) { it !is ProjectComponentIdentifier } + .artifactFiles + + val packageAwareArtifactFiles = variant.runtimeConfiguration + .artifactsFor(ArtifactType.SYMBOL_LIST_WITH_PACKAGE_NAME.type) + .artifactFiles + + val writeResourcesTask = project.tasks.register( + "preparePaparazzi${variantSlug}Resources", + PrepareResourcesTask::class.java + ) { task -> + val android = project.extensions.getByType(BaseExtension::class.java) + val nonTransitiveRClassEnabled = + (project.findProperty("android.nonTransitiveRClass") as? String)?.toBoolean() ?: true + + task.packageName.set(android.packageName()) + task.artifactFiles.from(packageAwareArtifactFiles) + task.nonTransitiveRClassEnabled.set(nonTransitiveRClassEnabled) + task.mergeResourcesOutputDir.set(buildDirectory.asRelativePathString(mergeResourcesOutputDir)) + task.targetSdkVersion.set(android.targetSdkVersion()) + task.compileSdkVersion.set(android.compileSdkVersion()) + task.mergeAssetsOutputDir.set(buildDirectory.asRelativePathString(mergeAssetsOutputDir)) + task.projectResourceDirs.from(localResourceDirs) + task.moduleResourceDirs.from(moduleResourceDirs) + task.aarExplodedDirs.from(aarExplodedDirs) + task.projectAssetDirs.from(localAssetDirs.plus(moduleAssetDirs)) + task.aarAssetDirs.from(aarAssetDirs) + task.paparazziResources.set(buildDirectory.file("intermediates/paparazzi/${variant.name}/resources.txt")) + } + + val testVariantSlug = testVariant.name.capitalize(Locale.US) + + project.plugins.withType(JavaBasePlugin::class.java) { + project.tasks.named("compile${testVariantSlug}JavaWithJavac") + .configure { it.dependsOn(writeResourcesTask) } + } + + project.plugins.withType(KotlinMultiplatformPluginWrapper::class.java) { + val multiplatformExtension = + project.extensions.getByType(KotlinMultiplatformExtension::class.java) + check(multiplatformExtension.targets.any { target -> target is KotlinAndroidTarget }) { + "There must be an Android target configured when using Paparazzi with the Kotlin Multiplatform Plugin" + } + project.tasks.named("compile${testVariantSlug}KotlinAndroid") + .configure { it.dependsOn(writeResourcesTask) } + } + + project.plugins.withType(KotlinAndroidPluginWrapper::class.java) { + project.tasks.named("compile${testVariantSlug}Kotlin") + .configure { it.dependsOn(writeResourcesTask) } + } + + val recordTaskProvider = project.tasks.register("recordPaparazzi$variantSlug", PaparazziTask::class.java) { + it.group = VERIFICATION_GROUP + it.description = "Record golden images for variant '${variant.name}'" + } + recordVariants.configure { it.dependsOn(recordTaskProvider) } + val verifyTaskProvider = project.tasks.register("verifyPaparazzi$variantSlug", PaparazziTask::class.java) { + it.group = VERIFICATION_GROUP + it.description = "Run screenshot tests for variant '${variant.name}'" + } + verifyVariants.configure { it.dependsOn(verifyTaskProvider) } + + val isRecordRun = project.objects.property(Boolean::class.java) + val isVerifyRun = project.objects.property(Boolean::class.java) + + project.gradle.taskGraph.whenReady { graph -> + isRecordRun.set(recordTaskProvider.map { graph.hasTask(it) }) + isVerifyRun.set(verifyTaskProvider.map { graph.hasTask(it) }) + } + + val testTaskProvider = project.tasks.named("test$testVariantSlug", Test::class.java) { test -> + test.systemProperties["paparazzi.test.resources"] = + writeResourcesTask.flatMap { it.paparazziResources.asFile }.get().path + test.systemProperties["paparazzi.project.dir"] = projectDirectory.toString() + test.systemProperties["paparazzi.build.dir"] = buildDirectory.get().toString() + test.systemProperties["paparazzi.artifacts.cache.dir"] = gradleUserHomeDir.path + test.systemProperties["kotlinx.coroutines.main.delay"] = true + test.systemProperties.putAll(project.properties.filterKeys { it.startsWith("app.cash.paparazzi") }) + + test.inputs.property("paparazzi.test.record", isRecordRun) + test.inputs.property("paparazzi.test.verify", isVerifyRun) + + test.inputs.dir(mergeResourcesOutputDir) + test.inputs.dir(mergeAssetsOutputDir) + test.inputs.files(nativePlatformFileCollection) + .withPropertyName("paparazzi.nativePlatform") + .withPathSensitivity(PathSensitivity.NONE) + + test.outputs.dir(reportOutputDir) + test.outputs.dir(snapshotOutputDir) + + test.doFirst { + // Note: these are lazy properties that are not resolvable in the Gradle configuration phase. + // They need special handling, so they're added as inputs.property above, and systemProperty here. + test.systemProperties["paparazzi.platform.data.root"] = + nativePlatformFileCollection.singleFile.absolutePath + test.systemProperties["paparazzi.test.record"] = isRecordRun.get() + test.systemProperties["paparazzi.test.verify"] = isVerifyRun.get() + } + + test.doLast { + val uri = reportOutputDir.get().asFile.toPath().resolve("index.html").toUri() + test.logger.log(LIFECYCLE, "See the Paparazzi report at: $uri") + } + } + + recordTaskProvider.configure { it.dependsOn(testTaskProvider) } + verifyTaskProvider.configure { it.dependsOn(testTaskProvider) } + } + } + + open class PaparazziTask : DefaultTask() { + @Option(option = "tests", description = "Sets test class or method name to be included, '*' is supported.") + open fun setTestNameIncludePatterns(testNamePattern: List<String>): PaparazziTask { + project.tasks.withType(Test::class.java).configureEach { + it.setTestNameIncludePatterns(testNamePattern) + } + return this + } + } + + private fun Project.setupNativePlatformDependency(): FileCollection { + val operatingSystem = OperatingSystem.current() + val nativeLibraryArtifactId = when { + operatingSystem.isMacOsX -> { + val osArch = System.getProperty("os.arch").lowercase(Locale.US) + if (osArch.startsWith("x86")) "macosx" else "macarm" + } + operatingSystem.isWindows -> "win" + else -> "linux" + } + + val nativePlatformConfiguration = configurations.create("nativePlatform") + nativePlatformConfiguration.dependencies.add( + dependencies.create("app.cash.paparazzi:layoutlib-native-$nativeLibraryArtifactId:$NATIVE_LIB_VERSION") + ) + dependencies.registerTransform(UnzipTransform::class.java) { transform -> + transform.from.attribute(ARTIFACT_TYPE_ATTRIBUTE, ArtifactTypeDefinition.JAR_TYPE) + transform.to.attribute(ARTIFACT_TYPE_ATTRIBUTE, ArtifactTypeDefinition.DIRECTORY_TYPE) + } + + return nativePlatformConfiguration + .artifactViewFor(ArtifactTypeDefinition.DIRECTORY_TYPE) + .files + } + + private fun Project.addTestDependency() { + val dependency = if (isInternal()) { + dependencies.project(mapOf("path" to ":paparazzi")) + } else { + dependencies.create("com.whoop.paparazzi:paparazzi:$VERSION") + } + configurations.getByName("testImplementation").dependencies.add(dependency) + } + + private fun Project.isInternal(): Boolean { + return properties["app.cash.paparazzi.internal"].toString() == "true" + } + + private fun BaseExtension.packageName(): String = namespace ?: "" + + private fun BaseExtension.compileSdkVersion(): String { + return compileSdkVersion!!.substringAfter("android-", DEFAULT_COMPILE_SDK_VERSION.toString()) + } + + private fun BaseExtension.targetSdkVersion(): String { + return defaultConfig.targetSdkVersion?.apiLevel?.toString() + ?: DEFAULT_COMPILE_SDK_VERSION.toString() + } +} + +private fun Directory.relativize(child: Directory): String { + return asFile.toPath().relativize(child.asFile.toPath()).invariantSeparatorsPathString +} + +private fun DirectoryProperty.asRelativePathString(child: Provider<Directory>): Provider<String> = + flatMap { root -> child.map { root.relativize(it) } } + +private const val DEFAULT_COMPILE_SDK_VERSION = 33 diff --git a/paparazzi/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/PrepareResourcesTask.kt b/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/PrepareResourcesTask.kt similarity index 61% rename from paparazzi/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/PrepareResourcesTask.kt rename to paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/PrepareResourcesTask.kt index 6006dd2a04..970617774e 100644 --- a/paparazzi/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/PrepareResourcesTask.kt +++ b/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/PrepareResourcesTask.kt @@ -15,15 +15,13 @@ */ package app.cash.paparazzi.gradle +import app.cash.paparazzi.gradle.utils.joinFiles import org.gradle.api.DefaultTask import org.gradle.api.file.ConfigurableFileCollection -import org.gradle.api.file.Directory -import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.Property import org.gradle.api.tasks.CacheableTask import org.gradle.api.tasks.Input -import org.gradle.api.tasks.InputDirectory import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.PathSensitive @@ -35,9 +33,9 @@ abstract class PrepareResourcesTask : DefaultTask() { @get:Input abstract val packageName: Property<String> - @get:InputDirectory - @get:PathSensitive(PathSensitivity.RELATIVE) - abstract val mergeResourcesOutput: DirectoryProperty + @Deprecated("legacy resource loading, to be removed in a future release") + @get:Input + abstract val mergeResourcesOutputDir: Property<String> @get:Input abstract val targetSdkVersion: Property<String> @@ -45,9 +43,29 @@ abstract class PrepareResourcesTask : DefaultTask() { @get:Input abstract val compileSdkVersion: Property<String> - @get:InputDirectory - @get:PathSensitive(PathSensitivity.RELATIVE) - abstract val mergeAssetsOutput: DirectoryProperty + @get:InputFiles + @get:PathSensitive(PathSensitivity.NONE) + abstract val projectResourceDirs: ConfigurableFileCollection + + @get:InputFiles + @get:PathSensitive(PathSensitivity.NONE) + abstract val moduleResourceDirs: ConfigurableFileCollection + + @get:InputFiles + @get:PathSensitive(PathSensitivity.NONE) + abstract val aarExplodedDirs: ConfigurableFileCollection + + @get:InputFiles + @get:PathSensitive(PathSensitivity.NONE) + abstract val projectAssetDirs: ConfigurableFileCollection + + @get:InputFiles + @get:PathSensitive(PathSensitivity.NONE) + abstract val aarAssetDirs: ConfigurableFileCollection + + @Deprecated("legacy asset loading, to be removed in a future release") + @get:Input + abstract val mergeAssetsOutputDir: Property<String> @get:Input abstract val nonTransitiveRClassEnabled: Property<Boolean> @@ -59,7 +77,9 @@ abstract class PrepareResourcesTask : DefaultTask() { @get:OutputFile abstract val paparazziResources: RegularFileProperty - private val buildDirectory = project.layout.buildDirectory + private val projectDirectory = project.layout.projectDirectory + + private val gradleUserHomeDirectory = projectDirectory.dir(project.gradle.gradleUserHomeDir.path) @TaskAction // TODO: figure out why this can't be removed as of Kotlin 1.6+ @@ -79,27 +99,32 @@ abstract class PrepareResourcesTask : DefaultTask() { } else { mainPackage } - val projectDirectory = buildDirectory.get() out.bufferedWriter() .use { it.write(mainPackage) it.newLine() - it.write(projectDirectory.relativize(mergeResourcesOutput.get())) + it.write(mergeResourcesOutputDir.get()) it.newLine() it.write(targetSdkVersion.get()) it.newLine() // Use compileSdkVersion for system framework resources. it.write("platforms/android-${compileSdkVersion.get()}/") it.newLine() - it.write(projectDirectory.relativize(mergeAssetsOutput.get())) + it.write(mergeAssetsOutputDir.get()) it.newLine() it.write(resourcePackageNames) it.newLine() + it.write(projectResourceDirs.joinFiles(projectDirectory)) + it.newLine() + it.write(moduleResourceDirs.joinFiles(projectDirectory)) + it.newLine() + it.write(aarExplodedDirs.joinFiles(gradleUserHomeDirectory)) + it.newLine() + it.write(projectAssetDirs.joinFiles(projectDirectory)) + it.newLine() + it.write(aarAssetDirs.joinFiles(gradleUserHomeDirectory)) + it.newLine() } } - - private fun Directory.relativize(child: Directory): String { - return asFile.toPath().relativize(child.asFile.toPath()).toFile().invariantSeparatorsPath - } } diff --git a/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/utils/Artifacts.kt b/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/utils/Artifacts.kt new file mode 100644 index 0000000000..9706178ed3 --- /dev/null +++ b/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/utils/Artifacts.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.gradle.utils + +import org.gradle.api.artifacts.ArtifactCollection +import org.gradle.api.artifacts.ArtifactView +import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.component.ComponentIdentifier +import org.gradle.api.artifacts.type.ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE + +internal fun Configuration.artifactsFor( + attrValue: String, + componentFilter: (ComponentIdentifier) -> Boolean = { true } +): ArtifactCollection = + artifactViewFor(attrValue, componentFilter).artifacts + +internal fun Configuration.artifactViewFor( + attrValue: String, + componentFilter: (ComponentIdentifier) -> Boolean = { true } +): ArtifactView = + incoming.artifactView { config -> + config.attributes.attribute(ARTIFACT_TYPE_ATTRIBUTE, attrValue) + config.componentFilter(componentFilter) + } diff --git a/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/utils/FileUtils.kt b/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/utils/FileUtils.kt new file mode 100644 index 0000000000..5448657137 --- /dev/null +++ b/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/utils/FileUtils.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.gradle.utils + +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.Directory +import java.io.File + +fun ConfigurableFileCollection.joinFiles(directory: Directory) = files.joinToString(",") { file -> + directory.relativize(file) +} + +fun Directory.relativize(child: File): String { + return asFile.toPath().relativize(child.toPath()).toFile().invariantSeparatorsPath +} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/java/app/cash/paparazzi/gradle/ImageSubject.kt b/paparazzi-gradle-plugin/src/test/java/app/cash/paparazzi/gradle/ImageSubject.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/java/app/cash/paparazzi/gradle/ImageSubject.kt rename to paparazzi-gradle-plugin/src/test/java/app/cash/paparazzi/gradle/ImageSubject.kt diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/java/app/cash/paparazzi/gradle/PaparazziPluginTest.kt b/paparazzi-gradle-plugin/src/test/java/app/cash/paparazzi/gradle/PaparazziPluginTest.kt similarity index 71% rename from paparazzi/paparazzi-gradle-plugin/src/test/java/app/cash/paparazzi/gradle/PaparazziPluginTest.kt rename to paparazzi-gradle-plugin/src/test/java/app/cash/paparazzi/gradle/PaparazziPluginTest.kt index 4aaabd46fc..848bf6459b 100644 --- a/paparazzi/paparazzi-gradle-plugin/src/test/java/app/cash/paparazzi/gradle/PaparazziPluginTest.kt +++ b/paparazzi-gradle-plugin/src/test/java/app/cash/paparazzi/gradle/PaparazziPluginTest.kt @@ -22,7 +22,65 @@ class PaparazziPluginTest { } @Test - fun missingAndroidLibraryPlugin() { + fun androidApplicationPlugin() { + val fixtureRoot = File("src/test/projects/supports-application-modules") + + val result = gradleRunner + .withArguments("testDebug", "--stacktrace") + .runFixture(fixtureRoot) { build() } + + assertThat(result.task(":preparePaparazziDebugResources")).isNotNull() + assertThat(result.task(":testDebugUnitTest")).isNotNull() + assertThat(result.output).doesNotContain( + "Currently, Paparazzi only works in Android library -- not application -- modules. " + + "See https://github.com/cashapp/paparazzi/issues/107" + ) + + val snapshotsDir = File(fixtureRoot, "build/reports/paparazzi/images") + val snapshots = snapshotsDir.listFiles() + assertThat(snapshots!!).hasLength(1) + + val snapshotImage = snapshots[0] + val goldenImage = File(fixtureRoot, "src/test/resources/launch.png") + assertThat(snapshotImage).isSimilarTo(goldenImage).withDefaultThreshold() + } + + @Test + fun androidDynamicFeaturePlugin() { + val fixtureRoot = File("src/test/projects/supports-dynamic-feature-modules") + + val result = gradleRunner + .withArguments(":dynamic_feature:testDebug", "--stacktrace") + .runFixture(fixtureRoot) { build() } + + assertThat(result.task(":dynamic_feature:preparePaparazziDebugResources")).isNotNull() + assertThat(result.task(":dynamic_feature:testDebugUnitTest")).isNotNull() + + val snapshotsDir = File(fixtureRoot, "dynamic_feature/build/reports/paparazzi/images") + val snapshots = snapshotsDir.listFiles() + assertThat(snapshots!!).hasLength(1) + + val snapshotImage = snapshots[0] + val goldenImage = File(fixtureRoot, "dynamic_feature/src/test/resources/launch.png") + assertThat(snapshotImage).isSimilarTo(goldenImage).withDefaultThreshold() + } + + @Test + fun missingSupportedPlugins() { + val fixtureRoot = File("src/test/projects/missing-supported-plugins") + + val result = gradleRunner + .withArguments("preparePaparazziDebugResources", "--stacktrace") + .runFixture(fixtureRoot) { buildAndFail() } + + assertThat(result.task(":preparePaparazziDebugResources")).isNull() + assertThat(result.output).contains( + "One of com.android.application, com.android.library, com.android.dynamic-feature must be applied for Paparazzi to work properly." + ) + } + + @Test + fun missingAndroidLibraryPluginWhenLegacyResourceLoadingIsOn() { val fixtureRoot = File("src/test/projects/missing-library-plugin") val result = gradleRunner @@ -36,7 +94,7 @@ class PaparazziPluginTest { } @Test - fun invalidAndroidApplicationPlugin() { + fun invalidAndroidApplicationPluginWhenLegacyResourceLoadingIsOn() { val fixtureRoot = File("src/test/projects/invalid-application-plugin") val result = gradleRunner @@ -98,20 +156,48 @@ class PaparazziPluginTest { } @Test - fun preferDslNamespaceOverManifestPackageDeclaration() { - val fixtureRoot = File("src/test/projects/prefer-dsl-namespace") + fun prepareResourcesCaching() { + val fixtureRoot = File("src/test/projects/prepare-resources-task-caching") - val result = gradleRunner - .withArguments("preparePaparazziDebugResources", "--stacktrace") + val firstRun = gradleRunner + .withArguments("testRelease", "testDebug", "--build-cache", "--stacktrace") .runFixture(fixtureRoot) { build() } - assertThat(result.task(":preparePaparazziDebugResources")).isNotNull() + with(firstRun.task(":preparePaparazziDebugResources")) { + assertThat(this).isNotNull() + assertThat(this!!.outcome).isNotEqualTo(FROM_CACHE) + } - val resourcesFile = File(fixtureRoot, "build/intermediates/paparazzi/debug/resources.txt") + with(firstRun.task(":preparePaparazziReleaseResources")) { + assertThat(this).isNotNull() + assertThat(this!!.outcome).isNotEqualTo(FROM_CACHE) + } + + var resourcesFile = File(fixtureRoot, "build/intermediates/paparazzi/debug/resources.txt") assertThat(resourcesFile.exists()).isTrue() + var resourceFileContents = resourcesFile.readLines() + assertThat(resourceFileContents.any { it.contains("release") }).isFalse() - val resourceFileContents = resourcesFile.readLines() - assertThat(resourceFileContents[0]).isEqualTo("app.cash.paparazzi.plugin.namespaced") + resourcesFile = File(fixtureRoot, "build/intermediates/paparazzi/release/resources.txt") + assertThat(resourcesFile.exists()).isTrue() + resourceFileContents = resourcesFile.readLines() + assertThat(resourceFileContents.any { it.contains("debug") }).isFalse() + + fixtureRoot.resolve("build").deleteRecursively() + + val secondRun = gradleRunner + .withArguments("testDebug", "--build-cache", "--stacktrace") + .runFixture(fixtureRoot) { build() } + + with(secondRun.task(":preparePaparazziDebugResources")) { + assertThat(this).isNotNull() + assertThat(this!!.outcome).isEqualTo(FROM_CACHE) + } + + resourcesFile = File(fixtureRoot, "build/intermediates/paparazzi/debug/resources.txt") + assertThat(resourcesFile.exists()).isTrue() + resourceFileContents = resourcesFile.readLines() + assertThat(resourceFileContents.any { it.contains("release") }).isFalse() } @Test @@ -135,19 +221,43 @@ class PaparazziPluginTest { } @Test - fun missingPlatformDirTest() { - val fixtureRoot = File("src/test/projects/missing-platform-dir") + fun buildClassAccess() { + val fixtureRoot = File("src/test/projects/build-class") - val result = gradleRunner + gradleRunner .withArguments("testDebug", "--stacktrace") - .forwardOutput() - .runFixture(fixtureRoot) { buildAndFail() } + .runFixture(fixtureRoot) { build() } - assertThat(result.task(":testDebug")).isNull() - assertThat(result.output).contains("java.io.FileNotFoundException") - assertThat(result.output).contains("Missing platform version oops") + val snapshotsDir = File(fixtureRoot, "custom/reports/paparazzi/images") + assertThat(snapshotsDir.exists()).isFalse() } +// @Test +// fun buildClassNextSdkAccess() { +// val fixtureRoot = File("src/test/projects/build-class-next-sdk") +// +// gradleRunner +// .withArguments("testDebug", "--stacktrace") +// .runFixture(fixtureRoot) { build() } +// +// val snapshotsDir = File(fixtureRoot, "custom/reports/paparazzi/debug/images") +// assertThat(snapshotsDir.exists()).isFalse() +// } + +// @Test +// fun missingPlatformDirTest() { +// val fixtureRoot = File("src/test/projects/missing-platform-dir") +// +// val result = gradleRunner +// .withArguments("testDebug", "--stacktrace") +// .forwardOutput() +// .runFixture(fixtureRoot) { buildAndFail() } +// +// assertThat(result.task(":testDebug")).isNull() +// assertThat(result.output).contains("java.io.FileNotFoundException") +// assertThat(result.output).contains("Missing platform version oops") +// } + @Test fun flagDebugLinkedObjectsIsOff() { val fixtureRoot = File("src/test/projects/flag-debug-linked-objects-off") @@ -170,6 +280,46 @@ class PaparazziPluginTest { assertThat(result.output).contains("Objects still linked from the DelegateManager:") } + @Test + fun flagLegacyResourceLoadingIsOn() { + val fixtureRoot = File("src/test/projects/flag-legacy-resource-loading-on") + + val result = gradleRunner + .withArguments("testDebug", "--stacktrace") + .runFixture(fixtureRoot) { build() } + + assertThat(result.task(":preparePaparazziDebugResources")).isNotNull() + assertThat(result.task(":testDebugUnitTest")).isNotNull() + + val snapshotsDir = File(fixtureRoot, "build/reports/paparazzi/images") + val snapshots = snapshotsDir.listFiles() + assertThat(snapshots!!).hasLength(1) + + val snapshotImage = snapshots[0] + val goldenImage = File(fixtureRoot, "src/test/resources/launch.png") + assertThat(snapshotImage).isSimilarTo(goldenImage).withDefaultThreshold() + } + + @Test + fun flagLegacyResourceLoadingIsOff() { + val fixtureRoot = File("src/test/projects/flag-legacy-resource-loading-off") + + val result = gradleRunner + .withArguments("testDebug", "--stacktrace") + .runFixture(fixtureRoot) { build() } + + assertThat(result.task(":preparePaparazziDebugResources")).isNotNull() + assertThat(result.task(":testDebugUnitTest")).isNotNull() + + val snapshotsDir = File(fixtureRoot, "build/reports/paparazzi/images") + val snapshots = snapshotsDir.listFiles() + assertThat(snapshots!!).hasLength(1) + + val snapshotImage = snapshots[0] + val goldenImage = File(fixtureRoot, "src/test/resources/launch.png") + assertThat(snapshotImage).isSimilarTo(goldenImage).withDefaultThreshold() + } + @Test fun cacheable() { val fixtureRoot = File("src/test/projects/cacheable") @@ -260,7 +410,7 @@ class PaparazziPluginTest { val result = gradleRunner .withArguments("module:recordPaparazziDebug", "--stacktrace") - .runFixture(fixtureRoot, moduleRoot) { build() } + .runFixture(fixtureRoot) { build() } assertThat(result.task(":module:testDebugUnitTest")).isNotNull() @@ -282,7 +432,7 @@ class PaparazziPluginTest { val result = gradleRunner .withArguments("module:recordPaparazziDebug", "--tests=*recordSecond", "--stacktrace") - .runFixture(fixtureRoot, moduleRoot) { build() } + .runFixture(fixtureRoot) { build() } assertThat(result.task(":module:testDebugUnitTest")).isNotNull() @@ -476,6 +626,44 @@ class PaparazziPluginTest { snapshotsDir.deleteRecursively() } + @Test + fun rerunTestsOnPropertyChange() { + val fixtureRoot = File("src/test/projects/rerun-property-change") + + // Take 1 + val firstRunResult = gradleRunner + .withArguments("testDebugUnitTest", "--stacktrace") + .forwardOutput() + .runFixture(fixtureRoot) { build() } + + with(firstRunResult.task(":testDebugUnitTest")) { + assertThat(this).isNotNull() + assertThat(this!!.outcome).isEqualTo(SUCCESS) + } + + // Take 2 + val secondRunResult = gradleRunner + .withArguments("recordPaparazziDebug", "--stacktrace") + .forwardOutput() + .runFixture(fixtureRoot) { build() } + + with(secondRunResult.task(":testDebugUnitTest")) { + assertThat(this).isNotNull() + assertThat(this!!.outcome).isEqualTo(SUCCESS) // not UP-TO-DATE + } + + // Take 3 + val thirdRunResult = gradleRunner + .withArguments("verifyPaparazziDebug", "--stacktrace") + .forwardOutput() + .runFixture(fixtureRoot) { build() } + + with(thirdRunResult.task(":testDebugUnitTest")) { + assertThat(this).isNotNull() + assertThat(this!!.outcome).isEqualTo(SUCCESS) // not UP-TO-DATE + } + } + @Test fun verifySuccess() { val fixtureRoot = File("src/test/projects/verify-mode-success") @@ -509,7 +697,7 @@ class PaparazziPluginTest { assertThat(result.task(":testDebugUnitTest")).isNotNull() - val failureDir = File(fixtureRoot, "out/failures") + val failureDir = File(fixtureRoot, "build/paparazzi/failures") val delta = File(failureDir, "delta-app.cash.paparazzi.plugin.test_VerifyTest_verify.png") assertThat(delta.exists()).isTrue() @@ -522,11 +710,10 @@ class PaparazziPluginTest { @Test fun verifySuccessMultiModule() { val fixtureRoot = File("src/test/projects/verify-mode-success-multiple-modules") - val moduleRoot = File(fixtureRoot, "module") val result = gradleRunner .withArguments("module:verifyPaparazziDebug", "--stacktrace") - .runFixture(fixtureRoot, moduleRoot) { build() } + .runFixture(fixtureRoot) { build() } assertThat(result.task(":module:testDebugUnitTest")).isNotNull() } @@ -538,11 +725,11 @@ class PaparazziPluginTest { val result = gradleRunner .withArguments("module:verifyPaparazziDebug", "--stacktrace") - .runFixture(fixtureRoot, moduleRoot) { buildAndFail() } + .runFixture(fixtureRoot) { buildAndFail() } assertThat(result.task(":module:testDebugUnitTest")).isNotNull() - val failureDir = File(moduleRoot, "out/failures") + val failureDir = File(moduleRoot, "build/paparazzi/failures") val delta = File(failureDir, "delta-app.cash.paparazzi.plugin.test_VerifyTest_verify.png") assertThat(delta.exists()).isTrue() @@ -573,6 +760,37 @@ class PaparazziPluginTest { assertThat(snapshots[2]).isSimilarTo(verticalScroll).withDefaultThreshold() } + @Test + fun widgets() { + val fixtureRoot = File("src/test/projects/widgets") + + gradleRunner + .withArguments("testDebug", "--stacktrace") + .runFixture(fixtureRoot) { build() } + + val snapshotsDir = File(fixtureRoot, "build/reports/paparazzi/images") + val snapshots = snapshotsDir.listFiles()?.sortedBy { it.lastModified() } + assertThat(snapshots!!).hasSize(2) + + val widgetImage = File(fixtureRoot, "src/test/resources/widget.png") + val fullScreenImage = File(fixtureRoot, "src/test/resources/full_screen.png") + assertThat(snapshots[0]).isSimilarTo(widgetImage).withDefaultThreshold() + assertThat(snapshots[1]).isSimilarTo(fullScreenImage).withDefaultThreshold() + } + + @Test + fun lifecycleOwnerUsages() { + val fixtureRoot = File("src/test/projects/lifecycle-usages") + + gradleRunner + .withArguments("testDebug", "--stacktrace") + .runFixture(fixtureRoot) { build() } + + val snapshotsDir = File(fixtureRoot, "build/reports/paparazzi/images") + val snapshots = snapshotsDir.listFiles() + assertThat(snapshots!!).hasLength(3) + } + @Test fun verifyResourcesGeneratedForJavaProject() { val fixtureRoot = File("src/test/projects/verify-resources-java") @@ -589,8 +807,11 @@ class PaparazziPluginTest { val resourceFileContents = resourcesFile.readLines() assertThat(resourceFileContents[0]).isEqualTo("app.cash.paparazzi.plugin.test") assertThat(resourceFileContents[1]).isEqualTo("intermediates/merged_res/debug") - assertThat(resourceFileContents[4]).isEqualTo("intermediates/assets/debug/mergeDebugAssets") - assertThat(resourceFileContents[5]).isEqualTo("app.cash.paparazzi.plugin.test") + assertThat(resourceFileContents[4]).isEqualTo("intermediates/assets/debug") + assertThat(resourceFileContents[5]).isEqualTo("app.cash.paparazzi.plugin.test,com.example.mylibrary,app.cash.paparazzi.plugin.test.module1,app.cash.paparazzi.plugin.test.module2") + assertThat(resourceFileContents[6]).isEqualTo("src/main/res,src/debug/res") + assertThat(resourceFileContents[7]).isEqualTo("module1/build/intermediates/packaged_res/debug,module2/build/intermediates/packaged_res/debug") + assertThat(resourceFileContents[8]).matches("^caches/transforms-3/[0-9a-f]{32}/transformed/external/res\$") } @Test @@ -609,8 +830,11 @@ class PaparazziPluginTest { val resourceFileContents = resourcesFile.readLines() assertThat(resourceFileContents[0]).isEqualTo("app.cash.paparazzi.plugin.test") assertThat(resourceFileContents[1]).isEqualTo("intermediates/merged_res/debug") - assertThat(resourceFileContents[4]).isEqualTo("intermediates/assets/debug/mergeDebugAssets") - assertThat(resourceFileContents[5]).isEqualTo("app.cash.paparazzi.plugin.test") + assertThat(resourceFileContents[4]).isEqualTo("intermediates/assets/debug") + assertThat(resourceFileContents[5]).isEqualTo("app.cash.paparazzi.plugin.test,com.example.mylibrary,app.cash.paparazzi.plugin.test.module1,app.cash.paparazzi.plugin.test.module2") + assertThat(resourceFileContents[6]).isEqualTo("src/main/res,src/debug/res") + assertThat(resourceFileContents[7]).isEqualTo("module1/build/intermediates/packaged_res/debug,module2/build/intermediates/packaged_res/debug") + assertThat(resourceFileContents[8]).matches("^caches/transforms-3/[0-9a-f]{32}/transformed/external/res\$") } @Test @@ -627,8 +851,8 @@ class PaparazziPluginTest { assertThat(resourcesFile.exists()).isTrue() val resourceFileContents = resourcesFile.readLines() - assertThat(resourceFileContents[2]).isEqualTo("31") - assertThat(resourceFileContents[3]).isEqualTo("platforms/android-31/") + assertThat(resourceFileContents[2]).isEqualTo("33") + assertThat(resourceFileContents[3]).isEqualTo("platforms/android-33/") } @Test @@ -646,26 +870,25 @@ class PaparazziPluginTest { val resourceFileContents = resourcesFile.readLines() assertThat(resourceFileContents[2]).isEqualTo("29") - assertThat(resourceFileContents[3]).isEqualTo("platforms/android-31/") + assertThat(resourceFileContents[3]).isEqualTo("platforms/android-33/") } @Test - fun verifyOpenAssets() { - val fixtureRoot = File("src/test/projects/open-assets") + fun verifyOpenAssetsLegacyAssetLoadingIsOff() { + val fixtureRoot = File("src/test/projects/open-assets-legacy-asset-loading-off") gradleRunner - .withArguments("testDebug", "--stacktrace") + .withArguments("consumer:testDebug", "--stacktrace") .runFixture(fixtureRoot) { build() } } @Test - fun openTransitiveAssets() { - val fixtureRoot = File("src/test/projects/open-transitive-assets") - val moduleRoot = File(fixtureRoot, "consumer") + fun verifyOpenAssetsLegacyAssetLoadingIsOn() { + val fixtureRoot = File("src/test/projects/open-assets-legacy-asset-loading-on") gradleRunner .withArguments("consumer:testDebug", "--stacktrace") - .runFixture(fixtureRoot, moduleRoot) { build() } + .runFixture(fixtureRoot) { build() } } @Test @@ -705,6 +928,23 @@ class PaparazziPluginTest { assertThat(snapshotImage).isSimilarTo(goldenImage).withDefaultThreshold() } + @Test + fun verifyRecyclerView() { + val fixtureRoot = File("src/test/projects/verify-recyclerview") + + gradleRunner + .withArguments("testDebug", "--stacktrace") + .runFixture(fixtureRoot) { build() } + + val snapshotsDir = File(fixtureRoot, "build/reports/paparazzi/images") + val snapshots = snapshotsDir.listFiles() + assertThat(snapshots!!).hasLength(1) + + val snapshotImage = snapshots[0] + val goldenImage = File(fixtureRoot, "src/test/resources/recycler_view.png") + assertThat(snapshotImage).isSimilarTo(goldenImage).withDefaultThreshold() + } + @Test fun withoutAppCompat() { val fixtureRoot = File("src/test/projects/appcompat-missing") @@ -714,7 +954,7 @@ class PaparazziPluginTest { .runFixture(fixtureRoot) { build() } val snapshotsDir = File(fixtureRoot, "build/reports/paparazzi/images") - val snapshotFile = File(snapshotsDir, "6dc7ccef5e1decc563e334e3d809fd68e6f6b122.png") + val snapshotFile = File(snapshotsDir, "9d3c31a9c79a363c26dc352b59e9e77083c300a7.png") assertThat(snapshotFile.exists()).isTrue() val goldenImage = File(fixtureRoot, "src/test/resources/arrow_missing.png") @@ -896,13 +1136,13 @@ class PaparazziPluginTest { } @Test - fun nonTransitiveResources() { - val fixtureRoot = File("src/test/projects/non-transitive-resources") + fun transitiveResources() { + val fixtureRoot = File("src/test/projects/transitive-resources") val moduleRoot = File(fixtureRoot, "module") gradleRunner .withArguments("module:testDebug", "--stacktrace") - .runFixture(fixtureRoot, moduleRoot) { build() } + .runFixture(fixtureRoot) { build() } val snapshotsDir = File(moduleRoot, "build/reports/paparazzi/images") val snapshots = snapshotsDir.listFiles() @@ -913,17 +1153,6 @@ class PaparazziPluginTest { assertThat(snapshotImage).isSimilarTo(goldenImage).withDefaultThreshold() } - @Test - fun nonTransitiveResourcesNoDeps() { - val fixtureRoot = File("src/test/projects/non-transitive-resources-no-deps") - - val result = gradleRunner - .withArguments("testDebug") - .runFixture(fixtureRoot) { build() } - - assertThat(result.output).doesNotContain("java.lang.ClassNotFoundException") - } - @Test fun compose() { val fixtureRoot = File("src/test/projects/compose") @@ -941,8 +1170,17 @@ class PaparazziPluginTest { } @Test - fun composeDispatcher() { - val fixtureRoot = File("src/test/projects/compose-dispatcher") + fun composeRecomposition() { + val fixtureRoot = File("src/test/projects/compose-recomposition") + + gradleRunner + .withArguments("verifyPaparazziDebug", "--stacktrace") + .runFixture(fixtureRoot) { build() } + } + + @Test + fun composeViewTreeLifecycle() { + val fixtureRoot = File("src/test/projects/compose-lifecycle-owner") gradleRunner .withArguments("testDebug", "--stacktrace") .runFixture(fixtureRoot) { build() } @@ -968,6 +1206,24 @@ class PaparazziPluginTest { assertThat(snapshotImage).isSimilarTo(goldenImage).withDefaultThreshold() } + @Test + fun composeA11y() { + val fixtureRoot = File("src/test/projects/compose-a11y") + gradleRunner + .withArguments("testDebug", "--stacktrace") + .runFixture(fixtureRoot) { build() } + + val snapshotsDir = File(fixtureRoot, "build/reports/paparazzi/images") + val snapshots = snapshotsDir.listFiles() + assertThat(snapshots!!).hasLength(1) + + val snapshotImage = snapshots[0] + var goldenImage = File(fixtureRoot, "src/test/resources/compose_a11y.png") + assertThat(snapshotImage).isSimilarTo(goldenImage).withDefaultThreshold() + goldenImage = File(fixtureRoot, "src/test/resources/compose_a11y_change_hierarchy_order.png") + assertThat(snapshotImage).isSimilarTo(goldenImage).withDefaultThreshold() + } + @Test fun immSoftInputInteraction() { val fixtureRoot = File("src/test/projects/imm-soft-input") @@ -1056,34 +1312,67 @@ class PaparazziPluginTest { assertThat(darkModeSnapshotImage).isSimilarTo(darkModeGoldenImage).withDefaultThreshold() } + @Test + fun disabledUnitTestVariant() { + val fixtureRoot = File("src/test/projects/disabled-unit-test-variant") + gradleRunner + .withArguments("testDebug") + .runFixture(fixtureRoot) { build() } + } + + @Test + fun verifyCoroutineDelay() { + val fixtureRoot = File("src/test/projects/coroutine-delay-main") + + val result = gradleRunner + .withArguments("testDebug", "--stacktrace") + .runFixture(fixtureRoot) { build() } + + assertThat(result.task(":testDebugUnitTest")).isNotNull() + } + + @Test + fun accessibilityErrorsLogged() { + val fixtureRoot = File("src/test/projects/validate-accessibility") + + val result = gradleRunner + .withArguments("testDebug", "--stacktrace") + .runFixture(fixtureRoot) { build() } + + assertThat(result.output).contains( + "\u001B[33mAccessibility issue of type LOW_CONTRAST on no-id:\u001B[0m " + + "The item's text contrast ratio is 1.00. This ratio is based on a text color of #FFFFFF " + + "and background color of #FFFFFF. Consider increasing this item's text contrast ratio to " + + "4.50 or greater." + ) + } + private fun GradleRunner.runFixture( projectRoot: File, - moduleRoot: File = projectRoot, action: GradleRunner.() -> BuildResult ): BuildResult { val settings = File(projectRoot, "settings.gradle") - if (!settings.exists()) { - settings.createNewFile() - settings.writeText("apply from: \"../test.settings.gradle\"") - settings.deleteOnExit() - } - - val mainSourceRoot = File(moduleRoot, "src/main") - val manifest = File(mainSourceRoot, "AndroidManifest.xml") - if (!mainSourceRoot.exists() || !manifest.exists()) { - mainSourceRoot.mkdirs() - manifest.createNewFile() - manifest.writeText("""<manifest package="app.cash.paparazzi.plugin.test"/>""") - manifest.deleteOnExit() - } - val gradleProperties = File(projectRoot, "gradle.properties") - if (!gradleProperties.exists()) { - gradleProperties.createNewFile() - gradleProperties.writeText("android.useAndroidX=true") - gradleProperties.deleteOnExit() + var generatedSettings = false + var generatedGradleProperties = false + + return try { + if (!settings.exists()) { + settings.createNewFile() + settings.writeText("apply from: \"../test.settings.gradle\"") + generatedSettings = true + } + + if (!gradleProperties.exists()) { + gradleProperties.createNewFile() + gradleProperties.writeText("android.useAndroidX=true") + generatedGradleProperties = true + } + + withProjectDir(projectRoot).action() + } finally { + if (generatedSettings) settings.delete() + if (generatedGradleProperties) gradleProperties.delete() } - - return withProjectDir(projectRoot).action() } } diff --git a/paparazzi-gradle-plugin/src/test/projects/appcompat-missing/build.gradle b/paparazzi-gradle-plugin/src/test/projects/appcompat-missing/build.gradle new file mode 100644 index 0000000000..411ab6a9d7 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/appcompat-missing/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/appcompat-missing/src/main/res/drawable/arrow_up.xml b/paparazzi-gradle-plugin/src/test/projects/appcompat-missing/src/main/res/drawable/arrow_up.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/appcompat-missing/src/main/res/drawable/arrow_up.xml rename to paparazzi-gradle-plugin/src/test/projects/appcompat-missing/src/main/res/drawable/arrow_up.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/appcompat-missing/src/main/res/layout/launch.xml b/paparazzi-gradle-plugin/src/test/projects/appcompat-missing/src/main/res/layout/launch.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/appcompat-missing/src/main/res/layout/launch.xml rename to paparazzi-gradle-plugin/src/test/projects/appcompat-missing/src/main/res/layout/launch.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/appcompat-missing/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt b/paparazzi-gradle-plugin/src/test/projects/appcompat-missing/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/appcompat-missing/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt rename to paparazzi-gradle-plugin/src/test/projects/appcompat-missing/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt diff --git a/paparazzi-gradle-plugin/src/test/projects/appcompat-missing/src/test/resources/arrow_missing.png b/paparazzi-gradle-plugin/src/test/projects/appcompat-missing/src/test/resources/arrow_missing.png new file mode 100644 index 0000000000..8da2be99c2 Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/appcompat-missing/src/test/resources/arrow_missing.png differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-fonts-code/build.gradle b/paparazzi-gradle-plugin/src/test/projects/appcompat-present/build.gradle similarity index 53% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-fonts-code/build.gradle rename to paparazzi-gradle-plugin/src/test/projects/appcompat-present/build.gradle index b588123cd6..9294b1d535 100644 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-fonts-code/build.gradle +++ b/paparazzi-gradle-plugin/src/test/projects/appcompat-present/build.gradle @@ -4,20 +4,19 @@ plugins { id 'app.cash.paparazzi' } -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - android { + namespace 'app.cash.paparazzi.plugin.test' compileSdk libs.versions.compileSdk.get() as int defaultConfig { minSdk libs.versions.minSdk.get() as int } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } } dependencies { diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/appcompat-present/src/main/res/drawable/arrow_up.xml b/paparazzi-gradle-plugin/src/test/projects/appcompat-present/src/main/res/drawable/arrow_up.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/appcompat-present/src/main/res/drawable/arrow_up.xml rename to paparazzi-gradle-plugin/src/test/projects/appcompat-present/src/main/res/drawable/arrow_up.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/appcompat-present/src/main/res/layout/launch.xml b/paparazzi-gradle-plugin/src/test/projects/appcompat-present/src/main/res/layout/launch.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/appcompat-present/src/main/res/layout/launch.xml rename to paparazzi-gradle-plugin/src/test/projects/appcompat-present/src/main/res/layout/launch.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/appcompat-present/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt b/paparazzi-gradle-plugin/src/test/projects/appcompat-present/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/appcompat-present/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt rename to paparazzi-gradle-plugin/src/test/projects/appcompat-present/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt diff --git a/paparazzi-gradle-plugin/src/test/projects/appcompat-present/src/test/resources/arrow_present.png b/paparazzi-gradle-plugin/src/test/projects/appcompat-present/src/test/resources/arrow_present.png new file mode 100644 index 0000000000..be7883614f Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/appcompat-present/src/test/resources/arrow_present.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/build-class-next-sdk/build.gradle b/paparazzi-gradle-plugin/src/test/projects/build-class-next-sdk/build.gradle new file mode 100644 index 0000000000..9f06cc144c --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/build-class-next-sdk/build.gradle @@ -0,0 +1,24 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdkPreview "UpsideDownCakePrivacySandbox" + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} + +dependencies { + testImplementation libs.truth +} diff --git a/paparazzi-gradle-plugin/src/test/projects/build-class-next-sdk/src/test/java/app/cash/paparazzi/plugin/test/BuildClassTest.kt b/paparazzi-gradle-plugin/src/test/projects/build-class-next-sdk/src/test/java/app/cash/paparazzi/plugin/test/BuildClassTest.kt new file mode 100644 index 0000000000..16b0b2a51c --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/build-class-next-sdk/src/test/java/app/cash/paparazzi/plugin/test/BuildClassTest.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.plugin.test + +import android.os.Build +import app.cash.paparazzi.Paparazzi +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test + +class BuildClassTest { + @get:Rule + val paparazzi = Paparazzi() + + @Test + fun verifyFields() { + assertThat(Build.ID).isEqualTo("URA8.230510.004") + assertThat(Build.DISPLAY).isEqualTo("sdk_phone_armv7-userdebug UpsideDownCakePrivacySandbox URA8.230510.004 10115423 test-keys") + assertThat(Build.PRODUCT).isEqualTo("unknown") + assertThat(Build.DEVICE).isEqualTo("generic") + assertThat(Build.BOARD).isEqualTo("unknown") + assertThat(Build.MANUFACTURER).isEqualTo("generic") + assertThat(Build.BRAND).isEqualTo("generic") + assertThat(Build.MODEL).isEqualTo("unknown") + assertThat(Build.SOC_MANUFACTURER).isEqualTo("unknown") + assertThat(Build.SOC_MODEL).isEqualTo("unknown") + assertThat(Build.BOOTLOADER).isEqualTo("unknown") + assertThat(Build.RADIO).isEqualTo("unknown") + assertThat(Build.HARDWARE).isEqualTo("unknown") + assertThat(Build.SKU).isEqualTo("unknown") + assertThat(Build.ODM_SKU).isEqualTo("unknown") + + assertThat(Build.VERSION.INCREMENTAL).isEqualTo("10115423") + assertThat(Build.VERSION.RELEASE).isNotNull() + assertThat(Build.VERSION.RELEASE_OR_CODENAME).isNotNull() + assertThat(Build.VERSION.BASE_OS).isEqualTo("") + assertThat(Build.VERSION.SECURITY_PATCH).isNotNull() + assertThat(Build.VERSION.MEDIA_PERFORMANCE_CLASS).isEqualTo(0) + assertThat(Build.VERSION.SDK).isNotNull() + assertThat(Build.VERSION.SDK_INT).isNotEqualTo(0) + assertThat(Build.VERSION.CODENAME).isEqualTo("UpsideDownCakePrivacySandbox") + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/build-class/build.gradle b/paparazzi-gradle-plugin/src/test/projects/build-class/build.gradle new file mode 100644 index 0000000000..43c47ff519 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/build-class/build.gradle @@ -0,0 +1,24 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} + +dependencies { + testImplementation libs.truth +} diff --git a/paparazzi-gradle-plugin/src/test/projects/build-class/src/test/java/app/cash/paparazzi/plugin/test/BuildClassTest.kt b/paparazzi-gradle-plugin/src/test/projects/build-class/src/test/java/app/cash/paparazzi/plugin/test/BuildClassTest.kt new file mode 100644 index 0000000000..83de1e1333 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/build-class/src/test/java/app/cash/paparazzi/plugin/test/BuildClassTest.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.plugin.test + +import app.cash.paparazzi.Paparazzi +import org.junit.Rule + +class BuildClassTest { + @get:Rule + val paparazzi = Paparazzi() + +// @Test +// fun verifyFields() { +// assertThat(Build.ID).isNotNull() +// assertThat(Build.DISPLAY).contains("test-keys") +// assertThat(Build.PRODUCT).isEqualTo("unknown") +// assertThat(Build.DEVICE).isEqualTo("generic") +// assertThat(Build.BOARD).isEqualTo("unknown") +// assertThat(Build.MANUFACTURER).isEqualTo("generic") +// assertThat(Build.BRAND).isEqualTo("generic") +// assertThat(Build.MODEL).isEqualTo("unknown") +// assertThat(Build.SOC_MANUFACTURER).isEqualTo("unknown") +// assertThat(Build.SOC_MODEL).isEqualTo("unknown") +// assertThat(Build.BOOTLOADER).isEqualTo("unknown") +// assertThat(Build.RADIO).isEqualTo("unknown") +// assertThat(Build.HARDWARE).isEqualTo("unknown") +// assertThat(Build.SKU).isEqualTo("unknown") +// assertThat(Build.ODM_SKU).isEqualTo("unknown") +// +// assertThat(Build.VERSION.INCREMENTAL).isNotEmpty() +// assertThat(Build.VERSION.RELEASE).isNotNull() +// assertThat(Build.VERSION.RELEASE_OR_CODENAME).isNotNull() +// assertThat(Build.VERSION.BASE_OS).isEqualTo("") +// assertThat(Build.VERSION.SECURITY_PATCH).isNotNull() +// assertThat(Build.VERSION.MEDIA_PERFORMANCE_CLASS).isEqualTo(0) +// assertThat(Build.VERSION.SDK).isNotNull() +// assertThat(Build.VERSION.SDK_INT).isNotEqualTo(0) +// assertThat(Build.VERSION.CODENAME).isNotNull() +// } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/cacheable/build.gradle b/paparazzi-gradle-plugin/src/test/projects/cacheable/build.gradle new file mode 100644 index 0000000000..411ab6a9d7 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/cacheable/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/cacheable/settings.gradle b/paparazzi-gradle-plugin/src/test/projects/cacheable/settings.gradle similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/cacheable/settings.gradle rename to paparazzi-gradle-plugin/src/test/projects/cacheable/settings.gradle diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/cacheable/src/test/java/app/cash/paparazzi/plugin/test/VerifyTest.kt b/paparazzi-gradle-plugin/src/test/projects/cacheable/src/test/java/app/cash/paparazzi/plugin/test/VerifyTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/cacheable/src/test/java/app/cash/paparazzi/plugin/test/VerifyTest.kt rename to paparazzi-gradle-plugin/src/test/projects/cacheable/src/test/java/app/cash/paparazzi/plugin/test/VerifyTest.kt diff --git a/paparazzi-gradle-plugin/src/test/projects/compose-a11y/build.gradle b/paparazzi-gradle-plugin/src/test/projects/compose-a11y/build.gradle new file mode 100644 index 0000000000..d160ca6155 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/compose-a11y/build.gradle @@ -0,0 +1,31 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } + buildFeatures { + compose true + } + composeOptions { + kotlinCompilerExtensionVersion libs.versions.composeCompiler.get() + } +} + +dependencies { + implementation libs.composeUi.material + implementation libs.androidx.appcompat +} diff --git a/paparazzi-gradle-plugin/src/test/projects/compose-a11y/src/main/java/app/cash/paparazzi/plugin/test/CompositeComposable.kt b/paparazzi-gradle-plugin/src/test/projects/compose-a11y/src/main/java/app/cash/paparazzi/plugin/test/CompositeComposable.kt new file mode 100644 index 0000000000..1b7dbff19e --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/compose-a11y/src/main/java/app/cash/paparazzi/plugin/test/CompositeComposable.kt @@ -0,0 +1,89 @@ +package app.cash.paparazzi.plugin.test + +import android.widget.TextView +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.selection.toggleable +import androidx.compose.material.Checkbox +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.viewinterop.AndroidView + +@Composable +fun CompositeComposable() { + Column { + Row( + Modifier + .toggleable( + value = true, + role = Role.Checkbox, + onValueChange = { } + ) + .fillMaxWidth() + ) { + Text( + "Option", + Modifier.weight(1f) + .semantics { contentDescription = "Custom content description for Text" } + ) + Checkbox(checked = true, onCheckedChange = null) + } + Box( + Modifier + .align(Alignment.CenterHorizontally) + .clickable(onClickLabel = "On Click Label") { } + ) + Row(modifier = Modifier.semantics(mergeDescendants = true) {}) { + Image( + imageVector = Icons.Filled.Add, + contentDescription = null // decorative + ) + Column(modifier = Modifier.semantics(mergeDescendants = true) {}) { + Text("Text") + Text( + text = "more text", + modifier = Modifier.semantics { contentDescription = "custom description" } + ) + Column(modifier = Modifier.semantics(mergeDescendants = true) {}) { + Text("Nested text") + Text(text = "more text", modifier = Modifier.semantics { contentDescription = "custom description" }) + } + } + } + + // Complex semantic layout + Row(modifier = Modifier.clickable(onClickLabel = "On Click Label") { }) { + Column(modifier = Modifier.semantics(mergeDescendants = true) {}) { + Text("Merged Text") + Text("More Text") + } + Column { + Text("Unmerged Text") + Text("More Text") + } + } + + Text("multi\nline\ntext") + + AndroidView( + modifier = Modifier.wrapContentSize(), + factory = { context -> + TextView(context).apply { + text = "Nested Android View" + } + } + ) + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/compose-a11y/src/main/java/app/cash/paparazzi/plugin/test/MixedView.kt b/paparazzi-gradle-plugin/src/test/projects/compose-a11y/src/main/java/app/cash/paparazzi/plugin/test/MixedView.kt new file mode 100644 index 0000000000..01428de12c --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/compose-a11y/src/main/java/app/cash/paparazzi/plugin/test/MixedView.kt @@ -0,0 +1,62 @@ +package app.cash.paparazzi.plugin.test + +import android.content.Context +import android.view.View.GONE +import android.widget.Button +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.compose.ui.platform.ComposeView + +class MixedView(context: Context) : LinearLayout(context) { + init { + orientation = LinearLayout.VERTICAL + + addView( + TextView(context).apply { + id = 1 + text = "Legacy Text View" + } + ) + + addView( + TextView(context).apply { + id = 1 + text = "Hidden Legacy Text View" + visibility = GONE + } + ) + + addView( + Button(context).apply { + id = 2 + text = "Legacy Button" + } + ) + + addView( + ImageView(context).apply { + id = 3 + contentDescription = "Legacy Image View" + } + ) + + addView( + ComposeView(context).apply { + id = 4 + setContent { + SimpleComposable() + } + } + ) + + addView( + ComposeView(context).apply { + id = 5 + setContent { + CompositeComposable() + } + } + ) + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/compose-a11y/src/main/java/app/cash/paparazzi/plugin/test/SimpleComposable.kt b/paparazzi-gradle-plugin/src/test/projects/compose-a11y/src/main/java/app/cash/paparazzi/plugin/test/SimpleComposable.kt new file mode 100644 index 0000000000..e27d74ff1b --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/compose-a11y/src/main/java/app/cash/paparazzi/plugin/test/SimpleComposable.kt @@ -0,0 +1,32 @@ +package app.cash.paparazzi.plugin.test + +import androidx.compose.foundation.layout.Column +import androidx.compose.material.Button +import androidx.compose.material.Checkbox +import androidx.compose.material.Icon +import androidx.compose.material.RadioButton +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.semantics + +@Composable +fun SimpleComposable() { + Column { + Button(onClick = {}) { + Text("On Click") + } + Button(onClick = {}, enabled = false) { + Text("Disabled Button") + } + Button(onClick = {}) { + Icon(Icons.Default.Add, contentDescription = "Add Item") + } + Checkbox(checked = true, onCheckedChange = {}) + RadioButton(selected = true, onClick = {}) + Text("Header", modifier = Modifier.semantics { heading() }) + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/compose-a11y/src/test/java/app/cash/paparazzi/plugin/test/ComposeA11yTest.kt b/paparazzi-gradle-plugin/src/test/projects/compose-a11y/src/test/java/app/cash/paparazzi/plugin/test/ComposeA11yTest.kt new file mode 100644 index 0000000000..fe38a3a0e9 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/compose-a11y/src/test/java/app/cash/paparazzi/plugin/test/ComposeA11yTest.kt @@ -0,0 +1,45 @@ +package app.cash.paparazzi.plugin.test + +import android.widget.LinearLayout +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.unit.dp +import app.cash.paparazzi.DeviceConfig +import app.cash.paparazzi.Paparazzi +import app.cash.paparazzi.accessibility.AccessibilityRenderExtension +import org.junit.Rule +import org.junit.Test + +class ComposeA11yTest { + @get:Rule + val paparazzi = Paparazzi( + theme = "Theme.AppCompat.Light.NoActionBar", + deviceConfig = DeviceConfig.PIXEL, + renderExtensions = setOf(AccessibilityRenderExtension()) + ) + + @Test + fun `mixed compose usage`() { + val mixedView = MixedView(paparazzi.context) + paparazzi.snapshot(mixedView) + } + + @Test + fun `verify changing view hierarchy order doesn't change accessibility colors`() { + val mixedView = MixedView(paparazzi.context).apply { + addView( + ComposeView(context).apply { + id = 10 + setContent { + Box(modifier = Modifier.size(50.dp)) {} + } + }, + 0, + LinearLayout.LayoutParams(0, 0) + ) + } + paparazzi.snapshot(mixedView) + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/compose-a11y/src/test/resources/compose_a11y.png b/paparazzi-gradle-plugin/src/test/projects/compose-a11y/src/test/resources/compose_a11y.png new file mode 100644 index 0000000000..d9f7c0079f Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/compose-a11y/src/test/resources/compose_a11y.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/compose-a11y/src/test/resources/compose_a11y_change_hierarchy_order.png b/paparazzi-gradle-plugin/src/test/projects/compose-a11y/src/test/resources/compose_a11y_change_hierarchy_order.png new file mode 100644 index 0000000000..d9f7c0079f Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/compose-a11y/src/test/resources/compose_a11y_change_hierarchy_order.png differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/compose-wear/build.gradle b/paparazzi-gradle-plugin/src/test/projects/compose-lifecycle-owner/build.gradle similarity index 62% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/compose-wear/build.gradle rename to paparazzi-gradle-plugin/src/test/projects/compose-lifecycle-owner/build.gradle index f6632c7cdb..c6213b8d4c 100644 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/compose-wear/build.gradle +++ b/paparazzi-gradle-plugin/src/test/projects/compose-lifecycle-owner/build.gradle @@ -4,20 +4,19 @@ plugins { id 'app.cash.paparazzi' } -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - android { + namespace 'app.cash.paparazzi.plugin.test' compileSdk libs.versions.compileSdk.get() as int defaultConfig { minSdk libs.versions.minSdk.get() as int } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } buildFeatures { compose true } diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/compose/src/main/java/app/cash/paparazzi/plugin/test/HelloPaparazzi.kt b/paparazzi-gradle-plugin/src/test/projects/compose-lifecycle-owner/src/main/java/app/cash/paparazzi/plugin/test/HelloPaparazzi.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/compose/src/main/java/app/cash/paparazzi/plugin/test/HelloPaparazzi.kt rename to paparazzi-gradle-plugin/src/test/projects/compose-lifecycle-owner/src/main/java/app/cash/paparazzi/plugin/test/HelloPaparazzi.kt diff --git a/paparazzi-gradle-plugin/src/test/projects/compose-lifecycle-owner/src/test/java/app/cash/paparazzi/plugin/test/ComposeLifecycleOwnerTest.kt b/paparazzi-gradle-plugin/src/test/projects/compose-lifecycle-owner/src/test/java/app/cash/paparazzi/plugin/test/ComposeLifecycleOwnerTest.kt new file mode 100644 index 0000000000..a651d88a6f --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/compose-lifecycle-owner/src/test/java/app/cash/paparazzi/plugin/test/ComposeLifecycleOwnerTest.kt @@ -0,0 +1,20 @@ +package app.cash.paparazzi.plugin.test + +import app.cash.paparazzi.Paparazzi +import app.cash.paparazzi.accessibility.AccessibilityRenderExtension +import org.junit.Rule +import org.junit.Test + +class ComposeLifecycleOwnerTest { + @get:Rule + val paparazzi = Paparazzi( + renderExtensions = setOf(AccessibilityRenderExtension()) + ) + + @Test + fun lifecycleOwnerAvailableWithRendererExtension() { + paparazzi.snapshot { + HelloPaparazzi() + } + } +} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/compose/build.gradle b/paparazzi-gradle-plugin/src/test/projects/compose-recomposition/build.gradle similarity index 62% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/compose/build.gradle rename to paparazzi-gradle-plugin/src/test/projects/compose-recomposition/build.gradle index f6632c7cdb..c6213b8d4c 100644 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/compose/build.gradle +++ b/paparazzi-gradle-plugin/src/test/projects/compose-recomposition/build.gradle @@ -4,20 +4,19 @@ plugins { id 'app.cash.paparazzi' } -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - android { + namespace 'app.cash.paparazzi.plugin.test' compileSdk libs.versions.compileSdk.get() as int defaultConfig { minSdk libs.versions.minSdk.get() as int } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } buildFeatures { compose true } diff --git a/paparazzi-gradle-plugin/src/test/projects/compose-recomposition/src/test/java/app/cash/paparazzi/plugin/test/RecomposeTest.kt b/paparazzi-gradle-plugin/src/test/projects/compose-recomposition/src/test/java/app/cash/paparazzi/plugin/test/RecomposeTest.kt new file mode 100644 index 0000000000..90f9ba94e7 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/compose-recomposition/src/test/java/app/cash/paparazzi/plugin/test/RecomposeTest.kt @@ -0,0 +1,42 @@ +package app.cash.paparazzi.plugin.test + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.Text +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import app.cash.paparazzi.Paparazzi +import org.junit.Rule +import org.junit.Test + +class RecomposeTest { + @get:Rule + val paparazzi = Paparazzi() + + @Test fun recomposesOnStateChange() { + paparazzi.snapshot { + var text by remember { mutableStateOf("Hello") } + LaunchedEffect(Unit) { + text = "Hello Paparazzi" + } + + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.White) + ) { + Text( + modifier = Modifier.align(Alignment.Center), + text = text + ) + } + } + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/compose-recomposition/src/test/snapshots/images/app.cash.paparazzi.plugin.test_RecomposeTest_recomposesOnStateChange.png b/paparazzi-gradle-plugin/src/test/projects/compose-recomposition/src/test/snapshots/images/app.cash.paparazzi.plugin.test_RecomposeTest_recomposesOnStateChange.png new file mode 100644 index 0000000000..43bc7ec19b Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/compose-recomposition/src/test/snapshots/images/app.cash.paparazzi.plugin.test_RecomposeTest_recomposesOnStateChange.png differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/compose-dispatcher/build.gradle b/paparazzi-gradle-plugin/src/test/projects/compose-wear/build.gradle similarity index 62% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/compose-dispatcher/build.gradle rename to paparazzi-gradle-plugin/src/test/projects/compose-wear/build.gradle index f6632c7cdb..c6213b8d4c 100644 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/compose-dispatcher/build.gradle +++ b/paparazzi-gradle-plugin/src/test/projects/compose-wear/build.gradle @@ -4,20 +4,19 @@ plugins { id 'app.cash.paparazzi' } -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - android { + namespace 'app.cash.paparazzi.plugin.test' compileSdk libs.versions.compileSdk.get() as int defaultConfig { minSdk libs.versions.minSdk.get() as int } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } buildFeatures { compose true } diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/compose-wear/src/main/java/app/cash/paparazzi/plugin/test/HelloPaparazzi.kt b/paparazzi-gradle-plugin/src/test/projects/compose-wear/src/main/java/app/cash/paparazzi/plugin/test/HelloPaparazzi.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/compose-wear/src/main/java/app/cash/paparazzi/plugin/test/HelloPaparazzi.kt rename to paparazzi-gradle-plugin/src/test/projects/compose-wear/src/main/java/app/cash/paparazzi/plugin/test/HelloPaparazzi.kt diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/compose-wear/src/test/java/app/cash/paparazzi/plugin/test/ComposeTest.kt b/paparazzi-gradle-plugin/src/test/projects/compose-wear/src/test/java/app/cash/paparazzi/plugin/test/ComposeTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/compose-wear/src/test/java/app/cash/paparazzi/plugin/test/ComposeTest.kt rename to paparazzi-gradle-plugin/src/test/projects/compose-wear/src/test/java/app/cash/paparazzi/plugin/test/ComposeTest.kt diff --git a/paparazzi-gradle-plugin/src/test/projects/compose-wear/src/test/resources/compose_wear.png b/paparazzi-gradle-plugin/src/test/projects/compose-wear/src/test/resources/compose_wear.png new file mode 100644 index 0000000000..284b52ca0b Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/compose-wear/src/test/resources/compose_wear.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/compose/build.gradle b/paparazzi-gradle-plugin/src/test/projects/compose/build.gradle new file mode 100644 index 0000000000..c6213b8d4c --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/compose/build.gradle @@ -0,0 +1,30 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } + buildFeatures { + compose true + } + composeOptions { + kotlinCompilerExtensionVersion libs.versions.composeCompiler.get() + } +} + +dependencies { + implementation libs.composeUi.material +} diff --git a/paparazzi-gradle-plugin/src/test/projects/compose/src/main/java/app/cash/paparazzi/plugin/test/HelloPaparazzi.kt b/paparazzi-gradle-plugin/src/test/projects/compose/src/main/java/app/cash/paparazzi/plugin/test/HelloPaparazzi.kt new file mode 100644 index 0000000000..44044b8a42 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/compose/src/main/java/app/cash/paparazzi/plugin/test/HelloPaparazzi.kt @@ -0,0 +1,48 @@ +package app.cash.paparazzi.plugin.test + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration + +@Composable +fun HelloPaparazzi() { + val text = "Hello, Paparazzi" + Column( + Modifier + .background(Color.White) + .fillMaxSize() + .wrapContentSize() + ) { + Text(text) + Text(text, style = TextStyle(fontFamily = FontFamily.Cursive)) + Text( + text = text, + style = TextStyle(textDecoration = TextDecoration.LineThrough) + ) + Text( + text = text, + style = TextStyle(textDecoration = TextDecoration.Underline) + ) + Text( + text = text, + style = TextStyle( + textDecoration = TextDecoration.combine( + listOf( + TextDecoration.Underline, + TextDecoration.LineThrough + ) + ), + fontWeight = FontWeight.Bold + ) + ) + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/compose/src/test/java/app/cash/paparazzi/plugin/test/ComposeReferenceLeakTest.kt b/paparazzi-gradle-plugin/src/test/projects/compose/src/test/java/app/cash/paparazzi/plugin/test/ComposeReferenceLeakTest.kt new file mode 100644 index 0000000000..e4f2a07b00 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/compose/src/test/java/app/cash/paparazzi/plugin/test/ComposeReferenceLeakTest.kt @@ -0,0 +1,40 @@ +package app.cash.paparazzi.plugin.test + +import androidx.compose.ui.platform.ComposeView +import app.cash.paparazzi.Paparazzi +import org.junit.AfterClass +import org.junit.Rule +import org.junit.Test +import java.lang.ref.WeakReference + +class ComposeReferenceLeakTest { + @get:Rule + val paparazzi = Paparazzi() + + @Test + fun cleansUpComposeReferences() { + composeView = ComposeView(paparazzi.context).apply { + setContent { + HelloPaparazzi() + } + + paparazzi.snapshot(this) + } + } + + companion object { + private var composeView: ComposeView? = null + + @AfterClass + @JvmStatic + fun teardown() { + assert(composeView != null) + val weakComposeView = WeakReference(composeView) + + composeView = null + System.gc() + + assert(weakComposeView.get() == null) + } + } +} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/compose/src/test/java/app/cash/paparazzi/plugin/test/ComposeTest.kt b/paparazzi-gradle-plugin/src/test/projects/compose/src/test/java/app/cash/paparazzi/plugin/test/ComposeTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/compose/src/test/java/app/cash/paparazzi/plugin/test/ComposeTest.kt rename to paparazzi-gradle-plugin/src/test/projects/compose/src/test/java/app/cash/paparazzi/plugin/test/ComposeTest.kt diff --git a/paparazzi-gradle-plugin/src/test/projects/compose/src/test/resources/compose_fonts.png b/paparazzi-gradle-plugin/src/test/projects/compose/src/test/resources/compose_fonts.png new file mode 100644 index 0000000000..0cfee2c1d7 Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/compose/src/test/resources/compose_fonts.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/configuration-cache/build.gradle b/paparazzi-gradle-plugin/src/test/projects/configuration-cache/build.gradle new file mode 100644 index 0000000000..411ab6a9d7 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/configuration-cache/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/configuration-cache/src/test/java/app/cash/paparazzi/plugin/test/VerifyTest.kt b/paparazzi-gradle-plugin/src/test/projects/configuration-cache/src/test/java/app/cash/paparazzi/plugin/test/VerifyTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/configuration-cache/src/test/java/app/cash/paparazzi/plugin/test/VerifyTest.kt rename to paparazzi-gradle-plugin/src/test/projects/configuration-cache/src/test/java/app/cash/paparazzi/plugin/test/VerifyTest.kt diff --git a/paparazzi-gradle-plugin/src/test/projects/coroutine-delay-main/build.gradle b/paparazzi-gradle-plugin/src/test/projects/coroutine-delay-main/build.gradle new file mode 100644 index 0000000000..c6213b8d4c --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/coroutine-delay-main/build.gradle @@ -0,0 +1,30 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } + buildFeatures { + compose true + } + composeOptions { + kotlinCompilerExtensionVersion libs.versions.composeCompiler.get() + } +} + +dependencies { + implementation libs.composeUi.material +} diff --git a/paparazzi-gradle-plugin/src/test/projects/coroutine-delay-main/src/test/java/app/cash/paparazzi/plugin/test/CoroutineDelayMainTest.kt b/paparazzi-gradle-plugin/src/test/projects/coroutine-delay-main/src/test/java/app/cash/paparazzi/plugin/test/CoroutineDelayMainTest.kt new file mode 100644 index 0000000000..c3a3433ac9 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/coroutine-delay-main/src/test/java/app/cash/paparazzi/plugin/test/CoroutineDelayMainTest.kt @@ -0,0 +1,35 @@ +package app.cash.paparazzi.plugin.test + +import android.os.SystemClock +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.platform.ComposeView +import app.cash.paparazzi.Paparazzi +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import kotlinx.coroutines.delay + +class CoroutineDelayMainTest { + @get:Rule + val paparazzi = Paparazzi() + + @Test fun delayUsesMainDispatcher() { + var start = 0L + var end = 0L + paparazzi.gif( + ComposeView(paparazzi.context).apply { + setContent { + start = SystemClock.uptimeMillis() + LaunchedEffect(Unit) { + delay(250) + end = SystemClock.uptimeMillis() + } + } + }, + end = 1000, + fps = 4 + ) + + assertEquals(250L, end - start) + } +} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-build-dir/build.gradle b/paparazzi-gradle-plugin/src/test/projects/custom-build-dir/build.gradle similarity index 50% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-build-dir/build.gradle rename to paparazzi-gradle-plugin/src/test/projects/custom-build-dir/build.gradle index 7ac61782f8..71ebf39d14 100644 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-build-dir/build.gradle +++ b/paparazzi-gradle-plugin/src/test/projects/custom-build-dir/build.gradle @@ -6,18 +6,17 @@ plugins { project.buildDir = 'custom' -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - android { + namespace 'app.cash.paparazzi.plugin.test' compileSdk libs.versions.compileSdk.get() as int defaultConfig { minSdk libs.versions.minSdk.get() as int } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } } diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-build-dir/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt b/paparazzi-gradle-plugin/src/test/projects/custom-build-dir/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-build-dir/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt rename to paparazzi-gradle-plugin/src/test/projects/custom-build-dir/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/appcompat-present/build.gradle b/paparazzi-gradle-plugin/src/test/projects/custom-fonts-code/build.gradle similarity index 53% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/appcompat-present/build.gradle rename to paparazzi-gradle-plugin/src/test/projects/custom-fonts-code/build.gradle index b588123cd6..9294b1d535 100644 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/appcompat-present/build.gradle +++ b/paparazzi-gradle-plugin/src/test/projects/custom-fonts-code/build.gradle @@ -4,20 +4,19 @@ plugins { id 'app.cash.paparazzi' } -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - android { + namespace 'app.cash.paparazzi.plugin.test' compileSdk libs.versions.compileSdk.get() as int defaultConfig { minSdk libs.versions.minSdk.get() as int } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } } dependencies { diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-fonts-code/src/main/res/font/cashmarket_medium.xml b/paparazzi-gradle-plugin/src/test/projects/custom-fonts-code/src/main/res/font/cashmarket_medium.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-fonts-code/src/main/res/font/cashmarket_medium.xml rename to paparazzi-gradle-plugin/src/test/projects/custom-fonts-code/src/main/res/font/cashmarket_medium.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-fonts-code/src/main/res/font/cashmarket_medium_italic.ttf b/paparazzi-gradle-plugin/src/test/projects/custom-fonts-code/src/main/res/font/cashmarket_medium_italic.ttf similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-fonts-code/src/main/res/font/cashmarket_medium_italic.ttf rename to paparazzi-gradle-plugin/src/test/projects/custom-fonts-code/src/main/res/font/cashmarket_medium_italic.ttf diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-fonts-code/src/main/res/font/cashmarket_medium_normal.otf b/paparazzi-gradle-plugin/src/test/projects/custom-fonts-code/src/main/res/font/cashmarket_medium_normal.otf similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-fonts-code/src/main/res/font/cashmarket_medium_normal.otf rename to paparazzi-gradle-plugin/src/test/projects/custom-fonts-code/src/main/res/font/cashmarket_medium_normal.otf diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-fonts-code/src/test/java/app/cash/paparazzi/plugin/test/CustomFontsTest.kt b/paparazzi-gradle-plugin/src/test/projects/custom-fonts-code/src/test/java/app/cash/paparazzi/plugin/test/CustomFontsTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-fonts-code/src/test/java/app/cash/paparazzi/plugin/test/CustomFontsTest.kt rename to paparazzi-gradle-plugin/src/test/projects/custom-fonts-code/src/test/java/app/cash/paparazzi/plugin/test/CustomFontsTest.kt diff --git a/paparazzi-gradle-plugin/src/test/projects/custom-fonts-code/src/test/resources/textviews.png b/paparazzi-gradle-plugin/src/test/projects/custom-fonts-code/src/test/resources/textviews.png new file mode 100644 index 0000000000..4578795632 Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/custom-fonts-code/src/test/resources/textviews.png differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-fonts-xml/build.gradle b/paparazzi-gradle-plugin/src/test/projects/custom-fonts-xml/build.gradle similarity index 53% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-fonts-xml/build.gradle rename to paparazzi-gradle-plugin/src/test/projects/custom-fonts-xml/build.gradle index b588123cd6..9294b1d535 100644 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-fonts-xml/build.gradle +++ b/paparazzi-gradle-plugin/src/test/projects/custom-fonts-xml/build.gradle @@ -4,20 +4,19 @@ plugins { id 'app.cash.paparazzi' } -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - android { + namespace 'app.cash.paparazzi.plugin.test' compileSdk libs.versions.compileSdk.get() as int defaultConfig { minSdk libs.versions.minSdk.get() as int } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } } dependencies { diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-fonts-xml/src/main/res/font/cashmarket_medium.xml b/paparazzi-gradle-plugin/src/test/projects/custom-fonts-xml/src/main/res/font/cashmarket_medium.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-fonts-xml/src/main/res/font/cashmarket_medium.xml rename to paparazzi-gradle-plugin/src/test/projects/custom-fonts-xml/src/main/res/font/cashmarket_medium.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-fonts-xml/src/main/res/font/cashmarket_medium_italic.ttf b/paparazzi-gradle-plugin/src/test/projects/custom-fonts-xml/src/main/res/font/cashmarket_medium_italic.ttf similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-fonts-xml/src/main/res/font/cashmarket_medium_italic.ttf rename to paparazzi-gradle-plugin/src/test/projects/custom-fonts-xml/src/main/res/font/cashmarket_medium_italic.ttf diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-fonts-xml/src/main/res/font/cashmarket_medium_normal.otf b/paparazzi-gradle-plugin/src/test/projects/custom-fonts-xml/src/main/res/font/cashmarket_medium_normal.otf similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-fonts-xml/src/main/res/font/cashmarket_medium_normal.otf rename to paparazzi-gradle-plugin/src/test/projects/custom-fonts-xml/src/main/res/font/cashmarket_medium_normal.otf diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-fonts-xml/src/main/res/layout/textviews.xml b/paparazzi-gradle-plugin/src/test/projects/custom-fonts-xml/src/main/res/layout/textviews.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-fonts-xml/src/main/res/layout/textviews.xml rename to paparazzi-gradle-plugin/src/test/projects/custom-fonts-xml/src/main/res/layout/textviews.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-fonts-xml/src/test/java/app/cash/paparazzi/plugin/test/CustomFontsTest.kt b/paparazzi-gradle-plugin/src/test/projects/custom-fonts-xml/src/test/java/app/cash/paparazzi/plugin/test/CustomFontsTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-fonts-xml/src/test/java/app/cash/paparazzi/plugin/test/CustomFontsTest.kt rename to paparazzi-gradle-plugin/src/test/projects/custom-fonts-xml/src/test/java/app/cash/paparazzi/plugin/test/CustomFontsTest.kt diff --git a/paparazzi-gradle-plugin/src/test/projects/custom-fonts-xml/src/test/resources/textviews.png b/paparazzi-gradle-plugin/src/test/projects/custom-fonts-xml/src/test/resources/textviews.png new file mode 100644 index 0000000000..4578795632 Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/custom-fonts-xml/src/test/resources/textviews.png differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/declare-android-plugin-after/build.gradle b/paparazzi-gradle-plugin/src/test/projects/declare-android-plugin-after/build.gradle similarity index 58% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/declare-android-plugin-after/build.gradle rename to paparazzi-gradle-plugin/src/test/projects/declare-android-plugin-after/build.gradle index cb02b35ed6..6b37337157 100644 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/declare-android-plugin-after/build.gradle +++ b/paparazzi-gradle-plugin/src/test/projects/declare-android-plugin-after/build.gradle @@ -3,18 +3,14 @@ plugins { id 'com.android.library' // intentionally declared after paparazzi plugin } -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - android { + namespace 'app.cash.paparazzi.plugin.test' compileSdk libs.versions.compileSdk.get() as int defaultConfig { minSdk libs.versions.minSdk.get() as int } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } } diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/different-target-sdk/build.gradle b/paparazzi-gradle-plugin/src/test/projects/different-target-sdk/build.gradle similarity index 62% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/different-target-sdk/build.gradle rename to paparazzi-gradle-plugin/src/test/projects/different-target-sdk/build.gradle index 085063ef1d..c3dc1b96e3 100644 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/different-target-sdk/build.gradle +++ b/paparazzi-gradle-plugin/src/test/projects/different-target-sdk/build.gradle @@ -3,16 +3,8 @@ plugins { id 'app.cash.paparazzi' } -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - android { + namespace 'app.cash.paparazzi.plugin.test' compileSdk libs.versions.compileSdk.get() as int defaultConfig { minSdk libs.versions.minSdk.get() as int diff --git a/paparazzi-gradle-plugin/src/test/projects/disabled-unit-test-variant/build.gradle b/paparazzi-gradle-plugin/src/test/projects/disabled-unit-test-variant/build.gradle new file mode 100644 index 0000000000..4fc7786a50 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/disabled-unit-test-variant/build.gradle @@ -0,0 +1,23 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } +} + +androidComponents { + beforeVariants(selector().all()) { + enableUnitTest = buildType == "debug" + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/edit-mode-intercept/build.gradle b/paparazzi-gradle-plugin/src/test/projects/edit-mode-intercept/build.gradle new file mode 100644 index 0000000000..411ab6a9d7 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/edit-mode-intercept/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/edit-mode-intercept/src/test/java/app/cash/paparazzi/plugin/test/EditModeTest.kt b/paparazzi-gradle-plugin/src/test/projects/edit-mode-intercept/src/test/java/app/cash/paparazzi/plugin/test/EditModeTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/edit-mode-intercept/src/test/java/app/cash/paparazzi/plugin/test/EditModeTest.kt rename to paparazzi-gradle-plugin/src/test/projects/edit-mode-intercept/src/test/java/app/cash/paparazzi/plugin/test/EditModeTest.kt diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/appcompat-missing/build.gradle b/paparazzi-gradle-plugin/src/test/projects/exclude-androidtest/build.gradle similarity index 52% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/appcompat-missing/build.gradle rename to paparazzi-gradle-plugin/src/test/projects/exclude-androidtest/build.gradle index 8a55ad8185..b85b5a58e9 100644 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/appcompat-missing/build.gradle +++ b/paparazzi-gradle-plugin/src/test/projects/exclude-androidtest/build.gradle @@ -1,21 +1,16 @@ plugins { id 'com.android.library' - id 'kotlin-android' id 'app.cash.paparazzi' } -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - android { + namespace 'app.cash.paparazzi.plugin.test' compileSdk libs.versions.compileSdk.get() as int defaultConfig { minSdk libs.versions.minSdk.get() as int } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } } diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/exclude-androidtest/src/androidTest/AndroidManifest.xml b/paparazzi-gradle-plugin/src/test/projects/exclude-androidtest/src/androidTest/AndroidManifest.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/exclude-androidtest/src/androidTest/AndroidManifest.xml rename to paparazzi-gradle-plugin/src/test/projects/exclude-androidtest/src/androidTest/AndroidManifest.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/flag-debug-linked-objects-on/build.gradle b/paparazzi-gradle-plugin/src/test/projects/flag-debug-linked-objects-off/build.gradle similarity index 55% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/flag-debug-linked-objects-on/build.gradle rename to paparazzi-gradle-plugin/src/test/projects/flag-debug-linked-objects-off/build.gradle index 1fee486356..3085ddd88c 100644 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/flag-debug-linked-objects-on/build.gradle +++ b/paparazzi-gradle-plugin/src/test/projects/flag-debug-linked-objects-off/build.gradle @@ -4,20 +4,19 @@ plugins { id 'app.cash.paparazzi' } -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - android { + namespace 'app.cash.paparazzi.plugin.test' compileSdk libs.versions.compileSdk.get() as int defaultConfig { minSdk libs.versions.minSdk.get() as int } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } } tasks.withType(Test).configureEach { diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/flag-debug-linked-objects-off/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt b/paparazzi-gradle-plugin/src/test/projects/flag-debug-linked-objects-off/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/flag-debug-linked-objects-off/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt rename to paparazzi-gradle-plugin/src/test/projects/flag-debug-linked-objects-off/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/flag-debug-linked-objects-off/build.gradle b/paparazzi-gradle-plugin/src/test/projects/flag-debug-linked-objects-on/build.gradle similarity index 55% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/flag-debug-linked-objects-off/build.gradle rename to paparazzi-gradle-plugin/src/test/projects/flag-debug-linked-objects-on/build.gradle index 1fee486356..3085ddd88c 100644 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/flag-debug-linked-objects-off/build.gradle +++ b/paparazzi-gradle-plugin/src/test/projects/flag-debug-linked-objects-on/build.gradle @@ -4,20 +4,19 @@ plugins { id 'app.cash.paparazzi' } -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - android { + namespace 'app.cash.paparazzi.plugin.test' compileSdk libs.versions.compileSdk.get() as int defaultConfig { minSdk libs.versions.minSdk.get() as int } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } } tasks.withType(Test).configureEach { diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/flag-debug-linked-objects-on/gradle.properties b/paparazzi-gradle-plugin/src/test/projects/flag-debug-linked-objects-on/gradle.properties similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/flag-debug-linked-objects-on/gradle.properties rename to paparazzi-gradle-plugin/src/test/projects/flag-debug-linked-objects-on/gradle.properties diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/flag-debug-linked-objects-on/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt b/paparazzi-gradle-plugin/src/test/projects/flag-debug-linked-objects-on/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/flag-debug-linked-objects-on/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt rename to paparazzi-gradle-plugin/src/test/projects/flag-debug-linked-objects-on/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt diff --git a/paparazzi-gradle-plugin/src/test/projects/flag-legacy-resource-loading-off/build.gradle b/paparazzi-gradle-plugin/src/test/projects/flag-legacy-resource-loading-off/build.gradle new file mode 100644 index 0000000000..3085ddd88c --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/flag-legacy-resource-loading-off/build.gradle @@ -0,0 +1,26 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} + +tasks.withType(Test).configureEach { + testLogging { + showStandardStreams true + } +} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/src/main/res/drawable/camera.png b/paparazzi-gradle-plugin/src/test/projects/flag-legacy-resource-loading-off/src/main/res/drawable/camera.png similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/src/main/res/drawable/camera.png rename to paparazzi-gradle-plugin/src/test/projects/flag-legacy-resource-loading-off/src/main/res/drawable/camera.png diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-snapshot/src/main/res/layout/launch.xml b/paparazzi-gradle-plugin/src/test/projects/flag-legacy-resource-loading-off/src/main/res/layout/launch.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-snapshot/src/main/res/layout/launch.xml rename to paparazzi-gradle-plugin/src/test/projects/flag-legacy-resource-loading-off/src/main/res/layout/launch.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-snapshot/src/main/res/values/colors.xml b/paparazzi-gradle-plugin/src/test/projects/flag-legacy-resource-loading-off/src/main/res/values/colors.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-snapshot/src/main/res/values/colors.xml rename to paparazzi-gradle-plugin/src/test/projects/flag-legacy-resource-loading-off/src/main/res/values/colors.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/non-transitive-resources-no-deps/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt b/paparazzi-gradle-plugin/src/test/projects/flag-legacy-resource-loading-off/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/non-transitive-resources-no-deps/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt rename to paparazzi-gradle-plugin/src/test/projects/flag-legacy-resource-loading-off/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt diff --git a/paparazzi-gradle-plugin/src/test/projects/flag-legacy-resource-loading-off/src/test/resources/launch.png b/paparazzi-gradle-plugin/src/test/projects/flag-legacy-resource-loading-off/src/test/resources/launch.png new file mode 100644 index 0000000000..8efeabbbab Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/flag-legacy-resource-loading-off/src/test/resources/launch.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/flag-legacy-resource-loading-on/build.gradle b/paparazzi-gradle-plugin/src/test/projects/flag-legacy-resource-loading-on/build.gradle new file mode 100644 index 0000000000..411ab6a9d7 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/flag-legacy-resource-loading-on/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/flag-legacy-resource-loading-on/gradle.properties b/paparazzi-gradle-plugin/src/test/projects/flag-legacy-resource-loading-on/gradle.properties new file mode 100644 index 0000000000..3de9cb5c45 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/flag-legacy-resource-loading-on/gradle.properties @@ -0,0 +1,2 @@ +android.useAndroidX=true +app.cash.paparazzi.legacy.resource.loading=true diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-snapshot/src/main/res/drawable/camera.png b/paparazzi-gradle-plugin/src/test/projects/flag-legacy-resource-loading-on/src/main/res/drawable/camera.png similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-snapshot/src/main/res/drawable/camera.png rename to paparazzi-gradle-plugin/src/test/projects/flag-legacy-resource-loading-on/src/main/res/drawable/camera.png diff --git a/paparazzi-gradle-plugin/src/test/projects/flag-legacy-resource-loading-on/src/main/res/layout/launch.xml b/paparazzi-gradle-plugin/src/test/projects/flag-legacy-resource-loading-on/src/main/res/layout/launch.xml new file mode 100644 index 0000000000..058b420e75 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/flag-legacy-resource-loading-on/src/main/res/layout/launch.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:gravity="center" + android:background="@color/launchBackground" + android:layout_width="match_parent" + android:layout_height="match_parent" + > + + <ImageView + android:layout_width="200dp" + android:layout_height="200dp" + android:src="@drawable/camera" + /> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center" + android:textColor="@color/cameraBody" + android:text="paparazzi" + android:textSize="70dp" + android:textFontWeight="100" + /> + +</LinearLayout> diff --git a/paparazzi-gradle-plugin/src/test/projects/flag-legacy-resource-loading-on/src/main/res/values/colors.xml b/paparazzi-gradle-plugin/src/test/projects/flag-legacy-resource-loading-on/src/main/res/values/colors.xml new file mode 100644 index 0000000000..cc0f1ba2db --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/flag-legacy-resource-loading-on/src/main/res/values/colors.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <color name="launchBackground">#dcdccc</color> + <color name="cameraBody">#332F21</color> +</resources> diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-snapshot/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt b/paparazzi-gradle-plugin/src/test/projects/flag-legacy-resource-loading-on/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-snapshot/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt rename to paparazzi-gradle-plugin/src/test/projects/flag-legacy-resource-loading-on/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt diff --git a/paparazzi-gradle-plugin/src/test/projects/flag-legacy-resource-loading-on/src/test/resources/launch.png b/paparazzi-gradle-plugin/src/test/projects/flag-legacy-resource-loading-on/src/test/resources/launch.png new file mode 100644 index 0000000000..8efeabbbab Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/flag-legacy-resource-loading-on/src/test/resources/launch.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/imm-soft-input/build.gradle b/paparazzi-gradle-plugin/src/test/projects/imm-soft-input/build.gradle new file mode 100644 index 0000000000..411ab6a9d7 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/imm-soft-input/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/imm-soft-input/src/test/java/app/cash/paparazzi/plugin/test/IMMTest.kt b/paparazzi-gradle-plugin/src/test/projects/imm-soft-input/src/test/java/app/cash/paparazzi/plugin/test/IMMTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/imm-soft-input/src/test/java/app/cash/paparazzi/plugin/test/IMMTest.kt rename to paparazzi-gradle-plugin/src/test/projects/imm-soft-input/src/test/java/app/cash/paparazzi/plugin/test/IMMTest.kt diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/invalid-application-plugin/build.gradle b/paparazzi-gradle-plugin/src/test/projects/invalid-application-plugin/build.gradle similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/invalid-application-plugin/build.gradle rename to paparazzi-gradle-plugin/src/test/projects/invalid-application-plugin/build.gradle diff --git a/paparazzi-gradle-plugin/src/test/projects/invalid-application-plugin/gradle.properties b/paparazzi-gradle-plugin/src/test/projects/invalid-application-plugin/gradle.properties new file mode 100644 index 0000000000..3de9cb5c45 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/invalid-application-plugin/gradle.properties @@ -0,0 +1,2 @@ +android.useAndroidX=true +app.cash.paparazzi.legacy.resource.loading=true diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/locale-qualifier/build.gradle b/paparazzi-gradle-plugin/src/test/projects/layout-direction/build.gradle similarity index 57% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/locale-qualifier/build.gradle rename to paparazzi-gradle-plugin/src/test/projects/layout-direction/build.gradle index 17cb2729bb..ca3ce39fcd 100644 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/locale-qualifier/build.gradle +++ b/paparazzi-gradle-plugin/src/test/projects/layout-direction/build.gradle @@ -4,20 +4,19 @@ plugins { id 'app.cash.paparazzi' } -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - android { + namespace 'app.cash.paparazzi.plugin.test' compileSdk libs.versions.compileSdk.get() as int defaultConfig { minSdk libs.versions.minSdk.get() as int } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } } dependencies { diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/layout-direction/src/main/res/layout/title_color.xml b/paparazzi-gradle-plugin/src/test/projects/layout-direction/src/main/res/layout/title_color.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/layout-direction/src/main/res/layout/title_color.xml rename to paparazzi-gradle-plugin/src/test/projects/layout-direction/src/main/res/layout/title_color.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/layout-direction/src/main/res/values-ar/strings.xml b/paparazzi-gradle-plugin/src/test/projects/layout-direction/src/main/res/values-ar/strings.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/layout-direction/src/main/res/values-ar/strings.xml rename to paparazzi-gradle-plugin/src/test/projects/layout-direction/src/main/res/values-ar/strings.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/layout-direction/src/main/res/values/strings.xml b/paparazzi-gradle-plugin/src/test/projects/layout-direction/src/main/res/values/strings.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/layout-direction/src/main/res/values/strings.xml rename to paparazzi-gradle-plugin/src/test/projects/layout-direction/src/main/res/values/strings.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/layout-direction/src/test/java/app/cash/paparazzi/plugin/test/LayoutDirectionTest.kt b/paparazzi-gradle-plugin/src/test/projects/layout-direction/src/test/java/app/cash/paparazzi/plugin/test/LayoutDirectionTest.kt similarity index 99% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/layout-direction/src/test/java/app/cash/paparazzi/plugin/test/LayoutDirectionTest.kt rename to paparazzi-gradle-plugin/src/test/projects/layout-direction/src/test/java/app/cash/paparazzi/plugin/test/LayoutDirectionTest.kt index be98a2d70c..5c68635c44 100644 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/layout-direction/src/test/java/app/cash/paparazzi/plugin/test/LayoutDirectionTest.kt +++ b/paparazzi-gradle-plugin/src/test/projects/layout-direction/src/test/java/app/cash/paparazzi/plugin/test/LayoutDirectionTest.kt @@ -24,7 +24,7 @@ class LayoutDirectionTest( AR( tag = "ar", direction = LayoutDirection.LTR - ); + ) } @get:Rule diff --git a/paparazzi-gradle-plugin/src/test/projects/layout-direction/src/test/resources/locale_ar.png b/paparazzi-gradle-plugin/src/test/projects/layout-direction/src/test/resources/locale_ar.png new file mode 100644 index 0000000000..557c7320ea Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/layout-direction/src/test/resources/locale_ar.png differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/appcompat-missing/src/test/resources/arrow_missing.png b/paparazzi-gradle-plugin/src/test/projects/layout-direction/src/test/resources/locale_default_rtl.png similarity index 54% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/appcompat-missing/src/test/resources/arrow_missing.png rename to paparazzi-gradle-plugin/src/test/projects/layout-direction/src/test/resources/locale_default_rtl.png index 203b0ed73a..ce09b0f3fc 100644 Binary files a/paparazzi/paparazzi-gradle-plugin/src/test/projects/appcompat-missing/src/test/resources/arrow_missing.png and b/paparazzi-gradle-plugin/src/test/projects/layout-direction/src/test/resources/locale_default_rtl.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/lifecycle-usages/build.gradle b/paparazzi-gradle-plugin/src/test/projects/lifecycle-usages/build.gradle new file mode 100644 index 0000000000..b4105ca5e5 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/lifecycle-usages/build.gradle @@ -0,0 +1,25 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} + +dependencies { + testImplementation 'androidx.activity:activity-compose:1.6.1' + testImplementation libs.truth +} diff --git a/paparazzi-gradle-plugin/src/test/projects/lifecycle-usages/src/test/java/app/cash/paparazzi/plugin/test/LifecycleUsageTest.kt b/paparazzi-gradle-plugin/src/test/projects/lifecycle-usages/src/test/java/app/cash/paparazzi/plugin/test/LifecycleUsageTest.kt new file mode 100644 index 0000000000..711fadf98f --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/lifecycle-usages/src/test/java/app/cash/paparazzi/plugin/test/LifecycleUsageTest.kt @@ -0,0 +1,79 @@ +package app.cash.paparazzi.plugin.test + +import android.graphics.Color +import android.view.View +import androidx.activity.OnBackPressedCallback +import androidx.activity.OnBackPressedDispatcher +import androidx.activity.findViewTreeOnBackPressedDispatcherOwner +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.ViewTreeLifecycleOwner +import androidx.savedstate.SavedStateRegistry +import androidx.savedstate.findViewTreeSavedStateRegistryOwner +import app.cash.paparazzi.Paparazzi +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test + +class LifecycleUsageTest { + @get:Rule + val paparazzi = Paparazzi() + + @Test fun LifecycleOwner() { + val view = View(paparazzi.context).apply { + setBackgroundColor(Color.BLUE) + } + var currentLifecycleState: Lifecycle.State? = null + + view.doOnAttach { + val lifecycleOwner = ViewTreeLifecycleOwner.get(view)!! + currentLifecycleState = lifecycleOwner.lifecycle.currentState + } + + paparazzi.snapshot(view) + assertThat(currentLifecycleState).isNotNull() + assertThat(currentLifecycleState).isEqualTo(Lifecycle.State.RESUMED) + } + + @Test fun SavedStateRegistryOwner() { + val view = View(paparazzi.context).apply { + setBackgroundColor(Color.RED) + } + var savedStateRegistry: SavedStateRegistry? = null + + view.doOnAttach { + val registryOwner = view.findViewTreeSavedStateRegistryOwner()!! + savedStateRegistry = registryOwner.savedStateRegistry + } + + paparazzi.snapshot(view) + assertThat(savedStateRegistry).isNotNull() + } + + @Test fun OnBackPressedDispatcherOwner() { + val view = View(paparazzi.context).apply { + setBackgroundColor(Color.YELLOW) + } + var dispatcher: OnBackPressedDispatcher? = null + + view.doOnAttach { + val dispatcherOwner = view.findViewTreeOnBackPressedDispatcherOwner()!! + dispatcher = dispatcherOwner.onBackPressedDispatcher + + dispatcher!!.addCallback( + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() = Unit + } + ) + } + + paparazzi.snapshot(view) + assertThat(dispatcher).isNotNull() + } + + private fun View.doOnAttach(action: (view: View) -> Unit) { + addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(view: View) = action(view) + override fun onViewDetachedFromWindow(view: View) = Unit + }) + } +} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/layout-direction/build.gradle b/paparazzi-gradle-plugin/src/test/projects/locale-qualifier/build.gradle similarity index 57% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/layout-direction/build.gradle rename to paparazzi-gradle-plugin/src/test/projects/locale-qualifier/build.gradle index 17cb2729bb..ca3ce39fcd 100644 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/layout-direction/build.gradle +++ b/paparazzi-gradle-plugin/src/test/projects/locale-qualifier/build.gradle @@ -4,20 +4,19 @@ plugins { id 'app.cash.paparazzi' } -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - android { + namespace 'app.cash.paparazzi.plugin.test' compileSdk libs.versions.compileSdk.get() as int defaultConfig { minSdk libs.versions.minSdk.get() as int } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } } dependencies { diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/locale-qualifier/src/main/res/layout/title_color.xml b/paparazzi-gradle-plugin/src/test/projects/locale-qualifier/src/main/res/layout/title_color.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/locale-qualifier/src/main/res/layout/title_color.xml rename to paparazzi-gradle-plugin/src/test/projects/locale-qualifier/src/main/res/layout/title_color.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/locale-qualifier/src/main/res/values-en-rGB/strings.xml b/paparazzi-gradle-plugin/src/test/projects/locale-qualifier/src/main/res/values-en-rGB/strings.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/locale-qualifier/src/main/res/values-en-rGB/strings.xml rename to paparazzi-gradle-plugin/src/test/projects/locale-qualifier/src/main/res/values-en-rGB/strings.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/locale-qualifier/src/main/res/values/strings.xml b/paparazzi-gradle-plugin/src/test/projects/locale-qualifier/src/main/res/values/strings.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/locale-qualifier/src/main/res/values/strings.xml rename to paparazzi-gradle-plugin/src/test/projects/locale-qualifier/src/main/res/values/strings.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/locale-qualifier/src/test/java/app/cash/paparazzi/plugin/test/LocaleQualifierTest.kt b/paparazzi-gradle-plugin/src/test/projects/locale-qualifier/src/test/java/app/cash/paparazzi/plugin/test/LocaleQualifierTest.kt similarity index 95% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/locale-qualifier/src/test/java/app/cash/paparazzi/plugin/test/LocaleQualifierTest.kt rename to paparazzi-gradle-plugin/src/test/projects/locale-qualifier/src/test/java/app/cash/paparazzi/plugin/test/LocaleQualifierTest.kt index a7d23e2d54..e43bc4b968 100644 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/locale-qualifier/src/test/java/app/cash/paparazzi/plugin/test/LocaleQualifierTest.kt +++ b/paparazzi-gradle-plugin/src/test/projects/locale-qualifier/src/test/java/app/cash/paparazzi/plugin/test/LocaleQualifierTest.kt @@ -13,7 +13,7 @@ class LocaleQualifierTest( @TestParameter locale: Locale ) { enum class Locale(val tag: String?) { - Default(null), GB("en-rGB"); + Default(null), GB("en-rGB") } @get:Rule diff --git a/paparazzi-gradle-plugin/src/test/projects/locale-qualifier/src/test/resources/locale_default.png b/paparazzi-gradle-plugin/src/test/projects/locale-qualifier/src/test/resources/locale_default.png new file mode 100644 index 0000000000..c2d45b57a4 Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/locale-qualifier/src/test/resources/locale_default.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/locale-qualifier/src/test/resources/locale_en_gb.png b/paparazzi-gradle-plugin/src/test/projects/locale-qualifier/src/test/resources/locale_en_gb.png new file mode 100644 index 0000000000..51bf477821 Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/locale-qualifier/src/test/resources/locale_en_gb.png differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/material-components-present/build.gradle b/paparazzi-gradle-plugin/src/test/projects/material-components-present/build.gradle similarity index 63% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/material-components-present/build.gradle rename to paparazzi-gradle-plugin/src/test/projects/material-components-present/build.gradle index bb098946b8..f31df237c9 100644 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/material-components-present/build.gradle +++ b/paparazzi-gradle-plugin/src/test/projects/material-components-present/build.gradle @@ -4,20 +4,16 @@ plugins { id 'app.cash.paparazzi' } -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - android { + namespace 'app.cash.paparazzi.plugin.test' compileSdk libs.versions.compileSdk.get() as int defaultConfig { minSdk libs.versions.minSdk.get() as int } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } } dependencies { diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/material-components-present/src/main/res/layout/button.xml b/paparazzi-gradle-plugin/src/test/projects/material-components-present/src/main/res/layout/button.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/material-components-present/src/main/res/layout/button.xml rename to paparazzi-gradle-plugin/src/test/projects/material-components-present/src/main/res/layout/button.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/material-components-present/src/test/java/app/cash/paparazzi/plugin/test/ButtonViewTest.kt b/paparazzi-gradle-plugin/src/test/projects/material-components-present/src/test/java/app/cash/paparazzi/plugin/test/ButtonViewTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/material-components-present/src/test/java/app/cash/paparazzi/plugin/test/ButtonViewTest.kt rename to paparazzi-gradle-plugin/src/test/projects/material-components-present/src/test/java/app/cash/paparazzi/plugin/test/ButtonViewTest.kt diff --git a/paparazzi-gradle-plugin/src/test/projects/material-components-present/src/test/resources/button.png b/paparazzi-gradle-plugin/src/test/projects/material-components-present/src/test/resources/button.png new file mode 100644 index 0000000000..f287edab1a Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/material-components-present/src/test/resources/button.png differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/missing-library-plugin/build.gradle b/paparazzi-gradle-plugin/src/test/projects/missing-library-plugin/build.gradle similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/missing-library-plugin/build.gradle rename to paparazzi-gradle-plugin/src/test/projects/missing-library-plugin/build.gradle diff --git a/paparazzi-gradle-plugin/src/test/projects/missing-library-plugin/gradle.properties b/paparazzi-gradle-plugin/src/test/projects/missing-library-plugin/gradle.properties new file mode 100644 index 0000000000..3de9cb5c45 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/missing-library-plugin/gradle.properties @@ -0,0 +1,2 @@ +android.useAndroidX=true +app.cash.paparazzi.legacy.resource.loading=true diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/missing-platform-dir/build.gradle b/paparazzi-gradle-plugin/src/test/projects/missing-platform-dir/build.gradle similarity index 55% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/missing-platform-dir/build.gradle rename to paparazzi-gradle-plugin/src/test/projects/missing-platform-dir/build.gradle index 9bf088537c..fd6501df2e 100644 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/missing-platform-dir/build.gradle +++ b/paparazzi-gradle-plugin/src/test/projects/missing-platform-dir/build.gradle @@ -4,20 +4,19 @@ plugins { id 'app.cash.paparazzi' } -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - android { + namespace 'app.cash.paparazzi.plugin.test' compileSdk libs.versions.compileSdk.get() as int defaultConfig { minSdk libs.versions.minSdk.get() as int } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } } tasks.withType(Test).configureEach { diff --git a/paparazzi-gradle-plugin/src/test/projects/missing-platform-dir/src/test/java/app/cash/paparazzi/plugin/test/MissingPlatformDirTest.kt b/paparazzi-gradle-plugin/src/test/projects/missing-platform-dir/src/test/java/app/cash/paparazzi/plugin/test/MissingPlatformDirTest.kt new file mode 100644 index 0000000000..f4f07bc08c --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/missing-platform-dir/src/test/java/app/cash/paparazzi/plugin/test/MissingPlatformDirTest.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2019 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.plugin.test + +class MissingPlatformDirTest { + +// @get:Rule +// val paparazzi = Paparazzi( +// environment = detectEnvironment().copy( +// platformDir = "${androidHome()}/platforms/android-oops" +// ) +// ) +// +// @Test fun test() {} +} diff --git a/paparazzi-gradle-plugin/src/test/projects/missing-supported-plugins/build.gradle b/paparazzi-gradle-plugin/src/test/projects/missing-supported-plugins/build.gradle new file mode 100644 index 0000000000..2e8210f469 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/missing-supported-plugins/build.gradle @@ -0,0 +1,3 @@ +plugins { + id 'app.cash.paparazzi' +} \ No newline at end of file diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/multiplatform-plugin-with-android/build.gradle b/paparazzi-gradle-plugin/src/test/projects/multiplatform-plugin-with-android/build.gradle similarity index 59% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/multiplatform-plugin-with-android/build.gradle rename to paparazzi-gradle-plugin/src/test/projects/multiplatform-plugin-with-android/build.gradle index 19be17be65..da05b71b8e 100644 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/multiplatform-plugin-with-android/build.gradle +++ b/paparazzi-gradle-plugin/src/test/projects/multiplatform-plugin-with-android/build.gradle @@ -8,18 +8,14 @@ kotlin { android() } -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - android { + namespace 'app.cash.paparazzi.plugin.test' compileSdk libs.versions.compileSdk.get() as int defaultConfig { minSdk libs.versions.minSdk.get() as int } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } } diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/multiplatform-plugin-without-android/build.gradle b/paparazzi-gradle-plugin/src/test/projects/multiplatform-plugin-without-android/build.gradle similarity index 57% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/multiplatform-plugin-without-android/build.gradle rename to paparazzi-gradle-plugin/src/test/projects/multiplatform-plugin-without-android/build.gradle index 7e8ab9c755..d4726a832e 100644 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/multiplatform-plugin-without-android/build.gradle +++ b/paparazzi-gradle-plugin/src/test/projects/multiplatform-plugin-without-android/build.gradle @@ -4,18 +4,14 @@ plugins { id 'app.cash.paparazzi' } -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - android { + namespace 'app.cash.paparazzi.plugin.test' compileSdk libs.versions.compileSdk.get() as int defaultConfig { minSdk libs.versions.minSdk.get() as int } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } } diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/night-mode-compose/build.gradle b/paparazzi-gradle-plugin/src/test/projects/night-mode-compose/build.gradle similarity index 65% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/night-mode-compose/build.gradle rename to paparazzi-gradle-plugin/src/test/projects/night-mode-compose/build.gradle index b198ffcc6c..f9500a3ddb 100644 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/night-mode-compose/build.gradle +++ b/paparazzi-gradle-plugin/src/test/projects/night-mode-compose/build.gradle @@ -4,20 +4,19 @@ plugins { id 'app.cash.paparazzi' } -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - android { + namespace 'app.cash.paparazzi.plugin.test' compileSdk libs.versions.compileSdk.get() as int defaultConfig { minSdk libs.versions.minSdk.get() as int } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } buildFeatures { compose true } diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/night-mode-compose/src/main/java/app/cash/paparazzi/plugin/test/LightDark.kt b/paparazzi-gradle-plugin/src/test/projects/night-mode-compose/src/main/java/app/cash/paparazzi/plugin/test/LightDark.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/night-mode-compose/src/main/java/app/cash/paparazzi/plugin/test/LightDark.kt rename to paparazzi-gradle-plugin/src/test/projects/night-mode-compose/src/main/java/app/cash/paparazzi/plugin/test/LightDark.kt diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/night-mode-compose/src/test/java/app/cash/paparazzi/plugin/test/NightModeTest.kt b/paparazzi-gradle-plugin/src/test/projects/night-mode-compose/src/test/java/app/cash/paparazzi/plugin/test/NightModeTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/night-mode-compose/src/test/java/app/cash/paparazzi/plugin/test/NightModeTest.kt rename to paparazzi-gradle-plugin/src/test/projects/night-mode-compose/src/test/java/app/cash/paparazzi/plugin/test/NightModeTest.kt diff --git a/paparazzi-gradle-plugin/src/test/projects/night-mode-compose/src/test/resources/dark_mode.png b/paparazzi-gradle-plugin/src/test/projects/night-mode-compose/src/test/resources/dark_mode.png new file mode 100644 index 0000000000..efe1e42a4b Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/night-mode-compose/src/test/resources/dark_mode.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/night-mode-compose/src/test/resources/light_mode.png b/paparazzi-gradle-plugin/src/test/projects/night-mode-compose/src/test/resources/light_mode.png new file mode 100644 index 0000000000..ad17dda146 Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/night-mode-compose/src/test/resources/light_mode.png differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/night-mode-xml/build.gradle b/paparazzi-gradle-plugin/src/test/projects/night-mode-xml/build.gradle similarity index 69% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/night-mode-xml/build.gradle rename to paparazzi-gradle-plugin/src/test/projects/night-mode-xml/build.gradle index b198ffcc6c..e1b1d9b8da 100644 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/night-mode-xml/build.gradle +++ b/paparazzi-gradle-plugin/src/test/projects/night-mode-xml/build.gradle @@ -4,20 +4,18 @@ plugins { id 'app.cash.paparazzi' } -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - android { compileSdk libs.versions.compileSdk.get() as int defaultConfig { minSdk libs.versions.minSdk.get() as int } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } buildFeatures { compose true } diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/night-mode-xml/src/main/res/layout-night/layout.xml b/paparazzi-gradle-plugin/src/test/projects/night-mode-xml/src/main/res/layout-night/layout.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/night-mode-xml/src/main/res/layout-night/layout.xml rename to paparazzi-gradle-plugin/src/test/projects/night-mode-xml/src/main/res/layout-night/layout.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/night-mode-xml/src/main/res/layout/layout.xml b/paparazzi-gradle-plugin/src/test/projects/night-mode-xml/src/main/res/layout/layout.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/night-mode-xml/src/main/res/layout/layout.xml rename to paparazzi-gradle-plugin/src/test/projects/night-mode-xml/src/main/res/layout/layout.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/night-mode-xml/src/test/java/app/cash/paparazzi/plugin/test/NightModeTest.kt b/paparazzi-gradle-plugin/src/test/projects/night-mode-xml/src/test/java/app/cash/paparazzi/plugin/test/NightModeTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/night-mode-xml/src/test/java/app/cash/paparazzi/plugin/test/NightModeTest.kt rename to paparazzi-gradle-plugin/src/test/projects/night-mode-xml/src/test/java/app/cash/paparazzi/plugin/test/NightModeTest.kt diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/night-mode-compose/src/test/resources/dark_mode.png b/paparazzi-gradle-plugin/src/test/projects/night-mode-xml/src/test/resources/dark_mode.png similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/night-mode-compose/src/test/resources/dark_mode.png rename to paparazzi-gradle-plugin/src/test/projects/night-mode-xml/src/test/resources/dark_mode.png diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/night-mode-compose/src/test/resources/light_mode.png b/paparazzi-gradle-plugin/src/test/projects/night-mode-xml/src/test/resources/light_mode.png similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/night-mode-compose/src/test/resources/light_mode.png rename to paparazzi-gradle-plugin/src/test/projects/night-mode-xml/src/test/resources/light_mode.png diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/nine-patch/build.gradle b/paparazzi-gradle-plugin/src/test/projects/nine-patch/build.gradle similarity index 56% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/nine-patch/build.gradle rename to paparazzi-gradle-plugin/src/test/projects/nine-patch/build.gradle index 3e0528ab8a..50aeb13aab 100644 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/nine-patch/build.gradle +++ b/paparazzi-gradle-plugin/src/test/projects/nine-patch/build.gradle @@ -4,21 +4,20 @@ plugins { id 'app.cash.paparazzi' } -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - android { + namespace 'app.cash.paparazzi.plugin.test' compileSdk libs.versions.compileSdk.get() as int defaultConfig { minSdk libs.versions.minSdk.get() as int vectorDrawables.useSupportLibrary = true } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } } dependencies { diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/nine-patch/src/main/res/drawable-nodpi/list_divider_light.9.png b/paparazzi-gradle-plugin/src/test/projects/nine-patch/src/main/res/drawable-nodpi/list_divider_light.9.png similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/nine-patch/src/main/res/drawable-nodpi/list_divider_light.9.png rename to paparazzi-gradle-plugin/src/test/projects/nine-patch/src/main/res/drawable-nodpi/list_divider_light.9.png diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/nine-patch/src/main/res/values/styles.xml b/paparazzi-gradle-plugin/src/test/projects/nine-patch/src/main/res/values/styles.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/nine-patch/src/main/res/values/styles.xml rename to paparazzi-gradle-plugin/src/test/projects/nine-patch/src/main/res/values/styles.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/nine-patch/src/test/java/app/cash/paparazzi/plugin/test/NinePatchTest.kt b/paparazzi-gradle-plugin/src/test/projects/nine-patch/src/test/java/app/cash/paparazzi/plugin/test/NinePatchTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/nine-patch/src/test/java/app/cash/paparazzi/plugin/test/NinePatchTest.kt rename to paparazzi-gradle-plugin/src/test/projects/nine-patch/src/test/java/app/cash/paparazzi/plugin/test/NinePatchTest.kt diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/nine-patch/src/test/resources/nine_patch.png b/paparazzi-gradle-plugin/src/test/projects/nine-patch/src/test/resources/nine_patch.png similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/nine-patch/src/test/resources/nine_patch.png rename to paparazzi-gradle-plugin/src/test/projects/nine-patch/src/test/resources/nine_patch.png diff --git a/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-off/consumer/build.gradle b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-off/consumer/build.gradle new file mode 100644 index 0000000000..59020f2c18 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-off/consumer/build.gradle @@ -0,0 +1,27 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test.consumer' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} + +dependencies { + implementation files('libs/external.aar') + implementation project(':producer1') + implementation project(':producer2') + testImplementation libs.truth +} diff --git a/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-off/consumer/libs/external.aar b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-off/consumer/libs/external.aar new file mode 100644 index 0000000000..c2354a94fd Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-off/consumer/libs/external.aar differ diff --git a/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-off/consumer/src/main/assets/consumer/secret.txt b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-off/consumer/src/main/assets/consumer/secret.txt new file mode 100644 index 0000000000..24a7421a2e --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-off/consumer/src/main/assets/consumer/secret.txt @@ -0,0 +1 @@ +consumer \ No newline at end of file diff --git a/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-off/consumer/src/test/java/app/cash/paparazzi/plugin/test/AssetAccessTest.kt b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-off/consumer/src/test/java/app/cash/paparazzi/plugin/test/AssetAccessTest.kt new file mode 100644 index 0000000000..e99d982086 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-off/consumer/src/test/java/app/cash/paparazzi/plugin/test/AssetAccessTest.kt @@ -0,0 +1,27 @@ +package app.cash.paparazzi.plugin.test + +import app.cash.paparazzi.Paparazzi +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test + +class AssetAccessTest { + @get:Rule + val paparazzi = Paparazzi() + + @Test + fun testViews() { + val pairs = mapOf( + "consumer/secret.txt" to "consumer", + "producer1/secret.txt" to "producer1", + "producer2/secret.txt" to "producer2", + "external/secret.txt" to "external" + ) + + pairs.forEach { (key, value) -> + val contents = + paparazzi.context.assets.open(key).bufferedReader().use { it.readText() } + assertThat(contents).isEqualTo(value) + } + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-off/producer1/build.gradle b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-off/producer1/build.gradle new file mode 100644 index 0000000000..a701fe70aa --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-off/producer1/build.gradle @@ -0,0 +1,19 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' +} + +android { + namespace 'app.cash.paparazzi.plugin.test.producer1' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-off/producer1/src/main/assets/producer1/secret.txt b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-off/producer1/src/main/assets/producer1/secret.txt new file mode 100644 index 0000000000..602a305e1e --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-off/producer1/src/main/assets/producer1/secret.txt @@ -0,0 +1 @@ +producer1 \ No newline at end of file diff --git a/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-off/producer2/build.gradle b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-off/producer2/build.gradle new file mode 100644 index 0000000000..35ca4059a2 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-off/producer2/build.gradle @@ -0,0 +1,19 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' +} + +android { + namespace 'app.cash.paparazzi.plugin.test.producer2' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-off/producer2/src/main/assets/producer2/secret.txt b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-off/producer2/src/main/assets/producer2/secret.txt new file mode 100644 index 0000000000..e5c372f0e2 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-off/producer2/src/main/assets/producer2/secret.txt @@ -0,0 +1 @@ +producer2 \ No newline at end of file diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/open-transitive-assets/settings.gradle b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-off/settings.gradle similarity index 58% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/open-transitive-assets/settings.gradle rename to paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-off/settings.gradle index 15d88b84b8..41f133f67c 100644 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/open-transitive-assets/settings.gradle +++ b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-off/settings.gradle @@ -1,4 +1,5 @@ apply from: '../test.settings.gradle' include ':consumer' -include ':producer' +include ':producer1' +include ':producer2' diff --git a/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-on/consumer/build.gradle b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-on/consumer/build.gradle new file mode 100644 index 0000000000..59020f2c18 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-on/consumer/build.gradle @@ -0,0 +1,27 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test.consumer' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} + +dependencies { + implementation files('libs/external.aar') + implementation project(':producer1') + implementation project(':producer2') + testImplementation libs.truth +} diff --git a/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-on/consumer/libs/external.aar b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-on/consumer/libs/external.aar new file mode 100644 index 0000000000..c2354a94fd Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-on/consumer/libs/external.aar differ diff --git a/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-on/consumer/src/main/assets/consumer/secret.txt b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-on/consumer/src/main/assets/consumer/secret.txt new file mode 100644 index 0000000000..24a7421a2e --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-on/consumer/src/main/assets/consumer/secret.txt @@ -0,0 +1 @@ +consumer \ No newline at end of file diff --git a/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-on/consumer/src/test/java/app/cash/paparazzi/plugin/test/AssetAccessTest.kt b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-on/consumer/src/test/java/app/cash/paparazzi/plugin/test/AssetAccessTest.kt new file mode 100644 index 0000000000..e99d982086 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-on/consumer/src/test/java/app/cash/paparazzi/plugin/test/AssetAccessTest.kt @@ -0,0 +1,27 @@ +package app.cash.paparazzi.plugin.test + +import app.cash.paparazzi.Paparazzi +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test + +class AssetAccessTest { + @get:Rule + val paparazzi = Paparazzi() + + @Test + fun testViews() { + val pairs = mapOf( + "consumer/secret.txt" to "consumer", + "producer1/secret.txt" to "producer1", + "producer2/secret.txt" to "producer2", + "external/secret.txt" to "external" + ) + + pairs.forEach { (key, value) -> + val contents = + paparazzi.context.assets.open(key).bufferedReader().use { it.readText() } + assertThat(contents).isEqualTo(value) + } + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-on/gradle.properties b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-on/gradle.properties new file mode 100644 index 0000000000..a2dfe3aa9b --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-on/gradle.properties @@ -0,0 +1,2 @@ +android.useAndroidX=true +app.cash.paparazzi.legacy.asset.loading=true diff --git a/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-on/producer1/build.gradle b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-on/producer1/build.gradle new file mode 100644 index 0000000000..a701fe70aa --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-on/producer1/build.gradle @@ -0,0 +1,19 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' +} + +android { + namespace 'app.cash.paparazzi.plugin.test.producer1' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-on/producer1/src/main/assets/producer1/secret.txt b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-on/producer1/src/main/assets/producer1/secret.txt new file mode 100644 index 0000000000..602a305e1e --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-on/producer1/src/main/assets/producer1/secret.txt @@ -0,0 +1 @@ +producer1 \ No newline at end of file diff --git a/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-on/producer2/build.gradle b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-on/producer2/build.gradle new file mode 100644 index 0000000000..35ca4059a2 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-on/producer2/build.gradle @@ -0,0 +1,19 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' +} + +android { + namespace 'app.cash.paparazzi.plugin.test.producer2' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-on/producer2/src/main/assets/producer2/secret.txt b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-on/producer2/src/main/assets/producer2/secret.txt new file mode 100644 index 0000000000..e5c372f0e2 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-on/producer2/src/main/assets/producer2/secret.txt @@ -0,0 +1 @@ +producer2 \ No newline at end of file diff --git a/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-on/settings.gradle b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-on/settings.gradle new file mode 100644 index 0000000000..41f133f67c --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/open-assets-legacy-asset-loading-on/settings.gradle @@ -0,0 +1,5 @@ +apply from: '../test.settings.gradle' + +include ':consumer' +include ':producer1' +include ':producer2' diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/cacheable/build.gradle b/paparazzi-gradle-plugin/src/test/projects/prefer-dsl-namespace/build.gradle similarity index 52% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/cacheable/build.gradle rename to paparazzi-gradle-plugin/src/test/projects/prefer-dsl-namespace/build.gradle index 8a55ad8185..60ac1844fb 100644 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/cacheable/build.gradle +++ b/paparazzi-gradle-plugin/src/test/projects/prefer-dsl-namespace/build.gradle @@ -1,21 +1,16 @@ plugins { id 'com.android.library' - id 'kotlin-android' id 'app.cash.paparazzi' } -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - android { + namespace 'app.cash.paparazzi.plugin.namespaced' compileSdk libs.versions.compileSdk.get() as int defaultConfig { minSdk libs.versions.minSdk.get() as int } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } } diff --git a/paparazzi-gradle-plugin/src/test/projects/prepare-resources-task-caching/build.gradle b/paparazzi-gradle-plugin/src/test/projects/prepare-resources-task-caching/build.gradle new file mode 100644 index 0000000000..411ab6a9d7 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/prepare-resources-task-caching/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/prepare-resources-task-caching/settings.gradle b/paparazzi-gradle-plugin/src/test/projects/prepare-resources-task-caching/settings.gradle new file mode 100644 index 0000000000..79b3ab9f07 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/prepare-resources-task-caching/settings.gradle @@ -0,0 +1,7 @@ +apply from: '../test.settings.gradle' + +buildCache { + local { + directory = new File(rootDir, 'build-cache') + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/record-mode-multiple-modules/module/build.gradle b/paparazzi-gradle-plugin/src/test/projects/record-mode-multiple-modules/module/build.gradle new file mode 100644 index 0000000000..411ab6a9d7 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/record-mode-multiple-modules/module/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/record-mode-multiple-modules/module/src/test/java/app/cash/paparazzi/plugin/test/RecordTest.kt b/paparazzi-gradle-plugin/src/test/projects/record-mode-multiple-modules/module/src/test/java/app/cash/paparazzi/plugin/test/RecordTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/record-mode-multiple-modules/module/src/test/java/app/cash/paparazzi/plugin/test/RecordTest.kt rename to paparazzi-gradle-plugin/src/test/projects/record-mode-multiple-modules/module/src/test/java/app/cash/paparazzi/plugin/test/RecordTest.kt diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/non-transitive-resources/settings.gradle b/paparazzi-gradle-plugin/src/test/projects/record-mode-multiple-modules/settings.gradle similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/non-transitive-resources/settings.gradle rename to paparazzi-gradle-plugin/src/test/projects/record-mode-multiple-modules/settings.gradle diff --git a/paparazzi-gradle-plugin/src/test/projects/record-mode-multiple-tests/module/build.gradle b/paparazzi-gradle-plugin/src/test/projects/record-mode-multiple-tests/module/build.gradle new file mode 100644 index 0000000000..411ab6a9d7 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/record-mode-multiple-tests/module/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/record-mode-multiple-tests/module/src/test/java/app/cash/paparazzi/plugin/test/RecordTest.kt b/paparazzi-gradle-plugin/src/test/projects/record-mode-multiple-tests/module/src/test/java/app/cash/paparazzi/plugin/test/RecordTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/record-mode-multiple-tests/module/src/test/java/app/cash/paparazzi/plugin/test/RecordTest.kt rename to paparazzi-gradle-plugin/src/test/projects/record-mode-multiple-tests/module/src/test/java/app/cash/paparazzi/plugin/test/RecordTest.kt diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/record-mode-multiple-modules/settings.gradle b/paparazzi-gradle-plugin/src/test/projects/record-mode-multiple-tests/settings.gradle similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/record-mode-multiple-modules/settings.gradle rename to paparazzi-gradle-plugin/src/test/projects/record-mode-multiple-tests/settings.gradle diff --git a/paparazzi-gradle-plugin/src/test/projects/record-mode/build.gradle b/paparazzi-gradle-plugin/src/test/projects/record-mode/build.gradle new file mode 100644 index 0000000000..411ab6a9d7 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/record-mode/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/record-mode/src/test/java/app/cash/paparazzi/plugin/test/RecordTest.kt b/paparazzi-gradle-plugin/src/test/projects/record-mode/src/test/java/app/cash/paparazzi/plugin/test/RecordTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/record-mode/src/test/java/app/cash/paparazzi/plugin/test/RecordTest.kt rename to paparazzi-gradle-plugin/src/test/projects/record-mode/src/test/java/app/cash/paparazzi/plugin/test/RecordTest.kt diff --git a/paparazzi-gradle-plugin/src/test/projects/rerun-asset-change/build.gradle b/paparazzi-gradle-plugin/src/test/projects/rerun-asset-change/build.gradle new file mode 100644 index 0000000000..411ab6a9d7 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/rerun-asset-change/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/rerun-asset-change/src/main/res/layout/root.xml b/paparazzi-gradle-plugin/src/test/projects/rerun-asset-change/src/main/res/layout/root.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/rerun-asset-change/src/main/res/layout/root.xml rename to paparazzi-gradle-plugin/src/test/projects/rerun-asset-change/src/main/res/layout/root.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/rerun-asset-change/src/test/java/app/cash/paparazzi/sample/RecordTest.kt b/paparazzi-gradle-plugin/src/test/projects/rerun-asset-change/src/test/java/app/cash/paparazzi/sample/RecordTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/rerun-asset-change/src/test/java/app/cash/paparazzi/sample/RecordTest.kt rename to paparazzi-gradle-plugin/src/test/projects/rerun-asset-change/src/test/java/app/cash/paparazzi/sample/RecordTest.kt diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/rerun-asset-change/src/test/resources/secret1.txt b/paparazzi-gradle-plugin/src/test/projects/rerun-asset-change/src/test/resources/secret1.txt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/rerun-asset-change/src/test/resources/secret1.txt rename to paparazzi-gradle-plugin/src/test/projects/rerun-asset-change/src/test/resources/secret1.txt diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/rerun-asset-change/src/test/resources/secret2.txt b/paparazzi-gradle-plugin/src/test/projects/rerun-asset-change/src/test/resources/secret2.txt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/rerun-asset-change/src/test/resources/secret2.txt rename to paparazzi-gradle-plugin/src/test/projects/rerun-asset-change/src/test/resources/secret2.txt diff --git a/paparazzi-gradle-plugin/src/test/projects/rerun-property-change/build.gradle b/paparazzi-gradle-plugin/src/test/projects/rerun-property-change/build.gradle new file mode 100644 index 0000000000..411ab6a9d7 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/rerun-property-change/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/rerun-report/src/test/java/app/cash/paparazzi/sample/RecordTest.kt b/paparazzi-gradle-plugin/src/test/projects/rerun-property-change/src/test/java/app/cash/paparazzi/sample/RecordTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/rerun-report/src/test/java/app/cash/paparazzi/sample/RecordTest.kt rename to paparazzi-gradle-plugin/src/test/projects/rerun-property-change/src/test/java/app/cash/paparazzi/sample/RecordTest.kt diff --git a/paparazzi-gradle-plugin/src/test/projects/rerun-property-change/src/test/snapshots/images/app.cash.paparazzi.plugin.test_RecordTest_record.png b/paparazzi-gradle-plugin/src/test/projects/rerun-property-change/src/test/snapshots/images/app.cash.paparazzi.plugin.test_RecordTest_record.png new file mode 100644 index 0000000000..8da2be99c2 Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/rerun-property-change/src/test/snapshots/images/app.cash.paparazzi.plugin.test_RecordTest_record.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/rerun-report/build.gradle b/paparazzi-gradle-plugin/src/test/projects/rerun-report/build.gradle new file mode 100644 index 0000000000..411ab6a9d7 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/rerun-report/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/rerun-snapshots/src/test/java/app/cash/paparazzi/sample/RecordTest.kt b/paparazzi-gradle-plugin/src/test/projects/rerun-report/src/test/java/app/cash/paparazzi/sample/RecordTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/rerun-snapshots/src/test/java/app/cash/paparazzi/sample/RecordTest.kt rename to paparazzi-gradle-plugin/src/test/projects/rerun-report/src/test/java/app/cash/paparazzi/sample/RecordTest.kt diff --git a/paparazzi-gradle-plugin/src/test/projects/rerun-resource-change/build.gradle b/paparazzi-gradle-plugin/src/test/projects/rerun-resource-change/build.gradle new file mode 100644 index 0000000000..411ab6a9d7 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/rerun-resource-change/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/rerun-resource-change/src/main/res/layout/root.xml b/paparazzi-gradle-plugin/src/test/projects/rerun-resource-change/src/main/res/layout/root.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/rerun-resource-change/src/main/res/layout/root.xml rename to paparazzi-gradle-plugin/src/test/projects/rerun-resource-change/src/main/res/layout/root.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/rerun-resource-change/src/test/java/app/cash/paparazzi/plugin/test/RecordTest.kt b/paparazzi-gradle-plugin/src/test/projects/rerun-resource-change/src/test/java/app/cash/paparazzi/plugin/test/RecordTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/rerun-resource-change/src/test/java/app/cash/paparazzi/plugin/test/RecordTest.kt rename to paparazzi-gradle-plugin/src/test/projects/rerun-resource-change/src/test/java/app/cash/paparazzi/plugin/test/RecordTest.kt diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/rerun-resource-change/src/test/resources/colors1.xml b/paparazzi-gradle-plugin/src/test/projects/rerun-resource-change/src/test/resources/colors1.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/rerun-resource-change/src/test/resources/colors1.xml rename to paparazzi-gradle-plugin/src/test/projects/rerun-resource-change/src/test/resources/colors1.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/rerun-resource-change/src/test/resources/colors2.xml b/paparazzi-gradle-plugin/src/test/projects/rerun-resource-change/src/test/resources/colors2.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/rerun-resource-change/src/test/resources/colors2.xml rename to paparazzi-gradle-plugin/src/test/projects/rerun-resource-change/src/test/resources/colors2.xml diff --git a/paparazzi-gradle-plugin/src/test/projects/rerun-snapshots/build.gradle b/paparazzi-gradle-plugin/src/test/projects/rerun-snapshots/build.gradle new file mode 100644 index 0000000000..411ab6a9d7 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/rerun-snapshots/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/rerun-snapshots/src/test/java/app/cash/paparazzi/sample/RecordTest.kt b/paparazzi-gradle-plugin/src/test/projects/rerun-snapshots/src/test/java/app/cash/paparazzi/sample/RecordTest.kt new file mode 100644 index 0000000000..d0b1e7129b --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/rerun-snapshots/src/test/java/app/cash/paparazzi/sample/RecordTest.kt @@ -0,0 +1,16 @@ +package app.cash.paparazzi.plugin.test + +import android.view.View +import app.cash.paparazzi.Paparazzi +import org.junit.Rule +import org.junit.Test + +class RecordTest { + @get:Rule + val paparazzi = Paparazzi() + + @Test + fun record() { + paparazzi.snapshot(View(paparazzi.context)) + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/supports-application-modules/build.gradle b/paparazzi-gradle-plugin/src/test/projects/supports-application-modules/build.gradle new file mode 100644 index 0000000000..d39aba51f5 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/supports-application-modules/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'com.android.application' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/supports-application-modules/src/main/AndroidManifest.xml b/paparazzi-gradle-plugin/src/test/projects/supports-application-modules/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..e194530e2f --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/supports-application-modules/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ +<!-- + ~ Copyright (C) 2018 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. + --> +<manifest> +</manifest> diff --git a/paparazzi-gradle-plugin/src/test/projects/supports-application-modules/src/main/res/drawable/camera.png b/paparazzi-gradle-plugin/src/test/projects/supports-application-modules/src/main/res/drawable/camera.png new file mode 100644 index 0000000000..6f37e30a98 Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/supports-application-modules/src/main/res/drawable/camera.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/supports-application-modules/src/main/res/layout/launch.xml b/paparazzi-gradle-plugin/src/test/projects/supports-application-modules/src/main/res/layout/launch.xml new file mode 100644 index 0000000000..058b420e75 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/supports-application-modules/src/main/res/layout/launch.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:gravity="center" + android:background="@color/launchBackground" + android:layout_width="match_parent" + android:layout_height="match_parent" + > + + <ImageView + android:layout_width="200dp" + android:layout_height="200dp" + android:src="@drawable/camera" + /> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center" + android:textColor="@color/cameraBody" + android:text="paparazzi" + android:textSize="70dp" + android:textFontWeight="100" + /> + +</LinearLayout> diff --git a/paparazzi-gradle-plugin/src/test/projects/supports-application-modules/src/main/res/values/colors.xml b/paparazzi-gradle-plugin/src/test/projects/supports-application-modules/src/main/res/values/colors.xml new file mode 100644 index 0000000000..cc0f1ba2db --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/supports-application-modules/src/main/res/values/colors.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <color name="launchBackground">#dcdccc</color> + <color name="cameraBody">#332F21</color> +</resources> diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/missing-platform-dir/src/test/java/app/cash/paparazzi/plugin/test/MissingPlatformDirTest.kt b/paparazzi-gradle-plugin/src/test/projects/supports-application-modules/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt similarity index 72% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/missing-platform-dir/src/test/java/app/cash/paparazzi/plugin/test/MissingPlatformDirTest.kt rename to paparazzi-gradle-plugin/src/test/projects/supports-application-modules/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt index 75fe227f46..407a87ec32 100644 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/missing-platform-dir/src/test/java/app/cash/paparazzi/plugin/test/MissingPlatformDirTest.kt +++ b/paparazzi-gradle-plugin/src/test/projects/supports-application-modules/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt @@ -15,19 +15,18 @@ */ package app.cash.paparazzi.plugin.test +import android.widget.LinearLayout import app.cash.paparazzi.Paparazzi -import app.cash.paparazzi.androidHome -import app.cash.paparazzi.detectEnvironment import org.junit.Rule import org.junit.Test -class MissingPlatformDirTest { +class LaunchViewTest { @get:Rule - val paparazzi = Paparazzi( - environment = detectEnvironment().copy( - platformDir = "${androidHome()}/platforms/android-oops" - ) - ) + val paparazzi = Paparazzi() - @Test fun test() {} + @Test + fun testViews() { + val launch = paparazzi.inflate<LinearLayout>(R.layout.launch) + paparazzi.snapshot(launch, "launch") + } } diff --git a/paparazzi-gradle-plugin/src/test/projects/supports-application-modules/src/test/resources/launch.png b/paparazzi-gradle-plugin/src/test/projects/supports-application-modules/src/test/resources/launch.png new file mode 100644 index 0000000000..8efeabbbab Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/supports-application-modules/src/test/resources/launch.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/supports-dynamic-feature-modules/app/build.gradle b/paparazzi-gradle-plugin/src/test/projects/supports-dynamic-feature-modules/app/build.gradle new file mode 100644 index 0000000000..977203f881 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/supports-dynamic-feature-modules/app/build.gradle @@ -0,0 +1,17 @@ +plugins { + id 'com.android.application' +} + +android { + namespace 'app.cash.paparazzi.plugin.dynamic.feature.app' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + + dynamicFeatures = [':dynamic_feature'] +} diff --git a/paparazzi-gradle-plugin/src/test/projects/supports-dynamic-feature-modules/app/src/main/AndroidManifest.xml b/paparazzi-gradle-plugin/src/test/projects/supports-dynamic-feature-modules/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..e194530e2f --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/supports-dynamic-feature-modules/app/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ +<!-- + ~ Copyright (C) 2018 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. + --> +<manifest> +</manifest> diff --git a/paparazzi-gradle-plugin/src/test/projects/supports-dynamic-feature-modules/dynamic_feature/build.gradle b/paparazzi-gradle-plugin/src/test/projects/supports-dynamic-feature-modules/dynamic_feature/build.gradle new file mode 100644 index 0000000000..a016222215 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/supports-dynamic-feature-modules/dynamic_feature/build.gradle @@ -0,0 +1,24 @@ +plugins { + id 'com.android.dynamic-feature' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.dynamic.feature.feature' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} + +dependencies { + implementation project(':app') +} diff --git a/paparazzi-gradle-plugin/src/test/projects/supports-dynamic-feature-modules/dynamic_feature/src/main/AndroidManifest.xml b/paparazzi-gradle-plugin/src/test/projects/supports-dynamic-feature-modules/dynamic_feature/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..6ded379395 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/supports-dynamic-feature-modules/dynamic_feature/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ +<!-- + ~ Copyright (C) 2018 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. + --> +<manifest package="app.cash.paparazzi.plugin.dynamic.feature.feature"> +</manifest> diff --git a/paparazzi-gradle-plugin/src/test/projects/supports-dynamic-feature-modules/dynamic_feature/src/main/res/drawable/camera.png b/paparazzi-gradle-plugin/src/test/projects/supports-dynamic-feature-modules/dynamic_feature/src/main/res/drawable/camera.png new file mode 100644 index 0000000000..6f37e30a98 Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/supports-dynamic-feature-modules/dynamic_feature/src/main/res/drawable/camera.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/supports-dynamic-feature-modules/dynamic_feature/src/main/res/layout/launch.xml b/paparazzi-gradle-plugin/src/test/projects/supports-dynamic-feature-modules/dynamic_feature/src/main/res/layout/launch.xml new file mode 100644 index 0000000000..058b420e75 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/supports-dynamic-feature-modules/dynamic_feature/src/main/res/layout/launch.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:gravity="center" + android:background="@color/launchBackground" + android:layout_width="match_parent" + android:layout_height="match_parent" + > + + <ImageView + android:layout_width="200dp" + android:layout_height="200dp" + android:src="@drawable/camera" + /> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center" + android:textColor="@color/cameraBody" + android:text="paparazzi" + android:textSize="70dp" + android:textFontWeight="100" + /> + +</LinearLayout> diff --git a/paparazzi-gradle-plugin/src/test/projects/supports-dynamic-feature-modules/dynamic_feature/src/main/res/values/colors.xml b/paparazzi-gradle-plugin/src/test/projects/supports-dynamic-feature-modules/dynamic_feature/src/main/res/values/colors.xml new file mode 100644 index 0000000000..cc0f1ba2db --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/supports-dynamic-feature-modules/dynamic_feature/src/main/res/values/colors.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <color name="launchBackground">#dcdccc</color> + <color name="cameraBody">#332F21</color> +</resources> diff --git a/paparazzi-gradle-plugin/src/test/projects/supports-dynamic-feature-modules/dynamic_feature/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt b/paparazzi-gradle-plugin/src/test/projects/supports-dynamic-feature-modules/dynamic_feature/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt new file mode 100644 index 0000000000..1901f4843e --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/supports-dynamic-feature-modules/dynamic_feature/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2019 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.plugin.test + +import android.widget.LinearLayout +import app.cash.paparazzi.Paparazzi +import org.junit.Rule +import org.junit.Test + +class LaunchViewTest { + @get:Rule + val paparazzi = Paparazzi() + + @Test + fun testViews() { + val launch = paparazzi.inflate<LinearLayout>(app.cash.paparazzi.plugin.dynamic.feature.feature.R.layout.launch) + paparazzi.snapshot(launch, "launch") + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/supports-dynamic-feature-modules/dynamic_feature/src/test/resources/launch.png b/paparazzi-gradle-plugin/src/test/projects/supports-dynamic-feature-modules/dynamic_feature/src/test/resources/launch.png new file mode 100644 index 0000000000..8efeabbbab Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/supports-dynamic-feature-modules/dynamic_feature/src/test/resources/launch.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/supports-dynamic-feature-modules/settings.gradle b/paparazzi-gradle-plugin/src/test/projects/supports-dynamic-feature-modules/settings.gradle new file mode 100644 index 0000000000..215f178c59 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/supports-dynamic-feature-modules/settings.gradle @@ -0,0 +1,4 @@ +apply from: '../test.settings.gradle' + +include ':app' +include ':dynamic_feature' diff --git a/paparazzi-gradle-plugin/src/test/projects/test.settings.gradle b/paparazzi-gradle-plugin/src/test/projects/test.settings.gradle new file mode 100644 index 0000000000..004460adfe --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/test.settings.gradle @@ -0,0 +1,16 @@ +dependencyResolutionManagement { + versionCatalogs { + libs { + from(files("../../../../gradle/libs.versions.toml")) + } + } + + repositories { + maven { + url "file://${rootDir}/../../../../../build/localMaven" + } + mavenCentral() + //mavenLocal() + google() + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/text-appearances-code/build.gradle b/paparazzi-gradle-plugin/src/test/projects/text-appearances-code/build.gradle new file mode 100644 index 0000000000..411ab6a9d7 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/text-appearances-code/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/text-appearances-code/src/main/res/values/colors.xml b/paparazzi-gradle-plugin/src/test/projects/text-appearances-code/src/main/res/values/colors.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/text-appearances-code/src/main/res/values/colors.xml rename to paparazzi-gradle-plugin/src/test/projects/text-appearances-code/src/main/res/values/colors.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/text-appearances-code/src/main/res/values/styles.xml b/paparazzi-gradle-plugin/src/test/projects/text-appearances-code/src/main/res/values/styles.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/text-appearances-code/src/main/res/values/styles.xml rename to paparazzi-gradle-plugin/src/test/projects/text-appearances-code/src/main/res/values/styles.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/text-appearances-code/src/test/java/app/cash/paparazzi/plugin/test/TextAppearanceTest.kt b/paparazzi-gradle-plugin/src/test/projects/text-appearances-code/src/test/java/app/cash/paparazzi/plugin/test/TextAppearanceTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/text-appearances-code/src/test/java/app/cash/paparazzi/plugin/test/TextAppearanceTest.kt rename to paparazzi-gradle-plugin/src/test/projects/text-appearances-code/src/test/java/app/cash/paparazzi/plugin/test/TextAppearanceTest.kt diff --git a/paparazzi-gradle-plugin/src/test/projects/text-appearances-code/src/test/resources/textappearances.png b/paparazzi-gradle-plugin/src/test/projects/text-appearances-code/src/test/resources/textappearances.png new file mode 100644 index 0000000000..e94a6b23d0 Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/text-appearances-code/src/test/resources/textappearances.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/text-appearances-xml/build.gradle b/paparazzi-gradle-plugin/src/test/projects/text-appearances-xml/build.gradle new file mode 100644 index 0000000000..411ab6a9d7 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/text-appearances-xml/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/text-appearances-xml/src/main/res/layout/text_appearance_test.xml b/paparazzi-gradle-plugin/src/test/projects/text-appearances-xml/src/main/res/layout/text_appearance_test.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/text-appearances-xml/src/main/res/layout/text_appearance_test.xml rename to paparazzi-gradle-plugin/src/test/projects/text-appearances-xml/src/main/res/layout/text_appearance_test.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/text-appearances-xml/src/main/res/values/colors.xml b/paparazzi-gradle-plugin/src/test/projects/text-appearances-xml/src/main/res/values/colors.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/text-appearances-xml/src/main/res/values/colors.xml rename to paparazzi-gradle-plugin/src/test/projects/text-appearances-xml/src/main/res/values/colors.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/text-appearances-xml/src/main/res/values/styles.xml b/paparazzi-gradle-plugin/src/test/projects/text-appearances-xml/src/main/res/values/styles.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/text-appearances-xml/src/main/res/values/styles.xml rename to paparazzi-gradle-plugin/src/test/projects/text-appearances-xml/src/main/res/values/styles.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/text-appearances-xml/src/test/java/app/cash/paparazzi/plugin/test/TextAppearanceTest.kt b/paparazzi-gradle-plugin/src/test/projects/text-appearances-xml/src/test/java/app/cash/paparazzi/plugin/test/TextAppearanceTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/text-appearances-xml/src/test/java/app/cash/paparazzi/plugin/test/TextAppearanceTest.kt rename to paparazzi-gradle-plugin/src/test/projects/text-appearances-xml/src/test/java/app/cash/paparazzi/plugin/test/TextAppearanceTest.kt diff --git a/paparazzi-gradle-plugin/src/test/projects/text-appearances-xml/src/test/resources/textappearances.png b/paparazzi-gradle-plugin/src/test/projects/text-appearances-xml/src/test/resources/textappearances.png new file mode 100644 index 0000000000..e94a6b23d0 Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/text-appearances-xml/src/test/resources/textappearances.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/transitive-resources/gradle.properties b/paparazzi-gradle-plugin/src/test/projects/transitive-resources/gradle.properties new file mode 100644 index 0000000000..69e36ee76d --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/transitive-resources/gradle.properties @@ -0,0 +1,2 @@ +android.useAndroidX=true +android.nonTransitiveRClass=false diff --git a/paparazzi-gradle-plugin/src/test/projects/transitive-resources/module/build.gradle b/paparazzi-gradle-plugin/src/test/projects/transitive-resources/module/build.gradle new file mode 100644 index 0000000000..9294b1d535 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/transitive-resources/module/build.gradle @@ -0,0 +1,24 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} + +dependencies { + implementation libs.androidx.appcompat +} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/non-transitive-resources/module/src/main/java/app/cash/paparazzi/plugin/test/AmountView.kt b/paparazzi-gradle-plugin/src/test/projects/transitive-resources/module/src/main/java/app/cash/paparazzi/plugin/test/AmountView.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/non-transitive-resources/module/src/main/java/app/cash/paparazzi/plugin/test/AmountView.kt rename to paparazzi-gradle-plugin/src/test/projects/transitive-resources/module/src/main/java/app/cash/paparazzi/plugin/test/AmountView.kt diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/non-transitive-resources/module/src/test/java/app/cash/paparazzi/plugin/test/AmountViewTest.kt b/paparazzi-gradle-plugin/src/test/projects/transitive-resources/module/src/test/java/app/cash/paparazzi/plugin/test/AmountViewTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/non-transitive-resources/module/src/test/java/app/cash/paparazzi/plugin/test/AmountViewTest.kt rename to paparazzi-gradle-plugin/src/test/projects/transitive-resources/module/src/test/java/app/cash/paparazzi/plugin/test/AmountViewTest.kt diff --git a/paparazzi-gradle-plugin/src/test/projects/transitive-resources/module/src/test/resources/five_bucks.png b/paparazzi-gradle-plugin/src/test/projects/transitive-resources/module/src/test/resources/five_bucks.png new file mode 100644 index 0000000000..4f91b2c839 Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/transitive-resources/module/src/test/resources/five_bucks.png differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/record-mode-multiple-tests/settings.gradle b/paparazzi-gradle-plugin/src/test/projects/transitive-resources/settings.gradle similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/record-mode-multiple-tests/settings.gradle rename to paparazzi-gradle-plugin/src/test/projects/transitive-resources/settings.gradle diff --git a/paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/build.gradle b/paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/build.gradle new file mode 100644 index 0000000000..411ab6a9d7 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/src/main/res/drawable/camera.png b/paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/src/main/res/drawable/camera.png new file mode 100644 index 0000000000..6f37e30a98 Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/src/main/res/drawable/camera.png differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/src/main/res/layout-sw600dp/launch.xml b/paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/src/main/res/layout-sw600dp/launch.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/src/main/res/layout-sw600dp/launch.xml rename to paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/src/main/res/layout-sw600dp/launch.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/src/main/res/layout/launch.xml b/paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/src/main/res/layout/launch.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/src/main/res/layout/launch.xml rename to paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/src/main/res/layout/launch.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt b/paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt rename to paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt diff --git a/paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/src/test/resources/nexus_7_launch.png b/paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/src/test/resources/nexus_7_launch.png new file mode 100644 index 0000000000..78ca1a1a22 Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/src/test/resources/nexus_7_launch.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/src/test/resources/pixel_3_launch.png b/paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/src/test/resources/pixel_3_launch.png new file mode 100644 index 0000000000..8d9edea139 Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/src/test/resources/pixel_3_launch.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/validate-accessibility/build.gradle b/paparazzi-gradle-plugin/src/test/projects/validate-accessibility/build.gradle new file mode 100644 index 0000000000..3085ddd88c --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/validate-accessibility/build.gradle @@ -0,0 +1,26 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} + +tasks.withType(Test).configureEach { + testLogging { + showStandardStreams true + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/validate-accessibility/src/test/java/app/cash/paparazzi/plugin/test/ValidateAccessibilityTest.kt b/paparazzi-gradle-plugin/src/test/projects/validate-accessibility/src/test/java/app/cash/paparazzi/plugin/test/ValidateAccessibilityTest.kt new file mode 100644 index 0000000000..d5841183b0 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/validate-accessibility/src/test/java/app/cash/paparazzi/plugin/test/ValidateAccessibilityTest.kt @@ -0,0 +1,35 @@ +package app.cash.paparazzi.plugin.test + +import android.graphics.Color +import android.widget.LinearLayout +import android.widget.TextView +import app.cash.paparazzi.Paparazzi +import org.junit.Rule +import org.junit.Test + +class ValidateAccessibilityTest { + @get:Rule + val paparazzi = Paparazzi(validateAccessibility = true) + + @Test + fun validateTextContrast() { + val textViewBad = TextView(paparazzi.context).apply { + text = "Low Contrast" + setTextColor(Color.WHITE) + setBackgroundColor(Color.WHITE) + } + + val textViewGood = TextView(paparazzi.context).apply { + text = "High Contrast" + setTextColor(Color.WHITE) + setBackgroundColor(Color.BLACK) + } + + val view = LinearLayout(paparazzi.context).apply { + addView(textViewBad) + addView(textViewGood) + } + + paparazzi.snapshot(view) + } +} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-aapt-code/build.gradle b/paparazzi-gradle-plugin/src/test/projects/verify-aapt-code/build.gradle similarity index 56% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-aapt-code/build.gradle rename to paparazzi-gradle-plugin/src/test/projects/verify-aapt-code/build.gradle index 3e0528ab8a..50aeb13aab 100644 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-aapt-code/build.gradle +++ b/paparazzi-gradle-plugin/src/test/projects/verify-aapt-code/build.gradle @@ -4,21 +4,20 @@ plugins { id 'app.cash.paparazzi' } -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - android { + namespace 'app.cash.paparazzi.plugin.test' compileSdk libs.versions.compileSdk.get() as int defaultConfig { minSdk libs.versions.minSdk.get() as int vectorDrawables.useSupportLibrary = true } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } } dependencies { diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-aapt-code/src/main/res/drawable/card_chip.xml b/paparazzi-gradle-plugin/src/test/projects/verify-aapt-code/src/main/res/drawable/card_chip.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-aapt-code/src/main/res/drawable/card_chip.xml rename to paparazzi-gradle-plugin/src/test/projects/verify-aapt-code/src/main/res/drawable/card_chip.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-aapt-code/src/test/java/app/cash/paparazzi/plugin/test/AaptDrawableTest.kt b/paparazzi-gradle-plugin/src/test/projects/verify-aapt-code/src/test/java/app/cash/paparazzi/plugin/test/AaptDrawableTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-aapt-code/src/test/java/app/cash/paparazzi/plugin/test/AaptDrawableTest.kt rename to paparazzi-gradle-plugin/src/test/projects/verify-aapt-code/src/test/java/app/cash/paparazzi/plugin/test/AaptDrawableTest.kt diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-aapt-code/src/test/resources/card_chip.png b/paparazzi-gradle-plugin/src/test/projects/verify-aapt-code/src/test/resources/card_chip.png new file mode 100644 index 0000000000..afa00c86d5 Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/verify-aapt-code/src/test/resources/card_chip.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-aapt-compose/build.gradle b/paparazzi-gradle-plugin/src/test/projects/verify-aapt-compose/build.gradle new file mode 100644 index 0000000000..c6213b8d4c --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/verify-aapt-compose/build.gradle @@ -0,0 +1,30 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } + buildFeatures { + compose true + } + composeOptions { + kotlinCompilerExtensionVersion libs.versions.composeCompiler.get() + } +} + +dependencies { + implementation libs.composeUi.material +} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-aapt-compose/src/main/java/app/cash/paparazzi/plugin/test/CardChip.kt b/paparazzi-gradle-plugin/src/test/projects/verify-aapt-compose/src/main/java/app/cash/paparazzi/plugin/test/CardChip.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-aapt-compose/src/main/java/app/cash/paparazzi/plugin/test/CardChip.kt rename to paparazzi-gradle-plugin/src/test/projects/verify-aapt-compose/src/main/java/app/cash/paparazzi/plugin/test/CardChip.kt diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-aapt-compose/src/main/res/drawable/card_chip.xml b/paparazzi-gradle-plugin/src/test/projects/verify-aapt-compose/src/main/res/drawable/card_chip.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-aapt-compose/src/main/res/drawable/card_chip.xml rename to paparazzi-gradle-plugin/src/test/projects/verify-aapt-compose/src/main/res/drawable/card_chip.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-aapt-compose/src/test/java/app/cash/paparazzi/plugin/test/ComposeTest.kt b/paparazzi-gradle-plugin/src/test/projects/verify-aapt-compose/src/test/java/app/cash/paparazzi/plugin/test/ComposeTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-aapt-compose/src/test/java/app/cash/paparazzi/plugin/test/ComposeTest.kt rename to paparazzi-gradle-plugin/src/test/projects/verify-aapt-compose/src/test/java/app/cash/paparazzi/plugin/test/ComposeTest.kt diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-aapt-compose/src/test/resources/compose_card_chip.png b/paparazzi-gradle-plugin/src/test/projects/verify-aapt-compose/src/test/resources/compose_card_chip.png new file mode 100644 index 0000000000..88e49e2700 Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/verify-aapt-compose/src/test/resources/compose_card_chip.png differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-aapt-xml/build.gradle b/paparazzi-gradle-plugin/src/test/projects/verify-aapt-xml/build.gradle similarity index 56% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-aapt-xml/build.gradle rename to paparazzi-gradle-plugin/src/test/projects/verify-aapt-xml/build.gradle index 3e0528ab8a..50aeb13aab 100644 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-aapt-xml/build.gradle +++ b/paparazzi-gradle-plugin/src/test/projects/verify-aapt-xml/build.gradle @@ -4,21 +4,20 @@ plugins { id 'app.cash.paparazzi' } -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - android { + namespace 'app.cash.paparazzi.plugin.test' compileSdk libs.versions.compileSdk.get() as int defaultConfig { minSdk libs.versions.minSdk.get() as int vectorDrawables.useSupportLibrary = true } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } } dependencies { diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-aapt-xml/src/main/res/drawable/card_chip.xml b/paparazzi-gradle-plugin/src/test/projects/verify-aapt-xml/src/main/res/drawable/card_chip.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-aapt-xml/src/main/res/drawable/card_chip.xml rename to paparazzi-gradle-plugin/src/test/projects/verify-aapt-xml/src/main/res/drawable/card_chip.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-aapt-xml/src/main/res/layout/aapt_drawable.xml b/paparazzi-gradle-plugin/src/test/projects/verify-aapt-xml/src/main/res/layout/aapt_drawable.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-aapt-xml/src/main/res/layout/aapt_drawable.xml rename to paparazzi-gradle-plugin/src/test/projects/verify-aapt-xml/src/main/res/layout/aapt_drawable.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-aapt-xml/src/test/java/app/cash/paparazzi/plugin/test/AaptDrawableTest.kt b/paparazzi-gradle-plugin/src/test/projects/verify-aapt-xml/src/test/java/app/cash/paparazzi/plugin/test/AaptDrawableTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-aapt-xml/src/test/java/app/cash/paparazzi/plugin/test/AaptDrawableTest.kt rename to paparazzi-gradle-plugin/src/test/projects/verify-aapt-xml/src/test/java/app/cash/paparazzi/plugin/test/AaptDrawableTest.kt diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-aapt-xml/src/test/resources/card_chip.png b/paparazzi-gradle-plugin/src/test/projects/verify-aapt-xml/src/test/resources/card_chip.png new file mode 100644 index 0000000000..afa00c86d5 Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/verify-aapt-xml/src/test/resources/card_chip.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure-multiple-modules/module/build.gradle b/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure-multiple-modules/module/build.gradle new file mode 100644 index 0000000000..411ab6a9d7 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure-multiple-modules/module/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure-multiple-modules/module/src/test/java/app/cash/paparazzi/plugin/test/VerifyTest.kt b/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure-multiple-modules/module/src/test/java/app/cash/paparazzi/plugin/test/VerifyTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure-multiple-modules/module/src/test/java/app/cash/paparazzi/plugin/test/VerifyTest.kt rename to paparazzi-gradle-plugin/src/test/projects/verify-mode-failure-multiple-modules/module/src/test/java/app/cash/paparazzi/plugin/test/VerifyTest.kt diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure-multiple-modules/module/src/test/resources/expected_delta.png b/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure-multiple-modules/module/src/test/resources/expected_delta.png new file mode 100644 index 0000000000..2b959f000a Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure-multiple-modules/module/src/test/resources/expected_delta.png differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure-multiple-modules/module/src/test/snapshots/images/app.cash.paparazzi.plugin.test_VerifyTest_verify.png b/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure-multiple-modules/module/src/test/snapshots/images/app.cash.paparazzi.plugin.test_VerifyTest_verify.png similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure-multiple-modules/module/src/test/snapshots/images/app.cash.paparazzi.plugin.test_VerifyTest_verify.png rename to paparazzi-gradle-plugin/src/test/projects/verify-mode-failure-multiple-modules/module/src/test/snapshots/images/app.cash.paparazzi.plugin.test_VerifyTest_verify.png diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure-multiple-modules/settings.gradle b/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure-multiple-modules/settings.gradle similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure-multiple-modules/settings.gradle rename to paparazzi-gradle-plugin/src/test/projects/verify-mode-failure-multiple-modules/settings.gradle diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure/build.gradle b/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure/build.gradle new file mode 100644 index 0000000000..411ab6a9d7 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure/src/test/java/app/cash/paparazzi/plugin/test/VerifyTest.kt b/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure/src/test/java/app/cash/paparazzi/plugin/test/VerifyTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure/src/test/java/app/cash/paparazzi/plugin/test/VerifyTest.kt rename to paparazzi-gradle-plugin/src/test/projects/verify-mode-failure/src/test/java/app/cash/paparazzi/plugin/test/VerifyTest.kt diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure/src/test/resources/expected_delta.png b/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure/src/test/resources/expected_delta.png new file mode 100644 index 0000000000..2b959f000a Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure/src/test/resources/expected_delta.png differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure/src/test/snapshots/images/app.cash.paparazzi.plugin.test_VerifyTest_verify.png b/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure/src/test/snapshots/images/app.cash.paparazzi.plugin.test_VerifyTest_verify.png similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure/src/test/snapshots/images/app.cash.paparazzi.plugin.test_VerifyTest_verify.png rename to paparazzi-gradle-plugin/src/test/projects/verify-mode-failure/src/test/snapshots/images/app.cash.paparazzi.plugin.test_VerifyTest_verify.png diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-mode-success-multiple-modules/module/build.gradle b/paparazzi-gradle-plugin/src/test/projects/verify-mode-success-multiple-modules/module/build.gradle new file mode 100644 index 0000000000..411ab6a9d7 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/verify-mode-success-multiple-modules/module/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-success-multiple-modules/module/src/test/java/app/cash/paparazzi/plugin/test/VerifyTest.kt b/paparazzi-gradle-plugin/src/test/projects/verify-mode-success-multiple-modules/module/src/test/java/app/cash/paparazzi/plugin/test/VerifyTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-success-multiple-modules/module/src/test/java/app/cash/paparazzi/plugin/test/VerifyTest.kt rename to paparazzi-gradle-plugin/src/test/projects/verify-mode-success-multiple-modules/module/src/test/java/app/cash/paparazzi/plugin/test/VerifyTest.kt diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-mode-success-multiple-modules/module/src/test/snapshots/images/app.cash.paparazzi.plugin.test_VerifyTest_verify.png b/paparazzi-gradle-plugin/src/test/projects/verify-mode-success-multiple-modules/module/src/test/snapshots/images/app.cash.paparazzi.plugin.test_VerifyTest_verify.png new file mode 100644 index 0000000000..8da2be99c2 Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/verify-mode-success-multiple-modules/module/src/test/snapshots/images/app.cash.paparazzi.plugin.test_VerifyTest_verify.png differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-success-multiple-modules/settings.gradle b/paparazzi-gradle-plugin/src/test/projects/verify-mode-success-multiple-modules/settings.gradle similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-success-multiple-modules/settings.gradle rename to paparazzi-gradle-plugin/src/test/projects/verify-mode-success-multiple-modules/settings.gradle diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-mode-success/build.gradle b/paparazzi-gradle-plugin/src/test/projects/verify-mode-success/build.gradle new file mode 100644 index 0000000000..411ab6a9d7 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/verify-mode-success/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-success/src/test/java/app/cash/paparazzi/plugin/test/VerifyTest.kt b/paparazzi-gradle-plugin/src/test/projects/verify-mode-success/src/test/java/app/cash/paparazzi/plugin/test/VerifyTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-success/src/test/java/app/cash/paparazzi/plugin/test/VerifyTest.kt rename to paparazzi-gradle-plugin/src/test/projects/verify-mode-success/src/test/java/app/cash/paparazzi/plugin/test/VerifyTest.kt diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-mode-success/src/test/snapshots/images/app.cash.paparazzi.plugin.test_VerifyTest_verify.png b/paparazzi-gradle-plugin/src/test/projects/verify-mode-success/src/test/snapshots/images/app.cash.paparazzi.plugin.test_VerifyTest_verify.png new file mode 100644 index 0000000000..8da2be99c2 Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/verify-mode-success/src/test/snapshots/images/app.cash.paparazzi.plugin.test_VerifyTest_verify.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-recyclerview/build.gradle b/paparazzi-gradle-plugin/src/test/projects/verify-recyclerview/build.gradle new file mode 100644 index 0000000000..534bec8656 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/verify-recyclerview/build.gradle @@ -0,0 +1,27 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } + buildFeatures { + viewBinding true + } +} + +dependencies { + implementation libs.androidx.recyclerview +} diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-recyclerview/src/main/java/app/cash/paparazzi/plugin/test/PaparazziFrameLayout.kt b/paparazzi-gradle-plugin/src/test/projects/verify-recyclerview/src/main/java/app/cash/paparazzi/plugin/test/PaparazziFrameLayout.kt new file mode 100644 index 0000000000..9ebe92de60 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/verify-recyclerview/src/main/java/app/cash/paparazzi/plugin/test/PaparazziFrameLayout.kt @@ -0,0 +1,16 @@ +package app.cash.paparazzi.plugin.test + +import android.content.Context +import android.widget.FrameLayout +import androidx.recyclerview.widget.LinearLayoutManager +import app.cash.paparazzi.plugin.test.databinding.InnerViewInflateBinding + +class PaparazziFrameLayout(context: Context) : FrameLayout(context) { + init { + inflate(context, R.layout.inner_view_inflate, this) + val binding = InnerViewInflateBinding.bind(this) + val recyclerView = binding.list + recyclerView.adapter = PaparazziRecyclerView.Adapter() + recyclerView.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-recyclerview/src/main/java/app/cash/paparazzi/plugin/test/PaparazziRecyclerView.kt b/paparazzi-gradle-plugin/src/test/projects/verify-recyclerview/src/main/java/app/cash/paparazzi/plugin/test/PaparazziRecyclerView.kt new file mode 100644 index 0000000000..d409c48a88 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/verify-recyclerview/src/main/java/app/cash/paparazzi/plugin/test/PaparazziRecyclerView.kt @@ -0,0 +1,21 @@ +package app.cash.paparazzi.plugin.test + +import android.content.Context +import android.util.AttributeSet +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView + +class PaparazziRecyclerView(context: Context, attrs: AttributeSet) : RecyclerView(context, attrs) { + class ViewHolder(val view: TextView) : RecyclerView.ViewHolder(view) + class Adapter : RecyclerView.Adapter<ViewHolder>() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder(TextView(parent.context)) + } + + override fun getItemCount(): Int = 5 + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.view.text = "Paparazzi" + } + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-recyclerview/src/main/res/layout/inner_view_inflate.xml b/paparazzi-gradle-plugin/src/test/projects/verify-recyclerview/src/main/res/layout/inner_view_inflate.xml new file mode 100644 index 0000000000..9624f65a70 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/verify-recyclerview/src/main/res/layout/inner_view_inflate.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<merge xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:parentTag="android.widget.FrameLayout" + > + + <app.cash.paparazzi.plugin.test.PaparazziRecyclerView + android:id="@+id/list" + android:layout_width="match_parent" + android:layout_height="match_parent" + /> + +</merge> diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-recyclerview/src/test/java/app/cash/paparazzi/plugin/test/RecyclerViewTest.kt b/paparazzi-gradle-plugin/src/test/projects/verify-recyclerview/src/test/java/app/cash/paparazzi/plugin/test/RecyclerViewTest.kt new file mode 100644 index 0000000000..a99f2523a7 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/verify-recyclerview/src/test/java/app/cash/paparazzi/plugin/test/RecyclerViewTest.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2019 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.plugin.test + +import app.cash.paparazzi.DeviceConfig.Companion.PIXEL_3 +import app.cash.paparazzi.Paparazzi +import org.junit.Rule +import org.junit.Test + +class RecyclerViewTest { + @get:Rule + val paparazzi = Paparazzi(deviceConfig = PIXEL_3) + + @Test + fun test() { + paparazzi.snapshot(PaparazziFrameLayout(paparazzi.context)) + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-recyclerview/src/test/resources/recycler_view.png b/paparazzi-gradle-plugin/src/test/projects/verify-recyclerview/src/test/resources/recycler_view.png new file mode 100644 index 0000000000..277ffae79c Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/verify-recyclerview/src/test/resources/recycler_view.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-rendering-modes/build.gradle b/paparazzi-gradle-plugin/src/test/projects/verify-rendering-modes/build.gradle new file mode 100644 index 0000000000..154d8b3818 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/verify-rendering-modes/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-rendering-modes/src/test/java/app/cash/paparazzi/plugin/test/RenderingModesTest.kt b/paparazzi-gradle-plugin/src/test/projects/verify-rendering-modes/src/test/java/app/cash/paparazzi/plugin/test/RenderingModesTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-rendering-modes/src/test/java/app/cash/paparazzi/plugin/test/RenderingModesTest.kt rename to paparazzi-gradle-plugin/src/test/projects/verify-rendering-modes/src/test/java/app/cash/paparazzi/plugin/test/RenderingModesTest.kt diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-rendering-modes/src/test/resources/horizontal_scroll.png b/paparazzi-gradle-plugin/src/test/projects/verify-rendering-modes/src/test/resources/horizontal_scroll.png new file mode 100644 index 0000000000..c13d8361db Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/verify-rendering-modes/src/test/resources/horizontal_scroll.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-rendering-modes/src/test/resources/normal.png b/paparazzi-gradle-plugin/src/test/projects/verify-rendering-modes/src/test/resources/normal.png new file mode 100644 index 0000000000..67eee33149 Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/verify-rendering-modes/src/test/resources/normal.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-rendering-modes/src/test/resources/vertical_scroll.png b/paparazzi-gradle-plugin/src/test/projects/verify-rendering-modes/src/test/resources/vertical_scroll.png new file mode 100644 index 0000000000..6382a6d93d Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/verify-rendering-modes/src/test/resources/vertical_scroll.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-resources-java/build.gradle b/paparazzi-gradle-plugin/src/test/projects/verify-resources-java/build.gradle new file mode 100644 index 0000000000..60f29bfdf0 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/verify-resources-java/build.gradle @@ -0,0 +1,22 @@ +plugins { + id 'com.android.library' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } +} + +dependencies { + implementation files('libs/external.aar') + implementation project("module1") + implementation project("module2") +} diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-resources-java/libs/external.aar b/paparazzi-gradle-plugin/src/test/projects/verify-resources-java/libs/external.aar new file mode 100644 index 0000000000..ce856d72b8 Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/verify-resources-java/libs/external.aar differ diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-resources-java/module1/build.gradle b/paparazzi-gradle-plugin/src/test/projects/verify-resources-java/module1/build.gradle new file mode 100644 index 0000000000..44f7528c59 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/verify-resources-java/module1/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test.module1' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-resources-java/module1/src/main/res/values/strings.xml b/paparazzi-gradle-plugin/src/test/projects/verify-resources-java/module1/src/main/res/values/strings.xml new file mode 100644 index 0000000000..045e125f3d --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/verify-resources-java/module1/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +</resources> diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-resources-java/module2/build.gradle b/paparazzi-gradle-plugin/src/test/projects/verify-resources-java/module2/build.gradle new file mode 100644 index 0000000000..5a8be9cf5d --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/verify-resources-java/module2/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test.module2' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-resources-java/module2/src/main/res/values/strings.xml b/paparazzi-gradle-plugin/src/test/projects/verify-resources-java/module2/src/main/res/values/strings.xml new file mode 100644 index 0000000000..045e125f3d --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/verify-resources-java/module2/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +</resources> diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-resources-java/settings.gradle b/paparazzi-gradle-plugin/src/test/projects/verify-resources-java/settings.gradle new file mode 100644 index 0000000000..a5664c0ce1 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/verify-resources-java/settings.gradle @@ -0,0 +1,4 @@ +apply from: '../test.settings.gradle' + +include ':module1' +include ':module2' diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-resources-java/src/main/res/values/strings.xml b/paparazzi-gradle-plugin/src/test/projects/verify-resources-java/src/main/res/values/strings.xml new file mode 100644 index 0000000000..045e125f3d --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/verify-resources-java/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +</resources> diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-resources-kotlin/build.gradle b/paparazzi-gradle-plugin/src/test/projects/verify-resources-kotlin/build.gradle new file mode 100644 index 0000000000..756e4c9d08 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/verify-resources-kotlin/build.gradle @@ -0,0 +1,23 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } +} + +dependencies { + implementation files('libs/external.aar') + implementation project("module1") + implementation project("module2") +} diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-resources-kotlin/libs/external.aar b/paparazzi-gradle-plugin/src/test/projects/verify-resources-kotlin/libs/external.aar new file mode 100644 index 0000000000..ce856d72b8 Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/verify-resources-kotlin/libs/external.aar differ diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-resources-kotlin/module1/build.gradle b/paparazzi-gradle-plugin/src/test/projects/verify-resources-kotlin/module1/build.gradle new file mode 100644 index 0000000000..44f7528c59 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/verify-resources-kotlin/module1/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test.module1' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-resources-kotlin/module1/src/main/res/values/strings.xml b/paparazzi-gradle-plugin/src/test/projects/verify-resources-kotlin/module1/src/main/res/values/strings.xml new file mode 100644 index 0000000000..045e125f3d --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/verify-resources-kotlin/module1/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +</resources> diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-resources-kotlin/module2/build.gradle b/paparazzi-gradle-plugin/src/test/projects/verify-resources-kotlin/module2/build.gradle new file mode 100644 index 0000000000..5a8be9cf5d --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/verify-resources-kotlin/module2/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test.module2' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-resources-kotlin/module2/src/main/res/values/strings.xml b/paparazzi-gradle-plugin/src/test/projects/verify-resources-kotlin/module2/src/main/res/values/strings.xml new file mode 100644 index 0000000000..045e125f3d --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/verify-resources-kotlin/module2/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +</resources> diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-resources-kotlin/settings.gradle b/paparazzi-gradle-plugin/src/test/projects/verify-resources-kotlin/settings.gradle new file mode 100644 index 0000000000..a5664c0ce1 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/verify-resources-kotlin/settings.gradle @@ -0,0 +1,4 @@ +apply from: '../test.settings.gradle' + +include ':module1' +include ':module2' diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-resources-kotlin/src/main/res/values/strings.xml b/paparazzi-gradle-plugin/src/test/projects/verify-resources-kotlin/src/main/res/values/strings.xml new file mode 100644 index 0000000000..045e125f3d --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/verify-resources-kotlin/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> +</resources> diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-snapshot/build.gradle b/paparazzi-gradle-plugin/src/test/projects/verify-snapshot/build.gradle new file mode 100644 index 0000000000..411ab6a9d7 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/verify-snapshot/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-snapshot/src/main/res/drawable/camera.png b/paparazzi-gradle-plugin/src/test/projects/verify-snapshot/src/main/res/drawable/camera.png new file mode 100644 index 0000000000..6f37e30a98 Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/verify-snapshot/src/main/res/drawable/camera.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-snapshot/src/main/res/layout/launch.xml b/paparazzi-gradle-plugin/src/test/projects/verify-snapshot/src/main/res/layout/launch.xml new file mode 100644 index 0000000000..058b420e75 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/verify-snapshot/src/main/res/layout/launch.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:gravity="center" + android:background="@color/launchBackground" + android:layout_width="match_parent" + android:layout_height="match_parent" + > + + <ImageView + android:layout_width="200dp" + android:layout_height="200dp" + android:src="@drawable/camera" + /> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center" + android:textColor="@color/cameraBody" + android:text="paparazzi" + android:textSize="70dp" + android:textFontWeight="100" + /> + +</LinearLayout> diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-snapshot/src/main/res/values/colors.xml b/paparazzi-gradle-plugin/src/test/projects/verify-snapshot/src/main/res/values/colors.xml new file mode 100644 index 0000000000..cc0f1ba2db --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/verify-snapshot/src/main/res/values/colors.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <color name="launchBackground">#dcdccc</color> + <color name="cameraBody">#332F21</color> +</resources> diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-snapshot/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt b/paparazzi-gradle-plugin/src/test/projects/verify-snapshot/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt new file mode 100644 index 0000000000..407a87ec32 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/verify-snapshot/src/test/java/app/cash/paparazzi/plugin/test/LaunchViewTest.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2019 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.plugin.test + +import android.widget.LinearLayout +import app.cash.paparazzi.Paparazzi +import org.junit.Rule +import org.junit.Test + +class LaunchViewTest { + @get:Rule + val paparazzi = Paparazzi() + + @Test + fun testViews() { + val launch = paparazzi.inflate<LinearLayout>(R.layout.launch) + paparazzi.snapshot(launch, "launch") + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-snapshot/src/test/resources/launch.png b/paparazzi-gradle-plugin/src/test/projects/verify-snapshot/src/test/resources/launch.png new file mode 100644 index 0000000000..8efeabbbab Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/verify-snapshot/src/test/resources/launch.png differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-svgs/build.gradle b/paparazzi-gradle-plugin/src/test/projects/verify-svgs/build.gradle similarity index 56% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-svgs/build.gradle rename to paparazzi-gradle-plugin/src/test/projects/verify-svgs/build.gradle index 3e0528ab8a..50aeb13aab 100644 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-svgs/build.gradle +++ b/paparazzi-gradle-plugin/src/test/projects/verify-svgs/build.gradle @@ -4,21 +4,20 @@ plugins { id 'app.cash.paparazzi' } -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - android { + namespace 'app.cash.paparazzi.plugin.test' compileSdk libs.versions.compileSdk.get() as int defaultConfig { minSdk libs.versions.minSdk.get() as int vectorDrawables.useSupportLibrary = true } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } } dependencies { diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/non-transitive-resources-no-deps/src/main/res/drawable/arrow_up.xml b/paparazzi-gradle-plugin/src/test/projects/verify-svgs/src/main/res/drawable/arrow_up.xml similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/non-transitive-resources-no-deps/src/main/res/drawable/arrow_up.xml rename to paparazzi-gradle-plugin/src/test/projects/verify-svgs/src/main/res/drawable/arrow_up.xml diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-svgs/src/test/java/app/cash/paparazzi/plugin/test/VectorDrawableTest.kt b/paparazzi-gradle-plugin/src/test/projects/verify-svgs/src/test/java/app/cash/paparazzi/plugin/test/VectorDrawableTest.kt similarity index 100% rename from paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-svgs/src/test/java/app/cash/paparazzi/plugin/test/VectorDrawableTest.kt rename to paparazzi-gradle-plugin/src/test/projects/verify-svgs/src/test/java/app/cash/paparazzi/plugin/test/VectorDrawableTest.kt diff --git a/paparazzi-gradle-plugin/src/test/projects/verify-svgs/src/test/resources/arrow_up.png b/paparazzi-gradle-plugin/src/test/projects/verify-svgs/src/test/resources/arrow_up.png new file mode 100644 index 0000000000..c0751549be Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/verify-svgs/src/test/resources/arrow_up.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/widgets/build.gradle b/paparazzi-gradle-plugin/src/test/projects/widgets/build.gradle new file mode 100644 index 0000000000..64b1f9362b --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/widgets/build.gradle @@ -0,0 +1,31 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'app.cash.paparazzi' +} + +android { + namespace 'app.cash.paparazzi.plugin.test' + compileSdk libs.versions.compileSdk.get() as int + defaultConfig { + minSdk libs.versions.minSdk.get() as int + } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } + kotlinOptions { + jvmTarget = libs.versions.javaTarget.get() + } + buildFeatures { + compose true + } + composeOptions { + kotlinCompilerExtensionVersion libs.versions.composeCompiler.get() + } +} + +dependencies { + testImplementation libs.composeUi.material + testImplementation libs.testParameterInjector +} diff --git a/paparazzi-gradle-plugin/src/test/projects/widgets/src/test/java/app/cash/paparazzi/plugin/test/RenderingModeTest.kt b/paparazzi-gradle-plugin/src/test/projects/widgets/src/test/java/app/cash/paparazzi/plugin/test/RenderingModeTest.kt new file mode 100644 index 0000000000..461e71ddbb --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/widgets/src/test/java/app/cash/paparazzi/plugin/test/RenderingModeTest.kt @@ -0,0 +1,114 @@ +package app.cash.paparazzi.plugin.test + +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.GradientDrawable +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import android.widget.Button +import android.widget.LinearLayout +import android.widget.TextView +import androidx.compose.foundation.layout.Box +import androidx.compose.ui.viewinterop.AndroidView +import app.cash.paparazzi.DeviceConfig +import app.cash.paparazzi.Paparazzi +import com.android.ide.common.rendering.api.SessionParams.RenderingMode +import com.android.ide.common.rendering.api.SessionParams.RenderingMode.NORMAL +import com.android.ide.common.rendering.api.SessionParams.RenderingMode.SHRINK +import com.google.testing.junit.testparameterinjector.TestParameter +import com.google.testing.junit.testparameterinjector.TestParameterInjector +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(TestParameterInjector::class) +class RenderingModeTest( + @TestParameter val mode: Mode +) { + @get:Rule + val paparazzi = Paparazzi( + deviceConfig = DeviceConfig.PIXEL_3, + renderingMode = mode.renderingMode, + showSystemUi = mode.showSystemUi + ) + + @Test fun default() { + paparazzi.snapshot { + Box { + AndroidView( + factory = { buildView(paparazzi.context) } + ) + } + } + } + + enum class Mode( + val renderingMode: RenderingMode, + val showSystemUi: Boolean + ) { + WIDGET(renderingMode = SHRINK, showSystemUi = false), + FULL_SCREEN(renderingMode = NORMAL, showSystemUi = true) + } + + private fun buildView(context: Context): View { + return LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + layoutParams = ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT) + addView( + TextView(context).apply { + id = 1 + text = "Text View Sample" + } + ) + + addView( + View(context).apply { + id = 2 + layoutParams = LinearLayout.LayoutParams(100, 100) + contentDescription = "Content Description Sample" + } + ) + + addView( + View(context).apply { + id = 3 + layoutParams = LinearLayout.LayoutParams(100, 100).apply { + setMargins(20, 20, 20, 20) + } + contentDescription = "Margin Sample" + } + ) + + addView( + View(context).apply { + id = 4 + layoutParams = LinearLayout.LayoutParams(100, 100).apply { + setMargins(20, 20, 20, 20) + } + foreground = GradientDrawable( + GradientDrawable.Orientation.TL_BR, + intArrayOf(Color.YELLOW, Color.BLUE) + ).apply { + shape = GradientDrawable.OVAL + } + contentDescription = "Foreground Drawable" + } + ) + + addView( + Button(context).apply { + id = 5 + layoutParams = LinearLayout.LayoutParams( + WRAP_CONTENT, + WRAP_CONTENT + ).apply { + gravity = Gravity.CENTER + } + text = "Button Sample" + } + ) + } + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/widgets/src/test/resources/full_screen.png b/paparazzi-gradle-plugin/src/test/projects/widgets/src/test/resources/full_screen.png new file mode 100644 index 0000000000..c5651562e7 Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/widgets/src/test/resources/full_screen.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/widgets/src/test/resources/widget.png b/paparazzi-gradle-plugin/src/test/projects/widgets/src/test/resources/widget.png new file mode 100644 index 0000000000..4041256d85 Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/widgets/src/test/resources/widget.png differ diff --git a/paparazzi/build.gradle b/paparazzi/build.gradle index 6f32e755db..31a6939da6 100644 --- a/paparazzi/build.gradle +++ b/paparazzi/build.gradle @@ -1,120 +1,150 @@ -apply plugin: 'com.github.ben-manes.versions' - -buildscript { - repositories { - mavenCentral() - google() - gradlePluginPortal() - //mavenLocal() - } +import org.jetbrains.kotlin.gradle.plugin.KotlinJvmPluginKt - dependencies { - classpath libs.plugin.kotlin - classpath libs.plugin.android - classpath libs.plugin.mavenPublish - classpath libs.plugin.grgit - classpath libs.plugin.dokka - classpath libs.plugin.versions - classpath libs.plugin.spotless - } +apply plugin: 'org.jetbrains.kotlin.jvm' +apply plugin: 'org.jetbrains.dokka' +apply plugin: 'com.vanniktech.maven.publish' +apply plugin: 'com.google.devtools.ksp' + +java { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() } -subprojects { - repositories { - mavenCentral() - google() - //mavenLocal() - } +def artifactType = Attribute.of('artifactType', String) - tasks.withType(Test).configureEach { - testLogging { - events 'passed', 'failed', 'skipped' - exceptionFormat 'full' - showExceptions true - showStackTraces true - showCauses true - } +configurations { + unzip { + attributes.attribute(artifactType, ArtifactTypeDefinition.DIRECTORY_TYPE) } +} - tasks.withType(JavaCompile).configureEach { - sourceCompatibility = libs.versions.javaTarget.get() - targetCompatibility = libs.versions.javaTarget.get() +dependencies { + registerTransform(org.gradle.api.internal.artifacts.transform.UnzipTransform) { + from.attribute(artifactType, ArtifactTypeDefinition.JAR_TYPE) + to.attribute(artifactType, ArtifactTypeDefinition.DIRECTORY_TYPE) } +} - tasks.withType(org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompile).configureEach { - kotlinOptions { - jvmTarget = libs.versions.javaTarget.get() - } - } +dependencies { + // Paparazzi is a Kotlin JVM module and thus cannot depend on Android library artifacts (AARs) + // TODO: Use Gradle transforms to extract/rename classes.jar's from AARs + api files('libs/compose-runtime-1.4.3.jar') + implementation files('libs/compose-ui-1.4.3.jar') // Need implementation dependency to reference AbstractComposeView for AccessibilityRenderExtension + // Needed to get compose bounds and annotated strings for Compose support for AccessibilityRenderExtension + compileOnly files('libs/compose-ui-geometry-1.4.3.jar') + compileOnly files('libs/compose-ui-text-1.4.3.jar') + compileOnly libs.androidx.lifecycleCommon + compileOnly files('libs/lifecycle-runtime-2.6.1.jar') + compileOnly files('libs/savedstate-1.2.1.jar') + compileOnly files('libs/androidx-activity-1.5.0.jar') - plugins.withId('com.vanniktech.maven.publish') { - publishing { - repositories { - maven { - name = "projectLocalMaven" - url = "${rootProject.buildDir}/localMaven" - } - /** - * Want to push to an internal repository for testing? - * Set the following properties in ~/.gradle/gradle.properties. - * - * internalUrl=YOUR_INTERNAL_URL - * internalUsername=YOUR_USERNAME - * internalPassword=YOUR_PASSWORD - */ - maven { - name = "internal" - url = providers.gradleProperty("internalUrl") - credentials(PasswordCredentials) - } - } + implementation libs.trove4j + api libs.layoutlib.native.jdk11 + api libs.tools.common + api libs.tools.layoutlib + api libs.tools.ninepatch + api libs.tools.sdkCommon + api libs.kxml2 + api libs.junit + api libs.androidx.annotations + api libs.guava + api libs.kotlinx.coroutines.core + api libs.okio + api platform(libs.kotlin.bom) + implementation libs.moshi.core + implementation libs.moshi.adapters + implementation libs.jcodec.core + implementation libs.jcodec.javase + implementation projects.paparazziAgent + + ksp libs.moshi.kotlinCodegen + + def osName = System.getProperty("os.name").toLowerCase(Locale.US) + if (osName.startsWith("mac")) { + def osArch = System.getProperty("os.arch").toLowerCase(Locale.US) + if (osArch.startsWith("x86")) { + unzip libs.layoutlib.native.macOsX + } else { + unzip libs.layoutlib.native.macArm } + } else if (osName.startsWith("windows")) { + unzip libs.layoutlib.native.windows + } else { + unzip libs.layoutlib.native.linux } - tasks.register('emptySourcesJar', Jar) { - // TODO: fetch sources from the corresponding AOSP repos. - archiveClassifier = 'sources' - } + testImplementation libs.truth - tasks.register('emptyJavadocJar', Jar) { - archiveClassifier = 'javadoc' - } + add(KotlinJvmPluginKt.PLUGIN_CLASSPATH_CONFIGURATION_NAME, libs.compose.compiler) +} + +tasks.named("dokkaGfm").configure { + outputDirectory = rootProject.file("docs/1.x") - apply plugin: 'com.diffplug.spotless' - spotless { - kotlin { - target("src/**/*.kt") - // ktlint doesn't honour .editorconfig yet: https://github.com/diffplug/spotless/issues/142 - ktlint(libs.versions.ktlint.get()).editorConfigOverride([ - 'insert_final_newline': 'true', - 'end_of_line': 'lf', - 'charset': 'utf-8', - 'indent_size': '2', - 'trim_trailing_whitespace': 'true', - 'kotlin_imports_layout': 'ascii' - ]) + dokkaSourceSets.named("main") { + configureEach { + reportUndocumented = false + skipDeprecated = true + jdkVersion = 8 + perPackageOption { + prefix = "app.cash.paparazzi.internal" + suppress = true + } } } } -tasks.register("clean", Delete).configure { - delete rootProject.buildDir -} +def generateTestConfig = tasks.register("generateTestConfig") { + def resources = "$buildDir/intermediates/paparazzi/resources.txt" + outputs.file(resources) -allprojects { project -> - tasks.register("mavenLocalize").configure { task -> - def projectRootDir = project.projectDir - task.doFirst { - projectRootDir.eachFileRecurse(groovy.io.FileType.FILES) { file -> - if (file.name != 'build.gradle') { - return - } - def text = file.text - file.withWriter { w -> - // Intentional concatenation to prevent self-replacement - w << text.replace("//" + "mavenLocal()", "mavenLocal()") - } - } + doLast { + File configFile = new File(resources) + configFile.withWriter('utf-8') { writer -> + writer.writeLine("app.cash.paparazzi") + writer.writeLine(".") + writer.writeLine("31") + writer.writeLine("platforms/android-31/") + writer.writeLine(".") + writer.writeLine("app.cash.paparazzi") + writer.writeLine("") + writer.writeLine("") + writer.writeLine("") + writer.writeLine("") + writer.writeLine("") } } } + +tasks.withType(Test).configureEach { + dependsOn(generateTestConfig) + systemProperty( + "paparazzi.test.resources", + generateTestConfig.map { it.outputs.files.singleFile }.get().path + ) + systemProperty( + "paparazzi.project.dir", + project.layout.projectDirectory.toString() + ) + systemProperty( + "paparazzi.build.dir", + project.layout.buildDirectory.get().toString() + ) + systemProperty( + "paparazzi.artifacts.cache.dir", + project.gradle.gradleUserHomeDir.path + ) + systemProperty( + "paparazzi.platform.data.root", + configurations.unzip.singleFile.absolutePath + ) + // Uncomment to debug JNI issues in layoutlib + // jvmArgs '-Xcheck:jni' + testLogging { + events 'passed', 'failed', 'skipped' + exceptionFormat 'FULL' + showCauses true + showExceptions true + showStackTraces true + } +} diff --git a/paparazzi/gradle.properties b/paparazzi/gradle.properties index b2c36aa280..13d36287e9 100644 --- a/paparazzi/gradle.properties +++ b/paparazzi/gradle.properties @@ -1,21 +1,4 @@ -GROUP=com.whoop.paparazzi -VERSION_NAME=1.101.1 - -POM_URL=https://github.com/WhoopInc/paparazzi/ -POM_SCM_URL=https://github.com/WhoopInc/paparazzi/ -POM_SCM_CONNECTION=scm:git:git://github.com/WhoopInc/paparazzi.git -POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/WhoopInc/paparazzi.git - -POM_LICENCE_NAME=The Apache Software License, Version 2.0 -POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt -POM_LICENCE_DIST=repo - -POM_DEVELOPER_ID=cashapp -POM_DEVELOPER_NAME=CashApp -POM_DEVELOPER_URL=https://github.com/WhoopInc/ - -SONATYPE_HOST=DEFAULT -RELEASE_SIGNING_ENABLED=false - -org.gradle.caching=true -org.gradle.parallel=true +POM_ARTIFACT_ID=paparazzi +POM_NAME=Paparazzi +POM_DESCRIPTION=An Android library to render your application screens without a physical device or emulator +POM_PACKAGING=jar diff --git a/paparazzi/libs/README.md b/paparazzi/libs/README.md deleted file mode 100644 index a5ac60cd99..0000000000 --- a/paparazzi/libs/README.md +++ /dev/null @@ -1,33 +0,0 @@ -Paparazzi Libraries -------------------- - -This project publishes artifacts from [Android Studio's][android_studio] UI renderer to Maven -Central so that we can consume them in Paparazzi. - -Note that layoutlib's version tracks [Android Studio Releases][studio_releases] and not Paparazzi. - -1. Build and upload: - - ``` - ./gradlew -p paparazzi publishMavenNativeLibraryPublicationToMavenCentralRepository - ``` - - This may take a few minutes. It clones a large repo (2.4 GiB) and then uploads a large artifact - (30 MiB) to Maven Central. - - -2. Visit [Sonatype Nexus][nexus] to promote the artifact. Or drop it if there is a problem! - - -Prerequisites -------------- - -In `~/.gradle/gradle.properties`, set the following: - - * `mavenCentralUsername` - Sonatype username for releasing to `app.cash`. - * `mavenCentralPassword` - Sonatype password for releasing to `app.cash`. - - -[android_studio]: https://developer.android.com/studio -[studio_releases]: https://developer.android.com/studio/releases -[nexus]: https://oss.sonatype.org/ diff --git a/paparazzi/libs/androidx-activity-1.5.0.jar b/paparazzi/libs/androidx-activity-1.5.0.jar new file mode 100644 index 0000000000..482de0b48e Binary files /dev/null and b/paparazzi/libs/androidx-activity-1.5.0.jar differ diff --git a/paparazzi/libs/compose-runtime-1.4.3.jar b/paparazzi/libs/compose-runtime-1.4.3.jar new file mode 100644 index 0000000000..2bf8de099d Binary files /dev/null and b/paparazzi/libs/compose-runtime-1.4.3.jar differ diff --git a/paparazzi/libs/compose-ui-1.4.3.jar b/paparazzi/libs/compose-ui-1.4.3.jar new file mode 100644 index 0000000000..bc31e26b55 Binary files /dev/null and b/paparazzi/libs/compose-ui-1.4.3.jar differ diff --git a/paparazzi/libs/compose-ui-geometry-1.4.3.jar b/paparazzi/libs/compose-ui-geometry-1.4.3.jar new file mode 100644 index 0000000000..67840d876d Binary files /dev/null and b/paparazzi/libs/compose-ui-geometry-1.4.3.jar differ diff --git a/paparazzi/libs/compose-ui-text-1.4.3.jar b/paparazzi/libs/compose-ui-text-1.4.3.jar new file mode 100644 index 0000000000..b915839814 Binary files /dev/null and b/paparazzi/libs/compose-ui-text-1.4.3.jar differ diff --git a/paparazzi/libs/lifecycle-runtime-2.6.1.jar b/paparazzi/libs/lifecycle-runtime-2.6.1.jar new file mode 100644 index 0000000000..bff7b3b7d1 Binary files /dev/null and b/paparazzi/libs/lifecycle-runtime-2.6.1.jar differ diff --git a/paparazzi/libs/savedstate-1.2.1.jar b/paparazzi/libs/savedstate-1.2.1.jar new file mode 100644 index 0000000000..b73828b5c5 Binary files /dev/null and b/paparazzi/libs/savedstate-1.2.1.jar differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/PaparazziPlugin.kt b/paparazzi/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/PaparazziPlugin.kt deleted file mode 100644 index f8395cf254..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/main/java/app/cash/paparazzi/gradle/PaparazziPlugin.kt +++ /dev/null @@ -1,268 +0,0 @@ -/* - * Copyright (C) 2019 Square, Inc. - * - * 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. - */ -package app.cash.paparazzi.gradle - -import app.cash.paparazzi.NATIVE_LIB_VERSION -import app.cash.paparazzi.VERSION -import com.android.build.gradle.BaseExtension -import com.android.build.gradle.LibraryExtension -import com.android.build.gradle.tasks.MergeSourceSetFolders -import com.android.ide.common.symbols.getPackageNameFromManifest -import org.gradle.api.Action -import org.gradle.api.DefaultTask -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.api.Task -import org.gradle.api.artifacts.type.ArtifactTypeDefinition -import org.gradle.api.attributes.Attribute -import org.gradle.api.file.FileCollection -import org.gradle.api.internal.artifacts.transform.UnzipTransform -import org.gradle.api.logging.LogLevel.LIFECYCLE -import org.gradle.api.plugins.JavaBasePlugin -import org.gradle.api.tasks.PathSensitivity -import org.gradle.api.tasks.TaskProvider -import org.gradle.api.tasks.options.Option -import org.gradle.api.tasks.testing.Test -import org.gradle.internal.os.OperatingSystem -import org.gradle.language.base.plugins.LifecycleBasePlugin.VERIFICATION_GROUP -import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension -import org.jetbrains.kotlin.gradle.plugin.KotlinAndroidPluginWrapper -import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper -import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget -import java.util.Locale - -@Suppress("unused") -class PaparazziPlugin : Plugin<Project> { - override fun apply(project: Project) { - project.afterEvaluate { - check(!project.plugins.hasPlugin("com.android.application")) { - error( - "Currently, Paparazzi only works in Android library -- not application -- modules. " + - "See https://github.com/cashapp/paparazzi/issues/107" - ) - } - check(project.plugins.hasPlugin("com.android.library")) { - "The Android Gradle library plugin must be applied for Paparazzi to work properly." - } - } - - project.plugins.withId("com.android.library") { setupPaparazzi(project) } - } - - private fun setupPaparazzi(project: Project) { - project.addTestDependency() - val nativePlatformFileCollection = project.setupNativePlatformDependency() - - // Create anchor tasks for all variants. - val verifyVariants = project.tasks.register("verifyPaparazzi") - val recordVariants = project.tasks.register("recordPaparazzi") - - val variants = project.extensions.getByType(LibraryExtension::class.java) - .libraryVariants - variants.all { variant -> - val variantSlug = variant.name.capitalize(Locale.US) - - val mergeResourcesOutputDir = variant.mergeResourcesProvider.flatMap { it.outputDir } - val mergeAssetsProvider = - project.tasks.named("merge${variantSlug}Assets") as TaskProvider<MergeSourceSetFolders> - val mergeAssetsOutputDir = mergeAssetsProvider.flatMap { it.outputDir } - val reportOutputDir = project.layout.buildDirectory.dir("reports/paparazzi") - val snapshotOutputDir = project.layout.projectDirectory.dir("src/test/snapshots") - - val packageAwareArtifacts = project.configurations - .getByName("${variant.name}RuntimeClasspath") - .incoming - .artifactView { - it.attributes.attribute(ARTIFACT_TYPE_ATTRIBUTE, "android-symbol-with-package-name") - } - .artifacts - - val writeResourcesTask = project.tasks.register( - "preparePaparazzi${variantSlug}Resources", - PrepareResourcesTask::class.java - ) { task -> - val android = project.extensions.getByType(BaseExtension::class.java) - val nonTransitiveRClassEnabled = - (project.findProperty("android.nonTransitiveRClass") as? String).toBoolean() - - task.packageName.set(android.packageName()) - task.artifactFiles.from(packageAwareArtifacts.artifactFiles) - task.nonTransitiveRClassEnabled.set(nonTransitiveRClassEnabled) - task.mergeResourcesOutput.set(mergeResourcesOutputDir) - task.targetSdkVersion.set(android.targetSdkVersion()) - task.compileSdkVersion.set(android.compileSdkVersion()) - task.mergeAssetsOutput.set(mergeAssetsOutputDir) - task.paparazziResources.set(project.layout.buildDirectory.file("intermediates/paparazzi/${variant.name}/resources.txt")) - } - - val testVariantSlug = variant.unitTestVariant.name.capitalize(Locale.US) - - project.plugins.withType(JavaBasePlugin::class.java) { - project.tasks.named("compile${testVariantSlug}JavaWithJavac") - .configure { it.dependsOn(writeResourcesTask) } - } - - project.plugins.withType(KotlinMultiplatformPluginWrapper::class.java) { - val multiplatformExtension = - project.extensions.getByType(KotlinMultiplatformExtension::class.java) - check(multiplatformExtension.targets.any { target -> target is KotlinAndroidTarget }) { - "There must be an Android target configured when using Paparazzi with the Kotlin Multiplatform Plugin" - } - project.tasks.named("compile${testVariantSlug}KotlinAndroid") - .configure { it.dependsOn(writeResourcesTask) } - } - - project.plugins.withType(KotlinAndroidPluginWrapper::class.java) { - project.tasks.named("compile${testVariantSlug}Kotlin") - .configure { it.dependsOn(writeResourcesTask) } - } - - val recordTaskProvider = project.tasks.register("recordPaparazzi$variantSlug", PaparazziTask::class.java) { - it.group = VERIFICATION_GROUP - } - recordVariants.configure { it.dependsOn(recordTaskProvider) } - val verifyTaskProvider = project.tasks.register("verifyPaparazzi$variantSlug", PaparazziTask::class.java) { - it.group = VERIFICATION_GROUP - } - verifyVariants.configure { it.dependsOn(verifyTaskProvider) } - - val isRecordRun = project.objects.property(Boolean::class.java) - val isVerifyRun = project.objects.property(Boolean::class.java) - - project.gradle.taskGraph.whenReady { graph -> - isRecordRun.set(recordTaskProvider.map { graph.hasTask(it) }) - isVerifyRun.set(verifyTaskProvider.map { graph.hasTask(it) }) - } - - val testTaskProvider = project.tasks.named("test$testVariantSlug", Test::class.java) { test -> - test.systemProperties["paparazzi.test.resources"] = - writeResourcesTask.flatMap { it.paparazziResources.asFile }.get().path - test.systemProperties["paparazzi.build.dir"] = - project.layout.buildDirectory.get().toString() - - test.inputs.dir(mergeResourcesOutputDir) - test.inputs.dir(mergeAssetsOutputDir) - test.inputs.files(nativePlatformFileCollection) - .withPropertyName("paparazzi.nativePlatform") - .withPathSensitivity(PathSensitivity.NONE) - - test.outputs.dir(reportOutputDir) - test.outputs.dir(snapshotOutputDir) - - val paparazziProperties = project.properties.filterKeys { it.startsWith("app.cash.paparazzi") } - - @Suppress("ObjectLiteralToLambda") - // why not a lambda? See: https://docs.gradle.org/7.2/userguide/validation_problems.html#implementation_unknown - test.doFirst(object : Action<Task> { - override fun execute(t: Task) { - test.systemProperties["paparazzi.platform.data.root"] = - nativePlatformFileCollection.singleFile.absolutePath - test.systemProperties["paparazzi.test.record"] = isRecordRun.get() - test.systemProperties["paparazzi.test.verify"] = isVerifyRun.get() - test.systemProperties.putAll(paparazziProperties) - } - }) - } - - recordTaskProvider.configure { it.dependsOn(testTaskProvider) } - verifyTaskProvider.configure { it.dependsOn(testTaskProvider) } - - testTaskProvider.configure { test -> - @Suppress("ObjectLiteralToLambda") - // why not a lambda? See: https://docs.gradle.org/7.2/userguide/validation_problems.html#implementation_unknown - test.doLast(object : Action<Task> { - override fun execute(t: Task) { - val uri = reportOutputDir.get().asFile.toPath().resolve("index.html").toUri() - test.logger.log(LIFECYCLE, "See the Paparazzi report at: $uri") - } - }) - } - } - } - - open class PaparazziTask : DefaultTask() { - @Option(option = "tests", description = "Sets test class or method name to be included, '*' is supported.") - open fun setTestNameIncludePatterns(testNamePattern: List<String>): PaparazziTask { - project.tasks.withType(Test::class.java).configureEach { - it.setTestNameIncludePatterns(testNamePattern) - } - return this - } - } - - private fun Project.setupNativePlatformDependency(): FileCollection { - val nativePlatformConfiguration = configurations.create("nativePlatform") - configurations.add(nativePlatformConfiguration) - - val operatingSystem = OperatingSystem.current() - val nativeLibraryArtifactId = when { - operatingSystem.isMacOsX -> { - val osArch = System.getProperty("os.arch").lowercase(Locale.US) - if (osArch.startsWith("x86")) "macosx" else "macarm" - } - operatingSystem.isWindows -> "win" - else -> "linux" - } - nativePlatformConfiguration.dependencies.add( - dependencies.create("app.cash.paparazzi:layoutlib-native-$nativeLibraryArtifactId:$NATIVE_LIB_VERSION") - ) - dependencies.registerTransform(UnzipTransform::class.java) { transform -> - transform.from.attribute(ARTIFACT_TYPE_ATTRIBUTE, ArtifactTypeDefinition.JAR_TYPE) - transform.to.attribute(ARTIFACT_TYPE_ATTRIBUTE, ArtifactTypeDefinition.DIRECTORY_TYPE) - } - - return nativePlatformConfiguration - .incoming - .artifactView { - it.attributes.attribute(ARTIFACT_TYPE_ATTRIBUTE, ArtifactTypeDefinition.DIRECTORY_TYPE) - } - .files - } - - private fun Project.addTestDependency() { - configurations.getByName("testImplementation").dependencies.add( - dependencies.create("com.whoop.paparazzi:paparazzi:$VERSION") - ) - } - - private fun BaseExtension.packageName(): String { - namespace?.let { return it } - - // TODO: explore whether AGP 7.x APIs can handle source set filtering - sourceSets - .filterNot { it.name.startsWith("androidTest") } - .map { it.manifest.srcFile } - .filter { it.exists() } - .forEach { - return getPackageNameFromManifest(it) - } - throw IllegalStateException("No source sets available") - } - - private fun BaseExtension.compileSdkVersion(): String { - return compileSdkVersion!!.substringAfter("android-", DEFAULT_COMPILE_SDK_VERSION.toString()) - } - - private fun BaseExtension.targetSdkVersion(): String { - return defaultConfig.targetSdkVersion?.apiLevel?.toString() - ?: DEFAULT_COMPILE_SDK_VERSION.toString() - } -} - -// TODO: Migrate to ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE when Gradle 7.3 is -// acceptable as the minimum supported version -private val ARTIFACT_TYPE_ATTRIBUTE = Attribute.of("artifactType", String::class.java) -private const val DEFAULT_COMPILE_SDK_VERSION = 31 diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/appcompat-present/src/test/resources/arrow_present.png b/paparazzi/paparazzi-gradle-plugin/src/test/projects/appcompat-present/src/test/resources/arrow_present.png deleted file mode 100644 index 4abd098518..0000000000 Binary files a/paparazzi/paparazzi-gradle-plugin/src/test/projects/appcompat-present/src/test/resources/arrow_present.png and /dev/null differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/compose-dispatcher/src/test/java/app/cash/paparazzi/plugin/test/AndroidUiDispatcherTest.kt b/paparazzi/paparazzi-gradle-plugin/src/test/projects/compose-dispatcher/src/test/java/app/cash/paparazzi/plugin/test/AndroidUiDispatcherTest.kt deleted file mode 100644 index c2eae1abbd..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/compose-dispatcher/src/test/java/app/cash/paparazzi/plugin/test/AndroidUiDispatcherTest.kt +++ /dev/null @@ -1,27 +0,0 @@ -package app.cash.paparazzi.plugin.test - -import androidx.compose.runtime.LaunchedEffect -import app.cash.paparazzi.Paparazzi -import org.junit.Rule -import org.junit.Test - -class AndroidUiDispatcherTest { - @get:Rule - val paparazzi = Paparazzi() - - @Test - fun androidUiDispatcherResets1() = assertSynchronousLaunchedEffect() - - @Test - fun androidUiDispatcherResets2() = assertSynchronousLaunchedEffect() - - private fun assertSynchronousLaunchedEffect() { - var launchedEffect = false - paparazzi.snapshot { - LaunchedEffect(Unit) { - launchedEffect = true - } - } - assert(launchedEffect) - } -} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/compose-wear/src/test/resources/compose_wear.png b/paparazzi/paparazzi-gradle-plugin/src/test/projects/compose-wear/src/test/resources/compose_wear.png deleted file mode 100644 index 532abdc92b..0000000000 Binary files a/paparazzi/paparazzi-gradle-plugin/src/test/projects/compose-wear/src/test/resources/compose_wear.png and /dev/null differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/compose/src/test/resources/compose_fonts.png b/paparazzi/paparazzi-gradle-plugin/src/test/projects/compose/src/test/resources/compose_fonts.png deleted file mode 100644 index d12d893662..0000000000 Binary files a/paparazzi/paparazzi-gradle-plugin/src/test/projects/compose/src/test/resources/compose_fonts.png and /dev/null differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/configuration-cache/build.gradle b/paparazzi/paparazzi-gradle-plugin/src/test/projects/configuration-cache/build.gradle deleted file mode 100644 index 8a55ad8185..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/configuration-cache/build.gradle +++ /dev/null @@ -1,21 +0,0 @@ -plugins { - id 'com.android.library' - id 'kotlin-android' - id 'app.cash.paparazzi' -} - -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - -android { - compileSdk libs.versions.compileSdk.get() as int - defaultConfig { - minSdk libs.versions.minSdk.get() as int - } -} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-fonts-code/src/test/resources/textviews.png b/paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-fonts-code/src/test/resources/textviews.png deleted file mode 100644 index b035ed74f8..0000000000 Binary files a/paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-fonts-code/src/test/resources/textviews.png and /dev/null differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-fonts-xml/src/test/resources/textviews.png b/paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-fonts-xml/src/test/resources/textviews.png deleted file mode 100644 index b035ed74f8..0000000000 Binary files a/paparazzi/paparazzi-gradle-plugin/src/test/projects/custom-fonts-xml/src/test/resources/textviews.png and /dev/null differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/edit-mode-intercept/build.gradle b/paparazzi/paparazzi-gradle-plugin/src/test/projects/edit-mode-intercept/build.gradle deleted file mode 100644 index 8a55ad8185..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/edit-mode-intercept/build.gradle +++ /dev/null @@ -1,21 +0,0 @@ -plugins { - id 'com.android.library' - id 'kotlin-android' - id 'app.cash.paparazzi' -} - -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - -android { - compileSdk libs.versions.compileSdk.get() as int - defaultConfig { - minSdk libs.versions.minSdk.get() as int - } -} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/exclude-androidtest/build.gradle b/paparazzi/paparazzi-gradle-plugin/src/test/projects/exclude-androidtest/build.gradle deleted file mode 100644 index e7159b5477..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/exclude-androidtest/build.gradle +++ /dev/null @@ -1,20 +0,0 @@ -plugins { - id 'com.android.library' - id 'app.cash.paparazzi' -} - -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - -android { - compileSdk libs.versions.compileSdk.get() as int - defaultConfig { - minSdk libs.versions.minSdk.get() as int - } -} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/imm-soft-input/build.gradle b/paparazzi/paparazzi-gradle-plugin/src/test/projects/imm-soft-input/build.gradle deleted file mode 100644 index 8a55ad8185..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/imm-soft-input/build.gradle +++ /dev/null @@ -1,21 +0,0 @@ -plugins { - id 'com.android.library' - id 'kotlin-android' - id 'app.cash.paparazzi' -} - -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - -android { - compileSdk libs.versions.compileSdk.get() as int - defaultConfig { - minSdk libs.versions.minSdk.get() as int - } -} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/layout-direction/src/test/resources/locale_ar.png b/paparazzi/paparazzi-gradle-plugin/src/test/projects/layout-direction/src/test/resources/locale_ar.png deleted file mode 100644 index 6515657623..0000000000 Binary files a/paparazzi/paparazzi-gradle-plugin/src/test/projects/layout-direction/src/test/resources/locale_ar.png and /dev/null differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/layout-direction/src/test/resources/locale_default_rtl.png b/paparazzi/paparazzi-gradle-plugin/src/test/projects/layout-direction/src/test/resources/locale_default_rtl.png deleted file mode 100644 index 6dd775e66e..0000000000 Binary files a/paparazzi/paparazzi-gradle-plugin/src/test/projects/layout-direction/src/test/resources/locale_default_rtl.png and /dev/null differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/locale-qualifier/src/test/resources/locale_default.png b/paparazzi/paparazzi-gradle-plugin/src/test/projects/locale-qualifier/src/test/resources/locale_default.png deleted file mode 100644 index 9bce9e2a75..0000000000 Binary files a/paparazzi/paparazzi-gradle-plugin/src/test/projects/locale-qualifier/src/test/resources/locale_default.png and /dev/null differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/locale-qualifier/src/test/resources/locale_en_gb.png b/paparazzi/paparazzi-gradle-plugin/src/test/projects/locale-qualifier/src/test/resources/locale_en_gb.png deleted file mode 100644 index 6a93526a0b..0000000000 Binary files a/paparazzi/paparazzi-gradle-plugin/src/test/projects/locale-qualifier/src/test/resources/locale_en_gb.png and /dev/null differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/material-components-present/src/test/resources/button.png b/paparazzi/paparazzi-gradle-plugin/src/test/projects/material-components-present/src/test/resources/button.png deleted file mode 100644 index 31bac08557..0000000000 Binary files a/paparazzi/paparazzi-gradle-plugin/src/test/projects/material-components-present/src/test/resources/button.png and /dev/null differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/night-mode-xml/src/test/resources/dark_mode.png b/paparazzi/paparazzi-gradle-plugin/src/test/projects/night-mode-xml/src/test/resources/dark_mode.png deleted file mode 100644 index a8eb98500b..0000000000 Binary files a/paparazzi/paparazzi-gradle-plugin/src/test/projects/night-mode-xml/src/test/resources/dark_mode.png and /dev/null differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/night-mode-xml/src/test/resources/light_mode.png b/paparazzi/paparazzi-gradle-plugin/src/test/projects/night-mode-xml/src/test/resources/light_mode.png deleted file mode 100644 index 6f2dd62b49..0000000000 Binary files a/paparazzi/paparazzi-gradle-plugin/src/test/projects/night-mode-xml/src/test/resources/light_mode.png and /dev/null differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/non-transitive-resources-no-deps/build.gradle b/paparazzi/paparazzi-gradle-plugin/src/test/projects/non-transitive-resources-no-deps/build.gradle deleted file mode 100644 index 8a55ad8185..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/non-transitive-resources-no-deps/build.gradle +++ /dev/null @@ -1,21 +0,0 @@ -plugins { - id 'com.android.library' - id 'kotlin-android' - id 'app.cash.paparazzi' -} - -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - -android { - compileSdk libs.versions.compileSdk.get() as int - defaultConfig { - minSdk libs.versions.minSdk.get() as int - } -} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/non-transitive-resources-no-deps/gradle.properties b/paparazzi/paparazzi-gradle-plugin/src/test/projects/non-transitive-resources-no-deps/gradle.properties deleted file mode 100644 index 0b88863382..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/non-transitive-resources-no-deps/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -android.useAndroidX=true -android.nonTransitiveRClass=true diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/non-transitive-resources-no-deps/src/main/res/layout/launch.xml b/paparazzi/paparazzi-gradle-plugin/src/test/projects/non-transitive-resources-no-deps/src/main/res/layout/launch.xml deleted file mode 100644 index 25db97810c..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/non-transitive-resources-no-deps/src/main/res/layout/launch.xml +++ /dev/null @@ -1,18 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<LinearLayout - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:background="@color/launchBackground" - android:gravity="center" - android:orientation="vertical" - > - - <ImageView - android:layout_width="200dp" - android:layout_height="200dp" - app:srcCompat="@drawable/arrow_up" - /> - -</LinearLayout> diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/non-transitive-resources/gradle.properties b/paparazzi/paparazzi-gradle-plugin/src/test/projects/non-transitive-resources/gradle.properties deleted file mode 100644 index 0b88863382..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/non-transitive-resources/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -android.useAndroidX=true -android.nonTransitiveRClass=true diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/non-transitive-resources/module/build.gradle b/paparazzi/paparazzi-gradle-plugin/src/test/projects/non-transitive-resources/module/build.gradle deleted file mode 100644 index 2a393c4ffd..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/non-transitive-resources/module/build.gradle +++ /dev/null @@ -1,25 +0,0 @@ -plugins { - id 'com.android.library' - id 'kotlin-android' - id 'app.cash.paparazzi' -} - -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - -android { - compileSdk libs.versions.compileSdk.get() as int - defaultConfig { - minSdk libs.versions.minSdk.get() as int - } -} - -dependencies { - implementation libs.androidx.appcompat -} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/non-transitive-resources/module/src/test/resources/five_bucks.png b/paparazzi/paparazzi-gradle-plugin/src/test/projects/non-transitive-resources/module/src/test/resources/five_bucks.png deleted file mode 100644 index f955f99499..0000000000 Binary files a/paparazzi/paparazzi-gradle-plugin/src/test/projects/non-transitive-resources/module/src/test/resources/five_bucks.png and /dev/null differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/open-assets/build.gradle b/paparazzi/paparazzi-gradle-plugin/src/test/projects/open-assets/build.gradle deleted file mode 100644 index e7159b5477..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/open-assets/build.gradle +++ /dev/null @@ -1,20 +0,0 @@ -plugins { - id 'com.android.library' - id 'app.cash.paparazzi' -} - -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - -android { - compileSdk libs.versions.compileSdk.get() as int - defaultConfig { - minSdk libs.versions.minSdk.get() as int - } -} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/open-assets/src/main/assets/secret.txt b/paparazzi/paparazzi-gradle-plugin/src/test/projects/open-assets/src/main/assets/secret.txt deleted file mode 100644 index 0b90e2b856..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/open-assets/src/main/assets/secret.txt +++ /dev/null @@ -1 +0,0 @@ -sup \ No newline at end of file diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/open-assets/src/test/java/app/cash/paparazzi/sample/AssetAccessTest.kt b/paparazzi/paparazzi-gradle-plugin/src/test/projects/open-assets/src/test/java/app/cash/paparazzi/sample/AssetAccessTest.kt deleted file mode 100644 index e6cdb0c6ad..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/open-assets/src/test/java/app/cash/paparazzi/sample/AssetAccessTest.kt +++ /dev/null @@ -1,18 +0,0 @@ -package app.cash.paparazzi.sample - -import app.cash.paparazzi.Paparazzi -import org.assertj.core.api.Assertions.assertThat -import org.junit.Rule -import org.junit.Test - -class AssetAccessTest { - @get:Rule - val paparazzi = Paparazzi() - - @Test - fun testViews() { - val contents = - paparazzi.context.assets.open("secret.txt").bufferedReader().use { it.readText() } - assertThat(contents).isEqualTo("sup") - } -} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/open-transitive-assets/consumer/build.gradle b/paparazzi/paparazzi-gradle-plugin/src/test/projects/open-transitive-assets/consumer/build.gradle deleted file mode 100644 index ae9dc452fc..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/open-transitive-assets/consumer/build.gradle +++ /dev/null @@ -1,26 +0,0 @@ -plugins { - id 'com.android.library' - id 'kotlin-android' - id 'app.cash.paparazzi' -} - -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - -android { - compileSdk libs.versions.compileSdk.get() as int - defaultConfig { - minSdk libs.versions.minSdk.get() as int - } -} - -dependencies { - implementation project(':producer') - testImplementation 'org.assertj:assertj-core:3.21.0' -} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/open-transitive-assets/consumer/src/test/java/app/cash/paparazzi/plugin/test/AssetAccessTest.kt b/paparazzi/paparazzi-gradle-plugin/src/test/projects/open-transitive-assets/consumer/src/test/java/app/cash/paparazzi/plugin/test/AssetAccessTest.kt deleted file mode 100644 index e6e91e6b48..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/open-transitive-assets/consumer/src/test/java/app/cash/paparazzi/plugin/test/AssetAccessTest.kt +++ /dev/null @@ -1,18 +0,0 @@ -package app.cash.paparazzi.plugin.test - -import app.cash.paparazzi.Paparazzi -import org.assertj.core.api.Assertions.assertThat -import org.junit.Rule -import org.junit.Test - -class AssetAccessTest { - @get:Rule - val paparazzi = Paparazzi() - - @Test - fun testViews() { - val contents = - paparazzi.context.assets.open("secret.txt").bufferedReader().use { it.readText() } - assertThat(contents).isEqualTo("sup") - } -} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/open-transitive-assets/producer/build.gradle b/paparazzi/paparazzi-gradle-plugin/src/test/projects/open-transitive-assets/producer/build.gradle deleted file mode 100644 index 4ed34c47a6..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/open-transitive-assets/producer/build.gradle +++ /dev/null @@ -1,20 +0,0 @@ -plugins { - id 'com.android.library' - id 'kotlin-android' -} - -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - -android { - compileSdk libs.versions.compileSdk.get() as int - defaultConfig { - minSdk libs.versions.minSdk.get() as int - } -} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/open-transitive-assets/producer/src/main/AndroidManifest.xml b/paparazzi/paparazzi-gradle-plugin/src/test/projects/open-transitive-assets/producer/src/main/AndroidManifest.xml deleted file mode 100644 index 7e91e59842..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/open-transitive-assets/producer/src/main/AndroidManifest.xml +++ /dev/null @@ -1 +0,0 @@ -<manifest package="app.cash.paparazzi.plugin.test.module"/> diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/open-transitive-assets/producer/src/main/assets/secret.txt b/paparazzi/paparazzi-gradle-plugin/src/test/projects/open-transitive-assets/producer/src/main/assets/secret.txt deleted file mode 100644 index 0b90e2b856..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/open-transitive-assets/producer/src/main/assets/secret.txt +++ /dev/null @@ -1 +0,0 @@ -sup \ No newline at end of file diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/prefer-dsl-namespace/build.gradle b/paparazzi/paparazzi-gradle-plugin/src/test/projects/prefer-dsl-namespace/build.gradle deleted file mode 100644 index 483f0faa4c..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/prefer-dsl-namespace/build.gradle +++ /dev/null @@ -1,21 +0,0 @@ -plugins { - id 'com.android.library' - id 'app.cash.paparazzi' -} - -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - -android { - compileSdk libs.versions.compileSdk.get() as int - defaultConfig { - minSdk libs.versions.minSdk.get() as int - } - namespace "app.cash.paparazzi.plugin.namespaced" -} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/record-mode-multiple-modules/module/build.gradle b/paparazzi/paparazzi-gradle-plugin/src/test/projects/record-mode-multiple-modules/module/build.gradle deleted file mode 100644 index 932b33f0e4..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/record-mode-multiple-modules/module/build.gradle +++ /dev/null @@ -1,21 +0,0 @@ -plugins { - id 'com.android.library' - id 'kotlin-android' - id 'app.cash.paparazzi' -} - -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - -android { - compileSdk libs.versions.compileSdk.get() as int - defaultConfig { - minSdk libs.versions.minSdk.get() as int - } -} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/record-mode-multiple-tests/module/build.gradle b/paparazzi/paparazzi-gradle-plugin/src/test/projects/record-mode-multiple-tests/module/build.gradle deleted file mode 100644 index 932b33f0e4..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/record-mode-multiple-tests/module/build.gradle +++ /dev/null @@ -1,21 +0,0 @@ -plugins { - id 'com.android.library' - id 'kotlin-android' - id 'app.cash.paparazzi' -} - -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - -android { - compileSdk libs.versions.compileSdk.get() as int - defaultConfig { - minSdk libs.versions.minSdk.get() as int - } -} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/record-mode/build.gradle b/paparazzi/paparazzi-gradle-plugin/src/test/projects/record-mode/build.gradle deleted file mode 100644 index 8a55ad8185..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/record-mode/build.gradle +++ /dev/null @@ -1,21 +0,0 @@ -plugins { - id 'com.android.library' - id 'kotlin-android' - id 'app.cash.paparazzi' -} - -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - -android { - compileSdk libs.versions.compileSdk.get() as int - defaultConfig { - minSdk libs.versions.minSdk.get() as int - } -} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/rerun-asset-change/build.gradle b/paparazzi/paparazzi-gradle-plugin/src/test/projects/rerun-asset-change/build.gradle deleted file mode 100644 index 8a55ad8185..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/rerun-asset-change/build.gradle +++ /dev/null @@ -1,21 +0,0 @@ -plugins { - id 'com.android.library' - id 'kotlin-android' - id 'app.cash.paparazzi' -} - -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - -android { - compileSdk libs.versions.compileSdk.get() as int - defaultConfig { - minSdk libs.versions.minSdk.get() as int - } -} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/rerun-report/build.gradle b/paparazzi/paparazzi-gradle-plugin/src/test/projects/rerun-report/build.gradle deleted file mode 100644 index 8a55ad8185..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/rerun-report/build.gradle +++ /dev/null @@ -1,21 +0,0 @@ -plugins { - id 'com.android.library' - id 'kotlin-android' - id 'app.cash.paparazzi' -} - -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - -android { - compileSdk libs.versions.compileSdk.get() as int - defaultConfig { - minSdk libs.versions.minSdk.get() as int - } -} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/rerun-resource-change/build.gradle b/paparazzi/paparazzi-gradle-plugin/src/test/projects/rerun-resource-change/build.gradle deleted file mode 100644 index 8a55ad8185..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/rerun-resource-change/build.gradle +++ /dev/null @@ -1,21 +0,0 @@ -plugins { - id 'com.android.library' - id 'kotlin-android' - id 'app.cash.paparazzi' -} - -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - -android { - compileSdk libs.versions.compileSdk.get() as int - defaultConfig { - minSdk libs.versions.minSdk.get() as int - } -} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/rerun-snapshots/build.gradle b/paparazzi/paparazzi-gradle-plugin/src/test/projects/rerun-snapshots/build.gradle deleted file mode 100644 index 8a55ad8185..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/rerun-snapshots/build.gradle +++ /dev/null @@ -1,21 +0,0 @@ -plugins { - id 'com.android.library' - id 'kotlin-android' - id 'app.cash.paparazzi' -} - -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - -android { - compileSdk libs.versions.compileSdk.get() as int - defaultConfig { - minSdk libs.versions.minSdk.get() as int - } -} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/test.settings.gradle b/paparazzi/paparazzi-gradle-plugin/src/test/projects/test.settings.gradle deleted file mode 100644 index 98939e6f45..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/test.settings.gradle +++ /dev/null @@ -1,7 +0,0 @@ -dependencyResolutionManagement { - versionCatalogs { - libs { - from(files("../../../../../gradle/libs.versions.toml")) - } - } -} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/text-appearances-code/build.gradle b/paparazzi/paparazzi-gradle-plugin/src/test/projects/text-appearances-code/build.gradle deleted file mode 100644 index 8a55ad8185..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/text-appearances-code/build.gradle +++ /dev/null @@ -1,21 +0,0 @@ -plugins { - id 'com.android.library' - id 'kotlin-android' - id 'app.cash.paparazzi' -} - -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - -android { - compileSdk libs.versions.compileSdk.get() as int - defaultConfig { - minSdk libs.versions.minSdk.get() as int - } -} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/text-appearances-code/src/test/resources/textappearances.png b/paparazzi/paparazzi-gradle-plugin/src/test/projects/text-appearances-code/src/test/resources/textappearances.png deleted file mode 100644 index d9405e298c..0000000000 Binary files a/paparazzi/paparazzi-gradle-plugin/src/test/projects/text-appearances-code/src/test/resources/textappearances.png and /dev/null differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/text-appearances-xml/build.gradle b/paparazzi/paparazzi-gradle-plugin/src/test/projects/text-appearances-xml/build.gradle deleted file mode 100644 index 8a55ad8185..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/text-appearances-xml/build.gradle +++ /dev/null @@ -1,21 +0,0 @@ -plugins { - id 'com.android.library' - id 'kotlin-android' - id 'app.cash.paparazzi' -} - -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - -android { - compileSdk libs.versions.compileSdk.get() as int - defaultConfig { - minSdk libs.versions.minSdk.get() as int - } -} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/text-appearances-xml/src/test/resources/textappearances.png b/paparazzi/paparazzi-gradle-plugin/src/test/projects/text-appearances-xml/src/test/resources/textappearances.png deleted file mode 100644 index d9405e298c..0000000000 Binary files a/paparazzi/paparazzi-gradle-plugin/src/test/projects/text-appearances-xml/src/test/resources/textappearances.png and /dev/null differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/build.gradle b/paparazzi/paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/build.gradle deleted file mode 100644 index 8a55ad8185..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/build.gradle +++ /dev/null @@ -1,21 +0,0 @@ -plugins { - id 'com.android.library' - id 'kotlin-android' - id 'app.cash.paparazzi' -} - -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - -android { - compileSdk libs.versions.compileSdk.get() as int - defaultConfig { - minSdk libs.versions.minSdk.get() as int - } -} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/src/test/resources/nexus_7_launch.png b/paparazzi/paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/src/test/resources/nexus_7_launch.png deleted file mode 100644 index 9e6c1cbd17..0000000000 Binary files a/paparazzi/paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/src/test/resources/nexus_7_launch.png and /dev/null differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/src/test/resources/pixel_3_launch.png b/paparazzi/paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/src/test/resources/pixel_3_launch.png deleted file mode 100644 index d275be855c..0000000000 Binary files a/paparazzi/paparazzi-gradle-plugin/src/test/projects/update-paparazzi-config/src/test/resources/pixel_3_launch.png and /dev/null differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-aapt-code/src/test/resources/card_chip.png b/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-aapt-code/src/test/resources/card_chip.png deleted file mode 100644 index b29df33800..0000000000 Binary files a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-aapt-code/src/test/resources/card_chip.png and /dev/null differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-aapt-compose/build.gradle b/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-aapt-compose/build.gradle deleted file mode 100644 index f6632c7cdb..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-aapt-compose/build.gradle +++ /dev/null @@ -1,31 +0,0 @@ -plugins { - id 'com.android.library' - id 'kotlin-android' - id 'app.cash.paparazzi' -} - -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - -android { - compileSdk libs.versions.compileSdk.get() as int - defaultConfig { - minSdk libs.versions.minSdk.get() as int - } - buildFeatures { - compose true - } - composeOptions { - kotlinCompilerExtensionVersion libs.versions.composeCompiler.get() - } -} - -dependencies { - implementation libs.composeUi.material -} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-aapt-compose/src/test/resources/compose_card_chip.png b/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-aapt-compose/src/test/resources/compose_card_chip.png deleted file mode 100644 index 33058fde3a..0000000000 Binary files a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-aapt-compose/src/test/resources/compose_card_chip.png and /dev/null differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-aapt-xml/src/test/resources/card_chip.png b/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-aapt-xml/src/test/resources/card_chip.png deleted file mode 100644 index b29df33800..0000000000 Binary files a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-aapt-xml/src/test/resources/card_chip.png and /dev/null differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure-multiple-modules/module/build.gradle b/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure-multiple-modules/module/build.gradle deleted file mode 100644 index 932b33f0e4..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure-multiple-modules/module/build.gradle +++ /dev/null @@ -1,21 +0,0 @@ -plugins { - id 'com.android.library' - id 'kotlin-android' - id 'app.cash.paparazzi' -} - -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - -android { - compileSdk libs.versions.compileSdk.get() as int - defaultConfig { - minSdk libs.versions.minSdk.get() as int - } -} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure-multiple-modules/module/src/test/resources/expected_delta.png b/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure-multiple-modules/module/src/test/resources/expected_delta.png deleted file mode 100644 index 4ac6fa4a17..0000000000 Binary files a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure-multiple-modules/module/src/test/resources/expected_delta.png and /dev/null differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure/build.gradle b/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure/build.gradle deleted file mode 100644 index 8a55ad8185..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure/build.gradle +++ /dev/null @@ -1,21 +0,0 @@ -plugins { - id 'com.android.library' - id 'kotlin-android' - id 'app.cash.paparazzi' -} - -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - -android { - compileSdk libs.versions.compileSdk.get() as int - defaultConfig { - minSdk libs.versions.minSdk.get() as int - } -} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure/src/test/resources/expected_delta.png b/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure/src/test/resources/expected_delta.png deleted file mode 100644 index 4ac6fa4a17..0000000000 Binary files a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-failure/src/test/resources/expected_delta.png and /dev/null differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-success-multiple-modules/module/build.gradle b/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-success-multiple-modules/module/build.gradle deleted file mode 100644 index 932b33f0e4..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-success-multiple-modules/module/build.gradle +++ /dev/null @@ -1,21 +0,0 @@ -plugins { - id 'com.android.library' - id 'kotlin-android' - id 'app.cash.paparazzi' -} - -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - -android { - compileSdk libs.versions.compileSdk.get() as int - defaultConfig { - minSdk libs.versions.minSdk.get() as int - } -} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-success-multiple-modules/module/src/test/snapshots/images/app.cash.paparazzi.plugin.test_VerifyTest_verify.png b/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-success-multiple-modules/module/src/test/snapshots/images/app.cash.paparazzi.plugin.test_VerifyTest_verify.png deleted file mode 100644 index 8eea5ec76c..0000000000 Binary files a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-success-multiple-modules/module/src/test/snapshots/images/app.cash.paparazzi.plugin.test_VerifyTest_verify.png and /dev/null differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-success/build.gradle b/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-success/build.gradle deleted file mode 100644 index 8a55ad8185..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-success/build.gradle +++ /dev/null @@ -1,21 +0,0 @@ -plugins { - id 'com.android.library' - id 'kotlin-android' - id 'app.cash.paparazzi' -} - -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - -android { - compileSdk libs.versions.compileSdk.get() as int - defaultConfig { - minSdk libs.versions.minSdk.get() as int - } -} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-success/src/test/snapshots/images/app.cash.paparazzi.plugin.test_VerifyTest_verify.png b/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-success/src/test/snapshots/images/app.cash.paparazzi.plugin.test_VerifyTest_verify.png deleted file mode 100644 index 8eea5ec76c..0000000000 Binary files a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-mode-success/src/test/snapshots/images/app.cash.paparazzi.plugin.test_VerifyTest_verify.png and /dev/null differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-rendering-modes/build.gradle b/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-rendering-modes/build.gradle deleted file mode 100644 index d18a133e4d..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-rendering-modes/build.gradle +++ /dev/null @@ -1,21 +0,0 @@ -plugins { - id 'com.android.library' - id 'kotlin-android' - id 'app.cash.paparazzi' -} - -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - -android { - compileSdk libs.versions.compileSdk.get() as int - defaultConfig { - minSdk libs.versions.minSdk.get() as int - } -} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-rendering-modes/src/test/resources/horizontal_scroll.png b/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-rendering-modes/src/test/resources/horizontal_scroll.png deleted file mode 100644 index dd67dc73e0..0000000000 Binary files a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-rendering-modes/src/test/resources/horizontal_scroll.png and /dev/null differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-rendering-modes/src/test/resources/normal.png b/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-rendering-modes/src/test/resources/normal.png deleted file mode 100644 index 43bd968a19..0000000000 Binary files a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-rendering-modes/src/test/resources/normal.png and /dev/null differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-rendering-modes/src/test/resources/vertical_scroll.png b/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-rendering-modes/src/test/resources/vertical_scroll.png deleted file mode 100644 index e3b714a0fd..0000000000 Binary files a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-rendering-modes/src/test/resources/vertical_scroll.png and /dev/null differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-resources-java/build.gradle b/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-resources-java/build.gradle deleted file mode 100644 index e7159b5477..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-resources-java/build.gradle +++ /dev/null @@ -1,20 +0,0 @@ -plugins { - id 'com.android.library' - id 'app.cash.paparazzi' -} - -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - -android { - compileSdk libs.versions.compileSdk.get() as int - defaultConfig { - minSdk libs.versions.minSdk.get() as int - } -} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-resources-kotlin/build.gradle b/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-resources-kotlin/build.gradle deleted file mode 100644 index 8a55ad8185..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-resources-kotlin/build.gradle +++ /dev/null @@ -1,21 +0,0 @@ -plugins { - id 'com.android.library' - id 'kotlin-android' - id 'app.cash.paparazzi' -} - -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - -android { - compileSdk libs.versions.compileSdk.get() as int - defaultConfig { - minSdk libs.versions.minSdk.get() as int - } -} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-snapshot/build.gradle b/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-snapshot/build.gradle deleted file mode 100644 index 8a55ad8185..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-snapshot/build.gradle +++ /dev/null @@ -1,21 +0,0 @@ -plugins { - id 'com.android.library' - id 'kotlin-android' - id 'app.cash.paparazzi' -} - -repositories { - maven { - url "file://${projectDir.absolutePath}/../../../../../build/localMaven" - } - mavenCentral() - //mavenLocal() - google() -} - -android { - compileSdk libs.versions.compileSdk.get() as int - defaultConfig { - minSdk libs.versions.minSdk.get() as int - } -} diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-snapshot/src/test/resources/launch.png b/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-snapshot/src/test/resources/launch.png deleted file mode 100644 index 70ad683354..0000000000 Binary files a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-snapshot/src/test/resources/launch.png and /dev/null differ diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-svgs/src/main/res/drawable/arrow_up.xml b/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-svgs/src/main/res/drawable/arrow_up.xml deleted file mode 100644 index e435070317..0000000000 --- a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-svgs/src/main/res/drawable/arrow_up.xml +++ /dev/null @@ -1,18 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:height="16dp" - android:tint="#000000" - android:viewportHeight="16" - android:viewportWidth="16" - android:width="16dp"> - - <path - android:fillColor="#333333" - android:pathData="M2.11612 6.61612C1.62796 7.10427 1.62796 7.89573 2.11612 8.38388C2.60427 8.87204 3.39573 8.87204 3.88388 8.38388L2.11612 6.61612ZM8 2.5L8.88388 1.61612C8.39573 1.12796 7.60427 1.12796 7.11612 1.61612L8 2.5ZM12.1161 8.38388C12.6043 8.87204 13.3957 8.87204 13.8839 8.38388C14.372 7.89573 14.372 7.10427 13.8839 6.61612L12.1161 8.38388ZM3.88388 8.38388L8.88388 3.38388L7.11612 1.61612L2.11612 6.61612L3.88388 8.38388ZM7.11612 3.38388L12.1161 8.38388L13.8839 6.61612L8.88388 1.61612L7.11612 3.38388Z" /> - - <path - android:pathData="M8 2.5V13.5" - android:strokeColor="#333333" - android:strokeLineCap="round" - android:strokeWidth="2.5" /> -</vector> diff --git a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-svgs/src/test/resources/arrow_up.png b/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-svgs/src/test/resources/arrow_up.png deleted file mode 100644 index ead29fa744..0000000000 Binary files a/paparazzi/paparazzi-gradle-plugin/src/test/projects/verify-svgs/src/test/resources/arrow_up.png and /dev/null differ diff --git a/paparazzi/paparazzi/build.gradle b/paparazzi/paparazzi/build.gradle deleted file mode 100644 index 01dac05ed7..0000000000 --- a/paparazzi/paparazzi/build.gradle +++ /dev/null @@ -1,130 +0,0 @@ -import org.jetbrains.kotlin.gradle.plugin.KotlinPluginKt - -apply plugin: 'org.jetbrains.kotlin.jvm' -apply plugin: 'org.jetbrains.kotlin.kapt' -apply plugin: 'org.jetbrains.dokka' -apply plugin: 'com.vanniktech.maven.publish' - -sourceCompatibility = libs.versions.javaTarget.get() -targetCompatibility = libs.versions.javaTarget.get() - -def artifactType = Attribute.of('artifactType', String) - -configurations { - unzip { - attributes.attribute(artifactType, ArtifactTypeDefinition.DIRECTORY_TYPE) - } -} - -dependencies { - registerTransform(org.gradle.api.internal.artifacts.transform.UnzipTransform) { - from.attribute(artifactType, ArtifactTypeDefinition.JAR_TYPE) - to.attribute(artifactType, ArtifactTypeDefinition.DIRECTORY_TYPE) - } -} - -dependencies { - // Paparazzi is a Kotlin JVM module and thus cannot depend on Android library artifacts (AARs) - // TODO: Use Gradle transforms to extract/rename classes.jar's from AARs - api files('libs/compose-runtime-1.2.1.jar') - compileOnly files('libs/compose-ui-1.2.1.jar') - compileOnly libs.androidx.lifecycleCommon - compileOnly files('libs/lifecycle-runtime-2.5.0.jar') - compileOnly files('libs/savedstate-1.2.0.jar') - - api libs.layoutlib.native.jdk11 - api libs.tools.common - api libs.tools.layoutlib - api libs.tools.ninepatch - api libs.tools.sdkCommon - api libs.kxml2 - api libs.junit - api libs.androidx.annotations - api libs.guava - api libs.kotlinx.coroutines.core - api libs.okio - api platform(libs.kotlin.bom) - implementation libs.moshi.core - implementation libs.moshi.adapters - kapt libs.moshi.kotlinCodegen - implementation libs.jcodec.core - implementation libs.jcodec.javase - implementation projects.paparazziAgent - - def osName = System.getProperty("os.name").toLowerCase(Locale.US) - if (osName.startsWith("mac")) { - def osArch = System.getProperty("os.arch").toLowerCase(Locale.US) - if (osArch.startsWith("x86")) { - unzip libs.layoutlib.native.macOsX - } else { - unzip libs.layoutlib.native.macArm - } - } else if (osName.startsWith("windows")) { - unzip libs.layoutlib.native.windows - } else { - unzip libs.layoutlib.native.linux - } - - testImplementation libs.assertj - - add(KotlinPluginKt.PLUGIN_CLASSPATH_CONFIGURATION_NAME, libs.compose.compiler) -} - -tasks.named("dokkaGfm").configure { - outputDirectory = rootProject.file("../docs/1.x") - - dokkaSourceSets.named("main") { - configureEach { - reportUndocumented = false - skipDeprecated = true - jdkVersion = 8 - perPackageOption { - prefix = "app.cash.paparazzi.internal" - suppress = true - } - } - } -} - -def generateTestConfig = tasks.register("generateTestConfig") { - def resources = "$buildDir/intermediates/paparazzi/resources.txt" - outputs.file(resources) - - doLast { - File configFile = new File(resources) - configFile.withWriter('utf-8') { writer -> - writer.writeLine("app.cash.paparazzi") - writer.writeLine(".") - writer.writeLine("31") - writer.writeLine("platforms/android-31/") - writer.writeLine(".") - writer.writeLine("app.cash.paparazzi") - } - } -} - -tasks.withType(Test).configureEach { - dependsOn(generateTestConfig) - systemProperty( - "paparazzi.test.resources", - generateTestConfig.map { it.outputs.files.singleFile }.get().path - ) - systemProperty( - "paparazzi.build.dir", - project.layout.buildDirectory.get().toString() - ) - systemProperty( - "paparazzi.platform.data.root", - configurations.unzip.singleFile.absolutePath - ) - // Uncomment to debug JNI issues in layoutlib - // jvmArgs '-Xcheck:jni' - testLogging { - events 'passed', 'failed', 'skipped', 'standardOut', 'standardError' - exceptionFormat 'FULL' - showCauses true - showExceptions true - showStackTraces true - showStandardStreams true - } -} diff --git a/paparazzi/paparazzi/gradle.properties b/paparazzi/paparazzi/gradle.properties deleted file mode 100644 index 13d36287e9..0000000000 --- a/paparazzi/paparazzi/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -POM_ARTIFACT_ID=paparazzi -POM_NAME=Paparazzi -POM_DESCRIPTION=An Android library to render your application screens without a physical device or emulator -POM_PACKAGING=jar diff --git a/paparazzi/paparazzi/libs/compose-runtime-1.2.1.jar b/paparazzi/paparazzi/libs/compose-runtime-1.2.1.jar deleted file mode 100644 index ab4a42593b..0000000000 Binary files a/paparazzi/paparazzi/libs/compose-runtime-1.2.1.jar and /dev/null differ diff --git a/paparazzi/paparazzi/libs/compose-ui-1.2.1.jar b/paparazzi/paparazzi/libs/compose-ui-1.2.1.jar deleted file mode 100644 index 04217bd422..0000000000 Binary files a/paparazzi/paparazzi/libs/compose-ui-1.2.1.jar and /dev/null differ diff --git a/paparazzi/paparazzi/libs/lifecycle-runtime-2.5.0.jar b/paparazzi/paparazzi/libs/lifecycle-runtime-2.5.0.jar deleted file mode 100644 index a440631455..0000000000 Binary files a/paparazzi/paparazzi/libs/lifecycle-runtime-2.5.0.jar and /dev/null differ diff --git a/paparazzi/paparazzi/libs/savedstate-1.2.0.jar b/paparazzi/paparazzi/libs/savedstate-1.2.0.jar deleted file mode 100644 index d791aa10a5..0000000000 Binary files a/paparazzi/paparazzi/libs/savedstate-1.2.0.jar and /dev/null differ diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Flags.kt b/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Flags.kt deleted file mode 100644 index 62c0b620a0..0000000000 --- a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Flags.kt +++ /dev/null @@ -1,5 +0,0 @@ -package app.cash.paparazzi - -object Flags { - const val DEBUG_LINKED_OBJECTS = "app.cash.paparazzi.debug.linked.objects" -} diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/accessibility/AccessibilityRenderExtension.kt b/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/accessibility/AccessibilityRenderExtension.kt deleted file mode 100644 index f6f16aa343..0000000000 --- a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/accessibility/AccessibilityRenderExtension.kt +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright (C) 2021 Square, Inc. - * - * 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. - */ -package app.cash.paparazzi.accessibility - -import android.graphics.drawable.GradientDrawable -import android.graphics.drawable.LayerDrawable -import android.util.TypedValue -import android.view.View -import android.view.ViewGroup -import android.widget.LinearLayout -import android.widget.TextView -import app.cash.paparazzi.RenderExtension -import app.cash.paparazzi.accessibility.RenderSettings.DEFAULT_DESCRIPTION_BACKGROUND_COLOR -import app.cash.paparazzi.accessibility.RenderSettings.DEFAULT_RECT_SIZE -import app.cash.paparazzi.accessibility.RenderSettings.DEFAULT_RENDER_ALPHA -import app.cash.paparazzi.accessibility.RenderSettings.DEFAULT_TEXT_COLOR -import app.cash.paparazzi.accessibility.RenderSettings.DEFAULT_TEXT_SIZE -import app.cash.paparazzi.accessibility.RenderSettings.getColor -import app.cash.paparazzi.accessibility.RenderSettings.toColorInt -import app.cash.paparazzi.accessibility.RenderSettings.withAlpha - -class AccessibilityRenderExtension : RenderExtension { - override fun renderView( - contentView: View - ): View { - val accessibilityViews = contentView.findAccessibilityViews() - accessibilityViews.forEach { view -> - val color = getColor(view) - val colorInt = color.toColorInt() - - val colorDrawable = GradientDrawable( - GradientDrawable.Orientation.TOP_BOTTOM, - intArrayOf(colorInt, colorInt) - ).apply { - setStroke(2, color.withAlpha(DEFAULT_RENDER_ALPHA * 2).toColorInt()) - } - - view.foreground = view.foreground?.let { drawable -> - // If there is an existing foreground layer the color on top of it. - LayerDrawable(arrayOf(drawable, colorDrawable)) - } ?: colorDrawable - } - - return LinearLayout(contentView.context).apply { - orientation = LinearLayout.HORIZONTAL - weightSum = 2f - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - ) - - val contentLayoutParams = contentView.layoutParams ?: generateLayoutParams(null) - addView( - contentView, - LinearLayout.LayoutParams( - contentLayoutParams.width, - contentLayoutParams.height, - 1f - ) - ) - addView( - buildAccessibilityView(contentView), - LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT, - 1f - ) - ) - } - } - - private fun View.findAccessibilityViews(): List<View> { - val accessibilityViews = mutableListOf<View>() - if (isImportantForAccessibility && !iterableTextForAccessibility.isNullOrBlank()) { - accessibilityViews.add(this) - } - - if (this is ViewGroup) { - (0 until childCount).forEach { - accessibilityViews += getChildAt(it).findAccessibilityViews() - } - } - - return accessibilityViews - } - - private fun buildAccessibilityView(contentView: View): View { - val linearLayout = LinearLayout(contentView.context).apply { - orientation = LinearLayout.VERTICAL - setBackgroundColor(DEFAULT_DESCRIPTION_BACKGROUND_COLOR.toColorInt()) - } - - fun renderAccessibility(view: View) { - if (view.isImportantForAccessibility && !view.iterableTextForAccessibility.isNullOrBlank()) { - linearLayout.addView(buildAccessibilityRow(view, view.iterableTextForAccessibility)) - } - - if (view is ViewGroup) { - (0 until view.childCount).forEach { - renderAccessibility(view.getChildAt(it)) - } - } - } - - renderAccessibility(contentView) - return linearLayout - } - - private fun buildAccessibilityRow(view: View, iterableTextForAccessibility: CharSequence): View { - val context = view.context - val color = getColor(view).toColorInt() - val margin = view.dip(8) - val innerMargin = view.dip(4) - - return LinearLayout(context).apply { - orientation = LinearLayout.HORIZONTAL - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ) - setPaddingRelative(margin, innerMargin, margin, innerMargin) - - addView( - View(context).apply { - layoutParams = ViewGroup.LayoutParams(dip(DEFAULT_RECT_SIZE), dip(DEFAULT_RECT_SIZE)) - background = GradientDrawable( - GradientDrawable.Orientation.TOP_BOTTOM, - intArrayOf(color, color) - ).apply { - cornerRadius = dip(DEFAULT_RECT_SIZE / 4f) - } - setPaddingRelative(innerMargin, innerMargin, innerMargin, innerMargin) - } - ) - addView( - TextView(context).apply { - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ) - text = iterableTextForAccessibility - textSize = DEFAULT_TEXT_SIZE - setTextColor(DEFAULT_TEXT_COLOR.toColorInt()) - setPaddingRelative(innerMargin, 0, innerMargin, 0) - } - ) - } - } -} - -private fun View.dip(value: Float): Float = - TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - value, - resources.displayMetrics - ) - -private fun View.dip(value: Int): Int = dip(value.toFloat()).toInt() diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/Renderer.kt b/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/Renderer.kt deleted file mode 100644 index f1d06c9ada..0000000000 --- a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/Renderer.kt +++ /dev/null @@ -1,216 +0,0 @@ -/* - * Copyright (C) 2016 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. - */ - -package app.cash.paparazzi.internal - -import app.cash.paparazzi.DeviceConfig -import app.cash.paparazzi.Environment -import app.cash.paparazzi.Flags -import app.cash.paparazzi.internal.parsers.LayoutPullParser -import com.android.ide.common.rendering.api.SessionParams -import com.android.ide.common.resources.deprecated.FrameworkResources -import com.android.ide.common.resources.deprecated.ResourceItem -import com.android.ide.common.resources.deprecated.ResourceRepository -import com.android.io.FolderWrapper -import com.android.layoutlib.bridge.Bridge -import com.android.layoutlib.bridge.android.RenderParamsFlags -import com.android.layoutlib.bridge.impl.DelegateManager -import java.awt.image.BufferedImage -import java.io.Closeable -import java.io.File -import java.io.IOException -import java.util.Locale - -/** View rendering. */ -internal class Renderer( - private val environment: Environment, - private val layoutlibCallback: PaparazziCallback, - private val logger: PaparazziLogger, - private val maxPercentDifference: Double -) : Closeable { - private var bridge: Bridge? = null - private lateinit var sessionParamsBuilder: SessionParamsBuilder - - /** Initialize the bridge and the resource maps. */ - fun prepare(): SessionParamsBuilder { - val platformDataResDir = File("${environment.platformDir}/data/res") - val frameworkResources = FrameworkResources(FolderWrapper(platformDataResDir)).apply { - loadResources() - loadPublicResources(logger) - } - - val projectResources = object : ResourceRepository(FolderWrapper(environment.resDir), false) { - override fun createResourceItem(name: String): ResourceItem { - return ResourceItem(name) - } - } - projectResources.loadResources() - - sessionParamsBuilder = SessionParamsBuilder( - layoutlibCallback = layoutlibCallback, - logger = logger, - frameworkResources = frameworkResources, - projectResources = projectResources, - assetRepository = PaparazziAssetRepository(environment.assetsDir) - ) - .plusFlag(RenderParamsFlags.FLAG_DO_NOT_RENDER_ON_CREATE, true) - .withTheme("AppTheme", true) - - val platformDataRoot = System.getProperty("paparazzi.platform.data.root") - ?: throw RuntimeException("Missing system property for 'paparazzi.platform.data.root'") - val platformDataDir = File(platformDataRoot, "data") - val fontLocation = File(platformDataDir, "fonts") - val nativeLibLocation = File(platformDataDir, getNativeLibDir()) - val icuLocation = File(platformDataDir, "icu" + File.separator + "icudt68l.dat") - val buildProp = File(environment.platformDir, "build.prop") - val attrs = File(platformDataResDir, "values" + File.separator + "attrs.xml") - val systemProperties = DeviceConfig.loadProperties(buildProp) + mapOf( - // We want Choreographer.USE_FRAME_TIME to be false so it uses System_Delegate.nanoTime() - "debug.choreographer.frametime" to "false" - ) - bridge = Bridge().apply { - check( - init( - systemProperties, - fontLocation, - nativeLibLocation.path, - icuLocation.path, - DeviceConfig.getEnumMap(attrs), - logger - ) - ) { "Failed to init Bridge." } - } - Bridge.getLock() - .lock() - try { - Bridge.setLog(logger) - } finally { - Bridge.getLock() - .unlock() - } - - return sessionParamsBuilder - } - - private fun getNativeLibDir(): String { - val osName = System.getProperty("os.name").toLowerCase(Locale.US) - val osLabel = when { - osName.startsWith("windows") -> "win" - osName.startsWith("mac") -> { - val osArch = System.getProperty("os.arch").lowercase(Locale.US) - if (osArch.startsWith("x86")) "mac" else "mac-arm" - } - else -> "linux" - } - return "$osLabel/lib64" - } - - override fun close() { - bridge = null - - Gc.gc() - - dumpDelegates() - } - - fun dumpDelegates() { - if (System.getProperty(Flags.DEBUG_LINKED_OBJECTS) != null) { - println("Objects still linked from the DelegateManager:") - DelegateManager.dump(System.out) - } - } - - fun render( - bridge: com.android.ide.common.rendering.api.Bridge, - params: SessionParams, - frameTimeNanos: Long - ): RenderResult { - val session = bridge.createSession(params) - - try { - if (frameTimeNanos != -1L) { - session.setElapsedFrameTimeNanos(frameTimeNanos) - } - - if (!session.result.isSuccess) { - logger.error(session.result.exception, session.result.errorMessage) - } else { - // Render the session with a timeout of 50s. - val renderResult = session.render(50000) - if (!renderResult.isSuccess) { - logger.error(session.result.exception, session.result.errorMessage) - } - } - - return session.toResult() - } finally { - session.dispose() - } - } - - /** Compares the golden image with the passed image. */ - fun verify( - goldenImageName: String, - image: BufferedImage - ) { - try { - val goldenImagePath = environment.appTestDir + "/golden/" + goldenImageName - ImageUtils.requireSimilar(goldenImagePath, image, maxPercentDifference) - } catch (e: IOException) { - logger.error(e, e.message) - } - } - - /** - * Create a new rendering session and test that rendering the given layout doesn't throw any - * exceptions and matches the provided image. - * - * If frameTimeNanos is >= 0 a frame will be executed during the rendering. The time indicates - * how far in the future is. - */ - @JvmOverloads - fun renderAndVerify( - sessionParams: SessionParams, - goldenFileName: String, - frameTimeNanos: Long = -1 - ): RenderResult { - val result = render(bridge!!, sessionParams, frameTimeNanos) - verify(goldenFileName, result.image) - return result - } - - fun createParserFromPath(layoutPath: String): LayoutPullParser = - LayoutPullParser.createFromPath("${environment.resDir}/layout/$layoutPath") - - /** - * Create a new rendering session and test that rendering the given layout on given device - * doesn't throw any exceptions and matches the provided image. - */ - @JvmOverloads - fun renderAndVerify( - layoutFileName: String, - goldenFileName: String, - deviceConfig: DeviceConfig = DeviceConfig.NEXUS_5 - ): RenderResult { - val sessionParams = sessionParamsBuilder - .copy( - layoutPullParser = createParserFromPath(layoutFileName), - deviceConfig = deviceConfig - ) - .build() - return renderAndVerify(sessionParams, goldenFileName) - } -} diff --git a/paparazzi/paparazzi/src/test/resources/accessibility-new-view.png b/paparazzi/paparazzi/src/test/resources/accessibility-new-view.png deleted file mode 100644 index 57f25b5ac9..0000000000 Binary files a/paparazzi/paparazzi/src/test/resources/accessibility-new-view.png and /dev/null differ diff --git a/paparazzi/paparazzi/src/test/resources/accessibility.png b/paparazzi/paparazzi/src/test/resources/accessibility.png deleted file mode 100644 index d7f3a16ddb..0000000000 Binary files a/paparazzi/paparazzi/src/test/resources/accessibility.png and /dev/null differ diff --git a/paparazzi/paparazzi/src/test/resources/without-layout-params.png b/paparazzi/paparazzi/src/test/resources/without-layout-params.png deleted file mode 100644 index e3da5cb566..0000000000 Binary files a/paparazzi/paparazzi/src/test/resources/without-layout-params.png and /dev/null differ diff --git a/paparazzi/settings.gradle b/paparazzi/settings.gradle deleted file mode 100644 index a037f41ea6..0000000000 --- a/paparazzi/settings.gradle +++ /dev/null @@ -1,20 +0,0 @@ -rootProject.name = 'paparazzi-libs' - -include ':paparazzi' -include ':paparazzi-agent' -include ':paparazzi-gradle-plugin' -include ':libs:layoutlib' -include ':libs:native-macarm' -include ':libs:native-macosx' -include ':libs:native-win' -include ':libs:native-linux' - -enableFeaturePreview('TYPESAFE_PROJECT_ACCESSORS') - -dependencyResolutionManagement { - versionCatalogs { - libs { - from(files("../gradle/libs.versions.toml")) - } - } -} diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/DeviceConfig.kt b/paparazzi/src/main/java/app/cash/paparazzi/DeviceConfig.kt similarity index 99% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/DeviceConfig.kt rename to paparazzi/src/main/java/app/cash/paparazzi/DeviceConfig.kt index ec5568608b..9da6882e4d 100644 --- a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/DeviceConfig.kt +++ b/paparazzi/src/main/java/app/cash/paparazzi/DeviceConfig.kt @@ -106,7 +106,7 @@ data class DeviceConfig( countryCodeQualifier = CountryCodeQualifier() layoutDirectionQualifier = LayoutDirectionQualifier(layoutDirection) networkCodeQualifier = NetworkCodeQualifier() - localeQualifier = if (locale != null) LocaleQualifier.getQualifier(locale) else LocaleQualifier(LocaleQualifier.FAKE_VALUE) + localeQualifier = if (locale != null) LocaleQualifier.getQualifier(locale) else LocaleQualifier() versionQualifier = VersionQualifier() } diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Environment.kt b/paparazzi/src/main/java/app/cash/paparazzi/Environment.kt similarity index 75% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Environment.kt rename to paparazzi/src/main/java/app/cash/paparazzi/Environment.kt index 9155340fbc..86a5555600 100644 --- a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Environment.kt +++ b/paparazzi/src/main/java/app/cash/paparazzi/Environment.kt @@ -29,7 +29,12 @@ data class Environment( val assetsDir: String, val packageName: String, val compileSdkVersion: Int, - val resourcePackageNames: List<String> + val resourcePackageNames: List<String>, + val localResourceDirs: List<String>, + val moduleResourceDirs: List<String>, + val libraryResourceDirs: List<String>, + val allModuleAssetDirs: List<String>, + val libraryAssetDirs: List<String> ) { init { val platformDirPath = Path.of(platformDir) @@ -53,7 +58,9 @@ fun detectEnvironment(): Environment { val resourcesFile = File(System.getProperty("paparazzi.test.resources")) val configLines = resourcesFile.readLines() + val projectDir = Paths.get(System.getProperty("paparazzi.project.dir")) val appTestDir = Paths.get(System.getProperty("paparazzi.build.dir")) + val artifactsCacheDir = Paths.get(System.getProperty("paparazzi.artifacts.cache.dir")) val androidHome = Paths.get(androidHome()) return Environment( platformDir = androidHome.resolve(configLines[3]).toString(), @@ -62,10 +69,18 @@ fun detectEnvironment(): Environment { assetsDir = appTestDir.resolve(configLines[4]).toString(), packageName = configLines[0], compileSdkVersion = configLines[2].toInt(), - resourcePackageNames = configLines[5].split(",") + resourcePackageNames = configLines[5].split(), + localResourceDirs = configLines[6].split().map { projectDir.resolve(it).toString() }, + moduleResourceDirs = configLines[7].split().map { projectDir.resolve(it).toString() }, + libraryResourceDirs = configLines[8].split().map { artifactsCacheDir.resolve(it).toString() }, + allModuleAssetDirs = configLines[9].split().map { projectDir.resolve(it).toString() }, + libraryAssetDirs = configLines[10].split().map { artifactsCacheDir.resolve(it).toString() } ) } +private fun String.split(): List<String> = + this.split(",").filter { it.isNotEmpty() } + private fun androidSdkPath(): String { val osName = System.getProperty("os.name").lowercase(Locale.US) val sdkPathDir = if (osName.startsWith("windows")) { diff --git a/paparazzi/src/main/java/app/cash/paparazzi/Flags.kt b/paparazzi/src/main/java/app/cash/paparazzi/Flags.kt new file mode 100644 index 0000000000..85bad341f3 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/Flags.kt @@ -0,0 +1,7 @@ +package app.cash.paparazzi + +object Flags { + const val DEBUG_LINKED_OBJECTS = "app.cash.paparazzi.debug.linked.objects" + const val LEGACY_RESOURCE_LOADING = "app.cash.paparazzi.legacy.resource.loading" + const val LEGACY_ASSET_LOADING = "app.cash.paparazzi.legacy.asset.loading" +} diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/HtmlReportWriter.kt b/paparazzi/src/main/java/app/cash/paparazzi/HtmlReportWriter.kt similarity index 98% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/HtmlReportWriter.kt rename to paparazzi/src/main/java/app/cash/paparazzi/HtmlReportWriter.kt index e1258c5e5e..3a12cbcda5 100644 --- a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/HtmlReportWriter.kt +++ b/paparazzi/src/main/java/app/cash/paparazzi/HtmlReportWriter.kt @@ -291,4 +291,5 @@ internal fun String.sanitizeForFilename(): String? { return filenameSafeChars.negate().replaceFrom(toLowerCase(Locale.US), '_') } -private fun isRecordingDefault() = System.getProperty("paparazzi.test.record")?.toBoolean() == true +internal fun isRecordingDefault(): Boolean = + System.getProperty("paparazzi.test.record")?.toBoolean() == true diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/InstantAnimationsRule.kt b/paparazzi/src/main/java/app/cash/paparazzi/InstantAnimationsRule.kt similarity index 100% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/InstantAnimationsRule.kt rename to paparazzi/src/main/java/app/cash/paparazzi/InstantAnimationsRule.kt diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Paparazzi.kt b/paparazzi/src/main/java/app/cash/paparazzi/Paparazzi.kt similarity index 75% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Paparazzi.kt rename to paparazzi/src/main/java/app/cash/paparazzi/Paparazzi.kt index e3d36113cc..cf2d6f72bb 100644 --- a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Paparazzi.kt +++ b/paparazzi/src/main/java/app/cash/paparazzi/Paparazzi.kt @@ -27,36 +27,32 @@ import android.view.BridgeInflater import android.view.Choreographer import android.view.LayoutInflater import android.view.View +import android.view.View.NO_ID import android.view.ViewGroup -import android.view.ViewGroup.LayoutParams -import android.widget.FrameLayout +import androidx.activity.setViewTreeOnBackPressedDispatcherOwner import androidx.annotation.LayoutRes import androidx.compose.runtime.Composable -import androidx.compose.runtime.Recomposer -import androidx.compose.ui.platform.AndroidUiDispatcher import androidx.compose.ui.platform.ComposeView import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LifecycleRegistry -import androidx.lifecycle.ViewTreeLifecycleOwner -import androidx.savedstate.SavedStateRegistry -import androidx.savedstate.SavedStateRegistryController -import androidx.savedstate.SavedStateRegistryOwner +import androidx.lifecycle.setViewTreeLifecycleOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner import app.cash.paparazzi.agent.AgentTestRule import app.cash.paparazzi.agent.InterceptorRegistrar -import app.cash.paparazzi.internal.ChoreographerDelegateInterceptor -import app.cash.paparazzi.internal.EditModeInterceptor -import app.cash.paparazzi.internal.IInputMethodManagerInterceptor import app.cash.paparazzi.internal.ImageUtils -import app.cash.paparazzi.internal.MatrixMatrixMultiplicationInterceptor -import app.cash.paparazzi.internal.MatrixVectorMultiplicationInterceptor import app.cash.paparazzi.internal.PaparazziCallback +import app.cash.paparazzi.internal.PaparazziLifecycleOwner import app.cash.paparazzi.internal.PaparazziLogger +import app.cash.paparazzi.internal.PaparazziOnBackPressedDispatcherOwner +import app.cash.paparazzi.internal.PaparazziSavedStateRegistryOwner import app.cash.paparazzi.internal.Renderer -import app.cash.paparazzi.internal.ResourcesInterceptor -import app.cash.paparazzi.internal.ServiceManagerInterceptor import app.cash.paparazzi.internal.SessionParamsBuilder +import app.cash.paparazzi.internal.interceptors.ChoreographerDelegateInterceptor +import app.cash.paparazzi.internal.interceptors.EditModeInterceptor +import app.cash.paparazzi.internal.interceptors.IInputMethodManagerInterceptor +import app.cash.paparazzi.internal.interceptors.MatrixMatrixMultiplicationInterceptor +import app.cash.paparazzi.internal.interceptors.MatrixVectorMultiplicationInterceptor +import app.cash.paparazzi.internal.interceptors.ResourcesInterceptor +import app.cash.paparazzi.internal.interceptors.ServiceManagerInterceptor import app.cash.paparazzi.internal.parsers.LayoutPullParser import com.android.ide.common.rendering.api.RenderSession import com.android.ide.common.rendering.api.Result @@ -71,14 +67,18 @@ import com.android.layoutlib.bridge.BridgeRenderSession import com.android.layoutlib.bridge.impl.RenderAction import com.android.layoutlib.bridge.impl.RenderSessionImpl import com.android.resources.ScreenRound +import com.android.tools.idea.validator.LayoutValidator +import com.android.tools.idea.validator.ValidatorData.Level +import com.android.tools.idea.validator.ValidatorData.Policy +import com.android.tools.idea.validator.ValidatorData.Type import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement import java.awt.geom.Ellipse2D import java.awt.image.BufferedImage import java.util.Date +import java.util.EnumSet import java.util.concurrent.TimeUnit -import kotlin.coroutines.ContinuationInterceptor class Paparazzi @JvmOverloads constructor( private val environment: Environment = detectEnvironment(), @@ -89,7 +89,9 @@ class Paparazzi @JvmOverloads constructor( private val maxPercentDifference: Double = 0.1, private val snapshotHandler: SnapshotHandler = determineHandler(maxPercentDifference), private val renderExtensions: Set<RenderExtension> = setOf(), - private val supportsRtl: Boolean = false + private val supportsRtl: Boolean = false, + private val showSystemUi: Boolean = false, + private val validateAccessibility: Boolean = false ) : TestRule { private val logger = PaparazziLogger() private lateinit var renderSession: RenderSessionImpl @@ -107,9 +109,10 @@ class Paparazzi @JvmOverloads constructor( private val contentRoot = """ |<?xml version="1.0" encoding="utf-8"?> - |<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" - | android:layout_width="match_parent" - | android:layout_height="match_parent"/> + |<${if (hasComposeRuntime) "app.cash.paparazzi.internal.ComposeViewAdapter" else "FrameLayout"} + | xmlns:android="http://schemas.android.com/apk/res/android" + | android:layout_width="${if (renderingMode.horizAction == RenderingMode.SizeAction.SHRINK) "wrap_content" else "match_parent"}" + | android:layout_height="${if (renderingMode.vertAction == RenderingMode.SizeAction.SHRINK) "wrap_content" else "match_parent"}"/> """.trimMargin() override fun apply( @@ -144,8 +147,6 @@ class Paparazzi @JvmOverloads constructor( } fun prepare(description: Description) { - forcePlatformSdkVersion(environment.compileSdkVersion) - val layoutlibCallback = PaparazziCallback(logger, environment.packageName, environment.resourcePackageNames) layoutlibCallback.initResources() @@ -153,16 +154,18 @@ class Paparazzi @JvmOverloads constructor( testName = description.toTestName() if (!isInitialized) { - renderer = Renderer(environment, layoutlibCallback, logger, maxPercentDifference) + renderer = Renderer(environment, layoutlibCallback, logger) sessionParamsBuilder = renderer.prepare() } + forcePlatformSdkVersion(environment.compileSdkVersion) sessionParamsBuilder = sessionParamsBuilder .copy( layoutPullParser = LayoutPullParser.createFromString(contentRoot), deviceConfig = deviceConfig, renderingMode = renderingMode, - supportsRtl = supportsRtl + supportsRtl = supportsRtl, + decor = showSystemUi ) .withTheme(theme) @@ -194,19 +197,9 @@ class Paparazzi @JvmOverloads constructor( fun snapshot(name: String? = null, composable: @Composable () -> Unit) { val hostView = ComposeView(context) - // During onAttachedToWindow, AbstractComposeView will attempt to resolve its parent's - // CompositionContext, which requires first finding the "content view", then using that to - // find a root view with a ViewTreeLifecycleOwner - val parent = FrameLayout(context).apply { id = android.R.id.content } - parent.addView(hostView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) - PaparazziComposeOwner.register(parent) hostView.setContent(composable) - try { - snapshot(parent, name) - } finally { - forceReleaseComposeReferenceLeaks() - } + snapshot(hostView, name) } @JvmOverloads @@ -240,6 +233,7 @@ class Paparazzi @JvmOverloads constructor( "Calling unsafeUpdateConfig requires at least one non-null argument." } + logger.flushErrors() renderSession.release() bridgeRenderSession.dispose() cleanupThread() @@ -292,6 +286,27 @@ class Paparazzi @JvmOverloads constructor( // Initialize the choreographer at time=0. } + if (hasComposeRuntime) { + // During onAttachedToWindow, AbstractComposeView will attempt to resolve its parent's + // CompositionContext, which requires first finding the "content view", then using that + // to find a root view with a ViewTreeLifecycleOwner + viewGroup.id = android.R.id.content + } + + if (hasLifecycleOwnerRuntime) { + val lifecycleOwner = PaparazziLifecycleOwner() + modifiedView.setViewTreeLifecycleOwner(lifecycleOwner) + + if (hasSavedStateRegistryOwnerRuntime) { + modifiedView.setViewTreeSavedStateRegistryOwner(PaparazziSavedStateRegistryOwner(lifecycleOwner)) + } + if (hasAndroidxActivityRuntime) { + modifiedView.setViewTreeOnBackPressedDispatcherOwner(PaparazziOnBackPressedDispatcherOwner(lifecycleOwner)) + } + // Must be changed after the SavedStateRegistryOwner above has finished restoring its state. + lifecycleOwner.registry.currentState = Lifecycle.State.RESUMED + } + viewGroup.addView(modifiedView) for (frame in 0 until frameCount) { val nowNanos = (startNanos + (frame * 1_000_000_000.0 / fps)).toLong() @@ -302,12 +317,21 @@ class Paparazzi @JvmOverloads constructor( } val image = bridgeRenderSession.image + if (validateAccessibility) { + require(renderExtensions.isEmpty()) { + "Running accessibility validation and render extensions simultaneously is not supported." + } + validateLayoutAccessibility(modifiedView, image) + } frameHandler.handle(scaleImage(frameImage(image))) } } } finally { viewGroup.removeView(modifiedView) AnimationHandler.sAnimatorHandler.set(null) + if (hasComposeRuntime) { + forceReleaseComposeReferenceLeaks() + } } } } @@ -328,9 +352,7 @@ class Paparazzi @JvmOverloads constructor( try { areCallbacksRunningField.setBoolean(choreographer, true) - // https://android.googlesource.com/platform/frameworks/layoutlib/+/d58aa4703369e109b24419548f38b422d5a44738/bridge/src/com/android/layoutlib/bridge/BridgeRenderSession.java#171 - // BridgeRenderSession.executeCallbacks aggressively tears down the main Looper and BridgeContext, so we call the static delegates ourselves. - Handler_Delegate.executeCallbacks() + executeHandlerCallbacks() val currentTimeMs = SystemClock_Delegate.uptimeMillis() val choreographerCallbacks = RenderAction.getCurrentContext().sessionInteractiveData.choreographerCallbacks @@ -373,7 +395,8 @@ class Paparazzi @JvmOverloads constructor( } private fun frameImage(image: BufferedImage): BufferedImage { - if (deviceConfig.screenRound == ScreenRound.ROUND) { + // On device sized screenshot, we should apply any device specific shapes. + if (renderingMode == RenderingMode.NORMAL && deviceConfig.screenRound == ScreenRound.ROUND) { val newImage = BufferedImage(image.width, image.height, image.type) val g = newImage.createGraphics() g.clip = Ellipse2D.Float(0f, 0f, image.height.toFloat(), image.width.toFloat()) @@ -386,7 +409,35 @@ class Paparazzi @JvmOverloads constructor( private fun scaleImage(image: BufferedImage): BufferedImage { val scale = ImageUtils.getThumbnailScale(image) - return ImageUtils.scale(image, scale, scale) + // Only scale images down so we don't waste storage space enlarging smaller layouts. + return if (scale < 1f) ImageUtils.scale(image, scale, scale) else image + } + + private fun validateLayoutAccessibility(view: View, image: BufferedImage? = null) { + LayoutValidator.updatePolicy( + Policy( + EnumSet.of(Type.ACCESSIBILITY, Type.RENDER, Type.INTERNAL_ERROR), + EnumSet.of(Level.ERROR, Level.WARNING) + ) + ) + + val validationResults = LayoutValidator.validate(view, image, 1f, 1f) + validationResults.issues.forEach { issue -> + val issueViewId = validationResults.srcMap[issue.mSrcId]?.id ?: NO_ID + val issueViewName = if (issueViewId != NO_ID) { + view.resources.getResourceName(issueViewId) + } else { + "no-id" + } + + logger.warning( + format = "\u001B[33mAccessibility issue of type {0} on {1}:\u001B[0m {2} \nSee: {3}", + issue.mCategory, + issueViewName, + issue.mMsg, + issue.mHelpfulUrl + ) + } } private fun Description.toTestName(): TestName { @@ -549,64 +600,19 @@ class Paparazzi @JvmOverloads constructor( } private fun forceReleaseComposeReferenceLeaks() { - val snapshotClass = Class.forName("androidx.compose.runtime.snapshots.SnapshotKt") - val applyObservers = snapshotClass - .getDeclaredField("applyObservers") - .apply { isAccessible = true } - .get(null) as MutableList<*> - val applyObserver = applyObservers.getOrNull(0) - if (applyObserver != null) { - val recomposer = applyObserver.javaClass - .getDeclaredField("this\$0") - .apply { isAccessible = true } - .get(applyObserver) as Recomposer - val compositionInvalidations = recomposer.javaClass - .getDeclaredField("compositionInvalidations") - .apply { isAccessible = true } - .get(recomposer) as MutableList<*> - val snapshotInvalidations = recomposer.javaClass - .getDeclaredField("snapshotInvalidations") - .apply { isAccessible = true } - .get(recomposer) as MutableList<*> - compositionInvalidations.clear() - snapshotInvalidations.clear() - applyObservers.clear() - } - - val dispatcher = - AndroidUiDispatcher.CurrentThread[ContinuationInterceptor] as AndroidUiDispatcher - val toRunTrampolined = dispatcher.javaClass - .getDeclaredField("toRunTrampolined") - .apply { isAccessible = true } - .get(dispatcher) as ArrayDeque<*> - toRunTrampolined.clear() - // Upon reference leaks being fixed, verify we don't need to reset these values for - // AndroidUiDispatcher to continue dispatching between tests. - dispatcher.javaClass - .getDeclaredField("scheduledTrampolineDispatch") - .apply { isAccessible = true } - .set(dispatcher, false) - dispatcher.javaClass - .getDeclaredField("scheduledFrameDispatch") - .apply { isAccessible = true } - .set(dispatcher, false) + // AndroidUiDispatcher is backed by a Handler, by executing one last time + // we give the dispatcher the ability to clean-up / release its callbacks. + executeHandlerCallbacks() } - private class PaparazziComposeOwner private constructor() : LifecycleOwner, SavedStateRegistryOwner { - private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this) - private val savedStateRegistryController = SavedStateRegistryController.create(this) - - override fun getLifecycle(): Lifecycle = lifecycleRegistry - override val savedStateRegistry: SavedStateRegistry = savedStateRegistryController.savedStateRegistry - - companion object { - fun register(view: View) { - val owner = PaparazziComposeOwner() - owner.savedStateRegistryController.performRestore(null) - owner.lifecycleRegistry.currentState = Lifecycle.State.CREATED - ViewTreeLifecycleOwner.set(view, owner) - view.setViewTreeSavedStateRegistryOwner(owner) - } + private fun executeHandlerCallbacks() { + // Avoid ConcurrentModificationException in + // RenderAction.currentContext.sessionInteractiveData.handlerMessageQueue.runnablesMap which is a WeakHashMap + // https://android.googlesource.com/platform/tools/adt/idea/+/c331c9b2f4334748c55c29adec3ad1cd67e45df2/designer/src/com/android/tools/idea/uibuilder/scene/LayoutlibSceneManager.java#1558 + synchronized(this) { + // https://android.googlesource.com/platform/frameworks/layoutlib/+/d58aa4703369e109b24419548f38b422d5a44738/bridge/src/com/android/layoutlib/bridge/BridgeRenderSession.java#171 + // BridgeRenderSession.executeCallbacks aggressively tears down the main Looper and BridgeContext, so we call the static delegates ourselves. + Handler_Delegate.executeCallbacks() } } @@ -622,6 +628,31 @@ class Paparazzi @JvmOverloads constructor( private val isVerifying: Boolean = System.getProperty("paparazzi.test.verify")?.toBoolean() == true + private val hasComposeRuntime: Boolean = isPresentInClasspath( + "androidx.compose.runtime.snapshots.SnapshotKt", + "androidx.compose.ui.platform.AndroidUiDispatcher" + ) + private val hasLifecycleOwnerRuntime = isPresentInClasspath( + "androidx.lifecycle.ViewTreeLifecycleOwner" + ) + private val hasSavedStateRegistryOwnerRuntime = isPresentInClasspath( + "androidx.savedstate.SavedStateRegistryController\$Companion" + ) + private val hasAndroidxActivityRuntime = isPresentInClasspath( + "androidx.activity.ViewTreeOnBackPressedDispatcherOwner" + ) + + private fun isPresentInClasspath(vararg classNames: String): Boolean { + return try { + for (className in classNames) { + Class.forName(className) + } + true + } catch (e: ClassNotFoundException) { + false + } + } + private fun determineHandler(maxPercentDifference: Double): SnapshotHandler = if (isVerifying) { SnapshotVerifier(maxPercentDifference) diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Reflections.kt b/paparazzi/src/main/java/app/cash/paparazzi/Reflections.kt similarity index 100% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Reflections.kt rename to paparazzi/src/main/java/app/cash/paparazzi/Reflections.kt diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/RenderExtension.kt b/paparazzi/src/main/java/app/cash/paparazzi/RenderExtension.kt similarity index 100% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/RenderExtension.kt rename to paparazzi/src/main/java/app/cash/paparazzi/RenderExtension.kt diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Snapshot.kt b/paparazzi/src/main/java/app/cash/paparazzi/Snapshot.kt similarity index 100% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Snapshot.kt rename to paparazzi/src/main/java/app/cash/paparazzi/Snapshot.kt diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/SnapshotHandler.kt b/paparazzi/src/main/java/app/cash/paparazzi/SnapshotHandler.kt similarity index 100% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/SnapshotHandler.kt rename to paparazzi/src/main/java/app/cash/paparazzi/SnapshotHandler.kt diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/SnapshotVerifier.kt b/paparazzi/src/main/java/app/cash/paparazzi/SnapshotVerifier.kt similarity index 100% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/SnapshotVerifier.kt rename to paparazzi/src/main/java/app/cash/paparazzi/SnapshotVerifier.kt diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/TestName.kt b/paparazzi/src/main/java/app/cash/paparazzi/TestName.kt similarity index 100% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/TestName.kt rename to paparazzi/src/main/java/app/cash/paparazzi/TestName.kt diff --git a/paparazzi/src/main/java/app/cash/paparazzi/accessibility/AccessibilityElement.kt b/paparazzi/src/main/java/app/cash/paparazzi/accessibility/AccessibilityElement.kt new file mode 100644 index 0000000000..fd589b93b5 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/accessibility/AccessibilityElement.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.accessibility + +import android.graphics.Rect + +internal data class AccessibilityElement( + val id: String, + val displayBounds: Rect, + val contentDescription: String +) { + val color = RenderSettings.getColor(id) +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/accessibility/AccessibilityOverlayDetailsView.kt b/paparazzi/src/main/java/app/cash/paparazzi/accessibility/AccessibilityOverlayDetailsView.kt new file mode 100644 index 0000000000..bcf92e112b --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/accessibility/AccessibilityOverlayDetailsView.kt @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.accessibility + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.RectF +import android.text.StaticLayout +import android.text.TextPaint +import android.text.TextUtils +import android.util.TypedValue +import android.widget.FrameLayout +import app.cash.paparazzi.accessibility.RenderSettings.DEFAULT_TEXT_COLOR +import app.cash.paparazzi.accessibility.RenderSettings.toColorInt +import java.lang.Float.max + +internal class AccessibilityOverlayDetailsView(context: Context) : FrameLayout(context) { + private val accessibilityElements = mutableListOf<AccessibilityElement>() + private val paint = Paint().apply { + isAntiAlias = true + style = Paint.Style.FILL + } + private val textPaint = Paint().apply { + style = Paint.Style.FILL + color = DEFAULT_TEXT_COLOR.toColorInt() + textSize = context.sp(RenderSettings.DEFAULT_TEXT_SIZE) + } + private val margin = context.dip(8f) + private val innerMargin = margin / 2f + private val rectSize = context.dip(RenderSettings.DEFAULT_RECT_SIZE.toFloat()) + private val cornerRadius = rectSize / 4f + + init { + // Required for onDraw to be called + setWillNotDraw(false) + + setBackgroundColor(RenderSettings.DEFAULT_DESCRIPTION_BACKGROUND_COLOR.toColorInt()) + } + + fun addElements(elements: Collection<AccessibilityElement>) { + accessibilityElements.addAll(elements) + invalidate() + } + + override fun draw(canvas: Canvas) { + super.draw(canvas) + + var lastYCoord = innerMargin + val textPaint = TextPaint(textPaint) + + accessibilityElements.forEach { + paint.color = it.color.toColorInt() + val badge = RectF(margin, lastYCoord, margin + rectSize, lastYCoord + rectSize) + canvas.drawRoundRect(badge, cornerRadius, cornerRadius, paint) + canvas.save() + + val text = it.contentDescription + val textLayout = StaticLayout.Builder + .obtain(text, 0, text.length, textPaint, width) + .setEllipsize(TextUtils.TruncateAt.END) + .build() + canvas.save() + + val textX = badge.right + innerMargin + val textY = badge.top + canvas.translate(textX, textY) + textLayout.draw(canvas) + canvas.restore() + + lastYCoord = max(badge.bottom + margin, textY + textLayout.height.toFloat()) + } + } + + private fun Context.sp(value: Float): Float = + TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_SP, + value, + resources.displayMetrics + ) + + private fun Context.dip(value: Float): Float = + TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + value, + resources.displayMetrics + ) +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/accessibility/AccessibilityOverlayView.kt b/paparazzi/src/main/java/app/cash/paparazzi/accessibility/AccessibilityOverlayView.kt new file mode 100644 index 0000000000..d36d697d68 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/accessibility/AccessibilityOverlayView.kt @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.accessibility + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.view.View +import android.widget.FrameLayout +import app.cash.paparazzi.accessibility.RenderSettings.toColorInt + +internal class AccessibilityOverlayView(context: Context) : FrameLayout(context) { + private val accessibilityElements = mutableListOf<AccessibilityElement>() + private val paint = Paint().apply { + isAntiAlias = true + style = Paint.Style.FILL + } + private val strokePaint = Paint().apply { + strokeWidth = 2f + style = Paint.Style.STROKE + } + + private lateinit var location: IntArray + + init { + // Required for onDraw to be called + setWillNotDraw(false) + + // We can't get the location on screen until the view is attached. + addOnAttachStateChangeListener(object : OnAttachStateChangeListener { + override fun onViewAttachedToWindow(view: View) { + location = IntArray(2).also(view::getLocationOnScreen) + } + + override fun onViewDetachedFromWindow(view: View) { + view.removeOnAttachStateChangeListener(this) + } + }) + } + + fun addElements(elements: Collection<AccessibilityElement>) { + accessibilityElements.addAll(elements) + invalidate() + } + + override fun draw(canvas: Canvas) { + super.draw(canvas) + accessibilityElements.forEach { + paint.color = it.color.toColorInt() + it.displayBounds.offset(-location[0], -location[1]) + + canvas.drawRect(it.displayBounds, paint) + + strokePaint.color = it.color.toColorInt() + strokePaint.alpha = RenderSettings.DEFAULT_RENDER_ALPHA * 2 + canvas.drawRect(it.displayBounds, strokePaint) + } + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/accessibility/AccessibilityRenderExtension.kt b/paparazzi/src/main/java/app/cash/paparazzi/accessibility/AccessibilityRenderExtension.kt new file mode 100644 index 0000000000..3e4fdb6d1a --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/accessibility/AccessibilityRenderExtension.kt @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.accessibility + +import android.graphics.Rect +import android.view.View +import android.view.View.VISIBLE +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.widget.FrameLayout +import android.widget.LinearLayout +import androidx.compose.ui.platform.AbstractComposeView +import androidx.compose.ui.platform.ViewRootForTest +import androidx.compose.ui.semantics.SemanticsActions +import androidx.compose.ui.semantics.SemanticsNode +import androidx.compose.ui.semantics.SemanticsProperties +import androidx.compose.ui.semantics.getAllSemanticsNodes +import androidx.compose.ui.semantics.getOrNull +import app.cash.paparazzi.RenderExtension +import com.android.internal.view.OneShotPreDrawListener + +class AccessibilityRenderExtension : RenderExtension { + override fun renderView( + contentView: View + ): View { + return LinearLayout(contentView.context).apply { + orientation = LinearLayout.HORIZONTAL + weightSum = 2f + layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT) + + val overlay = AccessibilityOverlayView(context).apply { + addView(contentView, FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)) + } + + val contentLayoutParams = contentView.layoutParams ?: generateLayoutParams(null) + addView(overlay, LinearLayout.LayoutParams(contentLayoutParams.width, contentLayoutParams.height, 1f)) + + val overlayDetailsView = AccessibilityOverlayDetailsView(context) + addView(overlayDetailsView, LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT, 1f)) + + OneShotPreDrawListener.add(this) { + val elements = buildList { + processAccessibleChildren { add(it) } + } + + overlayDetailsView.addElements(elements) + overlay.addElements(elements) + } + } + } + + private var unmergedNodes: List<SemanticsNode>? = null + + private fun View.processAccessibleChildren( + processElement: (AccessibilityElement) -> Unit + ) { + if (isImportantForAccessibility && !iterableTextForAccessibility.isNullOrBlank() && visibility == VISIBLE) { + val bounds = Rect().also(::getBoundsOnScreen) + + processElement( + AccessibilityElement( + id = "${this::class.simpleName}($iterableTextForAccessibility)", + displayBounds = bounds, + contentDescription = iterableTextForAccessibility!!.toString() + ) + ) + } + + if (this is AbstractComposeView) { + // ComposeView creates a child view `AndroidComposeView` for view root for test. + val viewRoot = getChildAt(0) as? ViewRootForTest + unmergedNodes = viewRoot?.semanticsOwner?.getAllSemanticsNodes(false) + viewRoot?.semanticsOwner?.rootSemanticsNode?.processAccessibleChildren(processElement) + } + + if (this is ViewGroup) { + (0 until childCount).forEach { + getChildAt(it).processAccessibleChildren(processElement) + } + } + } + + private fun SemanticsNode.processAccessibleChildren( + processElement: (AccessibilityElement) -> Unit + ) { + var accessibilityText: String? = null + accessibilityText = if (config.isMergingSemanticsOfDescendants) { + val unmergedNode = unmergedNodes?.filter { it.id == id } + unmergedNode?.first()?.let { node -> + node.findAllUnmergedNodes() + .mapNotNull { it.accessibilityText() } + .joinToString(", ") + .ifEmpty { null } + } + } else { + accessibilityText() + } + + if (accessibilityText != null) { + val displayBounds = with(boundsInWindow) { + Rect(left.toInt(), top.toInt(), right.toInt(), bottom.toInt()) + } + processElement( + AccessibilityElement( + // SemanticsNode.id is backed by AtomicInteger and is not guaranteed consistent across runs. + id = accessibilityText, + displayBounds = displayBounds, + contentDescription = accessibilityText + ) + ) + } + + children.forEach { + it.processAccessibleChildren(processElement) + } + } +} + +private fun SemanticsNode.findAllUnmergedNodes(): List<SemanticsNode> { + return buildList { + addAll( + children + .filter { !it.config.isMergingSemanticsOfDescendants } + .flatMap { it.findAllUnmergedNodes() } + ) + add(this@findAllUnmergedNodes) + } +} + +private fun SemanticsNode.accessibilityText() = + ( + config.getOrNull(SemanticsProperties.ContentDescription)?.joinToString(", ") + ?: config.getOrNull(SemanticsProperties.Text)?.joinToString(", ") + ?: config.getOrNull(SemanticsProperties.StateDescription) + ?: config.getOrNull(SemanticsActions.OnClick)?.label + ?: config.getOrNull(SemanticsProperties.Role)?.toString() + ).let { + // Escape newline characters to simplify accessibility text. + it?.replaceLineBreaks() + } + +private fun String.replaceLineBreaks() = + replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t") diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/accessibility/RenderSettings.kt b/paparazzi/src/main/java/app/cash/paparazzi/accessibility/RenderSettings.kt similarity index 84% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/accessibility/RenderSettings.kt rename to paparazzi/src/main/java/app/cash/paparazzi/accessibility/RenderSettings.kt index 768fc6ed11..4b8f0d2c4b 100644 --- a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/accessibility/RenderSettings.kt +++ b/paparazzi/src/main/java/app/cash/paparazzi/accessibility/RenderSettings.kt @@ -15,12 +15,11 @@ */ package app.cash.paparazzi.accessibility -import android.view.View import java.awt.Color internal object RenderSettings { const val DEFAULT_RENDER_ALPHA = 40 - val DEFAULT_RENDER_COLORS = listOf( + private val DEFAULT_RENDER_COLORS = listOf( Color.RED, Color.GREEN, Color.BLUE, @@ -37,12 +36,7 @@ internal object RenderSettings { private val colorMap = mutableMapOf<Int, Color>() - fun getColor(view: View): Color { - val key = "${view::class.simpleName}(${view.iterableTextForAccessibility})" - return getColor(key) - } - - private fun getColor(key: String): Color { + internal fun getColor(key: String): Color { val hashCode = key.hashCode() return colorMap.getOrPut(hashCode) { nextColor(hashCode).withAlpha(DEFAULT_RENDER_ALPHA) @@ -62,7 +56,7 @@ internal object RenderSettings { internal fun Color.toColorInt(): Int = android.graphics.Color.argb(alpha, red, green, blue) - internal fun Color.withAlpha(alpha: Int): Color { + private fun Color.withAlpha(alpha: Int): Color { return Color(red, green, blue, alpha) } } diff --git a/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/FrameworkResourceItem.java b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/FrameworkResourceItem.java new file mode 100644 index 0000000000..f2c07487ea --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/FrameworkResourceItem.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2018 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. + */ + +package app.cash.paparazzi.deprecated.com.android.ide.common.resources.deprecated; + +/** + * @deprecated This class is part of an obsolete resource repository system that is no longer used + * in production code. The class is preserved temporarily for LayoutLib tests. + */ +@Deprecated +class FrameworkResourceItem extends ResourceItem { + + FrameworkResourceItem(String name) { + super(name); + } + + @Override + public boolean isEditableDirectly() { + return false; + } + + @Override + public String toString() { + return "FrameworkResourceItem [mName=" + getName() + ", mFiles=" //$NON-NLS-1$ //$NON-NLS-2$ + + getSourceFileList() + "]"; //$NON-NLS-1$ + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/FrameworkResources.java b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/FrameworkResources.java new file mode 100644 index 0000000000..792cf914e1 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/FrameworkResources.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2018 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. + */ + +package app.cash.paparazzi.deprecated.com.android.ide.common.resources.deprecated; + +import app.cash.paparazzi.deprecated.com.android.io.IAbstractFile; +import app.cash.paparazzi.deprecated.com.android.io.IAbstractFolder; +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.resources.ResourceType; +import com.android.utils.ILogger; +import com.google.common.base.Charsets; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import org.kxml2.io.KXmlParser; +import org.xmlpull.v1.XmlPullParser; + +/** + * @deprecated This class is part of an obsolete resource repository system that is no longer used + * in production code. The class is preserved temporarily for LayoutLib tests. + */ +@Deprecated +public class FrameworkResources extends ResourceRepository { + + /** + * Map of {@link ResourceType} to list of items. It is guaranteed to contain a list for all + * possible values of ResourceType. + */ + protected final Map<ResourceType, List<ResourceItem>> mPublicResourceMap = + new EnumMap<ResourceType, List<ResourceItem>>(ResourceType.class); + + public FrameworkResources(@NonNull IAbstractFolder resFolder) { + super(resFolder, true /*isFrameworkRepository*/); + } + + /** + * Returns a {@link Collection} (always non null, but can be empty) of <b>public</b> + * {@link ResourceItem} matching a given {@link ResourceType}. + * + * @param type the type of the resources to return + * @return a collection of items, possibly empty. + */ + @Override + @NonNull + public List<ResourceItem> getResourceItemsOfType(@NonNull ResourceType type) { + return mPublicResourceMap.get(type); + } + + /** + * Returns whether the repository has <b>public</b> resources of a given {@link ResourceType}. + * @param type the type of resource to check. + * @return true if the repository contains resources of the given type, false otherwise. + */ + @Override + public boolean hasResourcesOfType(@NonNull ResourceType type) { + return !mPublicResourceMap.get(type).isEmpty(); + } + + @Override + @NonNull + protected ResourceItem createResourceItem(@NonNull String name) { + return new FrameworkResourceItem(name); + } + + /** + * Reads the public.xml file in data/res/values/ for a given resource folder and builds up + * a map of public resources. + * + * This map is a subset of the full resource map that only contains framework resources + * that are public. + * + * @param logger a logger to report issues to + */ + public void loadPublicResources(@Nullable ILogger logger) { + IAbstractFolder valueFolder = getResFolder().getFolder(SdkConstants.FD_RES_VALUES); + if (!valueFolder.exists()) { + return; + } + + IAbstractFile publicXmlFile = valueFolder.getFile("public.xml"); //$NON-NLS-1$ + if (publicXmlFile.exists()) { + Reader reader = null; + try { + reader = new BufferedReader(new InputStreamReader(publicXmlFile.getContents(), + Charsets.UTF_8)); + KXmlParser parser = new KXmlParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false); + parser.setInput(reader); + + ResourceType lastType = null; + String lastTypeName = ""; + while (true) { + int event = parser.next(); + if (event == XmlPullParser.START_TAG) { + // As of API 15 there are a number of "java-symbol" entries here + if (!parser.getName().equals("public")) { //$NON-NLS-1$ + continue; + } + + String name = null; + String typeName = null; + for (int i = 0, n = parser.getAttributeCount(); i < n; i++) { + String attribute = parser.getAttributeName(i); + + if (attribute.equals("name")) { //$NON-NLS-1$ + name = parser.getAttributeValue(i); + if (typeName != null) { + // Skip id attribute processing + break; + } + } else if (attribute.equals("type")) { //$NON-NLS-1$ + typeName = parser.getAttributeValue(i); + } + } + + if (name != null && typeName != null) { + ResourceType type = null; + if (typeName.equals(lastTypeName)) { + type = lastType; + } else { + type = ResourceType.fromXmlValue(typeName); + lastType = type; + lastTypeName = typeName; + } + if (type != null) { + ResourceItem match = null; + Map<String, ResourceItem> map = mResourceMap.get(type); + if (map != null) { + match = map.get(name); + } + + if (match != null) { + List<ResourceItem> publicList = mPublicResourceMap.get(type); + if (publicList == null) { + // Pick initial size for the list to hold the public + // resources. We could just use map.size() here, + // but they're usually much bigger; for example, + // in one platform version, there are 1500 drawables + // and 1200 strings but only 175 and 25 public ones + // respectively. + int size; + switch (type) { + case STYLE: size = 500; break; + case ATTR: size = 1050; break; + case DRAWABLE: size = 200; break; + case ID: size = 50; break; + case LAYOUT: + case COLOR: + case STRING: + case ANIM: + case INTERPOLATOR: + size = 30; + break; + default: + size = 10; + break; + } + publicList = new ArrayList<ResourceItem>(size); + mPublicResourceMap.put(type, publicList); + } + + publicList.add(match); + } else { + // log that there's a public resource that doesn't actually + // exist? + } + } else { + // log that there was a reference to a typo that doesn't actually + // exist? + } + } + } else if (event == XmlPullParser.END_DOCUMENT) { + break; + } + } + } catch (Exception e) { + if (logger != null) { + logger.error(e, "Can't read and parse public attribute list"); + } + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + // Nothing to be done here - we don't care if it closed or not. + } + } + } + } + + // put unmodifiable list for all res type in the public resource map + // this will simplify access + for (ResourceType type : ResourceType.values()) { + List<ResourceItem> list = mPublicResourceMap.get(type); + if (list == null) { + list = Collections.emptyList(); + } else { + list = Collections.unmodifiableList(list); + } + + // put the new list in the map + mPublicResourceMap.put(type, list); + } + } +} + diff --git a/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/IdGeneratingResourceFile.java b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/IdGeneratingResourceFile.java new file mode 100644 index 0000000000..6756f8480b --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/IdGeneratingResourceFile.java @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2018 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. + */ + +package app.cash.paparazzi.deprecated.com.android.ide.common.resources.deprecated; + +import app.cash.paparazzi.deprecated.com.android.ide.common.resources.deprecated.ValueResourceParser.IValueResourceRepository; +import app.cash.paparazzi.deprecated.com.android.io.IAbstractFile; +import app.cash.paparazzi.deprecated.com.android.io.StreamException; +import com.android.ide.common.rendering.api.DensityBasedResourceValueImpl; +import com.android.ide.common.rendering.api.ResourceNamespace; +import com.android.ide.common.rendering.api.ResourceReference; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.rendering.api.ResourceValueImpl; +import com.android.ide.common.resources.ResourceValueMap; +import com.android.ide.common.resources.configuration.DensityQualifier; +import com.android.ide.common.resources.configuration.ResourceQualifier; +import com.android.resources.ResourceType; +import java.io.IOException; +import java.util.Collection; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Set; + +/** + * @deprecated This class is part of an obsolete resource repository system that is no longer used + * in production code. The class is preserved temporarily for LayoutLib tests. + */ +@Deprecated +public final class IdGeneratingResourceFile extends ResourceFile + implements IValueResourceRepository { + + private final ResourceValueMap mIdResources = ResourceValueMap.create(); + + private final Collection<ResourceType> mResourceTypeList; + + private final String mFileName; + + private final ResourceType mFileType; + + private final ResourceValue mFileValue; + + public IdGeneratingResourceFile(IAbstractFile file, ResourceFolder folder, ResourceType type) { + super(file, folder); + + mFileType = type; + + // Set up our resource types + mResourceTypeList = EnumSet.of(mFileType, ResourceType.ID); + + // compute the resource name + mFileName = getFileName(type); + + // Get the resource value of this file as a whole layout + mFileValue = getFileValue(file, folder); + } + + @Override + protected void load(ScanningContext context) { + // Parse the file and look for @+id/ entries + parseFileForIds(context); + + // create the resource items in the repository + updateResourceItems(context); + } + + @Override + protected void update(ScanningContext context) { + // Copy the previous list of ID names + Set<String> oldIdNames = new HashSet<String>(mIdResources.keySet()); + + // reset current content. + mIdResources.clear(); + + // need to parse the file and find the IDs. + if (!parseFileForIds(context)) { + context.requestFullAapt(); + // Continue through to updating the resource item here since it + // will make for example layout rendering more accurate until + // aapt is re-run + } + + // We only need to update the repository if our IDs have changed + Set<String> keySet = mIdResources.keySet(); + assert keySet != oldIdNames; + if (oldIdNames.equals(keySet) == false) { + updateResourceItems(context); + } + } + + @Override + protected void dispose(ScanningContext context) { + ResourceRepository repository = getRepository(); + + // Remove declarations from this file from the repository + repository.removeFile(mResourceTypeList, this); + + // Ask for an ID refresh since we'll be taking away ID generating items + context.requestFullAapt(); + } + + @Override + public Collection<ResourceType> getResourceTypes() { + return mResourceTypeList; + } + + @Override + public boolean hasResources(ResourceType type) { + return (type == mFileType) || (type == ResourceType.ID && !mIdResources.isEmpty()); + } + + @Override + public ResourceValue getValue(ResourceType type, String name) { + // Check to see if they're asking for one of the right types: + if (type != mFileType && type != ResourceType.ID) { + return null; + } + + // If they're looking for a resource of this type with this name give them the whole file + if (type == mFileType && name.equals(mFileName)) { + return mFileValue; + } else { + // Otherwise try to return them an ID + // the map will return null if it's not found + return mIdResources.get(name); + } + } + + /** + * Looks through the file represented for Ids and adds them to + * our id repository + * + * @return true if parsing succeeds and false if it fails + */ + private boolean parseFileForIds(ScanningContext context) { + IdResourceParser parser = new IdResourceParser(this, context, isFramework()); + try { + IAbstractFile file = getFile(); + return parser.parse(mFileType, file.getOsLocation(), file.getContents()); + } catch (IOException e) { + // Pass + } catch (StreamException e) { + // Pass + } + + return false; + } + + /** + * Add the resources represented by this file to the repository + */ + private void updateResourceItems(ScanningContext context) { + ResourceRepository repository = getRepository(); + + // remove this file from all existing ResourceItem. + repository.removeFile(mResourceTypeList, this); + + // First add this as a layout file + ResourceItem item = repository.getResourceItem(mFileType, mFileName); + item.add(this); + + // Now iterate through our IDs and add + for (String idName : mIdResources.keySet()) { + item = repository.getResourceItem(ResourceType.ID, idName); + // add this file to the list of files generating ID resources. + item.add(this); + } + + // Ask the repository for an ID refresh + context.requestFullAapt(); + } + + /** + * Returns the resource value associated with this whole file as a layout resource + * @param file the file handler that represents this file + * @param folder the folder this file is under + * @return a resource value associated with this layout + */ + private ResourceValue getFileValue(IAbstractFile file, ResourceFolder folder) { + // test if there's a density qualifier associated with the resource + DensityQualifier qualifier = folder.getConfiguration().getDensityQualifier(); + + ResourceValue value; + if (!ResourceQualifier.isValid(qualifier)) { + value = + new ResourceValueImpl( + new ResourceReference( + ResourceNamespace.fromBoolean(isFramework()), + mFileType, + mFileName), + file.getOsLocation()); + } else { + value = + new DensityBasedResourceValueImpl( + new ResourceReference( + ResourceNamespace.fromBoolean(isFramework()), + mFileType, + mFileName), + file.getOsLocation(), + qualifier.getValue()); + } + return value; + } + + + /** + * Returns the name of this resource. + */ + private String getFileName(ResourceType type) { + // get the name from the filename. + String name = getFile().getName(); + + int pos = name.indexOf('.'); + if (pos != -1) { + name = name.substring(0, pos); + } + + return name; + } + + @Override + public void addResourceValue(ResourceValue value) { + // Just overwrite collisions. We're only interested in the unique + // IDs declared + mIdResources.put(value.getName(), value); + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/IdResourceParser.java b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/IdResourceParser.java new file mode 100644 index 0000000000..53a971f625 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/IdResourceParser.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2018 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. + */ + +package app.cash.paparazzi.deprecated.com.android.ide.common.resources.deprecated; + +import app.cash.paparazzi.deprecated.com.android.ide.common.resources.deprecated.ValueResourceParser.IValueResourceRepository; +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.ide.common.rendering.api.ResourceNamespace; +import com.android.ide.common.rendering.api.ResourceReference; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.rendering.api.ResourceValueImpl; +import com.android.resources.ResourceType; +import com.google.common.io.Closeables; +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import org.kxml2.io.KXmlParser; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +/** + * @deprecated This class is part of an obsolete resource repository system that is no longer used + * in production code. The class is preserved temporarily for LayoutLib tests. + */ +@Deprecated +public class IdResourceParser { + private final IValueResourceRepository mRepository; + private final boolean mIsFramework; + private ScanningContext mContext; + + /** + * Creates a new {@link IdResourceParser} + * + * @param repository value repository for registering resource declaration + * @param context a context object with state for the current update, such + * as a place to stash errors encountered + * @param isFramework true if scanning a framework resource + */ + public IdResourceParser( + @NonNull IValueResourceRepository repository, + @NonNull ScanningContext context, + boolean isFramework) { + mRepository = repository; + mContext = context; + mIsFramework = isFramework; + } + + /** + * Parse the given input and register ids with the given + * {@link IValueResourceRepository}. + * + * @param type the type of resource being scanned + * @param path the full OS path to the file being parsed + * @param input the input stream of the XML to be parsed (will be closed by this method) + * @return true if parsing succeeds and false if it fails + * @throws IOException if reading the contents fails + */ + public boolean parse(ResourceType type, final String path, InputStream input) + throws IOException { + KXmlParser parser = new KXmlParser(); + try { + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + + if (input instanceof FileInputStream) { + input = new BufferedInputStream(input); + } + parser.setInput(input, SdkConstants.UTF_8); + + return parse(type, path, parser); + } catch (XmlPullParserException e) { + String message = e.getMessage(); + + // Strip off position description + int index = message.indexOf("(position:"); //$NON-NLS-1$ (Hardcoded in KXml) + if (index != -1) { + message = message.substring(0, index); + } + + String error = String.format("%1$s:%2$d: Error: %3$s", //$NON-NLS-1$ + path, parser.getLineNumber(), message); + mContext.addError(error); + return false; + } catch (RuntimeException e) { + // Some exceptions are thrown by the KXmlParser that are not XmlPullParserExceptions, + // such as this one: + // java.lang.RuntimeException: Undefined Prefix: w in org.kxml2.io.KXmlParser@... + // at org.kxml2.io.KXmlParser.adjustNsp(Unknown Source) + // at org.kxml2.io.KXmlParser.parseStartTag(Unknown Source) + String message = e.getMessage(); + String error = String.format("%1$s:%2$d: Error: %3$s", //$NON-NLS-1$ + path, parser.getLineNumber(), message); + mContext.addError(error); + return false; + } finally { + try { + Closeables.close(input, true /* swallowIOException */); + } catch (IOException e) { + // cannot happen + } + } + } + + private boolean parse(ResourceType type, String path, KXmlParser parser) + throws XmlPullParserException, IOException { + boolean valid = true; + boolean checkForErrors = !mIsFramework && !mContext.needsFullAapt(); + + while (true) { + int event = parser.next(); + if (event == XmlPullParser.START_TAG) { + for (int i = 0, n = parser.getAttributeCount(); i < n; i++) { + String attribute = parser.getAttributeName(i); + String value = parser.getAttributeValue(i); + assert value != null : attribute; + + if (checkForErrors) { + String uri = parser.getAttributeNamespace(i); + if (!mContext.checkValue(uri, attribute, value)) { + mContext.requestFullAapt(); + checkForErrors = false; + valid = false; + } + } + + if (value.startsWith("@+")) { //$NON-NLS-1$ + // Strip out the @+id/ or @+android:id/ section + String id = value.substring(value.indexOf('/') + 1); + ResourceValue newId = + new ResourceValueImpl( + new ResourceReference( + ResourceNamespace.fromBoolean(mIsFramework), + ResourceType.ID, + id), + null); + mRepository.addResourceValue(newId); + } + } + } else if (event == XmlPullParser.END_DOCUMENT) { + break; + } + } + + return valid; + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/MultiResourceFile.java b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/MultiResourceFile.java new file mode 100644 index 0000000000..27abe8b8d5 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/MultiResourceFile.java @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2018 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. + */ + +package app.cash.paparazzi.deprecated.com.android.ide.common.resources.deprecated; + +import app.cash.paparazzi.deprecated.com.android.ide.common.resources.deprecated.ValueResourceParser.IValueResourceRepository; +import app.cash.paparazzi.deprecated.com.android.io.IAbstractFile; +import app.cash.paparazzi.deprecated.com.android.io.StreamException; + +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.rendering.api.ResourceValueImpl; +import com.android.ide.common.resources.ResourceValueMap; +import com.android.resources.ResourceType; +import com.android.utils.XmlUtils; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumMap; +import java.util.Map; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +/** + * @deprecated This class is part of an obsolete resource repository system that is no longer used + * in production code. The class is preserved temporarily for LayoutLib tests. + */ +@Deprecated +public final class MultiResourceFile extends ResourceFile implements IValueResourceRepository { + + private static final SAXParserFactory sParserFactory = XmlUtils.configureSaxFactory( + SAXParserFactory.newInstance(), false, false); + + private final Map<ResourceType, ResourceValueMap> mResourceItems = + new EnumMap<ResourceType, ResourceValueMap>(ResourceType.class); + + private Collection<ResourceType> mResourceTypeList = null; + + public MultiResourceFile(IAbstractFile file, ResourceFolder folder) { + super(file, folder); + } + + // Boolean flag to track whether a named element has been added or removed, thus requiring + // a new ID table to be generated + private boolean mNeedIdRefresh; + + @Override + protected void load(ScanningContext context) { + // need to parse the file and find the content. + parseFile(); + + // create new ResourceItems for the new content. + mResourceTypeList = Collections.unmodifiableCollection(mResourceItems.keySet()); + + // We need an ID generation step + mNeedIdRefresh = true; + + // create/update the resource items. + updateResourceItems(context); + } + + @Override + protected void update(ScanningContext context) { + // Reset the ID generation flag + mNeedIdRefresh = false; + + // Copy the previous version of our list of ResourceItems and types + Map<ResourceType, ResourceValueMap> oldResourceItems + = new EnumMap<ResourceType, ResourceValueMap>(mResourceItems); + + // reset current content. + mResourceItems.clear(); + + // need to parse the file and find the content. + parseFile(); + + // create new ResourceItems for the new content. + mResourceTypeList = Collections.unmodifiableCollection(mResourceItems.keySet()); + + // Check to see if any names have changed. If so, mark the flag so updateResourceItems + // can notify the ResourceRepository that an ID refresh is needed + if (oldResourceItems.keySet().equals(mResourceItems.keySet())) { + for (ResourceType type : mResourceTypeList) { + // We just need to check the names of the items. + // If there are new or removed names then we'll have to regenerate IDs + if (mResourceItems.get(type).keySet() + .equals(oldResourceItems.get(type).keySet()) == false) { + mNeedIdRefresh = true; + } + } + } else { + // If our type list is different, obviously the names will be different + mNeedIdRefresh = true; + } + // create/update the resource items. + updateResourceItems(context); + } + + @Override + protected void dispose(ScanningContext context) { + ResourceRepository repository = getRepository(); + + // only remove this file from all existing ResourceItem. + repository.removeFile(mResourceTypeList, this); + + // We'll need an ID refresh because we deleted items + context.requestFullAapt(); + + // don't need to touch the content, it'll get reclaimed as this objects disappear. + // In the mean time other objects may need to access it. + } + + @Override + public Collection<ResourceType> getResourceTypes() { + return mResourceTypeList; + } + + @Override + public boolean hasResources(ResourceType type) { + ResourceValueMap list = mResourceItems.get(type); + return (list != null && !list.isEmpty()); + } + + private void updateResourceItems(ScanningContext context) { + ResourceRepository repository = getRepository(); + + // remove this file from all existing ResourceItem. + repository.removeFile(mResourceTypeList, this); + + for (ResourceType type : mResourceTypeList) { + ResourceValueMap list = mResourceItems.get(type); + + if (list != null) { + Collection<ResourceValue> values = list.values(); + for (ResourceValue res : values) { + ResourceItem item = repository.getResourceItem(type, res.getName()); + + // add this file to the list of files generating this resource item. + item.add(this); + } + } + } + + // If we need an ID refresh, ask the repository for that now + if (mNeedIdRefresh) { + context.requestFullAapt(); + } + } + + /** + * Parses the file and creates a list of {@link ResourceType}. + */ + private void parseFile() { + try { + SAXParser parser = XmlUtils.createSaxParser(sParserFactory); + InputSource source = new InputSource(getFile().getContents()); + source.setEncoding(StandardCharsets.UTF_8.name()); + parser.parse(source, new ValueResourceParser(this, isFramework(), null)); + } catch (ParserConfigurationException e) { + } catch (SAXException e) { + } catch (IOException e) { + } catch (StreamException e) { + } + } + + /** + * Adds a resource item to the list + * @param value The value of the resource. + */ + @Override + public void addResourceValue(ResourceValue value) { + ResourceType resType = value.getResourceType(); + + ResourceValueMap list = mResourceItems.get(resType); + + // if the list does not exist, create it. + if (list == null) { + list = ResourceValueMap.create(); + mResourceItems.put(resType, list); + } else { + // look for a possible value already existing. + ResourceValue oldValue = list.get(value.getName()); + + if (oldValue instanceof ResourceValueImpl) { + ((ResourceValueImpl) oldValue).replaceWith(value); + return; + } + } + + // empty list or no match found? add the given resource + list.put(value.getName(), value); + } + + @Override + public ResourceValue getValue(ResourceType type, String name) { + // get the list for the given type + ResourceValueMap list = mResourceItems.get(type); + + if (list != null) { + return list.get(name); + } + + return null; + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/ResourceDeltaKind.java b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/ResourceDeltaKind.java new file mode 100644 index 0000000000..7cb2fddac0 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/ResourceDeltaKind.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2018 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. + */ + +package app.cash.paparazzi.deprecated.com.android.ide.common.resources.deprecated; + +/** + * @deprecated This class is part of an obsolete resource repository system that is no longer used + * in production code. The class is preserved temporarily for LayoutLib tests. + */ +@Deprecated +public enum ResourceDeltaKind { + CHANGED, ADDED, REMOVED +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/ResourceFile.java b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/ResourceFile.java new file mode 100644 index 0000000000..63a48654ce --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/ResourceFile.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2018 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. + */ + +package app.cash.paparazzi.deprecated.com.android.ide.common.resources.deprecated; + +import app.cash.paparazzi.deprecated.com.android.io.IAbstractFile; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.resources.configuration.Configurable; +import com.android.ide.common.resources.configuration.FolderConfiguration; +import com.android.resources.ResourceType; +import java.util.Collection; + +/** + * @deprecated This class is part of an obsolete resource repository system that is no longer used + * in production code. The class is preserved temporarily for LayoutLib tests. + */ +@Deprecated +public abstract class ResourceFile implements Configurable { + + private final IAbstractFile mFile; + private final ResourceFolder mFolder; + + protected ResourceFile(IAbstractFile file, ResourceFolder folder) { + mFile = file; + mFolder = folder; + } + + protected abstract void load(ScanningContext context); + protected abstract void update(ScanningContext context); + protected abstract void dispose(ScanningContext context); + + @Override + public FolderConfiguration getConfiguration() { + return mFolder.getConfiguration(); + } + + /** + * Returns the IFile associated with the ResourceFile. + */ + public final IAbstractFile getFile() { + return mFile; + } + + /** + * Returns the parent folder as a {@link ResourceFolder}. + */ + public final ResourceFolder getFolder() { + return mFolder; + } + + public final ResourceRepository getRepository() { + return mFolder.getRepository(); + } + + /** + * Returns whether the resource is a framework resource. + */ + public final boolean isFramework() { + return mFolder.getRepository().isFrameworkRepository(); + } + + /** + * Returns the list of {@link ResourceType} generated by the file. This is never null. + */ + public abstract Collection<ResourceType> getResourceTypes(); + + /** + * Returns whether the file generated a resource of a specific type. + * @param type The {@link ResourceType} + */ + public abstract boolean hasResources(ResourceType type); + + /** + * Returns the value of a resource generated by this file by {@link ResourceType} and name. + * <p>If no resource match, <code>null</code> is returned. + * @param type the type of the resource. + * @param name the name of the resource. + */ + public abstract ResourceValue getValue(ResourceType type, String name); + + @Override + public String toString() { + return mFile.toString(); + } +} + diff --git a/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/ResourceFolder.java b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/ResourceFolder.java new file mode 100644 index 0000000000..f73f210bbd --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/ResourceFolder.java @@ -0,0 +1,353 @@ +/* + * Copyright (C) 2018 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. + */ + +package app.cash.paparazzi.deprecated.com.android.ide.common.resources.deprecated; + +import app.cash.paparazzi.deprecated.com.android.io.IAbstractFile; +import app.cash.paparazzi.deprecated.com.android.io.IAbstractFolder; +import com.android.SdkConstants; +import com.android.ide.common.resources.configuration.Configurable; +import com.android.ide.common.resources.configuration.FolderConfiguration; +import com.android.resources.FolderTypeRelationship; +import com.android.resources.ResourceFolderType; +import com.android.resources.ResourceType; +import com.android.utils.SdkUtils; +import com.google.common.annotations.VisibleForTesting; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @deprecated This class is part of an obsolete resource repository system that is no longer used + * in production code. The class is preserved temporarily for LayoutLib tests. + */ +@Deprecated +public final class ResourceFolder implements Configurable { + final ResourceFolderType mType; + final FolderConfiguration mConfiguration; + IAbstractFolder mFolder; + List<ResourceFile> mFiles; + Map<String, ResourceFile> mNames; + private final ResourceRepository mRepository; + + /** + * Creates a new {@link ResourceFolder} + * @param type The type of the folder + * @param config The configuration of the folder + * @param folder The associated {@link IAbstractFolder} object. + * @param repository The associated {@link ResourceRepository} + */ + protected ResourceFolder(ResourceFolderType type, FolderConfiguration config, + app.cash.paparazzi.deprecated.com.android.io.IAbstractFolder folder, ResourceRepository repository) { + mType = type; + mConfiguration = config; + mFolder = folder; + mRepository = repository; + } + + /** + * Processes a file and adds it to its parent folder resource. + * + * @param file the underlying resource file. + * @param kind the file change kind. + * @param context a context object with state for the current update, such + * as a place to stash errors encountered + * @return the {@link ResourceFile} that was created. + */ + public ResourceFile processFile(IAbstractFile file, ResourceDeltaKind kind, + ScanningContext context) { + // look for this file if it's already been created + ResourceFile resFile = getFile(file, context); + + if (resFile == null) { + if (kind != ResourceDeltaKind.REMOVED) { + // create a ResourceFile for it. + + resFile = createResourceFile(file); + resFile.load(context); + + // add it to the folder + addFile(resFile); + } + } else { + if (kind == ResourceDeltaKind.REMOVED) { + removeFile(resFile, context); + } else { + resFile.update(context); + } + } + + return resFile; + } + + private ResourceFile createResourceFile(IAbstractFile file) { + // check if that's a single or multi resource type folder. We have a special case + // for ID generating resource types (layout/menu, and XML drawables, etc.). + // MultiResourceFile handles the case when several resource types come from a single file + // (values files). + + ResourceFile resFile; + if (mType != ResourceFolderType.VALUES) { + if (FolderTypeRelationship.isIdGeneratingFolderType(mType) && + SdkUtils.endsWithIgnoreCase(file.getName(), SdkConstants.DOT_XML)) { + List<ResourceType> types = FolderTypeRelationship.getRelatedResourceTypes(mType); + ResourceType primaryType = types.get(0); + resFile = new IdGeneratingResourceFile(file, this, primaryType); + } else { + resFile = new SingleResourceFile(file, this); + } + } else { + resFile = new MultiResourceFile(file, this); + } + return resFile; + } + + /** + * Adds a {@link ResourceFile} to the folder. + * + * @param file The {@link ResourceFile}. + */ + @VisibleForTesting + public void addFile(ResourceFile file) { + if (mFiles == null) { + int initialSize = 16; + if (mRepository.isFrameworkRepository()) { + String name = mFolder.getName(); + // Pick some reasonable initial sizes for framework data structures + // since they are typically (a) large and (b) their sizes are roughly known + // in advance + switch (mType) { + case DRAWABLE: { + // See if it's one of the -mdpi, -hdpi etc folders which + // are large (~1250 items) + int index = name.indexOf('-'); + if (index == -1) { + initialSize = 230; // "drawable" folder + } else { + index = name.indexOf('-', index + 1); + if (index == -1) { + // One of the "drawable-<density>" folders + initialSize = 1260; + } else { + // "drawable-sw600dp-hdpi" etc + initialSize = 30; + } + } + break; + } + case LAYOUT: { + // The main layout folder has about ~185 layouts in it; + // the others are small + if (name.indexOf('-') == -1) { + initialSize = 200; + } + break; + } + case VALUES: { + if (name.indexOf('-') == -1) { + initialSize = 32; + } else { + initialSize = 4; + } + break; + } + case ANIM: initialSize = 85; break; + case COLOR: initialSize = 32; break; + case RAW: initialSize = 4; break; + default: + // Stick with the 16 default + break; + } + } + + mFiles = new ArrayList<ResourceFile>(initialSize); + mNames = new HashMap<String, ResourceFile>(initialSize, 2.0f); + } + + mFiles.add(file); + mNames.put(file.getFile().getName(), file); + } + + protected void removeFile(ResourceFile file, ScanningContext context) { + file.dispose(context); + mFiles.remove(file); + mNames.remove(file.getFile().getName()); + } + + protected void dispose(ScanningContext context) { + if (mFiles != null) { + for (ResourceFile file : mFiles) { + file.dispose(context); + } + + mFiles.clear(); + mNames.clear(); + } + } + + /** + * Returns the {@link IAbstractFolder} associated with this object. + */ + public IAbstractFolder getFolder() { + return mFolder; + } + + /** + * Returns the {@link ResourceFolderType} of this object. + */ + public ResourceFolderType getType() { + return mType; + } + + public ResourceRepository getRepository() { + return mRepository; + } + + /** + * Returns the list of {@link ResourceType}s generated by the files inside this folder. + */ + public Collection<ResourceType> getResourceTypes() { + ArrayList<ResourceType> list = new ArrayList<ResourceType>(); + + if (mFiles != null) { + for (ResourceFile file : mFiles) { + Collection<ResourceType> types = file.getResourceTypes(); + + // loop through those and add them to the main list, + // if they are not already present + for (ResourceType resType : types) { + if (list.indexOf(resType) == -1) { + list.add(resType); + } + } + } + } + + return list; + } + + @Override + public FolderConfiguration getConfiguration() { + return mConfiguration; + } + + /** + * Returns whether the folder contains a file with the given name. + * @param name the name of the file. + */ + public boolean hasFile(String name) { + if (mNames != null && mNames.containsKey(name)) { + return true; + } + + // Note: mNames.containsKey(name) is faster, but doesn't give the same result; this + // method seems to be called on this ResourceFolder before it has been processed, + // so we need to use the file system check instead: + return mFolder.hasFile(name); + } + + /** + * Returns the {@link ResourceFile} matching a {@link IAbstractFile} object. + * + * @param file The {@link IAbstractFile} object. + * @param context a context object with state for the current update, such + * as a place to stash errors encountered + * @return the {@link ResourceFile} or null if no match was found. + */ + private ResourceFile getFile(IAbstractFile file, ScanningContext context) { + assert mFolder.equals(file.getParentFolder()); + + if (mNames != null) { + ResourceFile resFile = mNames.get(file.getName()); + if (resFile != null) { + return resFile; + } + } + + // If the file actually exists, the resource folder may not have been + // scanned yet; add it lazily + if (file.exists()) { + ResourceFile resFile = createResourceFile(file); + resFile.load(context); + addFile(resFile); + return resFile; + } + + return null; + } + + /** + * Returns the {@link ResourceFile} matching a given name. + * @param filename The name of the file to return. + * @return the {@link ResourceFile} or <code>null</code> if no match was found. + */ + public ResourceFile getFile(String filename) { + if (mNames != null) { + ResourceFile resFile = mNames.get(filename); + if (resFile != null) { + return resFile; + } + } + + // If the file actually exists, the resource folder may not have been + // scanned yet; add it lazily + IAbstractFile file = mFolder.getFile(filename); + if (file != null && file.exists()) { + ResourceFile resFile = createResourceFile(file); + resFile.load(new ScanningContext()); + addFile(resFile); + return resFile; + } + + return null; + } + + /** + * Returns whether a file in the folder is generating a resource of a specified type. + * @param type The {@link ResourceType} being looked up. + */ + public boolean hasResources(ResourceType type) { + // Check if the folder type is able to generate resource of the type that was asked. + // this is a first check to avoid going through the files. + List<ResourceFolderType> folderTypes = FolderTypeRelationship.getRelatedFolders(type); + + boolean valid = false; + for (ResourceFolderType rft : folderTypes) { + if (rft == mType) { + valid = true; + break; + } + } + + if (valid) { + if (mFiles != null) { + for (ResourceFile f : mFiles) { + if (f.hasResources(type)) { + return true; + } + } + } + } + return false; + } + + @Override + public String toString() { + return mFolder.toString(); + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/ResourceItem.java b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/ResourceItem.java new file mode 100644 index 0000000000..549e3cb323 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/ResourceItem.java @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2018 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. + */ + +package app.cash.paparazzi.deprecated.com.android.ide.common.resources.deprecated; + +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.resources.configuration.FolderConfiguration; +import com.android.resources.ResourceType; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * @deprecated This class is part of an obsolete resource repository system that is no longer used + * in production code. The class is preserved temporarily for LayoutLib tests. + */ +@Deprecated +public class ResourceItem implements Comparable<ResourceItem> { + + private static final Comparator<ResourceFile> sComparator = new Comparator<ResourceFile>() { + @Override + public int compare(ResourceFile file1, ResourceFile file2) { + // get both FolderConfiguration and compare them + FolderConfiguration fc1 = file1.getFolder().getConfiguration(); + FolderConfiguration fc2 = file2.getFolder().getConfiguration(); + + return fc1.compareTo(fc2); + } + }; + + private final String mName; + + /** + * List of files generating this ResourceItem. + */ + private final List<ResourceFile> mFiles = new ArrayList<ResourceFile>(); + + /** + * Constructs a new ResourceItem. + * @param name the name of the resource as it appears in the XML and R.java files. + */ + public ResourceItem(String name) { + mName = name; + } + + /** + * Returns the name of the resource. + */ + public final String getName() { + return mName; + } + + /** + * Compares the {@link ResourceItem} to another. + * @param other the ResourceItem to be compared to. + */ + @Override + public int compareTo(ResourceItem other) { + return mName.compareTo(other.mName); + } + + /** + * Returns whether the resource is editable directly. + * <p> + * This is typically the case for resources that don't have alternate versions, or resources + * of type {@link ResourceType#ID} that aren't declared inline. + */ + public boolean isEditableDirectly() { + return hasAlternates() == false; + } + + /** + * Returns whether the ID resource has been declared inline inside another resource XML file. + * If the resource type is not {@link ResourceType#ID}, this will always return {@code false}. + */ + public boolean isDeclaredInline() { + return false; + } + + /** + * Returns a {@link ResourceValue} for this item based on the given configuration. + * If the ResourceItem has several source files, one will be selected based on the config. + * @param type the type of the resource. This is necessary because ResourceItem doesn't embed + * its type, but ResourceValue does. + * @param referenceConfig the config of the resource item. + * @param isFramework whether the resource is a framework value. Same as the type. + * @return a ResourceValue or null if none match the config. + */ + public ResourceValue getResourceValue(ResourceType type, FolderConfiguration referenceConfig, + boolean isFramework) { + // look for the best match for the given configuration + // the match has to be of type ResourceFile since that's what the input list contains + ResourceFile match = (ResourceFile) referenceConfig.findMatchingConfigurable(mFiles); + + if (match != null) { + // get the value of this configured resource. + return match.getValue(type, mName); + } + + return null; + } + + /** + * Adds a new source file. + * @param file the source file. + */ + protected void add(ResourceFile file) { + mFiles.add(file); + } + + /** + * Removes a file from the list of source files. + * @param file the file to remove + */ + protected void removeFile(ResourceFile file) { + mFiles.remove(file); + } + + /** + * Returns {@code true} if the item has no source file. + * @return true if the item has no source file. + */ + protected boolean hasNoSourceFile() { + return mFiles.isEmpty(); + } + + /** + * Reset the item by emptying its source file list. + */ + protected void reset() { + mFiles.clear(); + } + + /** + * Returns the sorted list of {@link ResourceItem} objects for this resource item. + */ + public ResourceFile[] getSourceFileArray() { + ArrayList<ResourceFile> list = new ArrayList<ResourceFile>(); + list.addAll(mFiles); + + Collections.sort(list, sComparator); + + return list.toArray(new ResourceFile[0]); + } + + /** + * Returns the list of source file for this resource. + */ + public List<ResourceFile> getSourceFileList() { + return Collections.unmodifiableList(mFiles); + } + + /** + * Returns if the resource has at least one non-default version. + * + * @see ResourceFile#getConfiguration() + * @see FolderConfiguration#isDefault() + */ + public boolean hasAlternates() { + for (ResourceFile file : mFiles) { + if (file.getFolder().getConfiguration().isDefault() == false) { + return true; + } + } + + return false; + } + + /** + * Returns whether the resource has a default version, with no qualifier. + * + * @see ResourceFile#getConfiguration() + * @see FolderConfiguration#isDefault() + */ + public boolean hasDefault() { + for (ResourceFile file : mFiles) { + if (file.getFolder().getConfiguration().isDefault()) { + return true; + } + } + + // We only want to return false if there's no default and more than 0 items. + return (mFiles.isEmpty()); + } + + /** + * Returns the number of alternate versions for this resource. + * + * @see ResourceFile#getConfiguration() + * @see FolderConfiguration#isDefault() + */ + public int getAlternateCount() { + int count = 0; + for (ResourceFile file : mFiles) { + if (file.getFolder().getConfiguration().isDefault() == false) { + count++; + } + } + + return count; + } + + /** + * Returns a formatted string usable in an XML to use for the {@link ResourceItem}. + * @param system Whether this is a system resource or a project resource. + * @return a string in the format @[type]/[name] + */ + public String getXmlString(ResourceType type, boolean system) { + if (type == ResourceType.ID && isDeclaredInline()) { + return (system ? "@android:" : "@+") + type.getName() + "/" + mName; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + + return (system ? "@android:" : "@") + type.getName() + "/" + mName; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + + @Override + public String toString() { + return "ResourceItem [mName=" + mName + ", mFiles=" + mFiles + "]"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/ResourceRepository.java b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/ResourceRepository.java new file mode 100644 index 0000000000..f61f21298d --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/ResourceRepository.java @@ -0,0 +1,915 @@ +/* + * Copyright (C) 2018 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. + */ +package app.cash.paparazzi.deprecated.com.android.ide.common.resources.deprecated; + +import app.cash.paparazzi.deprecated.com.android.io.IAbstractFile; +import app.cash.paparazzi.deprecated.com.android.io.IAbstractFolder; +import app.cash.paparazzi.deprecated.com.android.io.IAbstractResource; +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.resources.ResourceValueMap; +import com.android.ide.common.resources.configuration.FolderConfiguration; +import com.android.ide.common.resources.configuration.LocaleQualifier; +import com.android.resources.FolderTypeRelationship; +import com.android.resources.ResourceFolderType; +import com.android.resources.ResourceType; +import com.android.resources.ResourceUrl; +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +import static com.android.SdkConstants.ATTR_REF_PREFIX; +import static com.android.SdkConstants.PREFIX_RESOURCE_REF; +import static com.android.SdkConstants.PREFIX_THEME_REF; +import static com.android.SdkConstants.RESOURCE_CLZ_ATTR; + +/** + * @deprecated This class is part of an obsolete resource repository system that is no longer used + * in production code. The class is preserved temporarily for LayoutLib tests. + */ +@Deprecated +public abstract class ResourceRepository { + private final IAbstractFolder mResourceFolder; + + protected Map<ResourceFolderType, List<ResourceFolder>> mFolderMap = + new EnumMap<>(ResourceFolderType.class); + + protected Map<ResourceType, Map<String, ResourceItem>> mResourceMap = + new EnumMap<>(ResourceType.class); + + private Map<Map<String, ResourceItem>, Collection<ResourceItem>> mReadOnlyListMap = + new IdentityHashMap<>(); + + private final boolean mFrameworkRepository; + private boolean mCleared = true; + private boolean mInitializing; + + /** + * Makes a resource repository. + * + * @param resFolder the resource folder of the repository. + * @param isFrameworkRepository whether the repository is for framework resources. + */ + protected ResourceRepository(@NonNull IAbstractFolder resFolder, + boolean isFrameworkRepository) { + mResourceFolder = resFolder; + mFrameworkRepository = isFrameworkRepository; + } + + public IAbstractFolder getResFolder() { + return mResourceFolder; + } + + public boolean isFrameworkRepository() { + return mFrameworkRepository; + } + + public synchronized void clear() { + mCleared = true; + mFolderMap = new EnumMap<ResourceFolderType, List<ResourceFolder>>( + ResourceFolderType.class); + mResourceMap = new EnumMap<ResourceType, Map<String, ResourceItem>>( + ResourceType.class); + + mReadOnlyListMap = + new IdentityHashMap<Map<String, ResourceItem>, Collection<ResourceItem>>(); + } + + /** + * Ensures that the repository has been initialized again after a call to + * {@link ResourceRepository#clear()}. + * + * @return true if the repository was just re-initialized. + */ + public synchronized boolean ensureInitialized() { + if (mCleared && !mInitializing) { + ScanningContext context = new ScanningContext(); + mInitializing = true; + + IAbstractResource[] resources = mResourceFolder.listMembers(); + + for (IAbstractResource res : resources) { + if (res instanceof IAbstractFolder) { + IAbstractFolder folder = (IAbstractFolder)res; + ResourceFolder resFolder = processFolder(folder); + + if (resFolder != null) { + // now we process the content of the folder + IAbstractResource[] files = folder.listMembers(); + + for (IAbstractResource fileRes : files) { + if (fileRes instanceof IAbstractFile) { + IAbstractFile file = (IAbstractFile)fileRes; + + resFolder.processFile(file, ResourceDeltaKind.ADDED, context); + } + } + } + } + } + + mInitializing = false; + mCleared = false; + return true; + } + + return false; + } + + /** + * Adds a Folder Configuration to the project. + * + * @param type The resource type. + * @param config The resource configuration. + * @param folder The workspace folder object. + * @return the {@link ResourceFolder} object associated to this folder. + */ + private ResourceFolder add( + @NonNull ResourceFolderType type, + @NonNull FolderConfiguration config, + @NonNull IAbstractFolder folder) { + // get the list for the resource type + List<ResourceFolder> list = mFolderMap.get(type); + + if (list == null) { + list = new ArrayList<ResourceFolder>(); + + ResourceFolder cf = new ResourceFolder(type, config, folder, this); + list.add(cf); + + mFolderMap.put(type, list); + + return cf; + } + + // look for an already existing folder configuration. + for (ResourceFolder cFolder : list) { + if (cFolder.mConfiguration.equals(config)) { + // config already exist. Nothing to be done really, besides making sure + // the IAbstractFolder object is up to date. + cFolder.mFolder = folder; + return cFolder; + } + } + + // If we arrive here, this means we didn't find a matching configuration. + // So we add one. + ResourceFolder cf = new ResourceFolder(type, config, folder, this); + list.add(cf); + + return cf; + } + + /** + * Removes a {@link ResourceFolder} associated with the specified {@link IAbstractFolder}. + * + * @param type The type of the folder + * @param removedFolder the IAbstractFolder object. + * @param context the scanning context + * @return the {@link ResourceFolder} that was removed, or null if no matches were found. + */ + @Nullable + public ResourceFolder removeFolder( + @NonNull ResourceFolderType type, + @NonNull IAbstractFolder removedFolder, + @Nullable ScanningContext context) { + ensureInitialized(); + + // get the list of folders for the resource type. + List<ResourceFolder> list = mFolderMap.get(type); + + if (list != null) { + int count = list.size(); + for (int i = 0 ; i < count ; i++) { + ResourceFolder resFolder = list.get(i); + IAbstractFolder folder = resFolder.getFolder(); + if (removedFolder.equals(folder)) { + // we found the matching ResourceFolder. we need to remove it. + list.remove(i); + + // remove its content + resFolder.dispose(context); + + return resFolder; + } + } + } + + return null; + } + + /** + * Returns true if this resource repository contains a resource of the given name. + * + * @param url the resource URL + * @return true if the resource is known + */ + public boolean hasResourceItem(@NonNull String url) { + // Handle theme references + if (url.startsWith(PREFIX_THEME_REF)) { + String remainder = url.substring(PREFIX_THEME_REF.length()); + if (url.startsWith(ATTR_REF_PREFIX)) { + url = PREFIX_RESOURCE_REF + url.substring(PREFIX_THEME_REF.length()); + return hasResourceItem(url); + } + int colon = url.indexOf(':'); + if (colon != -1) { + // Convert from ?android:progressBarStyleBig to ?android:attr/progressBarStyleBig + if (remainder.indexOf('/', colon) == -1) { + remainder = remainder.substring(0, colon) + RESOURCE_CLZ_ATTR + '/' + + remainder.substring(colon); + } + url = PREFIX_RESOURCE_REF + remainder; + return hasResourceItem(url); + } else { + int slash = url.indexOf('/'); + if (slash == -1) { + url = PREFIX_RESOURCE_REF + RESOURCE_CLZ_ATTR + '/' + remainder; + return hasResourceItem(url); + } + } + } + + if (!url.startsWith(PREFIX_RESOURCE_REF)) { + return false; + } + + assert url.startsWith("@") || url.startsWith("?") : url; + + ensureInitialized(); + + int typeEnd = url.indexOf('/', 1); + if (typeEnd != -1) { + int nameBegin = typeEnd + 1; + + // Skip @ and @+ + int typeBegin = url.startsWith("@+") ? 2 : 1; //$NON-NLS-1$ + + int colon = url.lastIndexOf(':', typeEnd); + if (colon != -1) { + typeBegin = colon + 1; + } + String typeName = url.substring(typeBegin, typeEnd); + ResourceType type = ResourceType.fromXmlValue(typeName); + if (type != null) { + String name = url.substring(nameBegin); + return hasResourceItem(type, name); + } + } + + return false; + } + + /** + * Returns true if this resource repository contains a resource of the given name. + * + * @param type the type of resource to look up + * @param name the name of the resource + * @return true if the resource is known + */ + public boolean hasResourceItem(@NonNull ResourceType type, @NonNull String name) { + ensureInitialized(); + + Map<String, ResourceItem> map = mResourceMap.get(type); + + if (map != null) { + + ResourceItem resourceItem = map.get(name); + if (resourceItem != null) { + return true; + } + } + + return false; + } + + /** + * Returns a {@link ResourceItem} matching the given {@link ResourceType} and name. If none + * exist, it creates one. + * + * @param type the resource type + * @param name the name of the resource. + * @return A resource item matching the type and name. + */ + @NonNull + public ResourceItem getResourceItem(@NonNull ResourceType type, @NonNull String name) { + ensureInitialized(); + + // looking for an existing ResourceItem with this type and name + ResourceItem item = findDeclaredResourceItem(type, name); + + // create one if there isn't one already, or if the existing one is inlined, since + // clearly we need a non inlined one (the inline one is removed too) + if (item == null || item.isDeclaredInline()) { + ResourceItem oldItem = item; + item = createResourceItem(name); + + Map<String, ResourceItem> map = mResourceMap.get(type); + + if (map == null) { + if (isFrameworkRepository()) { + // Pick initial size for the maps. Also change the load factor to 1.0 + // to avoid rehashing the whole table when we (as expected) get near + // the known rough size of each resource type map. + int size; + switch (type) { + // Based on counts in API 16. Going back to API 10, the counts + // are roughly 25-50% smaller (e.g. compared to the top 5 types below + // the fractions are 1107 vs 1734, 831 vs 1508, 895 vs 1255, + // 733 vs 1064 and 171 vs 783. + case PUBLIC: size = 1734; break; + case DRAWABLE: size = 1508; break; + case STRING: size = 1255; break; + case ATTR: size = 1064; break; + case STYLE: size = 783; break; + case ID: size = 347; break; + case STYLEABLE: + size = 210; + break; + case LAYOUT: size = 187; break; + case COLOR: size = 120; break; + case ANIM: size = 95; break; + case DIMEN: size = 81; break; + case BOOL: size = 54; break; + case INTEGER: size = 52; break; + case ARRAY: size = 51; break; + case PLURALS: size = 20; break; + case XML: size = 14; break; + case INTERPOLATOR : size = 13; break; + case ANIMATOR: size = 8; break; + case RAW: size = 4; break; + case MENU: size = 2; break; + case MIPMAP: size = 2; break; + case FRACTION: size = 1; break; + default: + size = 2; + } + map = new HashMap<>(size, 1.0f); + } else { + map = new HashMap<>(); + } + mResourceMap.put(type, map); + } + + map.put(item.getName(), item); + + if (oldItem != null) { + map.remove(oldItem.getName()); + } + } + + return item; + } + + /** + * Creates a resource item with the given name. + * @param name the name of the resource + * @return a new ResourceItem (or child class) instance. + */ + @NonNull + protected abstract ResourceItem createResourceItem(@NonNull String name); + + /** + * Processes a folder and adds it to the list of existing folders. + * @param folder the folder to process + * @return the ResourceFolder created from this folder, or null if the process failed. + */ + @Nullable + public ResourceFolder processFolder(@NonNull IAbstractFolder folder) { + ensureInitialized(); + + // split the name of the folder in segments. + String[] folderSegments = folder.getName().split(SdkConstants.RES_QUALIFIER_SEP); + + // get the enum for the resource type. + ResourceFolderType type = ResourceFolderType.getTypeByName(folderSegments[0]); + + if (type != null) { + // get the folder configuration. + FolderConfiguration config = FolderConfiguration.getConfig(folderSegments); + + if (config != null) { + return add(type, config, folder); + } + } + + return null; + } + + /** + * Returns a list of {@link ResourceFolder} for a specific {@link ResourceFolderType}. + * + * @param type The {@link ResourceFolderType} + */ + @Nullable + public List<ResourceFolder> getFolders(@NonNull ResourceFolderType type) { + ensureInitialized(); + + return mFolderMap.get(type); + } + + @NonNull + public List<ResourceType> getAvailableResourceTypes() { + ensureInitialized(); + + List<ResourceType> list = new ArrayList<ResourceType>(); + + // For each key, we check if there's a single ResourceType match. + // If not, we look for the actual content to give us the resource type. + + for (ResourceFolderType folderType : mFolderMap.keySet()) { + List<ResourceType> types = FolderTypeRelationship.getRelatedResourceTypes(folderType); + if (types.size() == 1) { + // before we add it we check if it's not already present, since a ResourceType + // could be created from multiple folders, even for the folders that only create + // one type of resource (drawable for instance, can be created from drawable/ and + // values/) + if (!list.contains(types.get(0))) { + list.add(types.get(0)); + } + } else { + // there isn't a single resource type out of this folder, so we look for all + // content. + List<ResourceFolder> folders = mFolderMap.get(folderType); + if (folders != null) { + for (ResourceFolder folder : folders) { + Collection<ResourceType> folderContent = folder.getResourceTypes(); + + // then we add them, but only if they aren't already in the list. + for (ResourceType folderResType : folderContent) { + if (!list.contains(folderResType)) { + list.add(folderResType); + } + } + } + } + } + } + + return list; + } + + /** + * Returns a list of {@link ResourceItem} matching a given {@link ResourceType}. + * @param type the type of the resource items to return + * @return a non null collection of resource items + */ + @NonNull + public Collection<ResourceItem> getResourceItemsOfType(@NonNull ResourceType type) { + ensureInitialized(); + + Map<String, ResourceItem> map = mResourceMap.get(type); + + if (map == null) { + return Collections.emptyList(); + } + + Collection<ResourceItem> roList = mReadOnlyListMap.get(map); + if (roList == null) { + roList = Collections.unmodifiableCollection(map.values()); + mReadOnlyListMap.put(map, roList); + } + + return roList; + } + + /** + * Returns whether the repository has resources of a given {@link ResourceType}. + * @param type the type of resource to check. + * @return true if the repository contains resources of the given type, false otherwise. + */ + public boolean hasResourcesOfType(@NonNull ResourceType type) { + ensureInitialized(); + + Map<String, ResourceItem> items = mResourceMap.get(type); + return (items != null && !items.isEmpty()); + } + + /** + * Returns the {@link ResourceFolder} associated with a {@link IAbstractFolder}. + * @param folder The {@link IAbstractFolder} object. + * @return the {@link ResourceFolder} or null if it was not found. + */ + @Nullable + public ResourceFolder getResourceFolder(@NonNull IAbstractFolder folder) { + ensureInitialized(); + + Collection<List<ResourceFolder>> values = mFolderMap.values(); + + for (List<ResourceFolder> list : values) { + for (ResourceFolder resFolder : list) { + IAbstractFolder wrapper = resFolder.getFolder(); + if (wrapper.equals(folder)) { + return resFolder; + } + } + } + + return null; + } + + /** + * Returns the {@link ResourceFile} matching the given name, + * {@link ResourceFolderType} and configuration. + * <p> + * This only works with files generating one resource named after the file + * (for instance, layouts, bitmap based drawable, xml, anims). + * + * @param name the resource name or file name + * @param type the folder type search for + * @param config the folder configuration to match for + * @return the matching file or <code>null</code> if no match was found. + */ + @Nullable + public ResourceFile getMatchingFile( + @NonNull String name, + @NonNull ResourceFolderType type, + @NonNull FolderConfiguration config) { + List<ResourceType> types = FolderTypeRelationship.getRelatedResourceTypes(type); + for (ResourceType t : types) { + if (t == ResourceType.ID) { + continue; + } + ResourceFile match = getMatchingFile(name, t, config); + if (match != null) { + return match; + } + } + + return null; + } + + /** + * Returns the {@link ResourceFile} matching the given name, + * {@link ResourceType} and configuration. + * <p> + * This only works with files generating one resource named after the file + * (for instance, layouts, bitmap based drawable, xml, anims). + * + * @param name the resource name or file name + * @param type the folder type search for + * @param config the folder configuration to match for + * @return the matching file or <code>null</code> if no match was found. + */ + @Nullable + public ResourceFile getMatchingFile( + @NonNull String name, + @NonNull ResourceType type, + @NonNull FolderConfiguration config) { + ensureInitialized(); + + String resourceName = name; + int dot = resourceName.indexOf('.'); + if (dot != -1) { + resourceName = resourceName.substring(0, dot); + } + + Map<String, ResourceItem> items = mResourceMap.get(type); + if (items != null) { + ResourceItem item = items.get(resourceName); + if (item != null) { + List<ResourceFile> files = item.getSourceFileList(); + if (files != null) { + if (files.size() > 1) { + ResourceValue value = item.getResourceValue(type, config, + isFrameworkRepository()); + if (value != null) { + String v = value.getValue(); + if (v != null) { + ResourceUrl url = ResourceUrl.parse(v); + if (url != null) { + return getMatchingFile(url.name, url.type, config); + } else { + // Looks like the resource value is pointing to a file + // It's most likely one of the source files for this + // resource item, so check those first + for (ResourceFile f : files) { + if (v.equals(f.getFile().getOsLocation())) { + // Found the file + return f; + } + } + + // No; look up the resource file from the full path + File file = new File(v); + if (file.exists()) { + ResourceFile f = findResourceFile(file); + if (f != null) { + return f; + } + } + } + } + } + } else if (files.size() == 1) { + // Single file: see if it matches + ResourceFile matchingFile = files.get(0); + if (matchingFile.getFolder().getConfiguration().isMatchFor(config)) { + return matchingFile; + } + } + } + } + } + + return null; + } + + /** + * Looks up the {@link ResourceFile} for the given {@link File}, if possible + * + * @param file the file + * @return the corresponding {@link ResourceFile}, or null if not a known {@link ResourceFile} + */ + @Nullable + protected ResourceFile findResourceFile(@NonNull File file) { + // Look up the right resource file for this path + String parentName = file.getParentFile().getName(); + IAbstractFolder folder = getResFolder().getFolder(parentName); + if (folder != null) { + ResourceFolder resourceFolder = getResourceFolder(folder); + if (resourceFolder == null) { + FolderConfiguration configForFolder = FolderConfiguration + .getConfigForFolder(parentName); + if (configForFolder != null) { + ResourceFolderType folderType = ResourceFolderType.getFolderType(parentName); + if (folderType != null) { + resourceFolder = add(folderType, configForFolder, folder); + } + } + } + if (resourceFolder != null) { + ResourceFile resourceFile = resourceFolder.getFile(file.getName()); + if (resourceFile != null) { + return resourceFile; + } + } + } + + return null; + } + + /** + * Returns the list of source files for a given resource. + * Optionally, if a {@link FolderConfiguration} is given, then only the best + * match for this config is returned. + * + * @param type the type of the resource. + * @param name the name of the resource. + * @param referenceConfig an optional config for which only the best match will be returned. + * + * @return a list of files generating this resource or null if it was not found. + */ + @Nullable + public List<ResourceFile> getSourceFiles(@NonNull ResourceType type, @NonNull String name, + @Nullable FolderConfiguration referenceConfig) { + ensureInitialized(); + + Collection<ResourceItem> items = getResourceItemsOfType(type); + + for (ResourceItem item : items) { + if (name.equals(item.getName())) { + if (referenceConfig != null) { + ResourceFile match = + referenceConfig.findMatchingConfigurable(item.getSourceFileList()); + if (match != null) { + return Collections.singletonList((ResourceFile) match); + } + + return null; + } + return item.getSourceFileList(); + } + } + + return null; + } + + /** + * Returns the resources values matching a given {@link FolderConfiguration}. + * + * @param referenceConfig the configuration that each value must match. + * @return a map with guaranteed to contain an entry for each {@link ResourceType} + */ + @NonNull + public Map<ResourceType, ResourceValueMap> getConfiguredResources( + @NonNull FolderConfiguration referenceConfig) { + ensureInitialized(); + + return doGetConfiguredResources(referenceConfig); + } + + /** + * Returns the resources values matching a given {@link FolderConfiguration} for the current + * project. + * + * @param referenceConfig the configuration that each value must match. + * @return a map with guaranteed to contain an entry for each {@link ResourceType} + */ + @NonNull + protected final Map<ResourceType, ResourceValueMap> doGetConfiguredResources( + @NonNull FolderConfiguration referenceConfig) { + ensureInitialized(); + + Map<ResourceType, ResourceValueMap> map = + new EnumMap<ResourceType, ResourceValueMap>(ResourceType.class); + + for (ResourceType key : ResourceType.values()) { + // get the local results and put them in the map + map.put(key, getConfiguredResource(key, referenceConfig)); + } + + return map; + } + + /** + * Returns the sorted list of languages used in the resources. + */ + @NonNull + public SortedSet<String> getLanguages() { + ensureInitialized(); + + SortedSet<String> set = new TreeSet<String>(); + + Collection<List<ResourceFolder>> folderList = mFolderMap.values(); + for (List<ResourceFolder> folderSubList : folderList) { + for (ResourceFolder folder : folderSubList) { + FolderConfiguration config = folder.getConfiguration(); + LocaleQualifier locale = config.getLocaleQualifier(); + if (locale != null && locale.hasLanguage()) { + set.add(locale.getLanguage()); + } + } + } + + return set; + } + + /** + * Returns the sorted list of regions used in the resources with the given language. + * + * @param currentLanguage the current language the region must be associated with. + */ + @NonNull + public SortedSet<String> getRegions(@NonNull String currentLanguage) { + ensureInitialized(); + + SortedSet<String> set = new TreeSet<String>(); + + Collection<List<ResourceFolder>> folderList = mFolderMap.values(); + for (List<ResourceFolder> folderSubList : folderList) { + for (ResourceFolder folder : folderSubList) { + FolderConfiguration config = folder.getConfiguration(); + + // get the language + LocaleQualifier locale = config.getLocaleQualifier(); + if (locale != null && currentLanguage.equals(locale.getLanguage()) + && locale.getRegion() != null) { + set.add(locale.getRegion()); + } + } + } + + return set; + } + + /** + * Loads the resources. + */ + public void loadResources() { + clear(); + ensureInitialized(); + } + + protected void removeFile(@NonNull Collection<ResourceType> types, + @NonNull ResourceFile file) { + ensureInitialized(); + + for (ResourceType type : types) { + removeFile(type, file); + } + } + + protected void removeFile(@NonNull ResourceType type, @NonNull ResourceFile file) { + Map<String, ResourceItem> map = mResourceMap.get(type); + if (map != null) { + Collection<ResourceItem> values = map.values(); + List<ResourceItem> toDelete = null; + for (ResourceItem item : values) { + item.removeFile(file); + if (item.hasNoSourceFile()) { + if (toDelete == null) { + toDelete = new ArrayList<ResourceItem>(values.size()); + } + toDelete.add(item); + } + } + if (toDelete != null) { + for (ResourceItem item : toDelete) { + map.remove(item.getName()); + } + } + } + } + + /** + * Returns a map of (resource name, resource value) for the given {@link ResourceType}. + * <p>The values returned are taken from the resource files best matching a given + * {@link FolderConfiguration}. + * + * @param type the type of the resources. + * @param referenceConfig the configuration to best match. + */ + @NonNull + private ResourceValueMap getConfiguredResource(@NonNull ResourceType type, + @NonNull FolderConfiguration referenceConfig) { + // get the resource item for the given type + Map<String, ResourceItem> items = mResourceMap.get(type); + if (items == null) { + return ResourceValueMap.create(); + } + + // create the map + ResourceValueMap map = ResourceValueMap.createWithExpectedSize(items.size()); + + for (ResourceItem item : items.values()) { + ResourceValue value = item.getResourceValue(type, referenceConfig, + isFrameworkRepository()); + if (value != null) { + map.put(item.getName(), value); + } + } + + return map; + } + + /** + * Cleans up the repository of resource items that have no source file anymore. + */ + public void postUpdateCleanUp() { + // Since removed files/folders remove source files from existing ResourceItem, loop through + // all resource items and remove the ones that have no source files. + + Collection<Map<String, ResourceItem>> maps = mResourceMap.values(); + for (Map<String, ResourceItem> map : maps) { + Set<String> keySet = map.keySet(); + Iterator<String> iterator = keySet.iterator(); + while (iterator.hasNext()) { + String name = iterator.next(); + ResourceItem resourceItem = map.get(name); + if (resourceItem.hasNoSourceFile()) { + iterator.remove(); + } + } + } + } + + /** + * Looks up an existing {@link ResourceItem} by {@link ResourceType} and name. + * Ignores inline resources. + * + * @param type the resource type. + * @param name the resource name. + * @return the existing ResourceItem or null if no match was found. + */ + @Nullable + private ResourceItem findDeclaredResourceItem(@NonNull ResourceType type, + @NonNull String name) { + Map<String, ResourceItem> map = mResourceMap.get(type); + + if (map != null) { + ResourceItem resourceItem = map.get(name); + if (resourceItem != null && !resourceItem.isDeclaredInline()) { + return resourceItem; + } + } + + return null; + } +} + diff --git a/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/ScanningContext.java b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/ScanningContext.java new file mode 100644 index 0000000000..153734ac2a --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/ScanningContext.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2018 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. + */ +package app.cash.paparazzi.deprecated.com.android.ide.common.resources.deprecated; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import java.util.ArrayList; +import java.util.List; + +/** + * @deprecated This class is part of an obsolete resource repository system that is no longer used + * in production code. The class is preserved temporarily for LayoutLib tests. + */ +@Deprecated +public class ScanningContext { + private boolean mNeedsFullAapt; + private List<String> mErrors; + + /** Constructs a new {@link ScanningContext} */ + public ScanningContext() { + super(); + } + + /** Returns a list of errors encountered during scanning, or null if there were no errors. */ + @Nullable + public List<String> getErrors() { + return mErrors; + } + + /** + * Adds the given error to the scanning context. The error should use the + * same syntax as real aapt error messages such that the aapt parser can + * properly detect the filename, line number, etc. + * + * @param error the error message, including file name and line number at + * the beginning + */ + public void addError(@NonNull String error) { + if (mErrors == null) { + mErrors = new ArrayList<>(); + } + mErrors.add(error); + } + + /** + * Marks that a full aapt compilation of the resources is necessary because it has + * detected a change that cannot be incrementally handled. + */ + protected void requestFullAapt() { + mNeedsFullAapt = true; + } + + /** + * Returns whether this repository has been marked as "dirty"; if one or + * more of the constituent files have declared that the resource item names + * that they provide have changed. + * + * @return true if a full aapt compilation is required + */ + public boolean needsFullAapt() { + return mNeedsFullAapt; + } + + /** + * Asks the context to check whether the given attribute name and value is valid + * in this context. + * + * @param uri the XML namespace URI + * @param name the attribute local name + * @param value the attribute value + * @return true if the attribute is valid + */ + public boolean checkValue(@Nullable String uri, @NonNull String name, @NonNull String value) { + return true; + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/SingleResourceFile.java b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/SingleResourceFile.java new file mode 100644 index 0000000000..8a7db55ea4 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/SingleResourceFile.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2018 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. + */ +package app.cash.paparazzi.deprecated.com.android.ide.common.resources.deprecated; + +import app.cash.paparazzi.deprecated.com.android.io.IAbstractFile; +import com.android.ide.common.rendering.api.DensityBasedResourceValueImpl; +import com.android.ide.common.rendering.api.ResourceNamespace; +import com.android.ide.common.rendering.api.ResourceReference; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.rendering.api.ResourceValueImpl; +import com.android.ide.common.resources.configuration.DensityQualifier; +import com.android.ide.common.resources.configuration.ResourceQualifier; +import com.android.resources.FolderTypeRelationship; +import com.android.resources.ResourceType; +import com.android.utils.SdkUtils; +import java.util.Collection; +import java.util.List; +import javax.xml.parsers.SAXParserFactory; + +import static com.android.SdkConstants.DOT_XML; + +/** + * @deprecated This class is part of an obsolete resource repository system that is no longer used + * in production code. The class is preserved temporarily for LayoutLib tests. + */ +@Deprecated +public class SingleResourceFile extends ResourceFile { + + private static final SAXParserFactory sParserFactory = SAXParserFactory.newInstance(); + static { + sParserFactory.setNamespaceAware(true); + } + + private final String mResourceName; + private final ResourceType mType; + private ResourceValue mValue; + + public SingleResourceFile(IAbstractFile file, ResourceFolder folder) { + super(file, folder); + + // we need to infer the type of the resource from the folder type. + // This is easy since this is a single Resource file. + List<ResourceType> types = FolderTypeRelationship.getRelatedResourceTypes(folder.getType()); + mType = types.get(0); + + // compute the resource name + mResourceName = getResourceName(mType); + + // test if there's a density qualifier associated with the resource + DensityQualifier qualifier = folder.getConfiguration().getDensityQualifier(); + + if (!ResourceQualifier.isValid(qualifier)) { + mValue = + new ResourceValueImpl( + new ResourceReference( + ResourceNamespace.fromBoolean(isFramework()), + mType, + getResourceName(mType)), + file.getOsLocation()); + } else { + mValue = + new DensityBasedResourceValueImpl( + new ResourceReference( + ResourceNamespace.fromBoolean(isFramework()), + mType, + getResourceName(mType)), + file.getOsLocation(), + qualifier.getValue()); + } + } + + @Override + protected void load(ScanningContext context) { + // get a resource item matching the given type and name + ResourceItem item = getRepository().getResourceItem(mType, mResourceName); + + // add this file to the list of files generating this resource item. + item.add(this); + + // Ask for an ID refresh since we're adding an item that will generate an ID + context.requestFullAapt(); + } + + @Override + protected void update(ScanningContext context) { + // when this happens, nothing needs to be done since the file only generates + // a single resources that doesn't actually change (its content is the file path) + + // However, we should check for newly introduced errors + // Parse the file and look for @+id/ entries + validateAttributes(context); + } + + @Override + protected void dispose(ScanningContext context) { + // only remove this file from the existing ResourceItem. + getFolder().getRepository().removeFile(mType, this); + + // Ask for an ID refresh since we're removing an item that previously generated an ID + context.requestFullAapt(); + + // don't need to touch the content, it'll get reclaimed as this objects disappear. + // In the mean time other objects may need to access it. + } + + @Override + public Collection<ResourceType> getResourceTypes() { + return FolderTypeRelationship.getRelatedResourceTypes(getFolder().getType()); + } + + @Override + public boolean hasResources(ResourceType type) { + return FolderTypeRelationship.match(type, getFolder().getType()); + } + + /* + * (non-Javadoc) + * @see com.android.ide.eclipse.editors.resources.manager.ResourceFile#getValue(com.android.ide.eclipse.common.resources.ResourceType, java.lang.String) + * + * This particular implementation does not care about the type or name since a + * SingleResourceFile represents a file generating only one resource. + * The value returned is the full absolute path of the file in OS form. + */ + @Override + public ResourceValue getValue(ResourceType type, String name) { + return mValue; + } + + /** + * Returns the name of the resources. + */ + private String getResourceName(ResourceType type) { + // get the name from the filename. + String name = getFile().getName(); + + int pos = name.indexOf('.'); + if (pos != -1) { + name = name.substring(0, pos); + } + + return name; + } + + /** + * Validates the associated resource file to make sure the attribute references are valid + * + * @return true if parsing succeeds and false if it fails + */ + private boolean validateAttributes(ScanningContext context) { + // We only need to check if it's a non-framework file (and an XML file; skip .png's) + if (!isFramework() && SdkUtils.endsWith(getFile().getName(), DOT_XML)) { + ValidatingResourceParser parser = new ValidatingResourceParser(context, false); + try { + IAbstractFile file = getFile(); + return parser.parse(file.getOsLocation(), file.getContents()); + } catch (Exception e) { + context.needsFullAapt(); + } + + return false; + } + + return true; + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/ValidatingResourceParser.java b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/ValidatingResourceParser.java new file mode 100644 index 0000000000..e1183e97c4 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/ValidatingResourceParser.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2018 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. + */ +package app.cash.paparazzi.deprecated.com.android.ide.common.resources.deprecated; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.google.common.io.Closeables; +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import org.kxml2.io.KXmlParser; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +/** + * @deprecated This class is part of an obsolete resource repository system that is no longer used + * in production code. The class is preserved temporarily for LayoutLib tests. + */ +@Deprecated +public class ValidatingResourceParser { + private final boolean mIsFramework; + private ScanningContext mContext; + + /** + * Creates a new {@link ValidatingResourceParser} + * + * @param context a context object with state for the current update, such + * as a place to stash errors encountered + * @param isFramework true if scanning a framework resource + */ + public ValidatingResourceParser( + @NonNull ScanningContext context, + boolean isFramework) { + mContext = context; + mIsFramework = isFramework; + } + + /** + * Parse the given input and return false if it contains errors, <b>or</b> if + * the context is already tagged as needing a full aapt run. + * + * @param path the full OS path to the file being parsed + * @param input the input stream of the XML to be parsed (will be closed by this method) + * @return true if parsing succeeds and false if it fails + * @throws IOException if reading the contents fails + */ + public boolean parse(final String path, InputStream input) + throws IOException { + // No need to validate framework files + if (mIsFramework) { + try { + Closeables.close(input, true /* swallowIOException */); + } catch (IOException e) { + // cannot happen + } + return true; + } + if (mContext.needsFullAapt()) { + try { + Closeables.close(input, true /* swallowIOException */); + } catch (IOException e) { + // cannot happen + } + return false; + } + + KXmlParser parser = new KXmlParser(); + try { + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + + if (input instanceof FileInputStream) { + input = new BufferedInputStream(input); + } + parser.setInput(input, SdkConstants.UTF_8); + + return parse(path, parser); + } catch (XmlPullParserException e) { + String message = e.getMessage(); + + // Strip off position description + int index = message.indexOf("(position:"); //$NON-NLS-1$ (Hardcoded in KXml) + if (index != -1) { + message = message.substring(0, index); + } + + String error = String.format("%1$s:%2$d: Error: %3$s", //$NON-NLS-1$ + path, parser.getLineNumber(), message); + mContext.addError(error); + return false; + } catch (RuntimeException e) { + // Some exceptions are thrown by the KXmlParser that are not XmlPullParserExceptions, + // such as this one: + // java.lang.RuntimeException: Undefined Prefix: w in org.kxml2.io.KXmlParser@... + // at org.kxml2.io.KXmlParser.adjustNsp(Unknown Source) + // at org.kxml2.io.KXmlParser.parseStartTag(Unknown Source) + String message = e.getMessage(); + String error = String.format("%1$s:%2$d: Error: %3$s", //$NON-NLS-1$ + path, parser.getLineNumber(), message); + mContext.addError(error); + return false; + } finally { + try { + Closeables.close(input, true /* swallowIOException */); + } catch (IOException e) { + // cannot happen + } + } + } + + private boolean parse(String path, KXmlParser parser) + throws XmlPullParserException, IOException { + boolean checkForErrors = !mIsFramework && !mContext.needsFullAapt(); + + while (true) { + int event = parser.next(); + if (event == XmlPullParser.START_TAG) { + for (int i = 0, n = parser.getAttributeCount(); i < n; i++) { + String attribute = parser.getAttributeName(i); + String value = parser.getAttributeValue(i); + assert value != null : attribute; + + if (checkForErrors) { + String uri = parser.getAttributeNamespace(i); + if (!mContext.checkValue(uri, attribute, value)) { + mContext.requestFullAapt(); + return false; + } + } + } + } else if (event == XmlPullParser.END_DOCUMENT) { + break; + } + } + + return true; + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/ValueResourceParser.java b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/ValueResourceParser.java new file mode 100644 index 0000000000..8b95b91458 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/ide/common/resources/deprecated/ValueResourceParser.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2018 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. + */ + +package app.cash.paparazzi.deprecated.com.android.ide.common.resources.deprecated; + +import com.android.ide.common.rendering.api.ArrayResourceValueImpl; +import com.android.ide.common.rendering.api.AttrResourceValueImpl; +import com.android.ide.common.rendering.api.ResourceNamespace; +import com.android.ide.common.rendering.api.ResourceReference; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.rendering.api.ResourceValueImpl; +import com.android.ide.common.rendering.api.StyleItemResourceValue; +import com.android.ide.common.rendering.api.StyleItemResourceValueImpl; +import com.android.ide.common.rendering.api.StyleResourceValueImpl; +import com.android.ide.common.rendering.api.StyleableResourceValueImpl; +import com.android.ide.common.resources.ValueXmlHelper; +import com.android.resources.ResourceType; +import com.google.common.base.Strings; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import static com.android.SdkConstants.ANDROID_NS_NAME_PREFIX; +import static com.android.SdkConstants.ANDROID_NS_NAME_PREFIX_LEN; +import static com.android.SdkConstants.ATTR_NAME; +import static com.android.SdkConstants.ATTR_PARENT; +import static com.android.SdkConstants.ATTR_VALUE; +import static com.android.SdkConstants.TAG_RESOURCES; + +/** + * @deprecated This class is part of an obsolete resource repository system that is no longer used + * in production code. The class is preserved temporarily for LayoutLib tests. + */ +@Deprecated +public final class ValueResourceParser extends DefaultHandler { + + private static final ResourceReference TMP_REF = + new ResourceReference(ResourceNamespace.RES_AUTO, ResourceType.STRING, "_tmp"); + + public interface IValueResourceRepository { + void addResourceValue(ResourceValue value); + } + + private boolean inResources; + private int mDepth; + private ResourceValueImpl mCurrentValue; + private ArrayResourceValueImpl mArrayResourceValue; + private StyleResourceValueImpl mCurrentStyle; + private StyleableResourceValueImpl mCurrentDeclareStyleable; + private AttrResourceValueImpl mCurrentAttr; + private IValueResourceRepository mRepository; + private final boolean mIsFramework; + private final String mLibraryName; + + public ValueResourceParser(IValueResourceRepository repository, boolean isFramework, String libraryName) { + mRepository = repository; + mIsFramework = isFramework; + mLibraryName = libraryName; + } + + @Override + public void endElement(String uri, String localName, String qName) throws SAXException { + if (mCurrentValue != null) { + String value = mCurrentValue.getValue(); + value = value == null ? "" : ValueXmlHelper.unescapeResourceString(value, false, true); + mCurrentValue.setValue(value); + } + + if (inResources && qName.equals(TAG_RESOURCES)) { + inResources = false; + } else if (mDepth == 2) { + mCurrentValue = null; + mCurrentStyle = null; + mCurrentDeclareStyleable = null; + mCurrentAttr = null; + mArrayResourceValue = null; + } else if (mDepth == 3) { + if (mArrayResourceValue != null && mCurrentValue != null) { + mArrayResourceValue.addElement(mCurrentValue.getValue()); + } + mCurrentValue = null; + //noinspection VariableNotUsedInsideIf + if (mCurrentDeclareStyleable != null) { + mCurrentAttr = null; + } + } + + mDepth--; + super.endElement(uri, localName, qName); + } + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) + throws SAXException { + try { + ResourceNamespace namespace = ResourceNamespace.fromBoolean(mIsFramework); + mDepth++; + if (!inResources && mDepth == 1) { + if (qName.equals(TAG_RESOURCES)) { + inResources = true; + } + } else if (mDepth == 2 && inResources) { + ResourceType type = + ResourceType.fromXmlTag( + new Object(), (t) -> qName, (t, name) -> attributes.getValue(name)); + + if (type != null) { + // get the resource name + String name = attributes.getValue(ATTR_NAME); + if (name != null) { + switch (type) { + case STYLE: + String parent = attributes.getValue(ATTR_PARENT); + mCurrentStyle = + new StyleResourceValueImpl( + namespace, name, parent, mLibraryName); + mRepository.addResourceValue(mCurrentStyle); + break; + case STYLEABLE: + mCurrentDeclareStyleable = + new StyleableResourceValueImpl( + namespace, name, null, mLibraryName); + mRepository.addResourceValue(mCurrentDeclareStyleable); + break; + case ATTR: + mCurrentAttr = + new AttrResourceValueImpl(namespace, name, mLibraryName); + mRepository.addResourceValue(mCurrentAttr); + break; + case ARRAY: + mArrayResourceValue = + new ArrayResourceValueImpl(namespace, name, mLibraryName); + mRepository.addResourceValue(mArrayResourceValue); + break; + default: + mCurrentValue = + new ResourceValueImpl( + namespace, type, name, null, mLibraryName); + mRepository.addResourceValue(mCurrentValue); + break; + } + } + } + } else if (mDepth == 3) { + // get the resource name + String name = attributes.getValue(ATTR_NAME); + if (!Strings.isNullOrEmpty(name)) { + if (mCurrentStyle != null) { + mCurrentValue = + new StyleItemResourceValueImpl( + mCurrentStyle.getNamespace(), name, null, mLibraryName); + mCurrentStyle.addItem((StyleItemResourceValue) mCurrentValue); + } else if (mCurrentDeclareStyleable != null) { + // is the attribute in the android namespace? + boolean isFramework = mIsFramework; + if (name.startsWith(ANDROID_NS_NAME_PREFIX)) { + name = name.substring(ANDROID_NS_NAME_PREFIX_LEN); + isFramework = true; + } + + mCurrentAttr = new AttrResourceValueImpl(namespace, name, mLibraryName); + mCurrentDeclareStyleable.addValue(mCurrentAttr); + + // also add it to the repository. + mRepository.addResourceValue(mCurrentAttr); + + } else if (mCurrentAttr != null) { + // get the enum/flag value + String value = attributes.getValue(ATTR_VALUE); + + try { + // Integer.decode/parseInt can't deal with hex value > 0x7FFFFFFF so we + // use Long.decode instead. + mCurrentAttr.addValue(name, Long.decode(value).intValue(), null); + } catch (NumberFormatException e) { + // pass, we'll just ignore this value + } + } + } else //noinspection VariableNotUsedInsideIf + if (mArrayResourceValue != null) { + // Create a temporary resource value to hold the item's value. The value is + // not added to the repository, since it's just a holder. The value will be set + // in the `characters` method and then added to mArrayResourceValue in `endElement`. + mCurrentValue = new ResourceValueImpl(TMP_REF, null); + } + } else if (mDepth == 4 && mCurrentAttr != null) { + // get the enum/flag name + String name = attributes.getValue(ATTR_NAME); + String value = attributes.getValue(ATTR_VALUE); + + try { + // Integer.decode/parseInt can't deal with hex value > 0x7FFFFFFF so we + // use Long.decode instead. + mCurrentAttr.addValue(name, Long.decode(value).intValue(), null); + } catch (NumberFormatException e) { + // pass, we'll just ignore this value + } + } + } finally { + super.startElement(uri, localName, qName, attributes); + } + } + + @Override + public void characters(char[] ch, int start, int length) { + if (mCurrentValue != null) { + String value = mCurrentValue.getValue(); + if (value == null) { + mCurrentValue.setValue(new String(ch, start, length)); + } else { + mCurrentValue.setValue(value + new String(ch, start, length)); + } + } + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/io/FileWrapper.java b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/io/FileWrapper.java new file mode 100644 index 0000000000..e471979292 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/io/FileWrapper.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2008 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. + */ + +package app.cash.paparazzi.deprecated.com.android.io; + + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; + +/** + * An implementation of {@link IAbstractFile} extending {@link File}. + */ +public class FileWrapper extends File implements IAbstractFile { + private static final long serialVersionUID = 1L; + + /** + * Creates a new File instance matching a given {@link File} object. + * @param file the file to match + */ + public FileWrapper(File file) { + super(file.getAbsolutePath()); + } + + /** + * Creates a new File instance from a parent abstract pathname and a child pathname string. + * @param parent the parent pathname + * @param child the child name + * + * @see File#File(File, String) + */ + public FileWrapper(File parent, String child) { + super(parent, child); + } + + /** + * Creates a new File instance by converting the given pathname string into an abstract + * pathname. + * @param osPathname the OS pathname + * + * @see File#File(String) + */ + public FileWrapper(String osPathname) { + super(osPathname); + } + + /** + * Creates a new File instance from a parent abstract pathname and a child pathname string. + * @param parent the parent pathname + * @param child the child name + * + * @see File#File(String, String) + */ + public FileWrapper(String parent, String child) { + super(parent, child); + } + + /** + * Creates a new File instance by converting the given <code>file:</code> URI into an + * abstract pathname. + * @param uri An absolute, hierarchical URI with a scheme equal to "file", a non-empty path + * component, and undefined authority, query, and fragment components + * + * @see File#File(URI) + */ + public FileWrapper(URI uri) { + super(uri); + } + + @Override + public InputStream getContents() throws StreamException { + try { + return new FileInputStream(this); + } catch (FileNotFoundException e) { + throw new StreamException(e, this, StreamException.Error.FILENOTFOUND); + } + } + + @Override + public void setContents(InputStream source) throws StreamException { + FileOutputStream fos = null; + try { + fos = new FileOutputStream(this); + + byte[] buffer = new byte[1024]; + int count = 0; + while ((count = source.read(buffer)) != -1) { + fos.write(buffer, 0, count); + } + } catch (IOException e) { + throw new StreamException(e, this); + } finally { + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + throw new StreamException(e, this); + } + } + } + } + + @Override + public OutputStream getOutputStream() throws StreamException { + try { + return new FileOutputStream(this); + } catch (FileNotFoundException e) { + throw new StreamException(e, this); + } + } + + @Override + public PreferredWriteMode getPreferredWriteMode() { + return PreferredWriteMode.OUTPUTSTREAM; + } + + @Override + public String getOsLocation() { + return getAbsolutePath(); + } + + @Override + public boolean exists() { + return isFile(); + } + + @Override + public long getModificationStamp() { + return lastModified(); + } + + @Override + public IAbstractFolder getParentFolder() { + String p = this.getParent(); + if (p == null) { + return null; + } + return new FolderWrapper(p); + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/io/FolderWrapper.java b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/io/FolderWrapper.java new file mode 100644 index 0000000000..b3a2f72a24 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/io/FolderWrapper.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2008 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. + */ + +package app.cash.paparazzi.deprecated.com.android.io; + + +import java.io.File; +import java.net.URI; +import java.util.ArrayList; + +/** + * An implementation of {@link IAbstractFolder} extending {@link File}. + */ +public class FolderWrapper extends File implements IAbstractFolder { + + private static final long serialVersionUID = 1L; + + /** + * Creates a new File instance from a parent abstract pathname and a child pathname string. + * @param parent the parent pathname + * @param child the child name + * + * @see File#File(File, String) + */ + public FolderWrapper(File parent, String child) { + super(parent, child); + } + + /** + * Creates a new File instance by converting the given pathname string into an abstract + * pathname. + * @param pathname the pathname + * + * @see File#File(String) + */ + public FolderWrapper(String pathname) { + super(pathname); + } + + /** + * Creates a new File instance from a parent abstract pathname and a child pathname string. + * @param parent the parent pathname + * @param child the child name + * + * @see File#File(String, String) + */ + public FolderWrapper(String parent, String child) { + super(parent, child); + } + + /** + * Creates a new File instance by converting the given <code>file:</code> URI into an + * abstract pathname. + * @param uri An absolute, hierarchical URI with a scheme equal to "file", a non-empty path + * component, and undefined authority, query, and fragment components + * + * @see File#File(URI) + */ + public FolderWrapper(URI uri) { + super(uri); + } + + /** + * Creates a new File instance matching a give {@link File} object. + * @param file the file to match + */ + public FolderWrapper(File file) { + super(file.getAbsolutePath()); + } + + @Override + public IAbstractResource[] listMembers() { + File[] files = listFiles(); + final int count = files == null ? 0 : files.length; + IAbstractResource[] afiles = new IAbstractResource[count]; + + if (files != null) { + for (int i = 0 ; i < count ; i++) { + File f = files[i]; + if (f.isFile()) { + afiles[i] = new FileWrapper(f); + } else if (f.isDirectory()) { + afiles[i] = new FolderWrapper(f); + } + } + } + + return afiles; + } + + @Override + public boolean hasFile(final String name) { + String[] match = list(new FilenameFilter() { + @Override + public boolean accept(IAbstractFolder dir, String filename) { + return name.equals(filename); + } + }); + + return match.length > 0; + } + + @Override + public IAbstractFile getFile(String name) { + return new FileWrapper(this, name); + } + + @Override + public IAbstractFolder getFolder(String name) { + return new FolderWrapper(this, name); + } + + @Override + public IAbstractFolder getParentFolder() { + String p = this.getParent(); + if (p == null) { + return null; + } + return new FolderWrapper(p); + } + + @Override + public String getOsLocation() { + return getAbsolutePath(); + } + + @Override + public boolean exists() { + return isDirectory(); + } + + @Override + public String[] list(FilenameFilter filter) { + File[] files = listFiles(); + if (files != null && files.length > 0) { + ArrayList<String> list = new ArrayList<String>(); + + for (File file : files) { + if (filter.accept(this, file.getName())) { + list.add(file.getName()); + } + } + + return list.toArray(new String[0]); + } + + return new String[0]; + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/io/IAbstractFile.java b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/io/IAbstractFile.java new file mode 100644 index 0000000000..2a44295216 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/io/IAbstractFile.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2008 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. + */ + +package app.cash.paparazzi.deprecated.com.android.io; + +import java.io.InputStream; +import java.io.OutputStream; + +/** + * A file. + */ +public interface IAbstractFile extends IAbstractResource { + enum PreferredWriteMode { + INPUTSTREAM, OUTPUTSTREAM + } + + /** + * Returns an {@link InputStream} object on the file content. + * + * The stream must be closed by the caller. + * + * @throws StreamException + */ + InputStream getContents() throws StreamException; + + /** + * Sets the content of the file. + * @param source the content + * @throws StreamException + */ + void setContents(InputStream source) throws StreamException; + + /** + * Returns an {@link OutputStream} to write into the file. + * @throws StreamException + */ + OutputStream getOutputStream() throws StreamException; + + /** + * Returns the preferred mode to write into the file. + */ + PreferredWriteMode getPreferredWriteMode(); + + /** + * Returns the last modification timestamp + */ + long getModificationStamp(); +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/io/IAbstractFolder.java b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/io/IAbstractFolder.java new file mode 100644 index 0000000000..a5daf10798 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/io/IAbstractFolder.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2008 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. + */ + +package app.cash.paparazzi.deprecated.com.android.io; + +import java.io.File; + +/** + * A folder. + */ +public interface IAbstractFolder extends IAbstractResource { + /** + * Instances of classes that implement this interface are used to + * filter filenames. + */ + interface FilenameFilter { + /** + * Tests if a specified file should be included in a file list. + * + * @param dir the directory in which the file was found. + * @param name the name of the file. + * @return <code>true</code> if and only if the name should be + * included in the file list; <code>false</code> otherwise. + */ + boolean accept(IAbstractFolder dir, String name); + } + + /** + * Returns true if the receiver contains a file with a given name + * @param name the name of the file. This is the name without the path leading to the + * parent folder. + */ + boolean hasFile(String name); + + /** + * Returns an {@link IAbstractFile} representing a child of the current folder with the + * given name. The file may not actually exist. + * @param name the name of the file. + */ + IAbstractFile getFile(String name); + + /** + * Returns an {@link IAbstractFolder} representing a child of the current folder with the + * given name. The folder may not actually exist. + * @param name the name of the folder. + */ + IAbstractFolder getFolder(String name); + + /** + * Returns a list of all existing file and directory members in this folder. + * The returned array can be empty but is never null. + */ + IAbstractResource[] listMembers(); + + /** + * Returns a list of all existing file and directory members in this folder + * that satisfy the specified filter. + * + * @param filter A filename filter instance. Must not be null. + * @return An array of file names (generated using {@link File#getName()}). + * The array can be empty but is never null. + */ + String[] list(FilenameFilter filter); +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/io/IAbstractResource.java b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/io/IAbstractResource.java new file mode 100644 index 0000000000..1004932f7d --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/io/IAbstractResource.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2008 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. + */ + +package app.cash.paparazzi.deprecated.com.android.io; + +/** + * Base representation of a file system resource. + * <p> + * This somewhat limited interface is designed to let classes use file-system resources, without + * having the manually handle either the standard Java file or the Eclipse file API.. + */ +public interface IAbstractResource { + + /** + * Returns the name of the resource. + */ + String getName(); + + /** + * Returns the OS path of the folder location (may be absolute). + */ + String getOsLocation(); + + /** + * Returns the path of the resource. + */ + String getPath(); + + /** + * Returns whether the resource actually exists. + */ + boolean exists(); + + /** + * Returns the parent folder or null if there is no parent. + */ + IAbstractFolder getParentFolder(); + + /** + * Deletes the resource. + */ + boolean delete(); +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/io/StreamException.java b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/io/StreamException.java new file mode 100644 index 0000000000..a4401a20a3 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/deprecated/com/android/io/StreamException.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2010 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. + */ + +package app.cash.paparazzi.deprecated.com.android.io; + +/** + * Exception thrown when {@link IAbstractFile#getContents()} fails. + */ +public class StreamException extends Exception { + private static final long serialVersionUID = 1L; + + public enum Error { + DEFAULT, OUTOFSYNC, FILENOTFOUND + } + + private final Error mError; + private final IAbstractFile mFile; + + public StreamException(Exception e, IAbstractFile file) { + this(e, file, Error.DEFAULT); + } + + public StreamException(Exception e, IAbstractFile file, Error error) { + super(e); + mFile = file; + mError = error; + } + + public Error getError() { + return mError; + } + + public IAbstractFile getFile() { + return mFile; + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/ComposeViewAdapter.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/ComposeViewAdapter.kt new file mode 100644 index 0000000000..4c90f35262 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/ComposeViewAdapter.kt @@ -0,0 +1,16 @@ +package app.cash.paparazzi.internal + +import android.content.Context +import android.util.AttributeSet +import android.widget.FrameLayout + +/** + * Ported from: https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/ComposeViewAdapter.kt?q=ComposeViewAdapter + * + * A wrapper layout for compose-based layouts which allows [android.view.WindowManagerImpl] to find + * a composable root + */ +class ComposeViewAdapter( + context: Context, + attrs: AttributeSet +) : FrameLayout(context, attrs) diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/DynamicResourceIdManager.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/DynamicResourceIdManager.kt similarity index 100% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/DynamicResourceIdManager.kt rename to paparazzi/src/main/java/app/cash/paparazzi/internal/DynamicResourceIdManager.kt diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/Gc.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/Gc.kt similarity index 100% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/Gc.kt rename to paparazzi/src/main/java/app/cash/paparazzi/internal/Gc.kt diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/ImageUtils.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/ImageUtils.kt similarity index 98% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/ImageUtils.kt rename to paparazzi/src/main/java/app/cash/paparazzi/internal/ImageUtils.kt index 70b2115c90..79da6bef3d 100644 --- a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/ImageUtils.kt +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/ImageUtils.kt @@ -53,8 +53,8 @@ internal object ImageUtils { /** Directory where to write the thumbnails and deltas. */ private val failureDir: File get() { - val workingDirString = System.getProperty("user.dir") - val failureDir = File(workingDirString, "out/failures") + val buildDirString = System.getProperty("paparazzi.build.dir") + val failureDir = File(buildDirString, "paparazzi/failures") failureDir.mkdirs() return failureDir } diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziAssetRepository.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziAssetRepository.kt similarity index 75% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziAssetRepository.kt rename to paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziAssetRepository.kt index 72bcf85e32..d6a57d2170 100644 --- a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziAssetRepository.kt +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziAssetRepository.kt @@ -23,7 +23,10 @@ import java.io.FileNotFoundException import java.io.IOException import java.io.InputStream -internal class PaparazziAssetRepository(private val assetPath: String) : AssetRepository() { +internal class PaparazziAssetRepository( + private val assetPath: String, + private val assetDirs: List<String> = emptyList() +) : AssetRepository() { @Throws(FileNotFoundException::class) private fun open(path: String): InputStream? { val asset = File(path) @@ -39,7 +42,19 @@ internal class PaparazziAssetRepository(private val assetPath: String) : AssetRe override fun openAsset( path: String, mode: Int - ): InputStream? = open("$assetPath/$path") + ): InputStream? { + if (assetDirs.isEmpty()) { + return open("$assetPath/$path") + } else { + for (assetDir in assetDirs) { + val assetFile = open("$assetDir/$path") + if (assetFile != null) { + return assetFile + } + } + return null + } + } @Throws(IOException::class) override fun openNonAsset( diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziCallback.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziCallback.kt similarity index 77% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziCallback.kt rename to paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziCallback.kt index 3f0fa6d4df..58583263f1 100644 --- a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziCallback.kt +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziCallback.kt @@ -18,6 +18,7 @@ package app.cash.paparazzi.internal import app.cash.paparazzi.internal.parsers.LayoutPullParser import app.cash.paparazzi.internal.parsers.TagSnapshot +import com.android.AndroidXConstants.CLASS_RECYCLER_VIEW_ADAPTER import com.android.ide.common.rendering.api.ActionBarCallback import com.android.ide.common.rendering.api.AdapterBinding import com.android.ide.common.rendering.api.ILayoutPullParser @@ -25,8 +26,6 @@ import com.android.ide.common.rendering.api.LayoutlibCallback import com.android.ide.common.rendering.api.ResourceNamespace.RES_AUTO import com.android.ide.common.rendering.api.ResourceReference import com.android.ide.common.rendering.api.ResourceValue -import com.android.ide.common.rendering.api.SessionParams.Key -import com.android.layoutlib.bridge.android.RenderParamsFlags import com.android.resources.ResourceType import com.android.resources.ResourceType.STYLE import com.google.common.io.ByteStreams @@ -52,9 +51,6 @@ internal class PaparazziCallback( private val aaptDeclaredResources = mutableMapOf<String, TagSnapshot>() private val dynamicResourceIdManager = DynamicResourceIdManager() - private var adaptiveIconMaskPath: String? = null - private var highQualityShadow = false - private var enableShadow = true private val loadedClasses = mutableMapOf<String, Class<*>>() @Throws(ClassNotFoundException::class) @@ -78,7 +74,7 @@ internal class PaparazziCallback( } else if (type.isArray && type.componentType == Int::class.javaPrimitiveType) { // Ignore. } else { - logger.error(null, "Unknown field type in R class: $type") + logger.error(null, "Unknown type ($type) in R class field: $field") } } catch (e: IllegalAccessException) { logger.error(e, "Malformed R class: %1\$s", "$rPackageName.R") @@ -93,11 +89,26 @@ internal class PaparazziCallback( name: String, constructorSignature: Array<Class<*>>, constructorArgs: Array<Any> + ): Any? = createNewInstance(name, constructorSignature, constructorArgs) + + override fun loadClass( + name: String, + constructorSignature: Array<Class<*>>, + constructorArgs: Array<Any> ): Any? { - val viewClass = Class.forName(name) - val viewConstructor = viewClass.getConstructor(*constructorSignature) - viewConstructor.isAccessible = true - return viewConstructor.newInstance(*constructorArgs) + // RecyclerView.Adapter is an abstract class, but its instance is needed for RecyclerView to work correctly. + // So, when LayoutLib asks for its instance, we define a new class which extends the Adapter class. + // We check whether the class being loaded is the support or the androidx one and use the appropriate adapter that references to the + // right namespace. + return try { + when (name) { + CLASS_RECYCLER_VIEW_ADAPTER.newName() -> createNewInstance(CN_ANDROIDX_CUSTOM_ADAPTER, EMPTY_CLASS_ARRAY, EMPTY_OBJECT_ARRAY) + CLASS_RECYCLER_VIEW_ADAPTER.oldName() -> createNewInstance(CN_SUPPORT_CUSTOM_ADAPTER, EMPTY_CLASS_ARRAY, EMPTY_OBJECT_ARRAY) + else -> createNewInstance(name, constructorSignature, constructorArgs) + } + } catch (e: ClassNotFoundException) { + null + } } override fun resolveResourceId(id: Int): ResourceReference? = @@ -145,9 +156,8 @@ internal class PaparazziCallback( ): Any? = null override fun getAdapterBinding( - adapterViewRef: ResourceReference, - adapterCookie: Any, - viewObject: Any + viewObject: Any?, + attributes: MutableMap<String, String>? ): AdapterBinding? = null override fun getActionBarCallback(): ActionBarCallback = actionBarCallback @@ -175,27 +185,9 @@ internal class PaparazziCallback( override fun createXmlParser(): XmlPullParser = KXmlParser() - override fun <T> getFlag(key: Key<T>?): T? { - return when (key) { - RenderParamsFlags.FLAG_KEY_APPLICATION_PACKAGE -> packageName as T - RenderParamsFlags.FLAG_KEY_ADAPTIVE_ICON_MASK_PATH -> adaptiveIconMaskPath as T? - RenderParamsFlags.FLAG_RENDER_HIGH_QUALITY_SHADOW -> highQualityShadow as T - RenderParamsFlags.FLAG_ENABLE_SHADOW -> enableShadow as T - else -> null - } - } + override fun getApplicationId(): String = packageName - fun setAdaptiveIconMaskPath(adaptiveIconMaskPath: String) { - this.adaptiveIconMaskPath = adaptiveIconMaskPath - } - - fun setHighQualityShadow(highQualityShadow: Boolean) { - this.highQualityShadow = highQualityShadow - } - - fun setEnableShadow(enableShadow: Boolean) { - this.enableShadow = enableShadow - } + override fun getResourcePackage(): String = packageName override fun findClass(name: String): Class<*> { val clazz = loadedClasses[name] @@ -220,4 +212,22 @@ internal class PaparazziCallback( private fun ResourceReference.transformStyleResource() = ResourceReference.style(namespace, name.replace('.', '_')) + + private fun createNewInstance( + name: String, + constructorSignature: Array<Class<*>>, + constructorArgs: Array<Any> + ): Any? { + val anyClass = Class.forName(name) + val anyConstructor = anyClass.getConstructor(*constructorSignature) + anyConstructor.isAccessible = true + return anyConstructor.newInstance(*constructorArgs) + } + + private companion object { + private val EMPTY_CLASS_ARRAY = emptyArray<Class<*>>() + private val EMPTY_OBJECT_ARRAY = emptyArray<Any>() + private const val CN_ANDROIDX_CUSTOM_ADAPTER = "com.android.layoutlib.bridge.android.androidx.Adapter" + private const val CN_SUPPORT_CUSTOM_ADAPTER = "com.android.layoutlib.bridge.android.support.Adapter" + } } diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziJson.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziJson.kt similarity index 100% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziJson.kt rename to paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziJson.kt diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziLogger.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziLogger.kt similarity index 98% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziLogger.kt rename to paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziLogger.kt index 01595f7def..04ee1f41a1 100644 --- a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziLogger.kt +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziLogger.kt @@ -116,6 +116,10 @@ internal class PaparazziLogger : ILayoutLog, ILogger { } } + fun flushErrors() { + errors.clear() + } + internal class MultipleFailuresException(private val causes: List<Throwable>) : Exception() { init { require(causes.isNotEmpty()) { "List of Throwables must not be empty" } diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziViewOwners.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziViewOwners.kt new file mode 100644 index 0000000000..9a09f33343 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziViewOwners.kt @@ -0,0 +1,30 @@ +package app.cash.paparazzi.internal + +import androidx.activity.OnBackPressedDispatcher +import androidx.activity.OnBackPressedDispatcherOwner +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import androidx.savedstate.SavedStateRegistry +import androidx.savedstate.SavedStateRegistryController +import androidx.savedstate.SavedStateRegistryOwner + +internal class PaparazziLifecycleOwner : LifecycleOwner { + val registry = LifecycleRegistry(this) + override val lifecycle: Lifecycle + get() = registry +} + +internal class PaparazziSavedStateRegistryOwner( + private val lifecycleOwner: LifecycleOwner +) : SavedStateRegistryOwner, LifecycleOwner by lifecycleOwner { + private val controller = SavedStateRegistryController.create(this).apply { performRestore(null) } + override val savedStateRegistry: SavedStateRegistry = controller.savedStateRegistry +} + +internal class PaparazziOnBackPressedDispatcherOwner( + private val lifecycleOwner: LifecycleOwner +) : OnBackPressedDispatcherOwner, LifecycleOwner by lifecycleOwner { + override val onBackPressedDispatcher: OnBackPressedDispatcher + get() = OnBackPressedDispatcher { /* Swallow all back-presses. */ } +} diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/RenderResult.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/RenderResult.kt similarity index 100% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/RenderResult.kt rename to paparazzi/src/main/java/app/cash/paparazzi/internal/RenderResult.kt diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/Renderer.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/Renderer.kt new file mode 100644 index 0000000000..554c0a9e89 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/Renderer.kt @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2016 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. + */ + +package app.cash.paparazzi.internal + +import app.cash.paparazzi.DeviceConfig +import app.cash.paparazzi.Environment +import app.cash.paparazzi.Flags +import app.cash.paparazzi.Paparazzi +import app.cash.paparazzi.deprecated.com.android.ide.common.resources.deprecated.FrameworkResources +import app.cash.paparazzi.deprecated.com.android.ide.common.resources.deprecated.ResourceItem +import app.cash.paparazzi.deprecated.com.android.ide.common.resources.deprecated.ResourceRepository +import app.cash.paparazzi.deprecated.com.android.io.FolderWrapper +import app.cash.paparazzi.getFieldReflectively +import app.cash.paparazzi.internal.resources.AarSourceResourceRepository +import app.cash.paparazzi.internal.resources.AppResourceRepository +import app.cash.paparazzi.internal.resources.FrameworkResourceRepository +import app.cash.paparazzi.setStaticValue +import com.android.layoutlib.bridge.Bridge +import com.android.layoutlib.bridge.android.RenderParamsFlags +import com.android.layoutlib.bridge.impl.DelegateManager +import java.io.Closeable +import java.io.File +import java.nio.file.Paths +import java.util.Locale +import kotlin.io.path.name + +/** View rendering. */ +internal class Renderer( + private val environment: Environment, + private val layoutlibCallback: PaparazziCallback, + private val logger: PaparazziLogger +) : Closeable { + private var bridge: Bridge? = null + private lateinit var sessionParamsBuilder: SessionParamsBuilder + + /** Initialize the bridge and the resource maps. */ + fun prepare(): SessionParamsBuilder { + val platformDataResDir = File("${environment.platformDir}/data/res") + + val useLegacyResourceLoading = System.getProperty(Flags.LEGACY_RESOURCE_LOADING).toBoolean() + val (frameworkResources, projectResources) = + if (useLegacyResourceLoading) { + ResourceRepositoryBridge.Legacy( + FrameworkResources(FolderWrapper(platformDataResDir)) + .apply { + loadResources() + loadPublicResources(logger) + } + ) to + ResourceRepositoryBridge.Legacy( + object : ResourceRepository(FolderWrapper(environment.resDir), false) { + override fun createResourceItem(name: String): ResourceItem { + return ResourceItem(name) + } + }.apply { loadResources() } + ) + } else { + ResourceRepositoryBridge.New( + FrameworkResourceRepository.create( + resourceDirectoryOrFile = platformDataResDir.toPath(), + languagesToLoad = emptySet(), + useCompiled9Patches = false + ) + ) to + ResourceRepositoryBridge.New( + AppResourceRepository.create( + localResourceDirectories = environment.localResourceDirs.map { File(it) }, + moduleResourceDirectories = environment.moduleResourceDirs.map { File(it) }, + libraryRepositories = environment.libraryResourceDirs.map { dir -> + val resourceDirPath = Paths.get(dir) + AarSourceResourceRepository.create( + resourceDirectoryOrFile = resourceDirPath, + libraryName = resourceDirPath.parent.fileName.name // segment before /res + ) + } + ) + ) + } + + val useLegacyAssetLoading = System.getProperty(Flags.LEGACY_ASSET_LOADING).toBoolean() + sessionParamsBuilder = SessionParamsBuilder( + layoutlibCallback = layoutlibCallback, + logger = logger, + frameworkResources = frameworkResources, + projectResources = projectResources, + assetRepository = PaparazziAssetRepository( + assetPath = environment.assetsDir, + assetDirs = if (useLegacyAssetLoading) { + emptyList() + } else { + environment.allModuleAssetDirs + environment.libraryAssetDirs + } + ) + ) + .plusFlag(RenderParamsFlags.FLAG_DO_NOT_RENDER_ON_CREATE, true) + .withTheme("AppTheme", true) + + val platformDataRoot = System.getProperty("paparazzi.platform.data.root") + ?: throw RuntimeException("Missing system property for 'paparazzi.platform.data.root'") + val platformDataDir = File(platformDataRoot, "data") + val fontLocation = File(platformDataDir, "fonts") + val nativeLibLocation = File(platformDataDir, getNativeLibDir()) + val icuLocation = File(platformDataDir, "icu" + File.separator + "icudt70l.dat") + val keyboardLocation = File(platformDataDir, "keyboards" + File.separator + "Generic.kcm") + val buildProp = File(environment.platformDir, "build.prop") + val attrs = File(platformDataResDir, "values" + File.separator + "attrs.xml") + val systemProperties = DeviceConfig.loadProperties(buildProp) + mapOf( + // We want Choreographer.USE_FRAME_TIME to be false so it uses System_Delegate.nanoTime() + "debug.choreographer.frametime" to "false" + ) + bridge = Bridge().apply { + check( + init( + systemProperties, + fontLocation, + nativeLibLocation.path, + icuLocation.path, + arrayOf(keyboardLocation.path), + DeviceConfig.getEnumMap(attrs), + logger + ) + ) { "Failed to init Bridge." } + } + configureBuildProperties() + Bridge.getLock() + .lock() + try { + Bridge.setLog(logger) + } finally { + Bridge.getLock() + .unlock() + } + + return sessionParamsBuilder + } + + private fun configureBuildProperties() { + val classLoader = Paparazzi::class.java.classLoader + val buildClass = try { + classLoader.loadClass("android.os.Build") + } catch (e: ClassNotFoundException) { + // Project unit tests don't load Android platform code + return + } + val originalBuildClass = try { + classLoader.loadClass("android.os._Original_Build") + } catch (e: ClassNotFoundException) { + // Project unit tests don't load Android platform code + return + } + + buildClass.fields.forEach { + try { + val originalField = originalBuildClass.getField(it.name) + buildClass.getFieldReflectively(it.name).setStaticValue(originalField.get(null)) + } catch (e: NoSuchFieldException) { + // android.os._Original_Build from layoutlib doesn't have this field, it's probably new. + // Just ignore it and keep the value in android.os.Build + } + } + + buildClass.classes.forEach { inner -> + val originalInnerClass = originalBuildClass.classes.single { it.simpleName == inner.simpleName } + inner.fields.forEach { + try { + val originalField = originalInnerClass.getField(it.name) + inner.getFieldReflectively(it.name).setStaticValue(originalField.get(null)) + } catch (e: NoSuchFieldException) { + // android.os._Original_Build from layoutlib doesn't have this field, it's probably new. + // Just ignore it and keep the value in android.os.Build + } + } + } + } + + private fun getNativeLibDir(): String { + val osName = System.getProperty("os.name").toLowerCase(Locale.US) + val osLabel = when { + osName.startsWith("windows") -> "win" + osName.startsWith("mac") -> { + val osArch = System.getProperty("os.arch").lowercase(Locale.US) + if (osArch.startsWith("x86")) "mac" else "mac-arm" + } + else -> "linux" + } + return "$osLabel/lib64" + } + + override fun close() { + bridge = null + + Gc.gc() + + dumpDelegates() + } + + fun dumpDelegates() { + if (System.getProperty(Flags.DEBUG_LINKED_OBJECTS) != null) { + println("Objects still linked from the DelegateManager:") + DelegateManager.dump(System.out) + } + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/ResourceRepositoryBridge.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/ResourceRepositoryBridge.kt new file mode 100644 index 0000000000..4d4fcba75f --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/ResourceRepositoryBridge.kt @@ -0,0 +1,9 @@ +package app.cash.paparazzi.internal + +import app.cash.paparazzi.deprecated.com.android.ide.common.resources.deprecated.ResourceRepository as LegacyResourceRepository +import com.android.ide.common.resources.ResourceRepository as NewResourceRepository + +sealed interface ResourceRepositoryBridge { + class Legacy(val repository: LegacyResourceRepository) : ResourceRepositoryBridge + class New(val repository: NewResourceRepository) : ResourceRepositoryBridge +} diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/SessionParamsBuilder.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/SessionParamsBuilder.kt similarity index 77% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/SessionParamsBuilder.kt rename to paparazzi/src/main/java/app/cash/paparazzi/internal/SessionParamsBuilder.kt index 521c8b95e1..b51bc6f67d 100644 --- a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/SessionParamsBuilder.kt +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/SessionParamsBuilder.kt @@ -17,6 +17,8 @@ package app.cash.paparazzi.internal import app.cash.paparazzi.DeviceConfig +import app.cash.paparazzi.internal.ResourceRepositoryBridge.Legacy +import app.cash.paparazzi.internal.ResourceRepositoryBridge.New import app.cash.paparazzi.internal.parsers.LayoutPullParser import com.android.SdkConstants import com.android.ide.common.rendering.api.AssetRepository @@ -27,7 +29,7 @@ import com.android.ide.common.rendering.api.SessionParams.Key import com.android.ide.common.rendering.api.SessionParams.RenderingMode import com.android.ide.common.resources.ResourceResolver import com.android.ide.common.resources.ResourceValueMap -import com.android.ide.common.resources.deprecated.ResourceRepository +import com.android.ide.common.resources.getConfiguredResources import com.android.layoutlib.bridge.Bridge import com.android.resources.LayoutDirection import com.android.resources.ResourceType @@ -36,9 +38,9 @@ import com.android.resources.ResourceType internal data class SessionParamsBuilder( private val layoutlibCallback: PaparazziCallback, private val logger: PaparazziLogger, - private val frameworkResources: ResourceRepository, + private val frameworkResources: ResourceRepositoryBridge, private val assetRepository: AssetRepository, - private val projectResources: ResourceRepository, + private val projectResources: ResourceRepositoryBridge, private val deviceConfig: DeviceConfig = DeviceConfig.NEXUS_5, private val renderingMode: RenderingMode = RenderingMode.NORMAL, private val targetSdk: Int = 22, @@ -78,12 +80,30 @@ internal data class SessionParamsBuilder( val folderConfiguration = deviceConfig.folderConfiguration val resourceResolver = ResourceResolver.create( mapOf<ResourceNamespace, Map<ResourceType, ResourceValueMap>>( - ResourceNamespace.ANDROID to frameworkResources.getConfiguredResources( - folderConfiguration - ), - ResourceNamespace.TODO() to projectResources.getConfiguredResources( - folderConfiguration - ) + when (frameworkResources) { + is Legacy -> + ResourceNamespace.ANDROID to + frameworkResources.repository.getConfiguredResources(folderConfiguration) + + is New -> + ResourceNamespace.ANDROID to + frameworkResources.repository.getConfiguredResources(folderConfiguration) + .row(ResourceNamespace.ANDROID) + }, + *when (projectResources) { + is Legacy -> { + arrayOf( + ResourceNamespace.TODO() to + projectResources.repository.getConfiguredResources(folderConfiguration) + ) + } + + is New -> + projectResources.repository.getConfiguredResources(folderConfiguration) + .rowMap() + .map { (key, value) -> key to value } + .toTypedArray() + } ), ResourceReference( ResourceNamespace.fromBoolean(!isProjectTheme), @@ -93,7 +113,7 @@ internal data class SessionParamsBuilder( ) val result = SessionParams( - layoutPullParser, renderingMode, projectKey /* for caching */, + layoutPullParser, renderingMode, projectKey, deviceConfig.hardwareConfig, resourceResolver, layoutlibCallback, minSdk, targetSdk, logger ) result.fontScale = deviceConfig.fontScale diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/ChoreographerDelegateInterceptor.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/interceptors/ChoreographerDelegateInterceptor.kt similarity index 83% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/ChoreographerDelegateInterceptor.kt rename to paparazzi/src/main/java/app/cash/paparazzi/internal/interceptors/ChoreographerDelegateInterceptor.kt index 156f7f42ac..f37835c066 100644 --- a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/ChoreographerDelegateInterceptor.kt +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/interceptors/ChoreographerDelegateInterceptor.kt @@ -1,4 +1,4 @@ -package app.cash.paparazzi.internal +package app.cash.paparazzi.internal.interceptors import android.view.Choreographer import com.android.internal.lang.System_Delegate diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/EditModeInterceptor.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/interceptors/EditModeInterceptor.kt similarity index 62% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/EditModeInterceptor.kt rename to paparazzi/src/main/java/app/cash/paparazzi/internal/interceptors/EditModeInterceptor.kt index c464a04b55..d744ba47f5 100644 --- a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/EditModeInterceptor.kt +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/interceptors/EditModeInterceptor.kt @@ -1,4 +1,4 @@ -package app.cash.paparazzi.internal +package app.cash.paparazzi.internal.interceptors object EditModeInterceptor { @JvmStatic diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/IInputMethodManagerInterceptor.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/interceptors/IInputMethodManagerInterceptor.kt similarity index 91% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/IInputMethodManagerInterceptor.kt rename to paparazzi/src/main/java/app/cash/paparazzi/internal/interceptors/IInputMethodManagerInterceptor.kt index 1908949f97..3b90e0eb86 100644 --- a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/IInputMethodManagerInterceptor.kt +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/interceptors/IInputMethodManagerInterceptor.kt @@ -1,4 +1,4 @@ -package app.cash.paparazzi.internal +package app.cash.paparazzi.internal.interceptors import android.os.IBinder import com.android.internal.view.IInputMethodManager diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/MatrixMatrixMultiplicationInterceptor.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/interceptors/MatrixMatrixMultiplicationInterceptor.kt similarity index 96% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/MatrixMatrixMultiplicationInterceptor.kt rename to paparazzi/src/main/java/app/cash/paparazzi/internal/interceptors/MatrixMatrixMultiplicationInterceptor.kt index 41994c2daa..56ef138555 100644 --- a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/MatrixMatrixMultiplicationInterceptor.kt +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/interceptors/MatrixMatrixMultiplicationInterceptor.kt @@ -1,4 +1,4 @@ -package app.cash.paparazzi.internal +package app.cash.paparazzi.internal.interceptors // Sampled from https://cs.android.com/android/platform/superproject/+/master:external/robolectric-shadows/shadows/framework/src/main/java/org/robolectric/shadows/ShadowOpenGLMatrix.java;l=10-67 object MatrixMatrixMultiplicationInterceptor { diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/MatrixVectorMultiplicationInterceptor.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/interceptors/MatrixVectorMultiplicationInterceptor.kt similarity index 97% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/MatrixVectorMultiplicationInterceptor.kt rename to paparazzi/src/main/java/app/cash/paparazzi/internal/interceptors/MatrixVectorMultiplicationInterceptor.kt index 5bfd20dc49..66acef2c21 100644 --- a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/MatrixVectorMultiplicationInterceptor.kt +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/interceptors/MatrixVectorMultiplicationInterceptor.kt @@ -1,4 +1,4 @@ -package app.cash.paparazzi.internal +package app.cash.paparazzi.internal.interceptors // Sampled from https://cs.android.com/android/platform/superproject/+/master:external/robolectric-shadows/shadows/framework/src/main/java/org/robolectric/shadows/ShadowOpenGLMatrix.java;l=69-121 object MatrixVectorMultiplicationInterceptor { diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/ResourcesInterceptor.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/interceptors/ResourcesInterceptor.kt similarity index 82% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/ResourcesInterceptor.kt rename to paparazzi/src/main/java/app/cash/paparazzi/internal/interceptors/ResourcesInterceptor.kt index a7f14c875e..a3c65a3c19 100644 --- a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/ResourcesInterceptor.kt +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/interceptors/ResourcesInterceptor.kt @@ -1,4 +1,4 @@ -package app.cash.paparazzi.internal +package app.cash.paparazzi.internal.interceptors import android.content.Context import android.graphics.Typeface diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/ServiceManagerInterceptor.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/interceptors/ServiceManagerInterceptor.kt similarity index 93% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/ServiceManagerInterceptor.kt rename to paparazzi/src/main/java/app/cash/paparazzi/internal/interceptors/ServiceManagerInterceptor.kt index 4726fad180..108c471756 100644 --- a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/ServiceManagerInterceptor.kt +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/interceptors/ServiceManagerInterceptor.kt @@ -1,4 +1,4 @@ -package app.cash.paparazzi.internal +package app.cash.paparazzi.internal.interceptors import android.os.IBinder diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/AaptAttrParser.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/AaptAttrParser.kt similarity index 100% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/AaptAttrParser.kt rename to paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/AaptAttrParser.kt diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/AaptAttrSnapshot.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/AaptAttrSnapshot.kt similarity index 100% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/AaptAttrSnapshot.kt rename to paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/AaptAttrSnapshot.kt diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/AttributeSnapshot.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/AttributeSnapshot.kt similarity index 100% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/AttributeSnapshot.kt rename to paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/AttributeSnapshot.kt diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/InMemoryParser.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/InMemoryParser.kt similarity index 100% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/InMemoryParser.kt rename to paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/InMemoryParser.kt diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/LayoutPullParser.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/LayoutPullParser.kt similarity index 100% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/LayoutPullParser.kt rename to paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/LayoutPullParser.kt diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/ResourceParser.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/ResourceParser.kt similarity index 99% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/ResourceParser.kt rename to paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/ResourceParser.kt index 574e18df8a..386997be3c 100644 --- a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/ResourceParser.kt +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/ResourceParser.kt @@ -119,7 +119,9 @@ class ResourceParser(inputStream: InputStream) : KXmlParser() { val prefixEnd = name.indexOf(':') return if (prefixEnd > 0) { name.substring(0, prefixEnd) - } else "" + } else { + "" + } } private fun findLocalNameByQualifiedName(name: String): String { diff --git a/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/TagSnapshot.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/TagSnapshot.kt similarity index 100% rename from paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/TagSnapshot.kt rename to paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/TagSnapshot.kt diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/AarSourceResourceRepository.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/AarSourceResourceRepository.kt new file mode 100644 index 0000000000..f2974eb2e6 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/AarSourceResourceRepository.kt @@ -0,0 +1,299 @@ +package app.cash.paparazzi.internal.resources + +import android.annotation.SuppressLint +import app.cash.paparazzi.internal.resources.base.BasicResourceItem +import com.android.SdkConstants.FN_ANDROID_MANIFEST_XML +import com.android.SdkConstants.FN_PUBLIC_TXT +import com.android.SdkConstants.FN_RESOURCE_TEXT +import com.android.ide.common.rendering.api.ResourceNamespace +import com.android.ide.common.resources.AndroidManifestPackageNameUtils +import com.android.ide.common.resources.ResourceItem +import com.android.ide.common.symbols.Symbol +import com.android.ide.common.symbols.SymbolIo +import com.android.ide.common.symbols.SymbolTable +import com.android.ide.common.util.PathString +import com.android.resources.ResourceType +import com.android.resources.ResourceVisibility.PUBLIC +import com.android.utils.Base128InputStream +import java.io.BufferedReader +import java.io.FileNotFoundException +import java.io.IOException +import java.io.InputStreamReader +import java.nio.charset.StandardCharsets.UTF_8 +import java.nio.file.Files +import java.nio.file.Path +import java.util.logging.Logger +import java.util.stream.Collectors + +/** + * A resource repository representing unpacked contents of a non-namespaced AAR. + * + * For performance reasons ID resources defined using @+id syntax in layout XML files are + * obtained from R.txt instead, when it is available. This means that + * [ResourceItem.getOriginalSource] method may return null for such ID resources. + */ +open class AarSourceResourceRepository( + loader: RepositoryLoader<out AarSourceResourceRepository>, + libraryName: String? +) : AbstractAarResourceRepository(loader.namespace, libraryName) { + protected val resourceDirectoryOrFile: Path + + /** + * Protocol used for constructing [PathString]s returned by the [BasicFileResourceItem.getSource] method. + */ + private val sourceFileProtocol: String + + /** + * Common prefix of paths of all file resources. Used to compose resource paths returned by + * the [BasicFileResourceItem.getSource] method. + */ + private val resourcePathPrefix: String + + /** + * Common prefix of URLs of all file resources. Used to compose resource URLs returned by + * the [BasicFileResourceItem.getValue] method. + */ + private val resourceUrlPrefix: String + + /** The package name read on-demand from the manifest. */ + private val manifestPackageName: Lazy<String?> + + init { + resourceDirectoryOrFile = loader.resourceDirectoryOrFile + sourceFileProtocol = loader.sourceFileProtocol + resourcePathPrefix = loader.resourcePathPrefix + resourceUrlPrefix = loader.resourceUrlPrefix + manifestPackageName = lazy { + try { + val manifestPath = getSourceFile("../$FN_ANDROID_MANIFEST_XML", true) + return@lazy AndroidManifestPackageNameUtils.getPackageNameFromManifestFile(manifestPath) + } catch (e: FileNotFoundException) { + return@lazy null + } catch (e: IOException) { + LOG.severe("Failed to read manifest $FN_ANDROID_MANIFEST_XML for $displayName: $e") + return@lazy null + } + } + } + + override val origin: Path + get() = resourceDirectoryOrFile + + override fun getPackageName(): String? = namespace.packageName ?: manifestPackageName.value + + override fun getSourceFile( + relativeResourcePath: String, + forFileResource: Boolean + ): PathString { + return PathString(sourceFileProtocol, resourcePathPrefix + relativeResourcePath) + } + + override fun getResourceUrl(relativeResourcePath: String): String = + "$resourceUrlPrefix$relativeResourcePath" + + /** + * Loads contents of the repository from the given input stream. + */ + @Throws(IOException::class) + fun loadFromStream( + stream: Base128InputStream, + stringCache: Map<String, String>, + namespaceResolverCache: MutableMap<NamespaceResolver, NamespaceResolver>? + ) = ResourceSerializationUtil.readResourcesFromStream( + stream, + stringCache, + namespaceResolverCache, + this, + ::addResourceItem + ) + + // For debugging only. + override fun toString(): String { + return "${javaClass.simpleName}@${Integer.toHexString(System.identityHashCode(this))} for $resourceDirectoryOrFile" + } + + private class Loader( + resourceDirectoryOrFile: Path, + resourceFilesAndFolders: Collection<PathString>?, + namespace: ResourceNamespace + ) : RepositoryLoader<AarSourceResourceRepository>(resourceDirectoryOrFile, resourceFilesAndFolders, namespace) { + private var rTxtIds: Set<String> = emptySet() + + @SuppressLint("NewApi") + override fun loadIdsFromRTxt(): Boolean { + if (zipFile == null) { + val rDotTxt = resourceDirectoryOrFile.resolveSibling(FN_RESOURCE_TEXT) + if (Files.exists(rDotTxt)) { + try { + val symbolTable = SymbolIo.readFromAaptNoValues(rDotTxt.toFile(), null) + rTxtIds = computeIds(symbolTable) + return true + } catch (e: Exception) { + LOG.warning("Failed to load id resources from $rDotTxt: $e") + } + } + } else { + val zipEntry = zipFile!!.getEntry(FN_RESOURCE_TEXT) + if (zipEntry != null) { + try { + BufferedReader( + InputStreamReader(zipFile!!.getInputStream(zipEntry), UTF_8) + ).use { reader -> + val symbolTable = SymbolIo.readFromAaptNoValues( + reader, + "$FN_RESOURCE_TEXT in $resourceDirectoryOrFile", + null + ) + rTxtIds = computeIds(symbolTable) + return true + } + } catch (e: Exception) { + LOG.warning( + "Failed to load id resources from $FN_RESOURCE_TEXT in $resourceDirectoryOrFile: $e" + ) + } + } + return false + } + return false + } + + override fun finishLoading(repository: AarSourceResourceRepository) { + super.finishLoading(repository) + createResourcesForRTxtIds(repository) + } + + /** + * Creates ID resources for the ID names in the R.txt file. + */ + private fun createResourcesForRTxtIds(repository: AarSourceResourceRepository) { + if (rTxtIds.isNotEmpty()) { + val configuration = getConfiguration(repository, ResourceItem.DEFAULT_CONFIGURATION) + val sourceFile = ResourceSourceFileImpl(null, configuration) + ResourceSourceFileImpl(null, configuration) + for (name in rTxtIds) { + addIdResourceItem(name, sourceFile) + } + addValueFileResources() + } + } + + @SuppressLint("NewApi") + override fun loadPublicResourceNames() { + if (zipFile == null) { + val file = resourceDirectoryOrFile.resolveSibling(FN_PUBLIC_TXT) + try { + Files.newBufferedReader(file).use { reader -> readPublicResourceNames(reader) } + } catch (e: NoSuchFileException) { + // The "public.txt" file does not exist - defaultVisibility will be PUBLIC. + defaultVisibility = PUBLIC + } catch (e: IOException) { + // Failure to load public resource names is not considered fatal. + LOG.warning("Error reading $file: $e") + } + } else { + val zipEntry = zipFile!!.getEntry(FN_PUBLIC_TXT) + if (zipEntry == null) { + // The "public.txt" file does not exist - defaultVisibility will be PUBLIC. + defaultVisibility = PUBLIC + } else { + try { + BufferedReader( + InputStreamReader( + zipFile!!.getInputStream(zipEntry), + UTF_8 + ) + ).use { reader -> + readPublicResourceNames(reader) + } + } catch (e: IOException) { + // Failure to load public resource names is not considered fatal. + LOG.warning("Error reading $FN_PUBLIC_TXT from $resourceDirectoryOrFile: $e") + } + } + } + } + + override fun addResourceItem( + item: BasicResourceItem, + repository: AarSourceResourceRepository + ) = repository.addResourceItem(item) + + @Throws(IOException::class) + private fun readPublicResourceNames(reader: BufferedReader) { + var maybeLine: String? + while (reader.readLine().also { maybeLine = it } != null) { + var line = maybeLine!! + // Lines in public.txt have the following format: <resource_type> <resource_name> + line = line.trim { it <= ' ' } + val delimiterPos = line.indexOf(' ') + if (delimiterPos > 0 && delimiterPos + 1 < line.length) { + val type = ResourceType.fromXmlTagName(line.substring(0, delimiterPos)) + if (type != null) { + val name = line.substring(delimiterPos + 1) + addPublicResourceName(type, name) + } + } + } + } + } + + companion object { + private fun computeIds(symbolTable: SymbolTable): Set<String> = + symbolTable.symbols + .row(ResourceType.ID) + .values + .stream() + .map(Symbol::canonicalName) + .collect(Collectors.toSet()) + + private val LOG: Logger = Logger.getLogger(AarSourceResourceRepository::class.java.name) + + /** + * Creates and loads a resource repository. + * + * @param resourceDirectoryOrFile the res directory or an AAR file containing resources + * @param libraryName the name of the library + * @return the created resource repository + */ + fun create(resourceDirectoryOrFile: Path, libraryName: String): AarSourceResourceRepository = + create(resourceDirectoryOrFile, null, ResourceNamespace.RES_AUTO, libraryName) + + /** + * Creates and loads a resource repository. + * + * @param resourceFolderRoot specifies the resource files to be loaded. The list of files to be loaded can be restricted by providing + * a not null `resourceFolderResources` list of files and subdirectories that should be loaded. + * @param resourceFolderResources A null value indicates that all files and subdirectories in `resourceFolderRoot` should be loaded. + * Otherwise files and subdirectories specified in `resourceFolderResources` are loaded. + * @param libraryName the name of the library + * @return the created resource repository + */ + fun create( + resourceFolderRoot: PathString, + resourceFolderResources: Collection<PathString>?, + libraryName: String + ): AarSourceResourceRepository { + val resDir = resourceFolderRoot.toPath() + check(resDir != null) + return create(resDir, resourceFolderResources, ResourceNamespace.RES_AUTO, libraryName) + } + + private fun create( + resourceDirectoryOrFile: Path, + resourceFilesAndFolders: Collection<PathString>?, + namespace: ResourceNamespace, + libraryName: String + ): AarSourceResourceRepository { + val loader = Loader(resourceDirectoryOrFile, resourceFilesAndFolders, namespace) + val repository = AarSourceResourceRepository(loader, libraryName) + + loader.loadRepositoryContents(repository) + + repository.populatePublicResourcesMap() + repository.freezeResources() + + return repository + } + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/AbstractAarResourceRepository.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/AbstractAarResourceRepository.kt new file mode 100644 index 0000000000..d36b9d2110 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/AbstractAarResourceRepository.kt @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.internal.resources + +import com.android.ide.common.rendering.api.ResourceNamespace +import com.android.ide.common.resources.AbstractResourceRepository +import com.android.ide.common.resources.ResourceItem +import com.android.ide.common.resources.ResourceItemWithVisibility +import com.android.ide.common.resources.ResourceVisitor +import com.android.ide.common.resources.ResourceVisitor.VisitResult +import com.android.ide.common.resources.ResourceVisitor.VisitResult.ABORT +import com.android.ide.common.resources.ResourceVisitor.VisitResult.CONTINUE +import com.android.resources.ResourceType +import com.android.resources.ResourceVisibility.PUBLIC +import com.google.common.collect.ArrayListMultimap +import com.google.common.collect.ImmutableListMultimap +import com.google.common.collect.ListMultimap +import java.util.EnumMap + +/** + * Ported from: [AbstractAarResourceRepository.java](https://cs.android.com/android-studio/platform/tools/base/+/47d204001bf0cb6273d8b135c7eece3a982cf0e0:resource-repository/main/java/com/android/resources/aar/AbstractAarResourceRepository.java) + */ +abstract class AbstractAarResourceRepository internal constructor( + private val namespace: ResourceNamespace, + override val libraryName: String? +) : AbstractResourceRepository(), LoadableResourceRepository { + protected val resources = + EnumMap<ResourceType, ListMultimap<String, ResourceItem>>(ResourceType::class.java) + private val publicResources = EnumMap<ResourceType, Set<ResourceItem>>(ResourceType::class.java) + + override fun getResourcesInternal( + namespace: ResourceNamespace, + resourceType: ResourceType + ): ListMultimap<String, ResourceItem> = + if (namespace != this.namespace) { + ImmutableListMultimap.of() + } else { + resources.getOrDefault(resourceType, ImmutableListMultimap.of()) + } + + private fun getOrCreateMap(resourceType: ResourceType): ListMultimap<String, ResourceItem> = + resources.computeIfAbsent(resourceType) { ArrayListMultimap.create<String, ResourceItem>() } + + protected fun addResourceItem(item: ResourceItem) { + val multimap = getOrCreateMap(item.type) + multimap.put(item.name, item) + } + + /** + * Populates the [publicResources] map. Has to be called after [resources] has been populated. + */ + protected fun populatePublicResourcesMap() { + resources.entries.forEach { (resourceType, items) -> + publicResources[resourceType] = items.values() + .filterIsInstance<ResourceItemWithVisibility>() + .filter { it.visibility == PUBLIC } + .toSet() + } + } + + /** + * Makes resource maps immutable. + */ + protected fun freezeResources() { + for ((key, value) in resources) { + resources[key] = ImmutableListMultimap.copyOf(value) + } + } + + override fun accept(visitor: ResourceVisitor): VisitResult { + if (visitor.shouldVisitNamespace(namespace)) { + if (acceptByResources(resources, visitor) == ABORT) { + return ABORT + } + } + return CONTINUE + } + + override fun getResources( + namespace: ResourceNamespace, + resourceType: ResourceType, + resourceName: String + ): List<ResourceItem> { + val map = getResourcesInternal(namespace, resourceType) + return map[resourceName] ?: emptyList() + } + + override fun getResources( + namespace: ResourceNamespace, + resourceType: ResourceType + ): ListMultimap<String, ResourceItem> = getResourcesInternal(namespace, resourceType) + + override fun getPublicResources( + namespace: ResourceNamespace, + type: ResourceType + ): Collection<ResourceItem> { + if (namespace != this.namespace) return emptySet() + return publicResources[type] ?: emptySet() + } + + override fun getNamespace(): ResourceNamespace = namespace + + override val displayName: String + get() = if (libraryName == null) "Android Framework" else libraryName!! + + override fun containsUserDefinedResources(): Boolean = false +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/AppResourceRepository.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/AppResourceRepository.kt new file mode 100644 index 0000000000..cfcfa3c407 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/AppResourceRepository.kt @@ -0,0 +1,38 @@ +package app.cash.paparazzi.internal.resources + +import java.io.File + +/** + * Ported from: [AppResourceRepository.java](https://cs.android.com/android-studio/platform/tools/adt/idea/+/308d48ccea9508984bce3e120a03ed9a105d4331:android/src/com/android/tools/idea/res/AppResourceRepository.java) + * + * Returns the repository with all non-framework resources available to a given module (in the current variant). + * This includes not just the resources defined in this module, but in any other modules that this module depends + * on, as well as any libraries those modules may depend on (e.g. appcompat). + * + * When a layout is rendered in the layout editor, it is getting resources from the app resource repository: + * it should see all the resources just like the app does. + */ +internal class AppResourceRepository private constructor( + displayName: String, + localResources: List<LocalResourceRepository>, + libraryResources: Collection<AarSourceResourceRepository> +) : MultiResourceRepository("$displayName with modules and libraries") { + + init { + setChildren(localResources, libraryResources) + } + + companion object { + fun create( + localResourceDirectories: List<File>, + moduleResourceDirectories: List<File>, + libraryRepositories: Collection<AarSourceResourceRepository> + ): AppResourceRepository { + return AppResourceRepository( + displayName = "", + listOf(ProjectResourceRepository.create(localResourceDirectories, moduleResourceDirectories)), + libraryRepositories + ) + } + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/CommentTrackingXmlPullParser.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/CommentTrackingXmlPullParser.kt new file mode 100644 index 0000000000..53a3520622 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/CommentTrackingXmlPullParser.kt @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.internal.resources + +import com.android.SdkConstants.TAG_EAT_COMMENT +import org.kxml2.io.KXmlParser +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserException +import java.io.IOException +import java.io.InputStream +import java.io.Reader + +/** + * Ported from: [CommentTrackingXmlPullParser.java](https://cs.android.com/android-studio/platform/tools/base/+/47d204001bf0cb6273d8b135c7eece3a982cf0e0:resource-repository/main/java/com/android/resources/base/CommentTrackingXmlPullParser.java) + * + * An [XmlPullParser] that keeps track of the last comment preceding an XML tag and special comments + * that are used in the framework resource files for describing groups of "attr" resources. Here is + * an example of an "attr" group comment: + * <pre> + * <!-- =========== --> + * <!-- Text styles --> + * <!-- =========== --> + * <eat-comment/> +</pre> * + */ +open class CommentTrackingXmlPullParser : KXmlParser() { + /** + * Returns the last encountered comment that is not an ASCII art. + */ + var lastComment: String? = null + private var tagEncounteredAfterComment: Boolean = false + private val attrGroupCommentStack = ArrayList<String?>(4) + + /** + * Initializes the parser. XML namespaces are supported by default. + */ + init { + try { + setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true) + } catch (e: XmlPullParserException) { + throw Error(e) // KXmlParser is guaranteed to support FEATURE_PROCESS_NAMESPACES. + } + } + + /** + * Returns the name of the current "attr" group, e.g. "Button Styles" group for "buttonStyleSmall" "attr" tag. + */ + val attrGroupComment: String? + get() = attrGroupCommentStack[attrGroupCommentStack.size - 1] + + @Throws(XmlPullParserException::class, IOException::class) + override fun nextToken(): Int { + val token = super.nextToken() + processToken(token) + return token + } + + @Throws(XmlPullParserException::class, IOException::class) + override operator fun next(): Int { + throw UnsupportedOperationException("Use nextToken() instead of next() for comment tracking to work") + } + + private fun processToken(token: Int) { + when (token) { + XmlPullParser.START_TAG -> { + if (tagEncounteredAfterComment) { + lastComment = null + } + tagEncounteredAfterComment = true + // Duplicate the last element in attrGroupCommentStack. + attrGroupCommentStack += attrGroupCommentStack.last() + assert(attrGroupCommentStack.size == depth + 1) + + if (TAG_EAT_COMMENT == name && prefix == null) { + // The framework attribute file follows a special convention where related attributes are grouped together, + // and there is always a set of comments that indicate these sections which look like this: + // <!-- =========== --> + // <!-- Text styles --> + // <!-- =========== --> + // <eat-comment/> + // These section headers are always immediately followed by an <eat-comment>. Not all <eat-comment/> sections are + // actually attribute headers, some are comments. We identify these by looking at the line length; category comments + // are short, and descriptive comments are longer. + if (lastComment != null && lastComment!!.length <= ATTR_GROUP_MAX_CHARACTERS && !lastComment!!.startsWith("TODO:")) { + var attrGroupComment = lastComment!! + if (attrGroupComment.endsWith(".")) { + // Strip the trailing period. + attrGroupComment = attrGroupComment.substring(0, attrGroupComment.length - 1) + } + // Replace the second to last element in attrGroupCommentStack. + attrGroupCommentStack[attrGroupCommentStack.size - 2] = attrGroupComment + } + } + } + + XmlPullParser.END_TAG -> { + lastComment = null + attrGroupCommentStack.removeLast() + } + + XmlPullParser.COMMENT -> { + val commentText = text.trim() + if (!isEmptyOrAsciiArt(commentText)) { + lastComment = commentText + tagEncounteredAfterComment = false + } + } + } + } + + @Throws(XmlPullParserException::class) + override fun setInput(reader: Reader) { + super.setInput(reader) + lastComment = null + attrGroupCommentStack.clear() + attrGroupCommentStack.add(null) + } + + @Throws(XmlPullParserException::class) + override fun setInput(inputStream: InputStream, encoding: String?) { + super.setInput(inputStream, encoding) + lastComment = null + attrGroupCommentStack.clear() + attrGroupCommentStack.add(null) + } + + companion object { + // Used for parsing group of attributes, used heuristically to skip long comments before <eat-comment/>. + private const val ATTR_GROUP_MAX_CHARACTERS = 40 + + private fun isEmptyOrAsciiArt(commentText: String): Boolean = + commentText.isEmpty() || commentText[0] == '*' || commentText[0] == '=' + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/FileFilter.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/FileFilter.kt new file mode 100644 index 0000000000..03b390fbf6 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/FileFilter.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.internal.resources + +import java.nio.file.Path +import java.nio.file.attribute.BasicFileAttributes + +/** + * Ported from: [FileFilter.java](https://cs.android.com/android-studio/platform/tools/base/+/18047faf69512736b8ddb1f6a6785f58d47c893f:resource-repository/main/java/com/android/resources/base/FileFilter.java) + * + * A filter used to select files when traversing the file system. + */ +internal interface FileFilter { + /** Returns true to skip the file or directory, or false to accept it. */ + fun isIgnored(fileOrDirectory: Path, attrs: BasicFileAttributes): Boolean +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/FrameworkResourceRepository.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/FrameworkResourceRepository.kt new file mode 100644 index 0000000000..8d1350e605 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/FrameworkResourceRepository.kt @@ -0,0 +1,400 @@ +package app.cash.paparazzi.internal.resources + +import android.annotation.SuppressLint +import app.cash.paparazzi.internal.resources.base.BasicResourceItem +import app.cash.paparazzi.internal.resources.base.BasicValueResourceItemBase +import com.android.SdkConstants.DOT_9PNG +import com.android.SdkConstants.FD_RES_RAW +import com.android.ide.common.rendering.api.ResourceNamespace +import com.android.ide.common.rendering.api.ResourceNamespace.Resolver +import com.android.ide.common.resources.configuration.FolderConfiguration +import com.android.ide.common.util.PathString +import com.android.resources.ResourceType +import com.android.utils.Base128InputStream +import com.google.common.collect.Maps +import com.google.common.collect.Sets +import java.io.IOException +import java.nio.file.Path +import java.nio.file.attribute.BasicFileAttributes +import java.util.TreeSet +import java.util.logging.Logger +import java.util.zip.ZipEntry +import java.util.zip.ZipFile + +/** + * Repository of resources of the Android framework. Most client code should use + * the ResourceRepositoryManager.getFrameworkResources method to obtain framework resources. + * + * The repository can be loaded either from a res directory containing XML files, or from + * framework_res.jar file, or from a binary cache file located under the directory returned by + * the [PathManager.getSystemPath] method. This binary cache file can be created as + * a side effect of loading the repository from a res directory. + * + * Loading from framework_res.jar or a binary cache file is 3-4 times faster than loading + * from res directory. + * + * @see FrameworkResJarCreator + */ +class FrameworkResourceRepository private constructor( + loader: RepositoryLoader<FrameworkResourceRepository>, + private val useCompiled9Patches: Boolean +) : AarSourceResourceRepository(loader, null) { + /** + * Checks if the repository contains resources for the given set of languages. + * + * @param languages the set of ISO 639 language codes to check + * @return true if the repository contains resources for all requested languages + */ + fun containsLanguages(languages: Set<String>): Boolean { + for (language: String in languages) { + if (getLanguageGroup(language) !in this.languageGroups) { + return false + } + } + return true + } + + /** + * Loads resources for requested languages that are not present in this resource repository. + * + * @param languagesToLoad the set of ISO 639 language codes, or null to load all available languages + * @return the new resource repository with additional resources, or this resource repository if it already contained + * all requested languages + */ + fun loadMissingLanguages( + languagesToLoad: Set<String>? + ): FrameworkResourceRepository { + val languageGroups = if (languagesToLoad == null) null else getLanguageGroups(languagesToLoad) + if (languageGroups != null && this.languageGroups.containsAll(languageGroups)) { + // The repository already contains all requested languages. + return this + } + + val loader = Loader(this, languageGroups) + val newRepository = FrameworkResourceRepository(loader, useCompiled9Patches) + newRepository.load(this, loader, languageGroups, loader.loadedLanguageGroups) + return newRepository + } + + private fun load( + sourceRepository: FrameworkResourceRepository?, + loader: Loader, + languageGroups: Set<String>?, + languageGroupsLoadedFromSourceRepositoryOrCache: Set<String> + ) { + val stringCache = Maps.newHashMapWithExpectedSize<String, String>(10000) + val namespaceResolverCache = HashMap<NamespaceResolver, NamespaceResolver>() + val configurationsToTakeOver = if (sourceRepository == null) { + setOf() + } else { + copyFromRepository(sourceRepository, stringCache, namespaceResolverCache) + } + + this.languageGroups += languageGroupsLoadedFromSourceRepositoryOrCache + if (languageGroups == null || + !languageGroupsLoadedFromSourceRepositoryOrCache.containsAll(languageGroups) + ) { + loader.loadRepositoryContents(this) + } + + populatePublicResourcesMap() + freezeResources() + takeOverConfigurations(configurationsToTakeOver) + } + + override fun getPackageName(): String? = ANDROID_NAMESPACE.packageName + + override fun getResourceTypes(namespace: ResourceNamespace): Set<ResourceType> = + if (namespace === ANDROID_NAMESPACE) Sets.immutableEnumSet(resources.keys) else setOf() + + /** + * Copies resources from another FrameworkResourceRepository. + * + * @param sourceRepository the repository to copy resources from + * @param stringCache the string cache to populate with the names of copied resources + * @param namespaceResolverCache the namespace resolver cache to populate with namespace resolvers referenced by the copied resources + * @return the [RepositoryConfiguration] objects referenced by the copied resources + */ + private fun copyFromRepository( + sourceRepository: FrameworkResourceRepository, + stringCache: MutableMap<String, String>, + namespaceResolverCache: MutableMap<NamespaceResolver, NamespaceResolver> + ): Set<RepositoryConfiguration> { + val resourceMaps = sourceRepository.resources.values + + // Copy resources from the source repository, get AarConfigurations that need to be taken over by this repository, + // and pre-populate string and namespace resolver caches. + val sourceConfigurations = Sets.newIdentityHashSet<RepositoryConfiguration>() + for (resourceMap in resourceMaps) { + for (item in resourceMap.values()) { + addResourceItem(item) + + sourceConfigurations += (item as BasicResourceItem).repositoryConfiguration + if (item is BasicValueResourceItemBase) { + val resolver = item.namespaceResolver + val namespaceResolver = if (resolver === Resolver.EMPTY_RESOLVER) { + NamespaceResolver.EMPTY + } else { + resolver as NamespaceResolver + } + namespaceResolverCache[namespaceResolver] = namespaceResolver + } + val name = item.name + stringCache[name] = name + } + } + + return sourceConfigurations + } + + val languageGroups: MutableSet<String> + get() { + val languages = TreeSet<String>() + for (resourceMap in resources.values) { + for (item in resourceMap.values()) { + languages += getLanguageGroup(item.configuration) + } + } + return languages + } + + private fun updateResourcePath(relativeResourcePath: String): String = + if (useCompiled9Patches && relativeResourcePath.endsWith(DOT_9PNG)) { + val beginning = + relativeResourcePath.substring(0, relativeResourcePath.length - DOT_9PNG.length) + val ending = relativeResourcePath.substring(relativeResourcePath.length) + "$beginning$COMPILED_9PNG_EXTENSION$ending" + } else { + relativeResourcePath + } + + override fun getResourceUrl(relativeResourcePath: String): String = + super.getResourceUrl(updateResourcePath(relativeResourcePath)) + + override fun getSourceFile(relativeResourcePath: String, forFileResource: Boolean): PathString = + super.getSourceFile(updateResourcePath(relativeResourcePath), forFileResource) + + private class Loader : RepositoryLoader<FrameworkResourceRepository> { + val publicXmlFileNames = listOf("public.xml", "public-final.xml", "public-staging.xml") + + val loadedLanguageGroups: Set<String> + + private var languageGroups: Set<String>? + + constructor( + resourceDirectoryOrFile: Path, + languageGroups: Set<String>? + ) : super(resourceDirectoryOrFile, null, ANDROID_NAMESPACE) { + this.languageGroups = languageGroups + loadedLanguageGroups = TreeSet() + } + + constructor( + sourceRepository: FrameworkResourceRepository, + languageGroups: Set<String>? + ) : super(sourceRepository.resourceDirectoryOrFile, null, ANDROID_NAMESPACE) { + this.languageGroups = languageGroups + loadedLanguageGroups = TreeSet(sourceRepository.languageGroups) + } + + @SuppressLint("NewApi") + override fun loadFromZip(repository: FrameworkResourceRepository) { + try { + ZipFile(resourceDirectoryOrFile.toFile()).use { zipFile -> + if (languageGroups == null) { + languageGroups = readLanguageGroups(zipFile) + } + + val stringCache = Maps.newHashMapWithExpectedSize<String, String>(10000) + val namespaceResolverCache = HashMap<NamespaceResolver, NamespaceResolver>() + + for (language in languageGroups!!) { + if (!loadedLanguageGroups.contains(language)) { + val entryName = getResourceTableNameForLanguage(language) + val zipEntry = zipFile.getEntry(entryName) + ?: if (language.isEmpty()) { + throw IOException("\"$entryName\" not found in $resourceDirectoryOrFile") + } else { + continue // Requested language may not be represented in the Android framework resources. + } + Base128InputStream(zipFile.getInputStream(zipEntry)).use { stream -> + repository.loadFromStream(stream, stringCache, namespaceResolverCache) + } + } + } + + repository.populatePublicResourcesMap() + repository.freezeResources() + } + } catch (e: Exception) { + LOG.severe("Failed to load resources from $resourceDirectoryOrFile: $e") + } + } + + override fun loadRepositoryContents(repository: FrameworkResourceRepository) { + super.loadRepositoryContents(repository) + val languageGroups = languageGroups ?: repository.languageGroups + repository.languageGroups += languageGroups + } + + @SuppressLint("NewApi") + override fun isIgnored(fileOrDirectory: Path, attrs: BasicFileAttributes): Boolean { + if (fileOrDirectory == resourceDirectoryOrFile) { + return false + } + + if (super.isIgnored(fileOrDirectory, attrs)) { + return true + } + + val fileName: String = fileOrDirectory.fileName.toString() + if (attrs.isDirectory) { + if (fileName.startsWith("values-mcc") || + fileName.startsWith(FD_RES_RAW) && + (fileName.length == FD_RES_RAW.length || fileName[FD_RES_RAW.length] == '-') + ) { + // Mobile country codes and raw resources are not used by LayoutLib. + return true + } + + // Skip folders that don't belong to languages in languageGroups or languages that were loaded earlier. + if (languageGroups != null || loadedLanguageGroups.isNotEmpty()) { + val config = FolderConfiguration.getConfigForFolder(fileName) ?: return true + val language = getLanguageGroup(config) + if ((languageGroups != null && !languageGroups!!.contains(language)) || + loadedLanguageGroups.contains(language) + ) { + return true + } + folderConfigCache[config.qualifierString] = config + } + } else if ((publicXmlFileNames.contains(fileName) || (fileName == "symbols.xml")) && + "values" == PathString(fileOrDirectory).parentFileName + ) { + // Skip files that don't contain resources. + return true + } else if (fileName.endsWith(COMPILED_9PNG_EXTENSION)) { + return true + } + + return false + } + + override fun addResourceItem( + item: BasicResourceItem, + repository: FrameworkResourceRepository + ) = repository.addResourceItem(item) + + override fun getKeyForVisibilityLookup(resourceName: String): String { + // This class obtains names of public resources from public.xml where all resource names are preserved + // in their original form. This is different from the superclass that obtains the names from public.txt + // where the names are transformed by replacing dots, colons and dashes with underscores. + return resourceName + } + + companion object { + @SuppressLint("NewApi") + private fun readLanguageGroups(zipFile: ZipFile): Set<String> { + val result = sortedSetOf<String>(comparator = Comparator.naturalOrder()) + result += "" + zipFile.stream().forEach { entry: ZipEntry -> + val name = entry.name + if (name.startsWith(RESOURCES_TABLE_PREFIX) && + name.endsWith(RESOURCE_TABLE_SUFFIX) && + name.length == RESOURCES_TABLE_PREFIX.length + RESOURCE_TABLE_SUFFIX.length + 2 && + Character.isLetter(name[RESOURCES_TABLE_PREFIX.length]) && + Character.isLetter(name[RESOURCES_TABLE_PREFIX.length + 1]) + ) { + result += name.substring( + startIndex = RESOURCES_TABLE_PREFIX.length, + endIndex = RESOURCES_TABLE_PREFIX.length + 2 + ) + } + } + return result.toSet() + } + } + } + + /** + * Redirects the [RepositoryConfiguration] inherited from another repository to point to this one, so that + * the other repository can be garbage collected. This has to be done after this repository is fully loaded. + * + * @param sourceConfigurations the configurations to reparent + */ + private fun takeOverConfigurations(sourceConfigurations: Set<RepositoryConfiguration>) { + for (configuration in sourceConfigurations) { + configuration.transferOwnershipTo(this) + } + } + + companion object { + private val ANDROID_NAMESPACE = ResourceNamespace.ANDROID + + /** Mapping from languages to language groups, e.g. Romansh is mapped to Italian. */ + private val LANGUAGE_TO_GROUP = mapOf("rm" to "it") + private const val RESOURCES_TABLE_PREFIX = "resources_" + private const val RESOURCE_TABLE_SUFFIX = ".bin" + private const val COMPILED_9PNG_EXTENSION = ".compiled.9.png" + private val LOG = Logger.getLogger(FrameworkResourceRepository::class.java.name) + + /** + * Creates an Android framework resource repository. + * + * @param resourceDirectoryOrFile the res directory or a jar file containing resources of the Android framework + * @param languagesToLoad the set of ISO 639 language codes, or null to load all available languages + * @param useCompiled9Patches whether to provide the compiled or non-compiled version of the framework 9-patches + * @return the created resource repository + */ + fun create( + resourceDirectoryOrFile: Path, + languagesToLoad: Set<String>?, + useCompiled9Patches: Boolean + ): FrameworkResourceRepository { + val languageGroups = if (languagesToLoad == null) null else getLanguageGroups(languagesToLoad) + + val loader = Loader(resourceDirectoryOrFile, languageGroups) + val repository = FrameworkResourceRepository(loader, useCompiled9Patches) + + repository.load(null, loader, languageGroups, loader.loadedLanguageGroups) + return repository + } + + /** + * Returns the name of the resource table file containing resources for the given language. + * + * @param language the two-letter language abbreviation, or an empty string for language-neutral resources + * @return the file name + */ + fun getResourceTableNameForLanguage(language: String): String = + if (language.isEmpty()) { + "resources.bin" + } else { + "$RESOURCES_TABLE_PREFIX$language$RESOURCE_TABLE_SUFFIX" + } + + fun getLanguageGroup(config: FolderConfiguration): String { + val locale = config.localeQualifier + return if (locale == null) "" else getLanguageGroup(locale.language ?: "") + } + + /** + * Maps some languages to others effectively grouping languages together. For example, Romansh language + * that has very few framework resources is grouped together with Italian. + * + * @param language the original language + * @return the language representing the corresponding group of languages + */ + private fun getLanguageGroup(language: String): String = + LANGUAGE_TO_GROUP.getOrDefault(language, language) + + private fun getLanguageGroups(languages: Set<String>): Set<String> { + val result = TreeSet<String>() + result += "" + for (language: String in languages) { + result += getLanguageGroup(language) + } + return result + } + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/LoadableResourceRepository.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/LoadableResourceRepository.kt new file mode 100644 index 0000000000..bebbcc055c --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/LoadableResourceRepository.kt @@ -0,0 +1,105 @@ +package app.cash.paparazzi.internal.resources + +import app.cash.paparazzi.internal.resources.base.BasicFileResourceItem +import com.android.ide.common.resources.SingleNamespaceResourceRepository +import com.android.ide.common.util.PathString +import com.android.resources.ResourceType +import com.android.resources.ResourceVisibility +import com.android.utils.Base128InputStream +import java.io.IOException +import java.nio.file.Path + +/** + * Repository of resources loaded from a file or a directory on disk. + */ +interface LoadableResourceRepository : SingleNamespaceResourceRepository { + /** + * Returns the name of the library, or null if this is not an AAR resource repository. + */ + val libraryName: String? + + /** + * Returns the name of this resource repository to display in the UI. + */ + val displayName: String + + /** + * Returns the file or directory this resource repository was loaded from. Resource repositories loaded from + * the same file or directory with different file filtering options have the same origin. + */ + val origin: Path + + /** + * Produces a string to be returned by the [BasicResourceItem.getValue] method. + * The string represents an URL in one of the following formats: + * + * * file URL, e.g. "file:///foo/bar/res/layout/my_layout.xml" + * * URL of a zipped element inside the res.apk file, e.g. "apk:///foo/bar/res.apk!/res/layout/my_layout.xml" + * + * + * @param relativeResourcePath the relative path of a file resource + * @return the URL pointing to the file resource + */ + fun getResourceUrl(relativeResourcePath: String): String + + /** + * Produces a [PathString] to be returned by the [BasicResourceItem.getSource] method. + * + * @param relativeResourcePath the relative path of the file the resource was created from + * @param forFileResource true is the resource is a file resource, false if it is a value resource + * @return the PathString to be returned by the [BasicResourceItem.getSource] method + */ + fun getSourceFile(relativeResourcePath: String, forFileResource: Boolean): PathString + + /** + * Produces a [PathString] to be returned by the [BasicResourceItem.getOriginalSource] method. + * + * @param relativeResourcePath the relative path of the file the resource was created from + * @param forFileResource true is the resource is a file resource, false if it is a value resource + * @return the PathString to be returned by the [BasicResourceItem.getOriginalSource] method + */ + fun getOriginalSourceFile( + relativeResourcePath: String, + forFileResource: Boolean + ): PathString? { + return getSourceFile(relativeResourcePath, forFileResource) + } + + /** + * Creates a [ResourceSourceFile] by reading its contents from the given stream. + * + * @param stream the stream to read data from + * @param configurations the repository configurations to select from when creating the ResourceSourceFile + * @return the created [ResourceSourceFile] + */ + @Throws(IOException::class) + fun deserializeResourceSourceFile( + stream: Base128InputStream, + configurations: List<RepositoryConfiguration> + ): ResourceSourceFile { + return ResourceSourceFileImpl.deserialize(stream, configurations) + } + + /** + * Creates a [BasicFileResourceItem] by reading its contents from the given stream. + * + * @param stream the stream to read data from + * @param resourceType the type of the resource + * @param name the name of the resource + * @param visibility the visibility of the resource + * @param configurations the repository configurations to select from when creating the ResourceSourceFile + * @return the created [BasicFileResourceItem] + */ + @Throws(IOException::class) + fun deserializeFileResourceItem( + stream: Base128InputStream, + resourceType: ResourceType, + name: String, + visibility: ResourceVisibility, + configurations: List<RepositoryConfiguration> + ): BasicFileResourceItem { + return BasicFileResourceItem.deserialize(stream, resourceType, name, visibility, configurations) + } + + fun containsUserDefinedResources(): Boolean +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/LocalResourceRepository.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/LocalResourceRepository.kt new file mode 100644 index 0000000000..679e894908 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/LocalResourceRepository.kt @@ -0,0 +1,44 @@ +package app.cash.paparazzi.internal.resources + +import com.android.ide.common.rendering.api.ResourceNamespace +import com.android.ide.common.resources.AbstractResourceRepository +import com.android.ide.common.resources.ResourceItem +import com.android.resources.ResourceType +import com.google.common.collect.ImmutableListMultimap +import com.google.common.collect.ListMultimap + +/** + * Repository for Android application resources, e.g. those that show up in {@code R}, + * not {@code android.R} (which are referred to as framework resources.). + */ +abstract class LocalResourceRepository protected constructor( + val displayName: String +) : AbstractResourceRepository() { + protected abstract fun getMap( + namespace: ResourceNamespace, + resourceType: ResourceType + ): ListMultimap<String, ResourceItem>? + + override fun getResourcesInternal( + namespace: ResourceNamespace, + resourceType: ResourceType + ): ListMultimap<String, ResourceItem> { + val map = getMap(namespace, resourceType) + return map ?: ImmutableListMultimap.of() + } + + // TODO(namespaces): Implement. + override fun getPublicResources( + namespace: ResourceNamespace, + type: ResourceType + ): Collection<ResourceItem> = throw UnsupportedOperationException("Not implemented yet") + + /** + * Package accessible version of [getMap]. + * Do not call outside of [MultiResourceRepository]. + */ + open fun getMapPackageAccessible( + namespace: ResourceNamespace, + type: ResourceType + ): ListMultimap<String, ResourceItem>? = getMap(namespace, type) +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/ModuleResourceRepository.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/ModuleResourceRepository.kt new file mode 100644 index 0000000000..d83a6a33a8 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/ModuleResourceRepository.kt @@ -0,0 +1,71 @@ +package app.cash.paparazzi.internal.resources + +import com.android.ide.common.rendering.api.ResourceNamespace +import com.android.ide.common.resources.SingleNamespaceResourceRepository +import java.io.File + +/** + * Ported from: [ModuleResourceRepository.java](https://cs.android.com/android-studio/platform/tools/adt/idea/+/c847337ee5caa1d57cb1cb991cfcafaf6c90d0c6:android/src/com/android/tools/idea/res/ModuleResourceRepository.java) + * + * A repository responsible for all resources defined in all resource folders of a given module. + * A resource folder can point to: + * - A resource source set (src/main/res, src/debug/res) + * - A location where [res values](https://developer.android.com/build/gradle-tips#share-custom-fields-and-resource-values-with-your-app-code) are generated + */ +internal class ModuleResourceRepository private constructor( + displayName: String, + private val namespace: ResourceNamespace, + delegates: List<LocalResourceRepository> +) : MultiResourceRepository(displayName), SingleNamespaceResourceRepository { + init { + setChildren(delegates, emptyList()) + } + + override fun getNamespace(): ResourceNamespace = namespace + + override fun getPackageName(): String? = namespace.packageName + override fun getNamespaces(): Set<ResourceNamespace> = + super<MultiResourceRepository>.getNamespaces() + + override fun getLeafResourceRepositories(): Collection<SingleNamespaceResourceRepository> = + super<MultiResourceRepository>.getLeafResourceRepositories() + + companion object { + /** + * Returns the resource repository for a single module (which can possibly have multiple resource folders). + * Does not include resources from any dependencies. + * + * @param namespace the namespace for the repository + * @return the resource repository + */ + fun forMainResources( + namespace: ResourceNamespace, + resourceDirectories: List<File> + ): LocalResourceRepository { + return ModuleResourceRepository( + displayName = "", // TODO + namespace = namespace, + delegates = addRepositoriesInReverseOverlayOrder(resourceDirectories, namespace) + ) + } + + /** + * Inserts repositories for the given [resourceDirectories] as [ResourceFolderRepository] instances, in the right order. + * + * [resourceDirectories] is assumed to be in the inverse order of what we need. The code in + * [MultiResourceRepository.getMap] gives priority to child repositories which are earlier + * in the list, so after creating repositories for every folder, we add them in reverse to the list. + * + * @param resourceDirectories directories for which repositories should be constructed + */ + private fun addRepositoriesInReverseOverlayOrder( + resourceDirectories: List<File>, + namespace: ResourceNamespace + ): List<LocalResourceRepository> = + buildList { + resourceDirectories.asReversed().forEach { + add(ResourceFolderRepository(it, namespace)) + } + } + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/MultiResourceRepository.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/MultiResourceRepository.kt new file mode 100644 index 0000000000..381160fe60 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/MultiResourceRepository.kt @@ -0,0 +1,485 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.internal.resources + +import com.android.ide.common.rendering.api.ResourceNamespace +import com.android.ide.common.resources.ResourceItem +import com.android.ide.common.resources.ResourceRepository +import com.android.ide.common.resources.ResourceTable +import com.android.ide.common.resources.ResourceVisitor +import com.android.ide.common.resources.ResourceVisitor.VisitResult +import com.android.ide.common.resources.ResourceVisitor.VisitResult.ABORT +import com.android.ide.common.resources.ResourceVisitor.VisitResult.CONTINUE +import com.android.ide.common.resources.SingleNamespaceResourceRepository +import com.android.ide.common.resources.configuration.FolderConfiguration +import com.android.resources.ResourceType +import com.google.common.collect.ArrayListMultimap +import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableListMultimap +import com.google.common.collect.ImmutableListMultimap.Builder +import com.google.common.collect.ListMultimap +import com.google.common.collect.Maps +import com.google.common.collect.Multimap +import com.google.common.collect.Multiset +import com.google.common.collect.Table +import com.google.common.collect.Tables +import java.util.function.Predicate +import kotlin.collections.Map.Entry + +/** + * Ported from: [MultiResourceRepository.java](https://cs.android.com/android-studio/platform/tools/adt/idea/+/55991b4380c1ac18e81151493f59228220c5b72a:android/src/com/android/tools/idea/res/MultiResourceRepository.java) + * + * A super class for several of the other repositories. Its only purpose is to be able to combine + * multiple resource repositories and expose it as a single one, applying the “override” semantics + * of resources: earlier children defining the same resource namespace/type/name combination will + * replace/hide any subsequent definitions of the same resource. + * + * <p>In the resource repository hierarchy, MultiResourceRepository is an internal node, never a leaf. + */ +internal abstract class MultiResourceRepository internal constructor(displayName: String) : + LocalResourceRepository(displayName) { + private var localResources = listOf<LocalResourceRepository>() + + private var libraryResources = listOf<AarSourceResourceRepository>() + + /** A concatenation of [localResources] and [libraryResources]. */ + private var children = listOf<ResourceRepository>() + + /** Leaf resource repositories keyed by namespace. */ + private var leafsByNamespace = + ImmutableListMultimap.of<ResourceNamespace, SingleNamespaceResourceRepository>() + + /** Contained single-namespace resource repositories keyed by namespace. */ + private var repositoriesByNamespace = + ImmutableListMultimap.of<ResourceNamespace, SingleNamespaceResourceRepository>() + + private var resourceComparator = + ResourceItemComparator(ResourcePriorityComparator(ImmutableList.of())) + + private val cachedMaps = ResourceTable() + + /** Names of resources from local leaf repositories. */ + private val resourceNames: Table<SingleNamespaceResourceRepository, ResourceType, Set<String>> = + Tables.newCustomTable(HashMap()) { Maps.newEnumMap(ResourceType::class.java) } + + fun setChildren( + localResources: List<LocalResourceRepository>, + libraryResources: Collection<AarSourceResourceRepository> + ) { + this.localResources = localResources.toList() + this.libraryResources = libraryResources.toList() + this.children = buildList(this.localResources.size + this.libraryResources.size) { + addAll(this@MultiResourceRepository.localResources) + addAll(this@MultiResourceRepository.libraryResources) + } + leafsByNamespace = + ImmutableListMultimap.builder<ResourceNamespace, SingleNamespaceResourceRepository>() + .apply { computeLeafs(this@MultiResourceRepository, this) } + .build() + repositoriesByNamespace = + ImmutableListMultimap.builder<ResourceNamespace, SingleNamespaceResourceRepository>() + .apply { computeNamespaceMap(this@MultiResourceRepository, this) } + .build() + resourceComparator = + ResourceItemComparator(ResourcePriorityComparator(leafsByNamespace.values())) + cachedMaps.clear() + } + + override fun getNamespaces(): Set<ResourceNamespace> = repositoriesByNamespace.keySet() + + override fun accept(visitor: ResourceVisitor): VisitResult { + for (namespace in namespaces) { + if (visitor.shouldVisitNamespace(namespace)) { + for (type in ResourceType.values()) { + if (visitor.shouldVisitResourceType(type)) { + val map = getMap(namespace, type) + if (map != null) { + for (item in map.values()) { + if (visitor.visit(item) == ABORT) { + return ABORT + } + } + } + } + } + } + } + return CONTINUE + } + + override fun getMap( + namespace: ResourceNamespace, + type: ResourceType + ): ListMultimap<String, ResourceItem>? { + val repositoriesForNamespace = leafsByNamespace[namespace] + if (repositoriesForNamespace.size == 1) { + val repository = repositoriesForNamespace[0] + return getResources(repository, namespace, type) + } + + var map = cachedMaps[namespace, type] + if (map != null) { + return map + } + + // Merge all items of the given type. + for (repository in repositoriesForNamespace) { + val items = getResources(repository, namespace, type) + if (!items.isEmpty) { + if (map == null) { + // Create a new map. + // We only add a duplicate item if there isn't an item with the same qualifiers, and it + // is not a styleable or an id. Styleables and ids are allowed to be defined in multiple + // places even with the same qualifiers. + map = + if (type === ResourceType.STYLEABLE || type === ResourceType.ID) { + ArrayListMultimap.create<String, ResourceItem>() + } else { + PerConfigResourceMap(resourceComparator) + } + cachedMaps.put(namespace, type, map) + } + map!!.putAll(items) + if (repository is LocalResourceRepository) { + resourceNames.put(repository, type, items.keySet().toSet()) + } + } + } + return map + } + + override fun getLeafResourceRepositories(): Collection<SingleNamespaceResourceRepository> = + leafsByNamespace.values() + + private class ResourcePriorityComparator(repositories: Collection<SingleNamespaceResourceRepository>) : + Comparator<ResourceItem> { + private val repositoryOrdering: MutableMap<SingleNamespaceResourceRepository, Int> + + init { + repositoryOrdering = HashMap(repositories.size) + var i = 0 + for (repository in repositories) { + repositoryOrdering[repository] = i++ + } + } + + override fun compare(item1: ResourceItem, item2: ResourceItem): Int { + return getOrdering(item1).compareTo(getOrdering(item2)) + } + + private fun getOrdering(item: ResourceItem): Int { + val ordering: Int = repositoryOrdering[item.repository] ?: 0 + assert(ordering >= 0) + return ordering + } + } + + /** + * Custom implementation of [ListMultimap] that may store multiple resource items for + * the same folder configuration, but for readers exposes ot most one resource item per folder + * configuration. + * + * + * This ListMultimap implementation is not as robust as Guava multimaps but is sufficient + * for MultiResourceRepository because the latter always copies data to immutable containers + * before exposing it to callers. + */ + private class PerConfigResourceMap(private val comparator: ResourceItemComparator) : + ListMultimap<String, ResourceItem> { + private val map: MutableMap<String, MutableList<ResourceItem>> = HashMap() + private var size = 0 + + private var values: Values? = null + + override fun get(key: String?): List<ResourceItem> { + val items: List<ResourceItem>? = key?.let { map[key] } + return items ?: ImmutableList.of() + } + + override fun keySet(): Set<String> = map.keys + + override fun keys(): Multiset<String> = throw UnsupportedOperationException() + + override fun values(): Collection<ResourceItem> { + var values = this.values + if (values == null) { + values = Values(size) + this.values = values + } + return values + } + + override fun entries(): Collection<Entry<String, ResourceItem>> = + throw UnsupportedOperationException() + + override fun removeAll(key: Any?): List<ResourceItem> { + val removed: List<ResourceItem>? = key?.let { map.remove(it) } + if (removed != null) { + size -= removed.size + } + return removed ?: ImmutableList.of() + } + + fun removeIf(key: String, filter: Predicate<in ResourceItem>): Boolean { + val list: MutableList<ResourceItem> = map[key] ?: return false + val oldSize = list.size + val removed = list.removeIf(filter) + size += list.size - oldSize + if (list.isEmpty()) { + map.remove(key) + } + return removed + } + + override fun clear() { + map.clear() + size = 0 + } + + override fun size() = size + + override fun isEmpty() = size == 0 + + override fun containsKey(key: Any?) = key?.let { map.containsKey(it) } ?: false + + override fun containsValue(value: Any?): Boolean = throw UnsupportedOperationException() + + override fun containsEntry(key: Any?, value: Any?): Boolean = + throw UnsupportedOperationException() + + override fun put(key: String, item: ResourceItem): Boolean { + val list = map.computeIfAbsent(key) { _ -> PerConfigResourceList() } + val oldSize = list.size + list += item + size += list.size - oldSize + return true + } + + override fun remove(key: Any?, value: Any?): Boolean = throw UnsupportedOperationException() + + override fun putAll(key: String, items: Iterable<ResourceItem>): Boolean { + if (items is Collection<*>) { + if (items.isEmpty()) { + return false + } + val list = map.computeIfAbsent(key) { _ -> PerConfigResourceList() } + val oldSize = list.size + val added = list.addAll(items as Collection<ResourceItem>) + size += list.size - oldSize + return added + } + var added = false + var list: MutableList<ResourceItem>? = null + var oldSize = 0 + for (item in items) { + if (list == null) { + list = map.computeIfAbsent(key) { _ -> PerConfigResourceList() } + oldSize = list.size + } + added = list.add(item) + } + if (list != null) { + size += list.size - oldSize + } + return added + } + + override fun putAll(multimap: Multimap<out String, out ResourceItem>): Boolean { + for ((key, items) in multimap.asMap().entries) { + if (!items.isEmpty()) { + val list = map.computeIfAbsent(key) { _ -> PerConfigResourceList() } + val oldSize = list.size + list += items + size += list.size - oldSize + } + } + return !multimap.isEmpty + } + + override fun replaceValues( + key: String?, + values: Iterable<ResourceItem> + ): List<ResourceItem> = throw UnsupportedOperationException() + + override fun asMap(): Map<String, Collection<ResourceItem>> = map + + /** + * This class has a split personality. The class may store multiple resource items for the same + * folder configuration, but for callers of non-mutating methods ([.get], + * [.size], [Iterator.next], etc) it exposes at most one resource item per + * folder configuration. Which of the resource items with the same folder configuration is + * visible to non-mutating methods is determined by [ResourcePriorityComparator]. + */ + private inner class PerConfigResourceList : java.util.AbstractList<ResourceItem>() { + + /** Resource items sorted by folder configurations. Nested lists are sorted by repository priority. */ + private val resourceItems: ArrayList<MutableList<ResourceItem>> = ArrayList() + override val size: Int + get() = resourceItems.size + + override fun get(index: Int): ResourceItem = resourceItems[index][0] + + override fun add(item: ResourceItem): Boolean { + add(item, 0) + return true + } + + override fun addAll(items: Collection<ResourceItem>): Boolean { + if (items.isEmpty()) { + return false + } + if (items.size == 1) { + return add(items.iterator().next()) + } + val sortedItems: List<ResourceItem> = sortedItems(items) + var start = 0 + for (item in sortedItems) { + start = add(item, start) + } + return true + } + + private fun add(item: ResourceItem, start: Int): Int { + var index = findConfigIndex(item, start, resourceItems.size) + if (index < 0) { + index = index.inv() + resourceItems.add(index, mutableListOf(item)) + } else { + val nested = resourceItems[index] + // Iterate backwards since it is likely to require fewer iterations. + var i = nested.size + while (--i >= 0) { + if (comparator.priorityComparator.compare(item, nested[i]) > 0) { + break + } + } + nested.add(i + 1, item) + } + return index + } + + private fun sortedItems(items: Collection<ResourceItem>): List<ResourceItem> = + items.sortedWith(comparator) + + /** + * Returns index in [.resourceItems] of the existing resource item with the same + * configuration as the `item` parameter. If [.resourceItems] doesn't contains + * resources with the same configuration, returns binary complement of the insertion point. + */ + private fun findConfigIndex(item: ResourceItem, start: Int, end: Int): Int { + val config: FolderConfiguration = item.configuration + var low = start + var high = end + while (low < high) { + val mid = low + high ushr 1 + val value: FolderConfiguration = resourceItems[mid][0].configuration + val c = value.compareTo(config) + if (c < 0) { + low = mid + 1 + } else if (c > 0) { + high = mid + } else { + return mid + } + } + return low.inv() // Not found. + } + } + + private inner class Values(override val size: Int) : AbstractCollection<ResourceItem>() { + override fun iterator(): Iterator<ResourceItem> { + return ValuesIterator() + } + + private inner class ValuesIterator : MutableIterator<ResourceItem> { + private val outerCursor: Iterator<List<ResourceItem>> = map.values.iterator() + private var currentList: List<ResourceItem>? = null + private var innerCursor = 0 + override fun hasNext(): Boolean = currentList != null || outerCursor.hasNext() + + override fun next(): ResourceItem { + if (currentList == null) { + currentList = outerCursor.next() + innerCursor = 0 + } + return try { + val item: ResourceItem = currentList!![innerCursor] + if (++innerCursor >= currentList!!.size) { + currentList = null + } + item + } catch (e: IndexOutOfBoundsException) { + throw NoSuchElementException() + } + } + + override fun remove() = throw UnsupportedOperationException() + } + } + } + + private class ResourceItemComparator(val priorityComparator: Comparator<ResourceItem>) : + Comparator<ResourceItem> { + override fun compare(item1: ResourceItem, item2: ResourceItem): Int { + val c: Int = item1.configuration.compareTo(item2.configuration) + return if (c != 0) { + c + } else priorityComparator.compare(item1, item2) + } + } + + companion object { + private fun computeLeafs( + repository: ResourceRepository, + result: Builder<ResourceNamespace, SingleNamespaceResourceRepository> + ) { + if (repository is MultiResourceRepository) { + for (child in repository.children) { + computeLeafs(child, result) + } + } else { + for (resourceRepository in repository.leafResourceRepositories) { + result.put(resourceRepository.namespace, resourceRepository) + } + } + } + + private fun computeNamespaceMap( + repository: ResourceRepository, + result: Builder<ResourceNamespace, SingleNamespaceResourceRepository> + ) { + if (repository is SingleNamespaceResourceRepository) { + result.put(repository.namespace, repository) + } else if (repository is MultiResourceRepository) { + for (child in (repository).children) { + computeNamespaceMap(child, result) + } + } + } + + private fun getResources( + repository: SingleNamespaceResourceRepository, + namespace: ResourceNamespace, + type: ResourceType + ): ListMultimap<String, ResourceItem> { + if (repository is LocalResourceRepository) { + val map = repository.getMapPackageAccessible(namespace, type) + return map ?: ImmutableListMultimap.of() + } + return repository.getResources(namespace, type) + } + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/NamespaceResolver.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/NamespaceResolver.kt new file mode 100644 index 0000000000..0945b863ea --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/NamespaceResolver.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.internal.resources + +import com.android.ide.common.rendering.api.ResourceNamespace +import com.android.utils.Base128InputStream +import com.android.utils.Base128InputStream.StreamFormatException +import org.xmlpull.v1.XmlPullParser + +/** + * Ported from: [NamespaceResolver.java](https://cs.android.com/android-studio/platform/tools/base/+/18047faf69512736b8ddb1f6a6785f58d47c893f:resource-repository/main/java/com/android/resources/base/NamespaceResolver.java) + * + * Simple implementation of the [ResourceNamespace.Resolver] interface intended to be used + * together with [XmlPullParser]. + */ +class NamespaceResolver : ResourceNamespace.Resolver { + /** Interleaved prefixes and the corresponding URIs in order of descending priority. */ + private val prefixesAndUris: Array<String> + + internal constructor(parser: XmlPullParser) { + val namespaceCount = parser.getNamespaceCount(parser.depth) + var j = namespaceCount * 2 + prefixesAndUris = arrayOfNulls<String>(j).apply { + for (i in 0 until namespaceCount) { + this[--j] = parser.getNamespaceUri(i) + this[--j] = parser.getNamespacePrefix(i) + } + }.requireNoNulls() + } + + private constructor(prefixesAndUris: Array<String>) { + this.prefixesAndUris = prefixesAndUris + } + + val namespaceCount: Int + get() = prefixesAndUris.size / 2 + + override fun prefixToUri(namespacePrefix: String): String? { + for (i in prefixesAndUris.indices step 2) { + if (namespacePrefix == prefixesAndUris[i]) { + return prefixesAndUris[i + 1] + } + } + return null + } + + override fun uriToPrefix(namespaceUri: String): String? { + for (i in prefixesAndUris.indices step 2) { + if (namespaceUri == prefixesAndUris[i + 1]) { + return prefixesAndUris[i] + } + } + return null + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || javaClass != other.javaClass) return false + val that = other as NamespaceResolver + return prefixesAndUris.contentEquals(that.prefixesAndUris) + } + + override fun hashCode(): Int = prefixesAndUris.contentHashCode() + + companion object { + private val EMPTY_STRING_ARRAY = arrayOfNulls<String>(0).requireNoNulls() + val EMPTY: NamespaceResolver = NamespaceResolver(EMPTY_STRING_ARRAY) + + /** + * Creates a namespace resolver by reading its contents from the given stream. + */ + fun deserialize(stream: Base128InputStream): NamespaceResolver { + val n = stream.readInt() * 2 + val prefixesAndUris = arrayOfNulls<String>(n).apply { + for (i in 0 until n) { + this[i] = stream.readString() ?: throw StreamFormatException.invalidFormat() + } + }.requireNoNulls() + return NamespaceResolver(prefixesAndUris) + } + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/ProjectResourceRepository.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/ProjectResourceRepository.kt new file mode 100644 index 0000000000..fa3b88e7c4 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/ProjectResourceRepository.kt @@ -0,0 +1,62 @@ +package app.cash.paparazzi.internal.resources + +import com.android.ide.common.rendering.api.ResourceNamespace +import java.io.File + +/** + * Ported from: [ProjectResourceRepository.java](https://cs.android.com/android-studio/platform/tools/adt/idea/+/c847337ee5caa1d57cb1cb991cfcafaf6c90d0c6:android/src/com/android/tools/idea/res/ProjectResourceRepository.java) + * + * The resource repository for a module along with all its (local) module dependencies. + * The repository doesn't contain resources from AAR dependencies. + */ +internal class ProjectResourceRepository private constructor( + displayName: String, + localResources: List<LocalResourceRepository> +) : MultiResourceRepository("$displayName with modules") { + init { + setChildren(localResources, emptyList()) + } + + companion object { + fun create( + resourceDirectories: List<File>, + moduleResourceDirectories: List<File> + ): ProjectResourceRepository { + return ProjectResourceRepository( + displayName = "main", + localResources = computeRepositories(resourceDirectories, moduleResourceDirectories) + ) + } + + private fun computeRepositories( + resourceDirectories: List<File>, + moduleResourceDirectories: List<File> + ): List<LocalResourceRepository> { + val main = getModuleResources(resourceDirectories) + + val resources = buildList(moduleResourceDirectories.size + 1) { + this += main + for (moduleResourceDirectory in moduleResourceDirectories) { + this += getModuleResources(listOf(moduleResourceDirectory)) + } + } + return resources + } + + private fun getModuleResources( + resourceDirectories: List<File> + ): LocalResourceRepository = + // TODO: need mapOf(package to listOf(resourceDirectory)) for each transitive project module + ModuleResourceRepository.forMainResources( + namespace = getNamespace(namespacing = ResourceNamespacing.DISABLED, packageName = "TODO"), + resourceDirectories = resourceDirectories + ) + + private fun getNamespace(namespacing: ResourceNamespacing, packageName: String?): ResourceNamespace { + if (namespacing === ResourceNamespacing.DISABLED || packageName == null) { + return ResourceNamespace.RES_AUTO + } + return ResourceNamespace.fromPackageName(packageName) + } + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/RepositoryConfiguration.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/RepositoryConfiguration.kt new file mode 100644 index 0000000000..904b083773 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/RepositoryConfiguration.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.internal.resources + +import com.android.ide.common.resources.configuration.FolderConfiguration +import com.android.utils.HashCodes + +/** + * A ([LoadableResourceRepository], [FolderConfiguration]) pair. Instances of [BasicResourceItem] contain + * a reference to an [RepositoryConfiguration] instead of two separate references to [LoadableResourceRepository] + * and [FolderConfiguration]. This indirection saves memory because the number of [RepositoryConfiguration] + * instances is a tiny fraction of the number of [BasicResourceItem] instances. + */ +class RepositoryConfiguration( + repository: LoadableResourceRepository, + val folderConfiguration: FolderConfiguration +) { + var repository = repository + private set + + /** + * Makes [repository] the owner of this [RepositoryConfiguration]. The new owner should be loaded from + * the same file or directory as the previous one, which means that changing the owner does not + * affect [equals] or [hashCode]. + */ + fun transferOwnershipTo(repository: LoadableResourceRepository) { + assert(this.repository.origin == repository.origin) + this.repository = repository + } + + /** + * Overridden to not distinguish between repositories loaded from the same file or folder. + */ + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RepositoryConfiguration + + if (repository.origin != other.repository.origin) return false + return folderConfiguration == other.folderConfiguration + } + + /** + * Overridden to not distinguish between repositories loaded from the same file or folder. + */ + override fun hashCode(): Int { + return HashCodes.mix(repository.origin.hashCode(), folderConfiguration.hashCode()) + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/RepositoryLoader.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/RepositoryLoader.kt new file mode 100644 index 0000000000..312783e62e --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/RepositoryLoader.kt @@ -0,0 +1,1224 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.internal.resources + +import app.cash.paparazzi.internal.resources.base.BasicArrayResourceItem +import app.cash.paparazzi.internal.resources.base.BasicAttrResourceItem +import app.cash.paparazzi.internal.resources.base.BasicDensityBasedFileResourceItem +import app.cash.paparazzi.internal.resources.base.BasicFileResourceItem +import app.cash.paparazzi.internal.resources.base.BasicForeignAttrResourceItem +import app.cash.paparazzi.internal.resources.base.BasicPluralsResourceItem +import app.cash.paparazzi.internal.resources.base.BasicResourceItem +import app.cash.paparazzi.internal.resources.base.BasicStyleResourceItem +import app.cash.paparazzi.internal.resources.base.BasicStyleableResourceItem +import app.cash.paparazzi.internal.resources.base.BasicTextValueResourceItem +import app.cash.paparazzi.internal.resources.base.BasicValueResourceItem +import app.cash.paparazzi.internal.resources.base.BasicValueResourceItemBase +import com.android.SdkConstants.ANDROID_NS_NAME +import com.android.SdkConstants.ATTR_FORMAT +import com.android.SdkConstants.ATTR_INDEX +import com.android.SdkConstants.ATTR_NAME +import com.android.SdkConstants.ATTR_PARENT +import com.android.SdkConstants.ATTR_QUANTITY +import com.android.SdkConstants.ATTR_TYPE +import com.android.SdkConstants.ATTR_VALUE +import com.android.SdkConstants.DOT_AAR +import com.android.SdkConstants.DOT_JAR +import com.android.SdkConstants.DOT_XML +import com.android.SdkConstants.DOT_ZIP +import com.android.SdkConstants.NEW_ID_PREFIX +import com.android.SdkConstants.PREFIX_RESOURCE_REF +import com.android.SdkConstants.PREFIX_THEME_REF +import com.android.SdkConstants.TAG_ATTR +import com.android.SdkConstants.TAG_EAT_COMMENT +import com.android.SdkConstants.TAG_ENUM +import com.android.SdkConstants.TAG_FLAG +import com.android.SdkConstants.TAG_ITEM +import com.android.SdkConstants.TAG_RESOURCES +import com.android.SdkConstants.TAG_SKIP +import com.android.SdkConstants.TOOLS_URI +import com.android.ide.common.rendering.api.AttrResourceValue +import com.android.ide.common.rendering.api.AttributeFormat +import com.android.ide.common.rendering.api.DensityBasedResourceValue +import com.android.ide.common.rendering.api.ResourceNamespace +import com.android.ide.common.rendering.api.StyleItemResourceValue +import com.android.ide.common.rendering.api.StyleItemResourceValueImpl +import com.android.ide.common.resources.ANDROID_AAPT_IGNORE +import com.android.ide.common.resources.AndroidAaptIgnore +import com.android.ide.common.resources.PatternBasedFileFilter +import com.android.ide.common.resources.ResourceItem +import com.android.ide.common.resources.ValueResourceNameValidator +import com.android.ide.common.resources.ValueXmlHelper +import com.android.ide.common.resources.configuration.FolderConfiguration +import com.android.ide.common.resources.resourceNameToFieldName +import com.android.ide.common.util.PathString +import com.android.io.CancellableFileIo +import com.android.resources.Arity +import com.android.resources.Density +import com.android.resources.FolderTypeRelationship +import com.android.resources.ResourceFolderType +import com.android.resources.ResourceFolderType.VALUES +import com.android.resources.ResourceType +import com.android.resources.ResourceType.ANIMATOR +import com.android.resources.ResourceType.ARRAY +import com.android.resources.ResourceType.ATTR +import com.android.resources.ResourceType.DRAWABLE +import com.android.resources.ResourceType.ID +import com.android.resources.ResourceType.INTERPOLATOR +import com.android.resources.ResourceType.LAYOUT +import com.android.resources.ResourceType.MENU +import com.android.resources.ResourceType.MIPMAP +import com.android.resources.ResourceType.PLURALS +import com.android.resources.ResourceType.STRING +import com.android.resources.ResourceType.STYLE +import com.android.resources.ResourceType.STYLEABLE +import com.android.resources.ResourceType.TRANSITION +import com.android.resources.ResourceVisibility +import com.android.utils.SdkUtils +import com.android.utils.XmlUtils +import com.google.common.collect.ArrayListMultimap +import com.google.common.collect.ListMultimap +import com.google.common.collect.Maps +import com.google.common.collect.Sets +import com.google.common.collect.Table +import com.google.common.collect.Tables +import org.kxml2.io.KXmlParser +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserException +import java.io.BufferedInputStream +import java.io.File +import java.io.IOException +import java.io.InputStream +import java.nio.file.FileVisitResult +import java.nio.file.FileVisitor +import java.nio.file.NoSuchFileException +import java.nio.file.Path +import java.nio.file.attribute.BasicFileAttributes +import java.util.EnumMap +import java.util.EnumSet +import java.util.logging.Logger +import java.util.zip.ZipFile + +/** + * Ported from: [RepositoryLoader.java](https://cs.android.com/android-studio/platform/tools/base/+/876aaf229c1e3b8144736d3338c628ba43ccac45:resource-repository/main/java/com/android/resources/base/RepositoryLoader.java) + */ +abstract class RepositoryLoader<T : LoadableResourceRepository>( + val resourceDirectoryOrFile: Path, + val resourceFilesAndFolders: Collection<PathString>?, + val namespace: ResourceNamespace +) : FileFilter { + /** The set of attribute formats that is used when no formats are explicitly specified and the attribute is not a flag or enum. */ + private val DEFAULT_ATTR_FORMATS: Set<AttributeFormat> = Sets.immutableEnumSet( + AttributeFormat.BOOLEAN, + AttributeFormat.COLOR, + AttributeFormat.DIMENSION, + AttributeFormat.FLOAT, + AttributeFormat.FRACTION, + AttributeFormat.INTEGER, + AttributeFormat.REFERENCE, + AttributeFormat.STRING + ) + private val fileFilter = + PatternBasedFileFilter(AndroidAaptIgnore(System.getenv(ANDROID_AAPT_IGNORE))) + + private val publicResources: MutableMap<ResourceType, MutableSet<String>> = + EnumMap(ResourceType::class.java) + private val attrs: ListMultimap<String, BasicAttrResourceItem> = ArrayListMultimap.create<String, BasicAttrResourceItem>() + private val attrCandidates: ListMultimap<String, BasicAttrResourceItem> = ArrayListMultimap.create<String, BasicAttrResourceItem>() + private val styleables: ListMultimap<String, BasicStyleableResourceItem> = ArrayListMultimap.create<String, BasicStyleableResourceItem>() + protected var defaultVisibility = ResourceVisibility.PRIVATE + + /** Cache of FolderConfiguration instances, keyed by qualifier strings (see [FolderConfiguration.getQualifierString]). */ + protected val folderConfigCache = hashMapOf<String, FolderConfiguration>() + private val configCache = hashMapOf<FolderConfiguration, RepositoryConfiguration>() + private val parser = ValueResourceXmlParser() + private val textExtractor = XmlTextExtractor() + private val urlParser = ResourceUrlParser() + + // Used to keep track of resources defined in the current value resource file. + private val valueFileResources: Table<ResourceType, String, BasicValueResourceItemBase> = + Tables.newCustomTable(EnumMap(ResourceType::class.java)) { LinkedHashMap() } + private val resourceDirectoryOrFilePath = PathString(resourceDirectoryOrFile) + private val isLoadingFromZipArchive = isZipArchive(resourceDirectoryOrFile) + + protected var zipFile: ZipFile? = null + + open fun loadRepositoryContents(repository: T) { + if (isLoadingFromZipArchive) { + loadFromZip(repository) + } else { + loadFromResFolder(repository) + } + } + + protected open fun loadFromZip(repository: T) { + try { + ZipFile(resourceDirectoryOrFile.toFile()).use { zipFile -> + this.zipFile = zipFile + loadPublicResourceNames() + val shouldParseResourceIds = !loadIdsFromRTxt() + + zipFile.stream().forEach { zipEntry -> + if (!zipEntry.isDirectory) { + val path = PathString(zipEntry.name) + loadResourceFile(path, repository, shouldParseResourceIds) + } + } + } + } catch (e: Exception) { + LOG.severe("Failed to load resources from $resourceDirectoryOrFile: $e") + } finally { + zipFile = null + } + + finishLoading(repository) + } + + protected open fun loadFromResFolder(repository: T) { + try { + if (CancellableFileIo.notExists(resourceDirectoryOrFile)) { + return // Don't report errors if the resource directory doesn't exist. This happens in some tests. + } + + loadPublicResourceNames() + val shouldParseResourceIds = !loadIdsFromRTxt() + + val sourceFilesAndFolders = + resourceFilesAndFolders?.map { it.toPath()!! } ?: listOf(resourceDirectoryOrFile) + for (file in findResourceFiles(sourceFilesAndFolders)) { + loadResourceFile(file, repository, shouldParseResourceIds) + } + } catch (e: Exception) { + LOG.severe("Failed to load resources from $resourceDirectoryOrFile: $e") + } + + finishLoading(repository) + } + + protected fun loadResourceFile( + file: PathString, + repository: T, + shouldParseResourceIds: Boolean + ) { + val folderName = file.parentFileName + if (folderName != null) { + val folderInfo = FolderInfo.create(folderName, folderConfigCache) + if (folderInfo != null) { + val configuration = getConfiguration(repository, folderInfo.configuration) + loadResourceFile(file, folderInfo, configuration, shouldParseResourceIds) + } + } + } + + protected open fun finishLoading(repository: T) = processAttrsAndStyleables() + + val sourceFileProtocol: String + get() = if (isLoadingFromZipArchive) JAR_PROTOCOL else "file" + + val resourcePathPrefix: String + get() = if (isLoadingFromZipArchive) { + "${portableFileName(resourceDirectoryOrFile.toString())}${JAR_SEPARATOR}res/" + } else { + "${portableFileName(resourceDirectoryOrFile.toString())}/" + } + + val resourceUrlPrefix: String + get() = if (isLoadingFromZipArchive) { + "$JAR_PROTOCOL://${portableFileName(resourceDirectoryOrFile.toString())}${JAR_SEPARATOR}res/" + } else { + "${portableFileName(resourceDirectoryOrFile.toString())}/" + } + + /** + * A hook for loading resource IDs from a R.txt file. This implementation does nothing but subclasses may override. + * + * @return true if the IDs were successfully loaded from R.txt + */ + protected open fun loadIdsFromRTxt() = false + + override fun isIgnored( + fileOrDirectory: Path, + attrs: BasicFileAttributes + ) = if (fileOrDirectory == resourceDirectoryOrFile) { + false + } else { + fileFilter.isIgnored(fileOrDirectory.toString(), attrs.isDirectory) + } + + protected open fun loadPublicResourceNames() { + // todo load public resources + } + + protected fun addPublicResourceName(type: ResourceType, name: String) { + val names = publicResources.computeIfAbsent(type) { HashSet() } + names += name + } + + private fun findResourceFiles(filesOrFolders: List<Path>): List<PathString> { + val fileCollector = ResourceFileCollector(this) + for (file in filesOrFolders) { + try { + CancellableFileIo.walkFileTree(file, fileCollector) + } catch (e: IOException) { + // All IOExceptions are logged by ResourceFileCollector. + } + } + for (e in fileCollector.ioErrors) { + LOG.severe("Failed to load resources from $resourceDirectoryOrFile: $e") + } + fileCollector.resourceFiles.sort() // Make sure that the files are in canonical order. + return fileCollector.resourceFiles + } + + protected fun getConfiguration( + repository: T, + folderConfiguration: FolderConfiguration + ): RepositoryConfiguration { + var repositoryConfiguration = configCache[folderConfiguration] + if (repositoryConfiguration != null) { + return repositoryConfiguration + } + + repositoryConfiguration = RepositoryConfiguration(repository, folderConfiguration!!) + configCache[folderConfiguration] = repositoryConfiguration + return repositoryConfiguration + } + + private fun loadResourceFile( + file: PathString, + folderInfo: FolderInfo, + configuration: RepositoryConfiguration, + shouldParseResourceIds: Boolean + ) { + if (folderInfo.resourceType == null) { + if (isXmlFile(file)) { + parseValueResourceFile(file, configuration) + } + } else { + if (shouldParseResourceIds && folderInfo.isIdGenerating && isXmlFile(file)) { + parseIdGeneratingResourceFile(file, configuration) + } + + val item = createFileResourceItem(file, folderInfo.resourceType, configuration) + addResourceItem(item) + } + } + + @Suppress("UNCHECKED_CAST") + private fun addResourceItem(item: BasicResourceItem) { + addResourceItem(item, item.repository as T) + } + + protected abstract fun addResourceItem(item: BasicResourceItem, repository: T) + + protected fun parseValueResourceFile( + file: PathString, + configuration: RepositoryConfiguration + ) { + try { + getInputStream(file).use { stream -> + val sourceFile = createResourceSourceFile(file, configuration) + parser.setInput(stream, null) + var event: Int + do { + event = parser.nextToken() + val depth = parser.depth + if (event == XmlPullParser.START_TAG) { + if (parser.prefix != null) { + continue + } + val tagName = parser.name + assert(depth <= 2) // Deeper tags should be consumed by the createResourceItem method. + if (depth == 1) { + if (tagName != TAG_RESOURCES) { + break + } + } else if (depth > 1) { + val resourceType = getResourceType(tagName, file) + if (resourceType != null && resourceType != ResourceType.PUBLIC) { + val resourceName = parser.getAttributeValue(null, ATTR_NAME) + if (resourceName != null) { + validateResourceName(resourceName, resourceType, file) + val item = createResourceItem(resourceType, resourceName, sourceFile) + addValueResourceItem(item) + } else { + // Skip the subtags when the tag of a valid resource type doesn't have a name. + skipSubTags() + } + } else { + skipSubTags() + } + } + } + } while (event != XmlPullParser.END_DOCUMENT) + } + } // KXmlParser throws RuntimeException for an undefined prefix and an illegal attribute name. + catch (e: IOException) { + handleParsingError(file, e) + } catch (e: XmlPullParserException) { + handleParsingError(file, e) + } catch (e: XmlSyntaxException) { + handleParsingError(file, e) + } catch (e: RuntimeException) { + handleParsingError(file, e) + } + addValueFileResources() + } + + protected open fun createResourceSourceFile( + file: PathString, + configuration: RepositoryConfiguration + ): ResourceSourceFile = ResourceSourceFileImpl(getResRelativePath(file), configuration) + + private fun addValueResourceItem(item: BasicValueResourceItemBase) { + // Add attr and styleable resources to intermediate maps to post-process them in the processAttrsAndStyleables + // method after all resources are loaded. + when (val resourceType: ResourceType = item.type) { + ATTR -> { + addAttr(item as BasicAttrResourceItem, attrs) + } + STYLEABLE -> { + styleables.put(item.name, item as BasicStyleableResourceItem) + } + else -> { + // For compatibility with resource merger code we add value resources first to a file-specific map, + // then move them to the global resource table. In case when there are multiple definitions of + // the same resource in a single XML file, this algorithm preserves only the last definition. + valueFileResources.put(resourceType, item.name, item) + } + } + } + + protected fun addValueFileResources() { + for (item in valueFileResources.values()) { + addResourceItem(item) + } + valueFileResources.clear() + } + + protected fun parseIdGeneratingResourceFile( + file: PathString, + configuration: RepositoryConfiguration + ) { + try { + getInputStream(file).use { stream -> + val sourceFile = createResourceSourceFile(file, configuration) + val parser = KXmlParser() + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true) + parser.setInput(stream, null) + var event: Int + do { + event = parser.nextToken() + if (event == XmlPullParser.START_TAG) { + val numAttributes = parser.attributeCount + for (i in 0 until numAttributes) { + val idValue = parser.getAttributeValue(i) + if (idValue.startsWith(NEW_ID_PREFIX) && idValue.length > NEW_ID_PREFIX.length) { + val resourceName = idValue.substring(NEW_ID_PREFIX.length) + addIdResourceItem(resourceName, sourceFile) + } + } + } + } while (event != XmlPullParser.END_DOCUMENT) + } + } // KXmlParser throws RuntimeException for an undefined prefix and an illegal attribute name. + catch (e: IOException) { + handleParsingError(file, e) + } catch (e: XmlPullParserException) { + handleParsingError(file, e) + } catch (e: java.lang.RuntimeException) { + handleParsingError(file, e) + } + addValueFileResources() + } + + protected open fun handleParsingError(file: PathString, e: java.lang.Exception) { + LOG.warning("Failed to parse $file: $e") + } + + @Throws(IOException::class) + protected open fun getInputStream(file: PathString): InputStream { + return if (zipFile == null) { + val path = file.toPath() + check(path != null) + BufferedInputStream(CancellableFileIo.newInputStream(path)) + } else { + val entry = zipFile!!.getEntry(file.portablePath) + ?: throw NoSuchFileException(file.portablePath) + BufferedInputStream(zipFile!!.getInputStream(entry)) + } + } + + protected fun addIdResourceItem( + resourceName: String, + sourceFile: ResourceSourceFile + ) { + val visibility = getVisibility(ID, resourceName) + val item = BasicValueResourceItem(ID, resourceName, sourceFile, visibility, null) + // Don't create duplicate ID resources. + if (!resourceAlreadyDefined(item)) { + addValueResourceItem(item) + } + } + + protected fun createFileResourceItem( + file: PathString, + resourceType: ResourceType, + configuration: RepositoryConfiguration + ): BasicFileResourceItem { + val resourceName = SdkUtils.fileNameToResourceName(file.fileName) + val visibility = getVisibility(resourceType, resourceName) + var density: Density? = null + if (DensityBasedResourceValue.isDensityBasedResourceType(resourceType)) { + val densityQualifier = configuration.folderConfiguration.densityQualifier + if (densityQualifier != null) { + density = densityQualifier.value + } + } + return createFileResourceItem(file, resourceType, resourceName, configuration, visibility, density) + } + + protected fun createFileResourceItem( + file: PathString, + type: ResourceType, + name: String, + configuration: RepositoryConfiguration, + visibility: ResourceVisibility, + density: Density? + ): BasicFileResourceItem { + val relativePath = getResRelativePath(file) + return if (density == null) { + BasicFileResourceItem(type, name, configuration, visibility, relativePath) + } else { + BasicDensityBasedFileResourceItem(type, name, configuration, visibility, relativePath, density) + } + } + + @Throws( + IOException::class, + XmlPullParserException::class, + XmlSyntaxException::class + ) + private fun createResourceItem( + type: ResourceType, + name: String, + sourceFile: ResourceSourceFile + ): BasicValueResourceItemBase { + return when (type) { + ARRAY -> createArrayItem(name, sourceFile) + ATTR -> createAttrItem(name, sourceFile) + PLURALS -> createPluralsItem(name, sourceFile) + STRING -> createStringItem(type, name, sourceFile, true) + STYLE -> createStyleItem(name, sourceFile) + STYLEABLE -> createStyleableItem(name, sourceFile) + ANIMATOR, DRAWABLE, INTERPOLATOR, LAYOUT, MENU, MIPMAP, TRANSITION -> createFileReferenceItem( + type, + name, + sourceFile + ) + + else -> createStringItem(type, name, sourceFile, false) + } + } + + @Throws( + IOException::class, + XmlPullParserException::class, + XmlSyntaxException::class + ) + private fun createArrayItem( + name: String, + sourceFile: ResourceSourceFile + ): BasicArrayResourceItem { + val indexValue = parser.getAttributeValue(TOOLS_URI, ATTR_INDEX) + val namespaceResolver = parser.namespaceResolver + val values = mutableListOf<String>() + forSubTags(TAG_ITEM) { + values += textExtractor.extractText(parser, false) + } + var index = 0 + if (indexValue != null) { + index = try { + Integer.parseUnsignedInt(indexValue) + } catch (e: NumberFormatException) { + throw XmlSyntaxException( + "The value of the " + namespaceResolver.prefixToUri(TOOLS_URI) + ':' + ATTR_INDEX + " attribute is not a valid number.", + parser, + getDisplayName(sourceFile) + ) + } + if (index >= values.size) { + throw XmlSyntaxException( + "The value of the " + namespaceResolver.prefixToUri(TOOLS_URI) + ':' + ATTR_INDEX + " attribute is out of bounds.", + parser, + getDisplayName(sourceFile) + ) + } + } + val visibility = getVisibility(ARRAY, name) + val item = BasicArrayResourceItem(name, sourceFile, visibility, values, index) + item.namespaceResolver = namespaceResolver + return item + } + + @Throws( + IOException::class, + XmlPullParserException::class, + XmlSyntaxException::class + ) + private fun createAttrItem( + name: String, + sourceFile: ResourceSourceFile + ): BasicAttrResourceItem { + val namespaceResolver = parser.namespaceResolver + val attrNamespace: ResourceNamespace? + urlParser.parseResourceUrl(name) + if (urlParser.hasNamespacePrefix(ANDROID_NS_NAME)) { + attrNamespace = ResourceNamespace.ANDROID + } else { + val prefix = urlParser.namespacePrefix + attrNamespace = ResourceNamespace.fromNamespacePrefix(prefix, namespace, parser.namespaceResolver) + if (attrNamespace == null) { + throw XmlSyntaxException( + "Undefined prefix of attr resource name \"$name\"", + parser, + getDisplayName(sourceFile) + ) + } + } + val name = urlParser.name + val description = parser.lastComment + val groupName = parser.attrGroupComment + val formatString = parser.getAttributeValue(null, ATTR_FORMAT) + val formats = if (formatString.isNullOrBlank()) { + EnumSet.noneOf(AttributeFormat::class.java) + } else { + AttributeFormat.parse(formatString) + } + + // The average number of enum or flag values is 7 for Android framework, so start with small maps. + val valueMap = Maps.newHashMapWithExpectedSize<String, Int>(8) + val descriptionMap = Maps.newHashMapWithExpectedSize<String, String>(8) + forSubTags(null) { + if (parser.prefix == null) { + val tagName = parser.name + val format = + if (tagName == TAG_ENUM) AttributeFormat.ENUM else if (tagName == TAG_FLAG) AttributeFormat.FLAGS else null + if (format != null) { + formats += format + val valueName = parser.getAttributeValue(null, ATTR_NAME) + if (valueName != null) { + val valueDescription = parser.lastComment + if (valueDescription != null) { + descriptionMap[valueName] = valueDescription + } + val value = parser.getAttributeValue(null, ATTR_VALUE) + var numericValue: Int? = null + if (value != null) { + try { + // Integer.decode/parseInt can't deal with hex value > 0x7FFFFFFF so we use Long.decode instead. + numericValue = java.lang.Long.decode(value).toInt() + } catch (ignored: NumberFormatException) { + } + } + valueMap[valueName] = numericValue + } + } + } + } + + val item: BasicAttrResourceItem = if (attrNamespace == namespace) { + val visibility = getVisibility(ATTR, name) + BasicAttrResourceItem(name, sourceFile, visibility, description, groupName, formats, valueMap, descriptionMap) + } else { + BasicForeignAttrResourceItem(attrNamespace, name, sourceFile, description, groupName, formats, valueMap, descriptionMap) + } + + item.namespaceResolver = namespaceResolver + return item + } + + @Throws( + IOException::class, + XmlPullParserException::class, + XmlSyntaxException::class + ) + private fun createPluralsItem( + name: String, + sourceFile: ResourceSourceFile + ): BasicPluralsResourceItem { + val defaultQuantity = parser.getAttributeValue(TOOLS_URI, ATTR_QUANTITY) + val namespaceResolver = parser.namespaceResolver + val values = EnumMap<Arity, String>(Arity::class.java) + forSubTags(TAG_ITEM) { + val quantityValue = parser.getAttributeValue(null, ATTR_QUANTITY) + if (quantityValue != null) { + val quantity = Arity.getEnum(quantityValue) + if (quantity != null) { + val text = textExtractor.extractText(parser, false) + values[quantity] = text + } + } + } + var defaultArity: Arity? = null + if (defaultQuantity != null) { + defaultArity = Arity.getEnum(defaultQuantity) + if (defaultArity == null || !values.containsKey(defaultArity)) { + throw XmlSyntaxException( + "Invalid value of the " + namespaceResolver.prefixToUri(TOOLS_URI) + ':' + ATTR_QUANTITY + " attribute.", + parser, + getDisplayName(sourceFile) + ) + } + } + val visibility = getVisibility(PLURALS, name) + val item = BasicPluralsResourceItem(name, sourceFile, visibility, values, defaultArity) + item.namespaceResolver = namespaceResolver + return item + } + + @Throws(IOException::class, XmlPullParserException::class) + private fun createStringItem( + type: ResourceType, + name: String, + sourceFile: ResourceSourceFile, + withRowXml: Boolean + ): BasicValueResourceItem { + val namespaceResolver = parser.namespaceResolver + val text = if (type == ResourceType.ID) null else textExtractor.extractText(parser, withRowXml) + val rawXml = if (type == ResourceType.ID) null else textExtractor.getRawXml() + assert(withRowXml || rawXml == null) // Text extractor doesn't extract raw XML unless asked to do it. + val visibility = getVisibility(type, name) + val item = if (rawXml == null) { + BasicValueResourceItem(type, name, sourceFile, visibility, text) + } else { + BasicTextValueResourceItem(type, name, sourceFile, visibility, text, rawXml) + } + item.namespaceResolver = namespaceResolver + return item + } + + @Throws(IOException::class, XmlPullParserException::class) + private fun createStyleItem( + name: String, + sourceFile: ResourceSourceFile + ): BasicStyleResourceItem { + val namespaceResolver = parser.namespaceResolver + var parentStyle = parser.getAttributeValue(null, ATTR_PARENT) + if (parentStyle != null && parentStyle.isNotEmpty()) { + urlParser.parseResourceUrl(parentStyle) + parentStyle = urlParser.qualifiedName + } + val styleItems = mutableListOf<StyleItemResourceValue>() + forSubTags(TAG_ITEM) { + val itemNamespaceResolver = parser.namespaceResolver + val itemName = parser.getAttributeValue(null, ATTR_NAME) + if (itemName != null) { + val text = textExtractor.extractText(parser, false) + val styleItem = StyleItemResourceValueImpl(namespace, itemName, text, sourceFile.repository.libraryName) + styleItem.namespaceResolver = itemNamespaceResolver + styleItems += styleItem + } + } + val visibility = getVisibility(STYLE, name) + val item = BasicStyleResourceItem(name, sourceFile, visibility, parentStyle, styleItems) + item.namespaceResolver = namespaceResolver + return item + } + + @Throws(IOException::class, XmlPullParserException::class) + private fun createStyleableItem( + name: String, + sourceFile: ResourceSourceFile + ): BasicStyleableResourceItem { + val namespaceResolver = parser.namespaceResolver + val attrs = mutableListOf<AttrResourceValue>() + forSubTags(TAG_ATTR) { + val attrName = parser.getAttributeValue(null, ATTR_NAME) + if (attrName != null) { + try { + val attr = createAttrItem(attrName, sourceFile) + // Mimic behavior of AAPT2 and put an attr reference inside a styleable resource. + attrs += (if (attr.formats.isEmpty()) attr else attr.createReference()) + + // Don't create top-level attr resources in a foreign namespace, or for attr references in the res-auto namespace. + // The second condition is determined by the fact that the attr in the res-auto namespace may have an explicit definition + // outside of this resource repository. + if (attr.namespace == namespace && (namespace != ResourceNamespace.RES_AUTO || attr.formats.isNotEmpty())) { + addAttr(attr, attrCandidates) + } + } catch (e: XmlSyntaxException) { + LOG.severe(e.toString()) + } + } + } + // AAPT2 treats all styleable resources as public. + // See https://android.googlesource.com/platform/frameworks/base/+/master/tools/aapt2/ResourceParser.cpp#1539 + val item = BasicStyleableResourceItem(name, sourceFile, ResourceVisibility.PUBLIC, attrs) + item.namespaceResolver = namespaceResolver + return item + } + + /** + * Adds attr definitions from [attrs], and attr definition candidates from [attrCandidates] + * if they don't match the attr definitions present in [attrs]. + */ + private fun processAttrsAndStyleables() { + for (attr in attrs.values()) { + addAttrWithAdjustedFormats(attr) + } + for (attr in attrCandidates.values()) { + val attrs = attrs[attr.name] + val i = findResourceWithSameNameAndConfiguration(attr, attrs) + if (i < 0) { + addAttrWithAdjustedFormats(attr) + } + } + + // Resolve attribute references where it can be done without loosing any data to reduce resource memory footprint. + for (styleable in styleables.values()) { + addResourceItem(resolveAttrReferences(styleable)) + } + } + + private fun addAttrWithAdjustedFormats(attr: BasicAttrResourceItem) { + var attr = attr + if (attr.formats.isEmpty()) { + attr = BasicAttrResourceItem(attr.name, attr.sourceFile, attr.visibility, attr.description, attr.groupName, DEFAULT_ATTR_FORMATS, emptyMap(), emptyMap()) + } + addResourceItem(attr) + } + + @Throws(IOException::class, XmlPullParserException::class) + private fun createFileReferenceItem( + type: ResourceType, + name: String, + sourceFile: ResourceSourceFile + ): BasicValueResourceItem { + val namespaceResolver = parser.namespaceResolver + var text = textExtractor.extractText(parser, false).trim() + if (text.isNotEmpty() && !text.startsWith(PREFIX_RESOURCE_REF) && !text.startsWith( + PREFIX_THEME_REF + ) + ) { + text = text.replace('/', File.separatorChar) + } + val visibility = getVisibility(type, name) + val item = BasicValueResourceItem(type, name, sourceFile, visibility, text) + item.namespaceResolver = namespaceResolver + return item + } + + @Throws(XmlSyntaxException::class) + private fun getResourceType( + tagName: String, + file: PathString + ): ResourceType? { + var type = ResourceType.fromXmlTagName(tagName) + if (type == null) { + if (TAG_EAT_COMMENT == tagName || TAG_SKIP == tagName) { + return null + } + if (tagName == TAG_ITEM) { + val typeAttr = parser.getAttributeValue(null, ATTR_TYPE) + if (typeAttr != null) { + type = ResourceType.fromClassName(typeAttr) + if (type != null) { + return type + } + + LOG.warning("Unrecognized type attribute \"$typeAttr\" at ${getDisplayName(file)} line ${parser.lineNumber}") + } + } else { + LOG.warning("Unrecognized tag name \"$tagName\" at ${getDisplayName(file)} line ${parser.lineNumber}") + } + } + + return type + } + + /** + * If `tagName` is null, calls `subtagVisitor.visitTag()` for every subtag of the current tag. + * If `tagName` is not null, calls `subtagVisitor.visitTag()` for every subtag of the current tag + * which name doesn't have a prefix and matches `tagName`. + */ + @Throws( + IOException::class, + XmlPullParserException::class + ) + private fun forSubTags(tagName: String?, subtagVisitor: XmlTagVisitor) { + val elementDepth: Int = parser.depth + var event: Int + do { + event = parser.nextToken() + if (event == XmlPullParser.START_TAG && (tagName == null || tagName == parser.name && parser.prefix == null)) { + subtagVisitor.visitTag() + } + } while (event != XmlPullParser.END_DOCUMENT && (event != XmlPullParser.END_TAG || parser.depth > elementDepth)) + } + + /** + * Skips all subtags of the current tag. When the method returns, the parser is positioned at the end tag + * of the current element. + */ + @Throws( + IOException::class, + XmlPullParserException::class + ) + private fun skipSubTags() { + val elementDepth: Int = parser.depth + var event: Int + do { + event = parser.nextToken() + } while (event != XmlPullParser.END_DOCUMENT && (event != XmlPullParser.END_TAG || parser.depth > elementDepth)) + } + + @Throws(XmlSyntaxException::class) + private fun validateResourceName( + resourceName: String, + resourceType: ResourceType, + file: PathString + ) { + val error = ValueResourceNameValidator.getErrorText(resourceName, resourceType) + if (error != null) { + throw XmlSyntaxException(error, parser, file.nativePath) + } + } + + private fun getDisplayName(file: PathString) = file.nativePath + + private fun getDisplayName(sourceFile: ResourceSourceFile): String { + val relativePath = sourceFile.relativePath + check(relativePath != null) + return getDisplayName(PathString(relativePath)) + } + + protected fun getVisibility( + resourceType: ResourceType, + resourceName: String + ): ResourceVisibility { + val names = publicResources[resourceType] + return if (names?.contains(getKeyForVisibilityLookup(resourceName)) == true) { + ResourceVisibility.PUBLIC + } else { + defaultVisibility + } + } + + /** + * Transforms the given resource name to a key for lookup in [publicResources]. + */ + protected open fun getKeyForVisibilityLookup(resourceName: String): String { + // In public.txt all resource names are transformed by replacing dots, colons and dashes with underscores. + return resourceNameToFieldName(resourceName) + } + + protected fun getResRelativePath(file: PathString): String { + if (file.isAbsolute) { + return resourceDirectoryOrFilePath.relativize(file).portablePath + } + + // The path is already relative, drop the first "res" segment. + assert(file.nameCount != 0) + assert(file.segment(0) == "res") + return file.subpath(1, file.nameCount).portablePath + } + + private fun interface XmlTagVisitor { + /** Is called when the parser is positioned at a [XmlPullParser.START_TAG]. */ + @Throws(IOException::class, XmlPullParserException::class) + fun visitTag() + } + + /** + * Information about a resource folder. + */ + protected class FolderInfo private constructor( + val folderType: ResourceFolderType, + val configuration: FolderConfiguration, + val resourceType: ResourceType?, + val isIdGenerating: Boolean + ) { + companion object { + /** + * Returns a FolderInfo for the given folder name. + * + * @param folderName the name of a resource folder + * @param folderConfigCache the cache of FolderConfiguration objects keyed by qualifier strings + * @return the FolderInfo object, or null if folderName is not a valid name of a resource folder + */ + fun create( + folderName: String, + folderConfigCache: MutableMap<String, FolderConfiguration> + ): FolderInfo? { + val folderType = + ResourceFolderType.getFolderType(folderName) ?: return null + val qualifier = FolderConfiguration.getQualifier(folderName) + val config = folderConfigCache.computeIfAbsent( + qualifier + ) { qualifierString: String? -> + FolderConfiguration.getConfigForQualifierString( + qualifierString + ) + } ?: return null + config.normalizeByRemovingRedundantVersionQualifier() + val resourceType: ResourceType? + val isIdGenerating: Boolean + if (folderType == VALUES) { + resourceType = null + isIdGenerating = false + } else { + resourceType = FolderTypeRelationship.getNonIdRelatedResourceType(folderType) + isIdGenerating = FolderTypeRelationship.isIdGeneratingFolderType(folderType) + } + return FolderInfo(folderType, config, resourceType, isIdGenerating) + } + } + } + + private class ResourceFileCollector(val fileFilter: FileFilter) : FileVisitor<Path> { + val resourceFiles = mutableListOf<PathString>() + val ioErrors = mutableListOf<IOException>() + + override fun preVisitDirectory( + dir: Path, + attrs: BasicFileAttributes + ) = if (fileFilter.isIgnored(dir, attrs)) { + FileVisitResult.SKIP_SUBTREE + } else { + FileVisitResult.CONTINUE + } + + override fun visitFile( + file: Path, + attrs: BasicFileAttributes + ): FileVisitResult { + if (fileFilter.isIgnored(file, attrs)) { + return FileVisitResult.SKIP_SUBTREE + } + resourceFiles += PathString(file) + return FileVisitResult.CONTINUE + } + + override fun visitFileFailed(file: Path, exc: IOException): FileVisitResult { + ioErrors += exc + return FileVisitResult.CONTINUE + } + + override fun postVisitDirectory( + dir: Path, + exc: IOException? + ) = FileVisitResult.CONTINUE + } + + private class XmlTextExtractor { + private val text = StringBuilder() + private val rawXml = StringBuilder() + private var nontrivialRawXml = false + + @Throws(IOException::class, XmlPullParserException::class) + fun extractText(parser: XmlPullParser, withRawXml: Boolean): String { + text.setLength(0) + rawXml.setLength(0) + nontrivialRawXml = false + val elementDepth = parser.depth + var event: Int + loop@ do { + event = parser.nextToken() + when (event) { + XmlPullParser.START_TAG -> { + val tagName = parser.name + if (withRawXml) { + nontrivialRawXml = true + rawXml.append('<') + val prefix = parser.prefix + if (prefix != null) { + rawXml.append(prefix).append(':') + } + rawXml.append(tagName) + val numAttr = parser.attributeCount + var i = 0 + while (i < numAttr) { + rawXml.append(' ') + val attributePrefix = parser.getAttributePrefix(i) + if (attributePrefix != null) { + rawXml.append(attributePrefix).append(':') + } + rawXml.append(parser.getAttributeName(i)).append('=').append('"') + XmlUtils.appendXmlAttributeValue(rawXml, parser.getAttributeValue(i)) + rawXml.append('"') + i++ + } + rawXml.append('>') + } + } + + XmlPullParser.END_TAG -> { + if (parser.depth <= elementDepth) { + break@loop + } + val tagName = parser.name + if (withRawXml) { + rawXml.append('<').append('/') + val prefix = parser.prefix + if (prefix != null) { + rawXml.append(prefix).append(':') + } + rawXml.append(tagName).append('>') + } + } + + XmlPullParser.ENTITY_REF, XmlPullParser.TEXT -> { + val textPiece = parser.text + text.append(textPiece) + if (withRawXml) { + rawXml.append(textPiece) + } + } + + XmlPullParser.CDSECT -> { + val textPiece = parser.text + text.append(textPiece) + if (withRawXml) { + nontrivialRawXml = true + rawXml.append("<![CDATA[").append(textPiece).append("]]>") + } + } + } + } while (event != XmlPullParser.END_DOCUMENT) + + return ValueXmlHelper.unescapeResourceString(text.toString(), false, true) + } + + fun getRawXml(): String? = if (nontrivialRawXml) rawXml.toString() else null + + companion object { + private fun isXliffNamespace(namespaceUri: String?) = + namespaceUri?.startsWith(ResourceItem.XLIFF_NAMESPACE_PREFIX) ?: false + } + } + + private class XmlSyntaxException(error: String, parser: XmlPullParser, filename: String) : + Exception(error + " at " + filename + " line " + parser.lineNumber) + + companion object { + private val LOG: Logger = Logger.getLogger(RepositoryLoader::class.java.name) + const val JAR_PROTOCOL = "jar" + const val JAR_SEPARATOR = "!/" + + @JvmStatic + protected fun isXmlFile(file: PathString) = isXmlFile(file.fileName) + + private fun isXmlFile(filename: String) = SdkUtils.endsWithIgnoreCase(filename, DOT_XML) + + private fun addAttr( + attr: BasicAttrResourceItem, + map: ListMultimap<String, BasicAttrResourceItem> + ) { + val attrs = map[attr.name] + val i = findResourceWithSameNameAndConfiguration(attr, attrs) + if (i >= 0) { + // Found a matching attr definition. + val existing = attrs[i] + if (attr.formats.isNotEmpty()) { + if (existing.formats.isEmpty()) { + attrs[i] = attr // Use the new attr since it contains more information than the existing one. + } else if (attr.formats != existing.formats) { + // Both, the existing and the new attr contain formats, but they are not the same. + // Assign union of formats to both attr definitions. + if (attr.formats.containsAll(existing.formats)) { + existing.formats = attr.formats + } else if (existing.formats.containsAll(attr.formats)) { + attr.formats = existing.formats + } else { + val formats: Set<AttributeFormat> = EnumSet.copyOf(attr.formats).apply { addAll(existing.formats) }.toSet() + attr.formats = formats + existing.formats = formats + } + } + } + if (existing.formats.isEmpty() && attr.formats.isNotEmpty()) { + attrs[i] = attr // Use the new attr since it contains more information than the existing one. + } + } else { + attrs += attr + } + } + + /** + * Returns a styleable with attr references replaced by attr definitions returned by + * the [BasicStyleableResourceItem.getCanonicalAttr] method. + */ + fun resolveAttrReferences(styleable: BasicStyleableResourceItem): BasicStyleableResourceItem { + var styleable = styleable + val repository = styleable.repository + val attributes = styleable.allAttributes + var resolvedAttributes: MutableList<AttrResourceValue>? = null + for (i in attributes.indices) { + val attr = attributes[i] + val canonicalAttr = BasicStyleableResourceItem.getCanonicalAttr(attr, repository) + if (canonicalAttr !== attr) { + if (resolvedAttributes == null) { + resolvedAttributes = ArrayList(attributes.size) + for (j in 0 until i) { + resolvedAttributes += attributes[j] + } + } + resolvedAttributes += canonicalAttr + } else { + resolvedAttributes?.add(attr) + } + } + if (resolvedAttributes != null) { + val namespaceResolver = styleable.namespaceResolver + styleable = BasicStyleableResourceItem( + styleable.name, + styleable.sourceFile, + styleable.visibility, + resolvedAttributes + ) + styleable.namespaceResolver = namespaceResolver + } + return styleable + } + + /** + * Checks if resource with the same name, type and configuration has already been defined. + * + * @param resource the resource to check + * @return true if a matching resource already exists + */ + private fun resourceAlreadyDefined(resource: BasicResourceItem): Boolean { + val repository = resource.repository + val items = repository.getResources(resource.namespace, resource.type, resource.name) + return findResourceWithSameNameAndConfiguration(resource, items) >= 0 + } + + private fun findResourceWithSameNameAndConfiguration( + resource: ResourceItem, + items: List<ResourceItem> + ): Int = items.indexOfFirst { it.configuration == resource.configuration } + + private fun isZipArchive(resourceDirectoryOrFile: Path): Boolean { + val filename = resourceDirectoryOrFile.fileName.toString() + return SdkUtils.endsWithIgnoreCase(filename, DOT_AAR) || + SdkUtils.endsWithIgnoreCase(filename, DOT_JAR) || + SdkUtils.endsWithIgnoreCase(filename, DOT_ZIP) + } + + fun portableFileName(fileName: String): String = fileName.replace(File.separatorChar, '/') + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/ResourceFile.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/ResourceFile.kt new file mode 100644 index 0000000000..bb4b31c70b --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/ResourceFile.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.internal.resources + +import app.cash.paparazzi.internal.resources.base.BasicResourceItem +import com.android.ide.common.resources.configuration.FolderConfiguration +import java.io.File + +/** + * Ported from: [ResourceItemSources.kt](https://cs.android.com/android-studio/platform/tools/adt/idea/+/1c8e6b0a85b2dc96826c185854504f7d476868c8:android/src/com/android/tools/idea/res/ResourceItemSources.kt) + * + * Represents a resource file from which [com.android.ide.common.resources.ResourceItem]s are + * created by [ResourceFolderRepository]. An [Iterable] of [BasicResourceItem]s. + */ +class ResourceFile( + val file: File?, + override val configuration: RepositoryConfiguration +) : ResourceSourceFile, Iterable<BasicResourceItem> { + private val items = mutableListOf<BasicResourceItem>() + override val repository: ResourceFolderRepository + get() = configuration.repository as ResourceFolderRepository + val folderConfiguration: FolderConfiguration + get() = configuration.folderConfiguration + + override fun iterator(): Iterator<BasicResourceItem> = items.iterator() + fun addItem(item: BasicResourceItem) { + items += item + } + + override val relativePath: String? + get() = file?.let { repository.resourceDir.toRelativeString(it) } + + fun isValid(): Boolean = file != null +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/ResourceFolderRepository.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/ResourceFolderRepository.kt new file mode 100644 index 0000000000..4b3c1bc889 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/ResourceFolderRepository.kt @@ -0,0 +1,248 @@ +package app.cash.paparazzi.internal.resources + +import android.annotation.SuppressLint +import app.cash.paparazzi.internal.resources.base.BasicFileResourceItem +import app.cash.paparazzi.internal.resources.base.BasicResourceItem +import app.cash.paparazzi.internal.resources.base.BasicValueResourceItemBase +import com.android.ide.common.rendering.api.ResourceNamespace +import com.android.ide.common.resources.ResourceItem +import com.android.ide.common.resources.ResourceVisitor +import com.android.ide.common.resources.ResourceVisitor.VisitResult +import com.android.ide.common.resources.ResourceVisitor.VisitResult.ABORT +import com.android.ide.common.resources.ResourceVisitor.VisitResult.CONTINUE +import com.android.ide.common.util.PathString +import com.android.resources.ResourceFolderType.VALUES +import com.android.resources.ResourceType +import com.android.utils.SdkUtils +import com.google.common.collect.LinkedListMultimap +import com.google.common.collect.ListMultimap +import java.io.File +import java.nio.file.Path +import java.nio.file.Paths +import java.util.EnumMap +import java.util.logging.Logger +import javax.lang.model.SourceVersion.isIdentifier +import javax.lang.model.SourceVersion.isKeyword +import kotlin.io.path.exists + +/** + * The [ResourceFolderRepository] is a leaf in the repository tree, and is used for user editable + * resources (e.g. the resources in the project, typically the res/main source set.) + * + * Each [ResourceFolderRepository] contains the resources provided by a single res folder. + */ +@SuppressLint("NewApi") +class ResourceFolderRepository( + val resourceDir: File, + private val namespace: ResourceNamespace +) : LocalResourceRepository(resourceDir.name), LoadableResourceRepository { + /** + * Common prefix of paths of all file resources. Used to compose resource paths returned by + * the [BasicFileResourceItem.getSource] method. + */ + private val resourcePathPrefix: String = "${resourceDir.path}/" + + /** + * Same as [resourcePathPrefix] but in a form of [PathString]. Used to produce + * resource paths returned by the [BasicResourceItem.getOriginalSource] method. + */ + private val resourcePathBase: PathString = PathString(resourcePathPrefix) + + private val resourceTable = + EnumMap<ResourceType, ListMultimap<String, ResourceItem>>(ResourceType::class.java) + + init { + Loader(this).load() + } + + override val libraryName: String? + get() = null // Resource folder is not a library. + + override val origin: Path + get() = Paths.get(resourceDir.path) + + override fun getResourceUrl(relativeResourcePath: String): String = + "$resourcePathPrefix$relativeResourcePath" + + override fun getSourceFile( + relativeResourcePath: String, + forFileResource: Boolean + ): PathString = resourcePathBase.resolve(relativeResourcePath) + + override fun getPackageName(): String? = namespace.packageName + + override fun containsUserDefinedResources(): Boolean = true + + /** + * Inserts the given resources into this repository. + */ + private fun commitToRepository(itemsByType: Map<ResourceType, ListMultimap<String, ResourceItem>>) { + for ((key, value) in itemsByType) { + getOrCreateMap(key).putAll(value) + } + } + + override fun accept(visitor: ResourceVisitor): VisitResult { + if (visitor.shouldVisitNamespace(namespace)) { + if (acceptByResources(resourceTable, visitor) == ABORT) { + return ABORT + } + } + return CONTINUE + } + + override fun getMap( + namespace: ResourceNamespace, + resourceType: ResourceType + ): ListMultimap<String, ResourceItem>? = + if (namespace != this.namespace) null else resourceTable[resourceType] + + private fun getOrCreateMap(type: ResourceType): ListMultimap<String, ResourceItem> = + // Use LinkedListMultimap to preserve ordering for editors that show original order. + resourceTable.computeIfAbsent(type) { LinkedListMultimap.create() } + + override fun getNamespace(): ResourceNamespace = namespace + + private fun checkResourceFilename(file: PathString): Boolean { + val fileNameToResourceName = SdkUtils.fileNameToResourceName(file.fileName) + return isIdentifier(fileNameToResourceName) && !isKeyword(fileNameToResourceName) + } + + private class Loader( + private val repository: ResourceFolderRepository + ) : RepositoryLoader<ResourceFolderRepository>( + resourceDirectoryOrFile = repository.resourceDir.toPath(), + resourceFilesAndFolders = null, + namespace = repository.namespace + ) { + private val resourceDir: File = repository.resourceDir + private val resources = EnumMap<ResourceType, ListMultimap<String, ResourceItem>>(ResourceType::class.java) + private val sources = mutableMapOf<File, ResourceFile>() + private val fileResources = mutableMapOf<File, BasicFileResourceItem>() + + // The following two fields are used as a cache of size one for quick conversion from a PathString to a File. + private var lastFile: File? = null + private var lastPathString: PathString? = null + + fun load() { + if (!resourceDirectoryOrFile.exists()) { + return + } + + scanResFolder() + populateRepository() + } + + private fun scanResFolder() { + try { + for (subDir in resourceDir.listFiles()!!.sorted()) { + if (subDir.isDirectory) { + val folderName = subDir.name + val folderInfo = FolderInfo.create(folderName, folderConfigCache) + if (folderInfo != null) { + val configuration = getConfiguration(repository, folderInfo.configuration) + for (file in subDir.listFiles()!!.sorted()) { + if (file.name.startsWith(".")) { + continue // Skip file with the name starting with a dot. + } + if ( + if (folderInfo.folderType == VALUES) { + sources.containsKey(file) + } else { + fileResources.containsKey(file) + } + ) { + continue + } + val pathString = PathString(file) + lastFile = file + lastPathString = pathString + loadResourceFile(pathString, folderInfo, configuration) + } + } + } + } + } catch (e: Exception) { + LOG.severe("Failed to load resources from $resourceDirectoryOrFile: $e") + } + + super.finishLoading(repository) + + // Associate file resources with sources. + for ((file, item) in fileResources.entries) { + val source = sources.computeIfAbsent(file) { file -> + ResourceFile(file, item.repositoryConfiguration) + } + source.addItem(item) + } + + // Populate the resources map. + val sortedSources = sources.values.toMutableList() + // Sort sources according to folder configurations to have deterministic ordering of resource items in resources. + sortedSources.sortWith(SOURCE_COMPARATOR) + for (source in sortedSources) { + for (item in source) { + getOrCreateMap(item.type).put(item.name, item) + } + } + } + + private fun loadResourceFile( + file: PathString, + folderInfo: FolderInfo, + configuration: RepositoryConfiguration + ) { + if (folderInfo.resourceType == null) { + if (isXmlFile(file)) { + parseValueResourceFile(file, configuration) + } + } else if (repository.checkResourceFilename(file)) { + if (isXmlFile(file) && folderInfo.isIdGenerating) { + parseIdGeneratingResourceFile(file, configuration) + } + val item = createFileResourceItem(file, folderInfo.resourceType, configuration) + addResourceItem(item, item.repository as ResourceFolderRepository) + } + } + + private fun populateRepository() { + repository.commitToRepository(resources) + } + + private fun getOrCreateMap(resourceType: ResourceType): ListMultimap<String, ResourceItem> = + resources.computeIfAbsent(resourceType) { LinkedListMultimap.create<String, ResourceItem>() } + + private fun getFile(file: PathString): File? = + if (file == lastPathString) lastFile else file.toFile() + + override fun addResourceItem(item: BasicResourceItem, repository: ResourceFolderRepository) { + if (item is BasicValueResourceItemBase) { + val sourceFile = item.sourceFile as ResourceFile + val file = sourceFile.file + if (file != null && !file.isDirectory) { + sourceFile.addItem(item) + sources[file] = sourceFile + } + } else if (item is BasicFileResourceItem) { + val file = getFile(item.source) + if (file != null && !file.isDirectory) { + fileResources[file] = item + } + } else { + throw IllegalArgumentException("Unexpected type: " + item.javaClass.name) + } + } + + override fun createResourceSourceFile( + file: PathString, + configuration: RepositoryConfiguration + ): ResourceSourceFile = ResourceFile(getFile(file), configuration) + } + + companion object { + private val LOG: Logger = Logger.getLogger(ResourceFolderRepository::class.java.name) + + private val SOURCE_COMPARATOR = + Comparator.comparing(ResourceFile::folderConfiguration) + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/ResourceNamespacing.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/ResourceNamespacing.kt new file mode 100644 index 0000000000..c2f489c2fb --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/ResourceNamespacing.kt @@ -0,0 +1,13 @@ +package app.cash.paparazzi.internal.resources + +enum class ResourceNamespacing { + /** + * Resources are not namespaced. + */ + DISABLED, + + /** + * Resources must be namespaced. + */ + REQUIRED +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/ResourceSerializationUtil.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/ResourceSerializationUtil.kt new file mode 100644 index 0000000000..659b4b9511 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/ResourceSerializationUtil.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.internal.resources + +import app.cash.paparazzi.internal.resources.base.BasicResourceItem +import com.android.ide.common.resources.configuration.FolderConfiguration +import com.android.utils.Base128InputStream +import com.android.utils.Base128InputStream.StreamFormatException +import java.io.IOException +import java.util.function.Consumer +import java.util.function.Function + +/** + * Ported from: [ResourceSerializationUtil.java](https://cs.android.com/android-studio/platform/tools/base/+/18047faf69512736b8ddb1f6a6785f58d47c893f:resource-repository/main/java/com/android/resources/base/ResourceSerializationUtil.java) + */ +object ResourceSerializationUtil { + /** + * Loads resources from the given input stream and passes them to the given consumer. + */ + @Throws(IOException::class) + fun readResourcesFromStream( + stream: Base128InputStream, + stringCache: Map<String, String>, + namespaceResolverCache: MutableMap<NamespaceResolver, NamespaceResolver>?, + repository: LoadableResourceRepository, + resourceConsumer: Consumer<BasicResourceItem> + ) { + // Enable string instance sharing to minimize memory consumption. + stream.setStringCache(stringCache) + + var n = stream.readInt() + if (n == 0) { + return // Nothing to load. + } + val configurations = (0 until n).map { + val configQualifier = stream.readString() ?: throw StreamFormatException.invalidFormat() + val folderConfig = FolderConfiguration.getConfigForQualifierString(configQualifier) + ?: throw StreamFormatException.invalidFormat() + RepositoryConfiguration(repository, folderConfig) + } + + n = stream.readInt() + val newSourceFiles = (0 until n).map { + repository.deserializeResourceSourceFile(stream, configurations) + } + + n = stream.readInt() + val newNamespaceResolvers = (0 until n).map { + var namespaceResolver = NamespaceResolver.deserialize(stream) + if (namespaceResolverCache != null) { + namespaceResolver = + namespaceResolverCache.computeIfAbsent(namespaceResolver, Function.identity()) + } + namespaceResolver + } + + n = stream.readInt() + (0 until n).forEach { _ -> + val item = BasicResourceItem.deserialize( + stream, + configurations, + newSourceFiles, + newNamespaceResolvers + ) + resourceConsumer.accept(item) + } + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/ResourceSourceFile.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/ResourceSourceFile.kt new file mode 100644 index 0000000000..42ae82f748 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/ResourceSourceFile.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.internal.resources + +/** + * Ported from: [ResourceSourceFile.kt](https://cs.android.com/android-studio/platform/tools/base/+/18047faf69512736b8ddb1f6a6785f58d47c893f:resource-repository/main/java/com/android/resources/base/ResourceSourceFile.kt) + * + * Represents an XML file from which an Android resource was created. + */ +interface ResourceSourceFile { + /** + * The path of the file relative to the resource directory, or null if the source file + * of the resource is not available. + */ + val relativePath: String? + + /** + * The configuration the resource file is associated with. + */ + val configuration: RepositoryConfiguration + + val repository: LoadableResourceRepository + get() = configuration.repository +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/ResourceSourceFileImpl.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/ResourceSourceFileImpl.kt new file mode 100644 index 0000000000..e2e2f5b42e --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/ResourceSourceFileImpl.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.internal.resources + +import com.android.utils.Base128InputStream +import java.io.IOException + +/** + * Ported from: [ResourceSourceFileImpl.kt](https://cs.android.com/android-studio/platform/tools/base/+/18047faf69512736b8ddb1f6a6785f58d47c893f:resource-repository/main/java/com/android/resources/base/ResourceSourceFileImpl.kt) + * + * A simple implementation of the [ResourceSourceFile] interface. + * + * [relativePath] path of the file relative to the resource directory, or null if the source file of the resource is not available + * [configuration] configuration the resource file is associated with + */ +data class ResourceSourceFileImpl( + override val relativePath: String?, + override val configuration: RepositoryConfiguration +) : ResourceSourceFile { + companion object { + /** + * Creates a [ResourceSourceFileImpl] by reading its contents from the given stream. + */ + @Throws(IOException::class) + fun deserialize( + stream: Base128InputStream, + configurations: List<RepositoryConfiguration> + ): ResourceSourceFileImpl { + val relativePath = stream.readString() + val configIndex = stream.readInt() + return ResourceSourceFileImpl(relativePath, configurations[configIndex]) + } + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/ResourceUrlParser.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/ResourceUrlParser.kt new file mode 100644 index 0000000000..73cb8ed0af --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/ResourceUrlParser.kt @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.internal.resources + +import com.android.SdkConstants.PREFIX_RESOURCE_REF +import com.android.SdkConstants.PREFIX_THEME_REF + +/** + * Ported from: [ResourceUrlParser.java](https://cs.android.com/android-studio/platform/tools/base/+/47d204001bf0cb6273d8b135c7eece3a982cf0e0:resource-repository/main/java/com/android/resources/base/ResourceUrlParser.java) + * + * Parser of resource URLs. Unlike [com.android.resources.ResourceUrl], this class is resilient to + * URL syntax errors that doesn't create any GC overhead. + */ +class ResourceUrlParser { + private var resourceUrl = "" + private var colonPos = 0 + private var slashPos = 0 + private var typeStart = 0 + private var namespacePrefixStart = 0 + private var nameStart = 0 + + /** + * Parses resource URL and sets the fields of this object to point to different parts of the URL. + * + * @param resourceUrl the resource URL to parse + */ + fun parseResourceUrl(resourceUrl: String) { + this.resourceUrl = resourceUrl + colonPos = -1 + slashPos = -1 + typeStart = -1 + namespacePrefixStart = -1 + + var prefixEnd = when { + resourceUrl.startsWith(PREFIX_RESOURCE_REF) -> if (resourceUrl.startsWith("@+")) 2 else 1 + resourceUrl.startsWith(PREFIX_THEME_REF) -> 1 + else -> 0 + } + if (resourceUrl.startsWith("*", prefixEnd)) { + prefixEnd++ + } + + val len = resourceUrl.length + var start = prefixEnd + loop@ for (i in prefixEnd until len) { + when (resourceUrl[i]) { + '/' -> + if (slashPos < 0) { + slashPos = i + typeStart = start + start = i + 1 + if (colonPos >= 0) { + break@loop + } + } + + ':' -> + if (colonPos < 0) { + colonPos = i + namespacePrefixStart = start + start = i + 1 + if (slashPos >= 0) { + break@loop + } + } + } + } + nameStart = start + } + + /** + * Returns the namespace prefix of the resource URL, or null if the URL doesn't contain a prefix. + */ + val namespacePrefix: String? + get() = if (colonPos >= 0) resourceUrl.substring(namespacePrefixStart, colonPos) else null + + /** + * Returns the type of the resource URL, or null if the URL don't contain a type. + */ + val type: String? + get() = if (slashPos >= 0) resourceUrl.substring(typeStart, slashPos) else null + + /** + * Returns the name part of the resource URL. + */ + val name: String + get() = resourceUrl.substring(nameStart) + + /** + * Returns the qualified name of the resource without any prefix or type. + */ + val qualifiedName: String + get() { + if (colonPos < 0) { + return name + } + return if (nameStart == colonPos + 1) { + resourceUrl.substring(namespacePrefixStart) + } else { + resourceUrl.substring(namespacePrefixStart, colonPos + 1) + name + } + } + + /** + * Checks if the resource URL has the given type. + */ + fun hasType(type: String): Boolean = + if (slashPos < 0) { + false + } else { + slashPos == typeStart + type.length && resourceUrl.startsWith(type, typeStart) + } + + /** + * Checks if the resource URL has the given namespace prefix. + */ + fun hasNamespacePrefix(namespacePrefix: String): Boolean = + if (colonPos < 0) { + false + } else { + colonPos == namespacePrefixStart + namespacePrefix.length && + resourceUrl.startsWith(namespacePrefix, namespacePrefixStart) + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/ValueResourceXmlParser.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/ValueResourceXmlParser.kt new file mode 100644 index 0000000000..706d6aa205 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/ValueResourceXmlParser.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.internal.resources + +import com.android.ide.common.rendering.api.ResourceNamespace +import org.xmlpull.v1.XmlPullParserException +import java.io.IOException +import java.io.InputStream +import java.io.Reader +import java.util.function.Function + +/** + * Ported from: [ValueResourceXmlParser.java](https://cs.android.com/android-studio/platform/tools/base/+/47d204001bf0cb6273d8b135c7eece3a982cf0e0:resource-repository/main/java/com/android/resources/base/ValueResourceXmlParser.java) + * + * XML pull parser for value resource files. Provides access to the resource namespace resolver + * for the current tag. + */ +internal class ValueResourceXmlParser : CommentTrackingXmlPullParser() { + internal val namespaceResolverCache = mutableMapOf<NamespaceResolver, NamespaceResolver>() + internal val resolverStack = ArrayDeque<NamespaceResolver>(4) + + /** + * Returns the namespace resolver for the current XML node. The parser has to be positioned on a start tag + * when this method is called. + */ + @get:Throws(XmlPullParserException::class) + val namespaceResolver: ResourceNamespace.Resolver + get() { + check(eventType == START_TAG) + if (resolverStack.isEmpty()) { + return ResourceNamespace.Resolver.EMPTY_RESOLVER + } + val resolver = resolverStack.last() + return if (resolver.namespaceCount == 0) ResourceNamespace.Resolver.EMPTY_RESOLVER else resolver + } + + @Throws(XmlPullParserException::class) + override fun setInput(reader: Reader) { + super.setInput(reader) + resolverStack.clear() + } + + @Throws(XmlPullParserException::class) + override fun setInput(inputStream: InputStream, encoding: String?) { + super.setInput(inputStream, encoding) + resolverStack.clear() + } + + @Throws(XmlPullParserException::class, IOException::class) + override fun nextToken(): Int { + val token = super.nextToken() + processToken(token) + return token + } + + @Throws(XmlPullParserException::class, IOException::class) + override operator fun next(): Int { + val token = super.next() + processToken(token) + return token + } + + @Throws(XmlPullParserException::class) + private fun processToken(token: Int) { + when (token) { + START_TAG -> { + val namespaceCount = getNamespaceCount(depth) + val parent = if (resolverStack.isEmpty()) null else resolverStack.last() + val current = if (parent != null && parent.namespaceCount == namespaceCount) parent else getOrCreateResolver + resolverStack += current + assert(resolverStack.size == depth) + } + + END_TAG -> resolverStack.removeLast() + } + } + + @get:Throws(XmlPullParserException::class) + private val getOrCreateResolver: NamespaceResolver + get() = namespaceResolverCache.computeIfAbsent(NamespaceResolver(this), Function.identity()) +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicArrayResourceItem.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicArrayResourceItem.kt new file mode 100644 index 0000000000..c4303b1d36 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicArrayResourceItem.kt @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.internal.resources.base + +import app.cash.paparazzi.internal.resources.ResourceSourceFile +import com.android.ide.common.rendering.api.ArrayResourceValue +import com.android.ide.common.rendering.api.ResourceNamespace +import com.android.resources.ResourceType +import com.android.resources.ResourceVisibility +import com.android.utils.Base128InputStream +import com.android.utils.Base128InputStream.StreamFormatException +import java.io.IOException +import java.util.Collections + +/** + * Ported from: [BasicArrayResourceItem.java](https://cs.android.com/android-studio/platform/tools/base/+/18047faf69512736b8ddb1f6a6785f58d47c893f:resource-repository/main/java/com/android/resources/base/BasicArrayResourceItem.java) + * + * Resource item representing an array resource. + */ +class BasicArrayResourceItem( + name: String, + sourceFile: ResourceSourceFile, + visibility: ResourceVisibility, + private val elements: List<String>, + private val defaultIndex: Int +) : BasicValueResourceItemBase(ResourceType.ARRAY, name, sourceFile, visibility), ArrayResourceValue { + init { + assert(elements.isEmpty() || defaultIndex < elements.size) + } + + override fun getElementCount(): Int = elements.size + + override fun getElement(index: Int): String = elements[index] + + override fun iterator() = Collections.unmodifiableList(elements).iterator() + + override fun getValue(): String? = if (elements.isEmpty()) null else elements[defaultIndex] + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (!super.equals(other)) return false + val that = other as BasicArrayResourceItem + return elements == that.elements + } + + companion object { + /** + * Creates a [BasicArrayResourceItem] by reading its contents from the given stream. + */ + @Throws(IOException::class) + fun deserialize( + stream: Base128InputStream, + name: String, + visibility: ResourceVisibility, + sourceFile: ResourceSourceFile, + resolver: ResourceNamespace.Resolver + ): BasicArrayResourceItem { + val n = stream.readInt() + val elements = if (n == 0) { + emptyList() + } else { + buildList(n) { + for (i in 0 until n) { + add(stream.readString()!!) + } + } + } + val defaultIndex = stream.readInt() + if (elements.isNotEmpty() && defaultIndex >= elements.size) { + throw StreamFormatException.invalidFormat() + } + val item = BasicArrayResourceItem(name, sourceFile, visibility, elements, defaultIndex) + item.namespaceResolver = resolver + return item + } + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicAttrReference.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicAttrReference.kt new file mode 100644 index 0000000000..640b63ee66 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicAttrReference.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.internal.resources.base + +import app.cash.paparazzi.internal.resources.ResourceSourceFile +import com.android.ide.common.rendering.api.AttrResourceValue +import com.android.ide.common.rendering.api.AttributeFormat +import com.android.ide.common.rendering.api.ResourceNamespace +import com.android.resources.ResourceType +import com.android.resources.ResourceVisibility +import com.android.utils.HashCodes + +/** + * Ported from: [BasicAttrReference.java](https://cs.android.com/android-studio/platform/tools/base/+/18047faf69512736b8ddb1f6a6785f58d47c893f:resource-repository/main/java/com/android/resources/base/BasicAttrReference.java) + * + * Resource value representing a reference to an attr resource, but potentially with its own description + * and group name. Unlike [BasicAttrResourceItem], does not contain formats and enum or flag information. + */ +class BasicAttrReference( + private val namespace: ResourceNamespace, + name: String, + sourceFile: ResourceSourceFile, + visibility: ResourceVisibility, + private val description: String?, + private val groupName: String? +) : BasicValueResourceItemBase(ResourceType.ATTR, name, sourceFile, visibility), AttrResourceValue { + override fun getNamespace(): ResourceNamespace = namespace + + override fun getFormats(): Set<AttributeFormat> = emptySet() + + override fun getAttributeValues(): Map<String, Int> = emptyMap() + + override fun getValueDescription(valueName: String): String? = null + + override fun getDescription(): String? = description + + override fun getGroupName(): String? = groupName + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (!super.equals(other)) return false + val that = other as BasicAttrReference + return namespace == that.namespace && + description == that.description && + groupName == that.groupName + } + + override fun hashCode(): Int { + // groupName is not included in hash code intentionally since it doesn't improve quality of hashing. + return HashCodes.mix(super.hashCode(), namespace.hashCode(), description.hashCode()) + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicAttrResourceItem.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicAttrResourceItem.kt new file mode 100644 index 0000000000..54ed7c441e --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicAttrResourceItem.kt @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.internal.resources.base + +import app.cash.paparazzi.internal.resources.ResourceSourceFile +import com.android.SdkConstants +import com.android.ide.common.rendering.api.AttrResourceValue +import com.android.ide.common.rendering.api.AttributeFormat +import com.android.ide.common.rendering.api.ResourceNamespace +import com.android.resources.ResourceType +import com.android.resources.ResourceVisibility +import com.android.utils.Base128InputStream +import com.android.utils.Base128InputStream.StreamFormatException +import java.io.IOException +import java.util.EnumSet + +/** + * Ported from: [BasicAttrResourceItem.java](https://cs.android.com/android-studio/platform/tools/base/+/18047faf69512736b8ddb1f6a6785f58d47c893f:resource-repository/main/java/com/android/resources/base/BasicAttrResourceItem.java) + * + * Resource item representing an attr resource. + */ +open class BasicAttrResourceItem( + name: String, + sourceFile: ResourceSourceFile, + visibility: ResourceVisibility, + private val description: String?, + private val groupName: String?, + formats: Set<AttributeFormat>, + valueMap: Map<String, Int>, + valueDescriptionMap: Map<String, String> +) : BasicValueResourceItemBase(ResourceType.ATTR, name, sourceFile, visibility), AttrResourceValue { + private var formats: Set<AttributeFormat> = formats.toSet() + + /** The keys are enum or flag names, the values are corresponding numeric values. */ + private val valueMap: Map<String, Int> = valueMap.toMap() + + /** The keys are enum or flag names, the values are the value descriptions. */ + private val valueDescriptionMap: Map<String, String> = valueDescriptionMap.toMap() + + override fun getFormats(): Set<AttributeFormat> = formats + + /** + * Replaces the set of the allowed attribute formats. Intended to be called only by the resource repository code. + * + * @param formats the new set of the allowed attribute formats + */ + fun setFormats(formats: Set<AttributeFormat>) { + this.formats = formats.toSet() + } + + override fun getAttributeValues(): Map<String, Int> = valueMap + + override fun getValueDescription(valueName: String): String? = valueDescriptionMap[valueName] + + override fun getDescription(): String? = description + + override fun getGroupName(): String? = groupName + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (!super.equals(other)) return false + val that = other as BasicAttrResourceItem + return description == that.description && + groupName == that.groupName && + formats == that.formats && + valueMap == that.valueMap && + valueDescriptionMap == that.valueDescriptionMap + } + + /** + * Creates and returns an [BasicAttrReference] pointing to this attribute. + */ + fun createReference(): BasicAttrReference { + val attrReference = + BasicAttrReference(namespace, name, sourceFile, visibility, description, groupName) + attrReference.namespaceResolver = namespaceResolver + return attrReference + } + + companion object { + /** + * Creates a [BasicAttrResourceItem] by reading its contents from the given stream. + */ + @Throws(IOException::class) + fun deserialize( + stream: Base128InputStream, + name: String, + visibility: ResourceVisibility, + sourceFile: ResourceSourceFile, + resolver: ResourceNamespace.Resolver + ): BasicValueResourceItemBase { + val namespaceSuffix = stream.readString() + val description = stream.readString() + val groupName = stream.readString() + + var formatMask = stream.readInt() + val formats = EnumSet.noneOf(AttributeFormat::class.java) + val attributeFormatValues = AttributeFormat.values() + var ordinal = 0 + while (ordinal < attributeFormatValues.size && formatMask != 0) { + if (formatMask and 0x1 != 0) { + formats.add(attributeFormatValues[ordinal]) + } + ordinal++ + formatMask = formatMask ushr 1 + } + val n = stream.readInt() + val (valueMap, descriptionMap) = if (n == 0) { + emptyMap<String, Int>() to emptyMap<String, String>() + } else { + val valueTempMap = LinkedHashMap<String, Int>(n) + val descriptionTempMap = LinkedHashMap<String, String>(n) + for (i in 0 until n) { + val valueName = stream.readString()!! + val value = stream.readInt() + if (value != Int.MIN_VALUE) { + valueTempMap[valueName] = value - 1 + } + val valueDescription = stream.readString() + if (valueDescription != null) { + descriptionTempMap[valueName] = valueDescription + } + } + valueTempMap.toMap() to descriptionTempMap.toMap() + } + + val item: BasicValueResourceItemBase = + if (formats.isEmpty() && valueMap.isEmpty()) { + val namespace = if (namespaceSuffix == null) { + sourceFile.repository.namespace + } else { + ResourceNamespace.fromNamespaceUri(SdkConstants.URI_DOMAIN_PREFIX + namespaceSuffix) + } ?: throw StreamFormatException.invalidFormat() + BasicAttrReference(namespace, name, sourceFile, visibility, description, groupName) + } else if (namespaceSuffix == null) { + BasicAttrResourceItem( + name, sourceFile, visibility, description, groupName, formats, valueMap, descriptionMap + ) + } else { + val namespace = + ResourceNamespace.fromNamespaceUri(SdkConstants.URI_DOMAIN_PREFIX + namespaceSuffix) + ?: throw StreamFormatException.invalidFormat() + BasicForeignAttrResourceItem( + namespace, name, sourceFile, description, groupName, formats, valueMap, descriptionMap + ) + } + item.namespaceResolver = resolver + return item + } + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicDensityBasedFileResourceItem.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicDensityBasedFileResourceItem.kt new file mode 100644 index 0000000000..496251f6a0 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicDensityBasedFileResourceItem.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.internal.resources.base + +import app.cash.paparazzi.internal.resources.RepositoryConfiguration +import com.android.ide.common.rendering.api.DensityBasedResourceValue +import com.android.resources.Density +import com.android.resources.ResourceType +import com.android.resources.ResourceVisibility +import com.android.utils.HashCodes +import com.google.common.base.MoreObjects + +/** + * Ported from: [BasicDensityBasedFileResourceItem.java](https://cs.android.com/android-studio/platform/tools/base/+/47d204001bf0cb6273d8b135c7eece3a982cf0e0:resource-repository/main/java/com/android/resources/base/BasicDensityBasedFileResourceItem.java) + * + * Resource item representing a density-specific file resource inside an AAR, e.g. a drawable or a layout. + */ +class BasicDensityBasedFileResourceItem( + type: ResourceType, + name: String, + configuration: RepositoryConfiguration, + visibility: ResourceVisibility, + relativePath: String, + private val density: Density +) : BasicFileResourceItem(type, name, configuration, visibility, relativePath), + DensityBasedResourceValue { + override fun getResourceDensity(): Density = density + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (!super.equals(other)) return false + val that = other as BasicDensityBasedFileResourceItem + return density == that.density + } + + override fun hashCode(): Int { + return HashCodes.mix(super.hashCode(), density.hashCode()) + } + + override fun toString(): String { + return MoreObjects.toStringHelper(this) + .add("name", name) + .add("namespace", namespace) + .add("type", resourceType) + .add("source", source) + .add("density", resourceDensity) + .toString() + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicFileResourceItem.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicFileResourceItem.kt new file mode 100644 index 0000000000..e9745e6e94 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicFileResourceItem.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.internal.resources.base + +import app.cash.paparazzi.internal.resources.RepositoryConfiguration +import com.android.ide.common.rendering.api.ResourceNamespace.Resolver +import com.android.ide.common.rendering.api.ResourceReference +import com.android.ide.common.util.PathString +import com.android.resources.Density +import com.android.resources.ResourceType +import com.android.resources.ResourceVisibility +import com.android.utils.Base128InputStream +import com.android.utils.Base128InputStream.StreamFormatException +import com.android.utils.HashCodes +import java.io.IOException + +/** + * Ported from: [BasicFileResourceItem.java](https://cs.android.com/android-studio/platform/tools/base/+/18047faf69512736b8ddb1f6a6785f58d47c893f:resource-repository/main/java/com/android/resources/base/BasicFileResourceItem.java) + * + * Resource item representing a file resource, e.g. a drawable or a layout. + */ +open class BasicFileResourceItem( + type: ResourceType, + name: String, + override val repositoryConfiguration: RepositoryConfiguration, + visibility: ResourceVisibility, + private val relativePath: String +) : BasicResourceItem(type, name, visibility) { + override fun isFileBased(): Boolean = true + + override fun getReference(): ResourceReference? = null + + override fun getNamespaceResolver(): Resolver = Resolver.EMPTY_RESOLVER + + override fun getValue(): String = repository.getResourceUrl(relativePath) + + /** + * The returned PathString points either to a file on disk, or to a ZIP entry inside a res.apk file. + * In the latter case the filesystem URI part points to res.apk itself, e.g. `"zip:///foo/bar/res.apk"`. + * The path part is the path of the ZIP entry containing the resource. + */ + override fun getSource(): PathString = repository.getSourceFile(relativePath, true) + + override fun getOriginalSource(): PathString? = + repository.getOriginalSourceFile(relativePath, true) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (!super.equals(other)) return false + val that = other as BasicFileResourceItem + return repositoryConfiguration == that.repositoryConfiguration && relativePath == that.relativePath + } + + override fun hashCode(): Int { + return HashCodes.mix(super.hashCode(), relativePath.hashCode()) + } + + companion object { + /** + * Creates a [BasicFileResourceItem] by reading its contents from the given stream. + */ + @Throws(IOException::class) + fun deserialize( + stream: Base128InputStream, + resourceType: ResourceType, + name: String, + visibility: ResourceVisibility, + configurations: List<RepositoryConfiguration> + ): BasicFileResourceItem { + val relativePath = stream.readString() ?: throw StreamFormatException.invalidFormat() + val configuration = configurations[stream.readInt()] + val encodedDensity = stream.readInt() + if (encodedDensity == 0) { + return BasicFileResourceItem(resourceType, name, configuration, visibility, relativePath) + } + val density = Density.values()[encodedDensity - 1] + return BasicDensityBasedFileResourceItem( + resourceType, name, configuration, visibility, relativePath, density + ) + } + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicForeignAttrResourceItem.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicForeignAttrResourceItem.kt new file mode 100644 index 0000000000..72a5b34d5f --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicForeignAttrResourceItem.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.internal.resources.base + +import app.cash.paparazzi.internal.resources.ResourceSourceFile +import com.android.ide.common.rendering.api.AttributeFormat +import com.android.ide.common.rendering.api.ResourceNamespace +import com.android.resources.ResourceVisibility + +/** + * Ported from: [BasicForeignAttrResourceItem.java](https://cs.android.com/android-studio/platform/tools/base/+/47d204001bf0cb6273d8b135c7eece3a982cf0e0:resource-repository/main/java/com/android/resources/base/BasicForeignAttrResourceItem.java) + * + * Resource item representing an attr resource that is defined in a namespace different from the namespace + * of the owning AAR. + */ +class BasicForeignAttrResourceItem( + private val namespace: ResourceNamespace, + name: String, + sourceFile: ResourceSourceFile, + description: String?, + groupName: String?, + formats: Set<AttributeFormat>, + valueMap: Map<String, Int>, + valueDescriptionMap: Map<String, String> +) : BasicAttrResourceItem( + name = name, + sourceFile = sourceFile, + visibility = ResourceVisibility.PUBLIC, + description = description, + groupName = groupName, + formats = formats, + valueMap = valueMap, + valueDescriptionMap = valueDescriptionMap +) { + override fun getNamespace(): ResourceNamespace = namespace +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicPluralsResourceItem.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicPluralsResourceItem.kt new file mode 100644 index 0000000000..1b18ebbb1d --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicPluralsResourceItem.kt @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.internal.resources.base + +import app.cash.paparazzi.internal.resources.ResourceSourceFile +import com.android.ide.common.rendering.api.PluralsResourceValue +import com.android.ide.common.rendering.api.ResourceNamespace +import com.android.resources.Arity +import com.android.resources.ResourceType +import com.android.resources.ResourceVisibility +import com.android.utils.Base128InputStream +import com.android.utils.Base128InputStream.StreamFormatException +import java.io.IOException + +/** + * Ported from: [BasicPluralsResourceItem.java](https://cs.android.com/android-studio/platform/tools/base/+/18047faf69512736b8ddb1f6a6785f58d47c893f:resource-repository/main/java/com/android/resources/base/BasicPluralsResourceItem.java) + * + * Resource item representing a plurals resource. + */ +class BasicPluralsResourceItem private constructor( + name: String, + sourceFile: ResourceSourceFile, + visibility: ResourceVisibility, + private val arities: Array<Arity>, + private val values: Array<String>, + private val defaultIndex: Int +) : BasicValueResourceItemBase(ResourceType.PLURALS, name, sourceFile, visibility), + PluralsResourceValue { + constructor( + name: String, + sourceFile: ResourceSourceFile, + visibility: ResourceVisibility, + quantityValues: Map<Arity, String>, + defaultArity: Arity? + ) : this( + name, + sourceFile, + visibility, + quantityValues.keys.toTypedArray(), + quantityValues.values.toTypedArray(), + getIndex(defaultArity, quantityValues.keys) + ) + + init { + assert(arities.size == values.size) + assert(values.isEmpty() || defaultIndex < values.size) + } + + override fun getPluralsCount(): Int = arities.size + + override fun getQuantity(index: Int): String = arities[index].getName() + + override fun getValue(index: Int): String = values[index] + + override fun getValue(quantity: String): String? { + val index = arities.indexOfFirst { it.getName() == quantity } + return if (index != -1) values[index] else null + } + + override fun getValue(): String? = if (values.isEmpty()) null else values[defaultIndex] + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (!super.equals(other)) return false + val that = other as BasicPluralsResourceItem + return arities.contentEquals(that.arities) && values.contentEquals(that.values) + } + + companion object { + private fun getIndex(arity: Arity?, arities: Collection<Arity>): Int { + if (arity == null || arities.isEmpty()) { + return 0 + } + val index = arities.indexOf(arity) + return if (index != -1) index else throw IllegalArgumentException() + } + + /** + * Creates a [BasicPluralsResourceItem] by reading its contents from the given stream. + */ + @Throws(IOException::class) + fun deserialize( + stream: Base128InputStream, + name: String, + visibility: ResourceVisibility, + sourceFile: ResourceSourceFile, + resolver: ResourceNamespace.Resolver + ): BasicPluralsResourceItem { + val n = stream.readInt() + val (arities, values) = if (n == 0) { + Arity.EMPTY_ARRAY to emptyArray() + } else { + val arityList = mutableListOf<Arity>() + val valuesList = mutableListOf<String>() + for (i in 0 until n) { + arityList.add(Arity.values()[stream.readInt()]) + valuesList.add(stream.readString()!!) + } + arityList.toTypedArray() to valuesList.toTypedArray() + } + + val defaultIndex = stream.readInt() + if (values.isNotEmpty() && defaultIndex >= values.size) { + throw StreamFormatException.invalidFormat() + } + val item = + BasicPluralsResourceItem(name, sourceFile, visibility, arities, values, defaultIndex) + item.namespaceResolver = resolver + return item + } + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicResourceItem.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicResourceItem.kt new file mode 100644 index 0000000000..ca5b6ad98e --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicResourceItem.kt @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.internal.resources.base + +import app.cash.paparazzi.internal.resources.LoadableResourceRepository +import app.cash.paparazzi.internal.resources.RepositoryConfiguration +import app.cash.paparazzi.internal.resources.ResourceSourceFile +import com.android.ide.common.rendering.api.ResourceNamespace +import com.android.ide.common.rendering.api.ResourceNamespace.Resolver +import com.android.ide.common.rendering.api.ResourceReference +import com.android.ide.common.rendering.api.ResourceValue +import com.android.ide.common.resources.ResourceItemWithVisibility +import com.android.ide.common.resources.configuration.FolderConfiguration +import com.android.resources.ResourceType +import com.android.resources.ResourceVisibility +import com.android.utils.Base128InputStream +import com.android.utils.Base128InputStream.StreamFormatException +import com.android.utils.HashCodes +import com.google.common.base.MoreObjects +import java.io.IOException + +/** + * Ported from: [BasicResourceItemBase.java](https://cs.android.com/android-studio/platform/tools/base/+/18047faf69512736b8ddb1f6a6785f58d47c893f:resource-repository/main/java/com/android/resources/base/BasicResourceItemBase.java) + * + * Base class for [com.android.ide.common.resources.ResourceItem]s. + * + * A merger of [BasicResourceItemBase] and [BasicResourceItem] from AOSP, to simplify. + */ +abstract class BasicResourceItem( + private val type: ResourceType, + private val name: String, + visibility: ResourceVisibility +) : ResourceItemWithVisibility, ResourceValue { + // Store enums as their ordinals in byte form to minimize memory footprint. + private val typeOrdinal: Byte = type.ordinal.toByte() + private val visibilityOrdinal: Byte = visibility.ordinal.toByte() + + override fun getType(): ResourceType = resourceType + + override fun getNamespace(): ResourceNamespace = repository.namespace + + override fun getName(): String = name + + override fun getLibraryName(): String? = repository.libraryName + + override fun getResourceType() = ResourceType.values()[typeOrdinal.toInt()] + + override fun getVisibility() = ResourceVisibility.values()[visibilityOrdinal.toInt()] + + override fun getReferenceToSelf(): ResourceReference = asReference() + + override fun getResourceValue(): ResourceValue = this + + override fun isUserDefined(): Boolean = repository.containsUserDefinedResources() + + override fun isFramework(): Boolean = namespace == ResourceNamespace.ANDROID + + override fun asReference(): ResourceReference = ResourceReference(namespace, resourceType, name) + + /** + * Returns the repository this resource belongs to. + * + * Framework resource items may move between repositories with the same origin. + * @see RepositoryConfiguration.transferOwnershipTo + */ + override fun getRepository(): LoadableResourceRepository = repositoryConfiguration.repository + + override fun getConfiguration(): FolderConfiguration = repositoryConfiguration.folderConfiguration + + abstract val repositoryConfiguration: RepositoryConfiguration + + override fun getKey(): String { + val qualifiers = configuration.qualifierString + return if (qualifiers.isNotEmpty()) { + "${type.getName()}-$qualifiers/$name" + } else { + "${type.getName()}/$name" + } + } + + override fun setValue(value: String?): Unit = throw UnsupportedOperationException() + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + if (other == null || javaClass != other.javaClass) { + return false + } + val that = other as BasicResourceItem + return typeOrdinal == that.typeOrdinal && + name == that.name && + visibilityOrdinal == that.visibilityOrdinal + } + + override fun hashCode(): Int { + // The visibilityOrdinal field is intentionally not included in hash code because having two + // resource items differing only by visibility in the same hash table is extremely unlikely. + return HashCodes.mix(typeOrdinal.toInt(), name.hashCode()) + } + + override fun toString(): String { + return MoreObjects.toStringHelper(this) + .add("namespace", namespace) + .add("type", resourceType) + .add("name", name) + .add("value", value) + .toString() + } + + companion object { + /** + * Creates a resource item by reading its contents from the given stream. + */ + @Throws(IOException::class) + fun deserialize( + stream: Base128InputStream, + configurations: List<RepositoryConfiguration>, + sourceFiles: List<ResourceSourceFile>, + namespaceResolvers: List<Resolver> + ): BasicResourceItem { + assert(configurations.isNotEmpty()) + + val encodedType = stream.readInt() + val isFileBased = encodedType and 0x1 != 0 + val resourceType = ResourceType.values()[encodedType ushr 1] + val name = stream.readString() ?: throw StreamFormatException.invalidFormat() + val visibility = ResourceVisibility.values()[stream.readInt()] + + if (isFileBased) { + val repository = configurations[0].repository + return repository.deserializeFileResourceItem( + stream, resourceType, name, visibility, configurations + ) + } + + return BasicValueResourceItemBase.deserialize( + stream, resourceType, name, visibility, configurations, sourceFiles, namespaceResolvers + ) + } + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicStyleResourceItem.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicStyleResourceItem.kt new file mode 100644 index 0000000000..44b3d33676 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicStyleResourceItem.kt @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.internal.resources.base + +import app.cash.paparazzi.internal.resources.ResourceSourceFile +import com.android.ide.common.rendering.api.ResourceNamespace +import com.android.ide.common.rendering.api.ResourceReference +import com.android.ide.common.rendering.api.StyleItemResourceValue +import com.android.ide.common.rendering.api.StyleItemResourceValueImpl +import com.android.ide.common.rendering.api.StyleResourceValue +import com.android.resources.ResourceType +import com.android.resources.ResourceVisibility +import com.android.utils.Base128InputStream +import com.android.utils.Base128InputStream.StreamFormatException +import com.google.common.collect.ImmutableTable +import com.google.common.collect.Table +import java.io.IOException +import java.util.logging.Logger + +/** + * Ported from: [BasicStyleResourceItem.java](https://cs.android.com/android-studio/platform/tools/base/+/f3c4ef558ce2a3ac9eacf7007aee8f6e056235eb:resource-repository/main/java/com/android/resources/base/BasicStyleResourceItem.java) + * + * Resource item representing a style resource. + */ +class BasicStyleResourceItem( + name: String, + sourceFile: ResourceSourceFile, + visibility: ResourceVisibility, + private val parentStyle: String?, + styleItems: Collection<StyleItemResourceValue> +) : BasicValueResourceItemBase(ResourceType.STYLE, name, sourceFile, visibility), + StyleResourceValue { + /** Style items keyed by the namespace and the name of the attribute they define. */ + private val styleItemTable: Table<ResourceNamespace, String, StyleItemResourceValue> + + init { + val tableBuilder = ImmutableTable.builder<ResourceNamespace, String, StyleItemResourceValue>() + val duplicateCheckMap = mutableMapOf<ResourceReference, StyleItemResourceValue>() + for (styleItem in styleItems) { + val attr = styleItem.attr + if (attr != null) { + // Check for duplicate style item definitions. Such duplicate definitions are present in the framework resources. + val previouslyDefined = duplicateCheckMap.put(attr, styleItem) + if (previouslyDefined == null) { + tableBuilder.put(attr.namespace, attr.name, styleItem) + } else if (previouslyDefined != styleItem) { + LOG.warning("Conflicting definitions of \"${styleItem.attrName}\" in style \"$name\"") + } + } + } + styleItemTable = tableBuilder.build() + } + + override fun getParentStyleName(): String? = parentStyle + + override fun getItem(namespace: ResourceNamespace, name: String): StyleItemResourceValue? = + styleItemTable.get(namespace, name) + + override fun getItem(attr: ResourceReference): StyleItemResourceValue? = + if (attr.resourceType == ResourceType.ATTR) { + styleItemTable.get(attr.namespace, attr.name) + } else { + null + } + + override fun getDefinedItems(): Collection<StyleItemResourceValue> = styleItemTable.values() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (!super.equals(other)) return false + val that = other as BasicStyleResourceItem + return parentStyle == that.parentStyle && styleItemTable == that.styleItemTable + } + + companion object { + private val LOG: Logger = Logger.getLogger(BasicStyleResourceItem::class.java.name) + + /** + * Creates a [BasicStyleResourceItem] by reading its contents from the given stream. + */ + @Throws(IOException::class) + fun deserialize( + stream: Base128InputStream, + name: String, + visibility: ResourceVisibility, + sourceFile: ResourceSourceFile, + resolver: ResourceNamespace.Resolver, + namespaceResolvers: List<ResourceNamespace.Resolver> + ): BasicStyleResourceItem { + val repository = sourceFile.repository + val namespace = repository.namespace + val libraryName = repository.libraryName + val parentStyle = stream.readString() + val n = stream.readInt() + val styleItems = if (n == 0) { + emptyList() + } else { + buildList { + for (i in 0 until n) { + val attrName = stream.readString() ?: throw StreamFormatException.invalidFormat() + val value = stream.readString() + val itemResolver = namespaceResolvers[stream.readInt()] + val styleItem = StyleItemResourceValueImpl(namespace, attrName, value, libraryName) + styleItem.namespaceResolver = itemResolver + add(styleItem) + } + } + } + val item = BasicStyleResourceItem(name, sourceFile, visibility, parentStyle, styleItems) + item.namespaceResolver = resolver + return item + } + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicStyleableResourceItem.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicStyleableResourceItem.kt new file mode 100644 index 0000000000..81f0d6370b --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicStyleableResourceItem.kt @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.internal.resources.base + +import app.cash.paparazzi.internal.resources.RepositoryConfiguration +import app.cash.paparazzi.internal.resources.ResourceSourceFile +import com.android.ide.common.rendering.api.AttrResourceValue +import com.android.ide.common.rendering.api.ResourceNamespace +import com.android.ide.common.rendering.api.StyleableResourceValue +import com.android.ide.common.resources.ResourceRepository +import com.android.resources.ResourceType +import com.android.resources.ResourceVisibility +import com.android.utils.Base128InputStream +import com.android.utils.Base128InputStream.StreamFormatException +import java.io.IOException + +/** + * Ported from: [BasicStyleableResourceItem.java](https://cs.android.com/android-studio/platform/tools/base/+/18047faf69512736b8ddb1f6a6785f58d47c893f:resource-repository/main/java/com/android/resources/base/BasicStyleableResourceItem.java) + * + * Resource item representing a styleable resource. + */ +class BasicStyleableResourceItem( + name: String, + sourceFile: ResourceSourceFile, + visibility: ResourceVisibility, + attrs: List<AttrResourceValue> +) : BasicValueResourceItemBase(ResourceType.STYLEABLE, name, sourceFile, visibility), + StyleableResourceValue { + private val attrs: List<AttrResourceValue> = attrs.toList() + + override fun getAllAttributes(): List<AttrResourceValue> = attrs + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (!super.equals(other)) return false + val that = other as BasicStyleableResourceItem + return attrs == that.attrs + } + + companion object { + /** + * Creates a [BasicStyleableResourceItem] by reading its contents from the given stream. + */ + @Throws(IOException::class) + fun deserialize( + stream: Base128InputStream, + name: String, + visibility: ResourceVisibility, + sourceFile: ResourceSourceFile, + resolver: ResourceNamespace.Resolver, + configurations: List<RepositoryConfiguration>, + sourceFiles: List<ResourceSourceFile>, + namespaceResolvers: List<ResourceNamespace.Resolver> + ): BasicStyleableResourceItem { + val n = stream.readInt() + val attrs = if (n == 0) { + emptyList() + } else { + buildList { + for (i in 0 until n) { + val attrItem = deserialize(stream, configurations, sourceFiles, namespaceResolvers) + if (attrItem !is AttrResourceValue) { + throw StreamFormatException.invalidFormat() + } + add(getCanonicalAttr(attrItem as AttrResourceValue, sourceFile.repository)) + } + } + } + val item = BasicStyleableResourceItem(name, sourceFile, visibility, attrs) + item.namespaceResolver = resolver + return item + } + + /** + * For an attr reference that doesn't contain formats tries to find an attr definition the reference is pointing to. + * If such attr definition belongs to this resource repository and has the same description and group name as + * the attr reference, returns the attr definition. Otherwise returns the attr reference passed as the parameter. + */ + fun getCanonicalAttr( + attr: AttrResourceValue, + repository: ResourceRepository + ): AttrResourceValue { + if (attr.formats.isEmpty()) { + val items = repository.getResources(attr.namespace, ResourceType.ATTR, attr.name) + val item = items.filterIsInstance<AttrResourceValue>() + .find { it.description == attr.description && it.groupName == attr.groupName } + if (item != null) { + return item + } + } + return attr + } + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicTextValueResourceItem.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicTextValueResourceItem.kt new file mode 100644 index 0000000000..2ee28112c5 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicTextValueResourceItem.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.internal.resources.base + +import app.cash.paparazzi.internal.resources.ResourceSourceFile +import com.android.ide.common.rendering.api.TextResourceValue +import com.android.resources.ResourceType +import com.android.resources.ResourceVisibility +import com.android.utils.HashCodes + +/** + * Ported from: [BasicTextValueResourceItem.java](https://cs.android.com/android-studio/platform/tools/base/+/47d204001bf0cb6273d8b135c7eece3a982cf0e0:resource-repository/main/java/com/android/resources/base/BasicTextValueResourceItem.java) + * + * Resource item representing a value resource, e.g. a string or a color. + */ +class BasicTextValueResourceItem( + type: ResourceType, + name: String, + sourceFile: ResourceSourceFile, + visibility: ResourceVisibility, + textValue: String?, + private val rawXmlValue: String? +) : BasicValueResourceItem(type, name, sourceFile, visibility, textValue), TextResourceValue { + override fun getRawXmlValue(): String? = rawXmlValue ?: value + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (!super.equals(other)) return false + val that = other as BasicTextValueResourceItem + return rawXmlValue == that.rawXmlValue + } + + override fun hashCode(): Int { + return HashCodes.mix(super.hashCode(), rawXmlValue.hashCode()) + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicValueResourceItem.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicValueResourceItem.kt new file mode 100644 index 0000000000..79e5589086 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicValueResourceItem.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.internal.resources.base + +import app.cash.paparazzi.internal.resources.ResourceSourceFile +import com.android.ide.common.rendering.api.ResourceNamespace +import com.android.resources.ResourceType +import com.android.resources.ResourceVisibility +import com.android.utils.Base128InputStream +import com.android.utils.HashCodes +import java.io.IOException + +/** + * Ported from: [BasicValueResourceItem.java](https://cs.android.com/android-studio/platform/tools/base/+/18047faf69512736b8ddb1f6a6785f58d47c893f:resource-repository/main/java/com/android/resources/base/BasicValueResourceItem.java) + * + * Resource item representing a value resource, e.g. a string or a color. + */ +open class BasicValueResourceItem( + type: ResourceType, + name: String, + sourceFile: ResourceSourceFile, + visibility: ResourceVisibility, + private val value: String? +) : BasicValueResourceItemBase(type, name, sourceFile, visibility) { + override fun getValue() = value + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (!super.equals(other)) return false + val that = other as BasicValueResourceItem + return value == that.value + } + + override fun hashCode(): Int { + return HashCodes.mix(super.hashCode(), value.hashCode()) + } + + companion object { + /** + * Creates a [BasicValueResourceItem] by reading its contents from the given stream. + */ + @Throws(IOException::class) + fun deserialize( + stream: Base128InputStream, + resourceType: ResourceType, + name: String, + visibility: ResourceVisibility, + sourceFile: ResourceSourceFile, + resolver: ResourceNamespace.Resolver + ): BasicValueResourceItem { + val value = stream.readString() + val rawXmlValue = stream.readString() + val item = if (rawXmlValue == null) { + BasicValueResourceItem(resourceType, name, sourceFile, visibility, value) + } else { + BasicTextValueResourceItem(resourceType, name, sourceFile, visibility, value, rawXmlValue) + } + item.namespaceResolver = resolver + return item + } + } +} diff --git a/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicValueResourceItemBase.kt b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicValueResourceItemBase.kt new file mode 100644 index 0000000000..8054e541c3 --- /dev/null +++ b/paparazzi/src/main/java/app/cash/paparazzi/internal/resources/base/BasicValueResourceItemBase.kt @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2023 Square, Inc. + * + * 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. + */ +package app.cash.paparazzi.internal.resources.base + +import app.cash.paparazzi.internal.resources.RepositoryConfiguration +import app.cash.paparazzi.internal.resources.ResourceSourceFile +import com.android.ide.common.rendering.api.ResourceNamespace +import com.android.ide.common.util.PathString +import com.android.resources.ResourceType +import com.android.resources.ResourceType.ARRAY +import com.android.resources.ResourceType.ATTR +import com.android.resources.ResourceType.PLURALS +import com.android.resources.ResourceType.STYLE +import com.android.resources.ResourceType.STYLEABLE +import com.android.resources.ResourceVisibility +import com.android.utils.Base128InputStream +import com.android.utils.HashCodes +import java.io.IOException + +/** + * Ported from: [BasicValueResourceItemBase.java](https://cs.android.com/android-studio/platform/tools/base/+/18047faf69512736b8ddb1f6a6785f58d47c893f:resource-repository/main/java/com/android/resources/base/BasicValueResourceItemBase.java) + * + * Base class for value resource items. + */ +abstract class BasicValueResourceItemBase( + type: ResourceType, + name: String, + val sourceFile: ResourceSourceFile, + visibility: ResourceVisibility +) : BasicResourceItem(type, name, visibility) { + private var namespaceResolver = ResourceNamespace.Resolver.EMPTY_RESOLVER + + override fun getValue(): String? = null + + override fun isFileBased(): Boolean = false + + override val repositoryConfiguration: RepositoryConfiguration + get() = sourceFile.configuration + + override fun getNamespaceResolver(): ResourceNamespace.Resolver = namespaceResolver + + fun setNamespaceResolver(resolver: ResourceNamespace.Resolver) { + namespaceResolver = resolver + } + + override fun getSource(): PathString? = originalSource + + override fun getOriginalSource(): PathString? { + val sourcePath = sourceFile.relativePath + return if (sourcePath == null) null else repository.getOriginalSourceFile(sourcePath, false) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (!super.equals(other)) return false + val that = other as BasicValueResourceItemBase + return sourceFile == that.sourceFile + } + + override fun hashCode(): Int { + return HashCodes.mix(super.hashCode(), sourceFile.hashCode()) + } + + companion object { + /** + * Creates a resource item by reading its contents from the given stream. + */ + @Throws(IOException::class) + fun deserialize( + stream: Base128InputStream, + resourceType: ResourceType, + name: String, + visibility: ResourceVisibility, + configurations: List<RepositoryConfiguration>, + sourceFiles: List<ResourceSourceFile>, + namespaceResolvers: List<ResourceNamespace.Resolver> + ): BasicValueResourceItemBase { + val sourceFile = sourceFiles[stream.readInt()] + val resolver = namespaceResolvers[stream.readInt()] + return when (resourceType) { + ARRAY -> BasicArrayResourceItem.deserialize(stream, name, visibility, sourceFile, resolver) + + ATTR -> BasicAttrResourceItem.deserialize(stream, name, visibility, sourceFile, resolver) + + PLURALS -> + BasicPluralsResourceItem.deserialize(stream, name, visibility, sourceFile, resolver) + + STYLE -> BasicStyleResourceItem.deserialize( + stream, name, visibility, sourceFile, resolver, namespaceResolvers + ) + + STYLEABLE -> BasicStyleableResourceItem.deserialize( + stream, name, visibility, sourceFile, resolver, configurations, sourceFiles, + namespaceResolvers + ) + + else -> BasicValueResourceItem.deserialize( + stream, resourceType, name, visibility, sourceFile, resolver + ) + } + } + } +} diff --git a/paparazzi/paparazzi/src/main/resources/index.html b/paparazzi/src/main/resources/index.html similarity index 100% rename from paparazzi/paparazzi/src/main/resources/index.html rename to paparazzi/src/main/resources/index.html diff --git a/paparazzi/paparazzi/src/main/resources/paparazzi.js b/paparazzi/src/main/resources/paparazzi.js similarity index 100% rename from paparazzi/paparazzi/src/main/resources/paparazzi.js rename to paparazzi/src/main/resources/paparazzi.js diff --git a/paparazzi/src/test/java/app/cash/paparazzi/FileSubject.kt b/paparazzi/src/test/java/app/cash/paparazzi/FileSubject.kt new file mode 100644 index 0000000000..3744e9198b --- /dev/null +++ b/paparazzi/src/test/java/app/cash/paparazzi/FileSubject.kt @@ -0,0 +1,55 @@ +package app.cash.paparazzi + +import com.google.common.truth.FailureMetadata +import com.google.common.truth.Subject +import com.google.common.truth.Subject.Factory +import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import java.io.File + +internal class FileSubject private constructor( + metadata: FailureMetadata, + private val actual: File? +) : Subject(metadata, actual) { + fun hasContent(expected: String) { + assertThat(actual).isNotNull() + requireNotNull(actual) // smart cast + + assertThat(actual.readText()).isEqualTo(expected) + } + + fun isEmptyDirectory() { + assertThat(actual).isNotNull() + requireNotNull(actual) // smart cast + + assertWithMessage("File $actual is not a directory").that(actual.isDirectory).isTrue() + assertWithMessage("Directory $actual is not empty") + .that(actual.listFiles()) + .isEmpty() + } + + fun exists() { + assertThat(actual).isNotNull() + require(actual is File) // smart cast + + assertWithMessage("File $actual does not exist").that(actual.exists()).isTrue() + } + + fun doesNotExist() { + assertThat(actual).isNotNull() + require(actual is File) // smart cast + + assertWithMessage("File $actual does exist").that(actual.exists()).isFalse() + } + + companion object { + private val FILE_SUBJECT_FACTORY = Factory<FileSubject, File> { metadata, actual -> + FileSubject(metadata, actual) + } + + fun assertThat(actual: File?): FileSubject { + return assertAbout(FILE_SUBJECT_FACTORY).that(actual) + } + } +} diff --git a/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/HtmlReportWriterTest.kt b/paparazzi/src/test/java/app/cash/paparazzi/HtmlReportWriterTest.kt similarity index 94% rename from paparazzi/paparazzi/src/test/java/app/cash/paparazzi/HtmlReportWriterTest.kt rename to paparazzi/src/test/java/app/cash/paparazzi/HtmlReportWriterTest.kt index dc38b1cf5d..e6694b82e2 100644 --- a/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/HtmlReportWriterTest.kt +++ b/paparazzi/src/test/java/app/cash/paparazzi/HtmlReportWriterTest.kt @@ -15,14 +15,14 @@ */ package app.cash.paparazzi -import org.assertj.core.api.Assertions.assertThat +import app.cash.paparazzi.FileSubject.Companion.assertThat +import com.google.common.truth.Truth.assertThat import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder import java.awt.image.BufferedImage import java.io.File import java.nio.file.Files -import java.nio.file.Path import java.nio.file.attribute.BasicFileAttributes import java.nio.file.attribute.FileTime import java.time.Instant @@ -62,7 +62,6 @@ class HtmlReportWriterTest { |window.all_runs = [ | "run_one" |]; - | """.trimMargin() ) @@ -79,7 +78,6 @@ class HtmlReportWriterTest { | "file": "images/$anyImageHash.png" | } |]; - | """.trimMargin() ) } @@ -109,8 +107,8 @@ class HtmlReportWriterTest { } } - assertThat(File(reportRoot.root, "images")).isEmptyDirectory - assertThat(File(reportRoot.root, "videos")).isEmptyDirectory + assertThat(File(reportRoot.root, "images")).isEmptyDirectory() + assertThat(File(reportRoot.root, "videos")).isEmptyDirectory() } @Test @@ -126,9 +124,8 @@ class HtmlReportWriterTest { testName = TestName("app.cash.paparazzi", "HomeView", "testSettings"), timestamp = now.toDate() ) - val file = + val golden = File("${snapshotRoot.root}/images/app.cash.paparazzi_HomeView_testSettings_test.png") - val golden = file.toPath() // precondition assertThat(golden).doesNotExist() @@ -163,7 +160,7 @@ class HtmlReportWriterTest { private fun Instant.toDate() = Date(toEpochMilli()) - private fun Path.lastModifiedTime(): FileTime { - return Files.readAttributes(this, BasicFileAttributes::class.java).lastModifiedTime() + private fun File.lastModifiedTime(): FileTime { + return Files.readAttributes(this.toPath(), BasicFileAttributes::class.java).lastModifiedTime() } } diff --git a/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/InstantAnimationsRuleTest.kt b/paparazzi/src/test/java/app/cash/paparazzi/InstantAnimationsRuleTest.kt similarity index 92% rename from paparazzi/paparazzi/src/test/java/app/cash/paparazzi/InstantAnimationsRuleTest.kt rename to paparazzi/src/test/java/app/cash/paparazzi/InstantAnimationsRuleTest.kt index acc064de95..0d0b7d57cc 100644 --- a/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/InstantAnimationsRuleTest.kt +++ b/paparazzi/src/test/java/app/cash/paparazzi/InstantAnimationsRuleTest.kt @@ -21,7 +21,7 @@ import android.animation.ValueAnimator import android.graphics.Canvas import android.view.animation.LinearInterpolator import android.widget.TextView -import org.assertj.core.api.Assertions.assertThat +import com.google.common.truth.Truth.assertThat import org.junit.Rule import org.junit.Test @@ -66,11 +66,8 @@ class InstantAnimationsRuleTest { animator.start() paparazzi.snapshot(view) - assertThat(log).containsExactly( - "onAnimationStart", - "onAnimationEnd", - "onDraw text=1.0" - ) + assertThat(log) + .containsExactly("onAnimationStart", "onAnimationEnd", "onDraw text=1.0").inOrder() log.clear() } } diff --git a/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/PaparazziTest.kt b/paparazzi/src/test/java/app/cash/paparazzi/PaparazziTest.kt similarity index 98% rename from paparazzi/paparazzi/src/test/java/app/cash/paparazzi/PaparazziTest.kt rename to paparazzi/src/test/java/app/cash/paparazzi/PaparazziTest.kt index cec48e335a..53e8323887 100644 --- a/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/PaparazziTest.kt +++ b/paparazzi/src/test/java/app/cash/paparazzi/PaparazziTest.kt @@ -28,7 +28,7 @@ import android.view.animation.LinearInterpolator import android.widget.Button import android.widget.TextView import com.android.internal.lang.System_Delegate -import org.assertj.core.api.Assertions.assertThat +import com.google.common.truth.Truth.assertThat import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -212,7 +212,7 @@ class PaparazziTest { true } - assertThat(thrown).isTrue + assertThat(thrown).isTrue() } private val time: Long diff --git a/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/R.kt b/paparazzi/src/test/java/app/cash/paparazzi/R.kt similarity index 100% rename from paparazzi/paparazzi/src/test/java/app/cash/paparazzi/R.kt rename to paparazzi/src/test/java/app/cash/paparazzi/R.kt diff --git a/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/accessibility/AccessibilityRenderExtensionTest.kt b/paparazzi/src/test/java/app/cash/paparazzi/accessibility/AccessibilityRenderExtensionTest.kt similarity index 100% rename from paparazzi/paparazzi/src/test/java/app/cash/paparazzi/accessibility/AccessibilityRenderExtensionTest.kt rename to paparazzi/src/test/java/app/cash/paparazzi/accessibility/AccessibilityRenderExtensionTest.kt diff --git a/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/internal/PaparazziJsonTest.kt b/paparazzi/src/test/java/app/cash/paparazzi/internal/PaparazziJsonTest.kt similarity index 95% rename from paparazzi/paparazzi/src/test/java/app/cash/paparazzi/internal/PaparazziJsonTest.kt rename to paparazzi/src/test/java/app/cash/paparazzi/internal/PaparazziJsonTest.kt index 975203732f..e21f9ebf6a 100644 --- a/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/internal/PaparazziJsonTest.kt +++ b/paparazzi/src/test/java/app/cash/paparazzi/internal/PaparazziJsonTest.kt @@ -16,7 +16,7 @@ package app.cash.paparazzi.internal import app.cash.paparazzi.TestName -import org.assertj.core.api.Assertions.assertThat +import com.google.common.truth.Truth.assertThat import org.junit.Test class PaparazziJsonTest { diff --git a/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/internal/PaparazziLoggerTest.kt b/paparazzi/src/test/java/app/cash/paparazzi/internal/PaparazziLoggerTest.kt similarity index 84% rename from paparazzi/paparazzi/src/test/java/app/cash/paparazzi/internal/PaparazziLoggerTest.kt rename to paparazzi/src/test/java/app/cash/paparazzi/internal/PaparazziLoggerTest.kt index e1ab916384..db61589e1b 100644 --- a/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/internal/PaparazziLoggerTest.kt +++ b/paparazzi/src/test/java/app/cash/paparazzi/internal/PaparazziLoggerTest.kt @@ -1,8 +1,8 @@ package app.cash.paparazzi.internal import app.cash.paparazzi.internal.PaparazziLogger.MultipleFailuresException -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.api.Assertions.fail +import com.google.common.truth.Truth.assertThat +import org.junit.Assert.fail import org.junit.Test import java.io.FileNotFoundException @@ -47,4 +47,12 @@ class PaparazziLoggerTest { assertThat(ignored.message).contains("java.lang.IllegalStateException: error2") } } + + @Test + fun testFlushErrors() { + val logger = PaparazziLogger() + logger.error(FileNotFoundException("error1"), null) + logger.flushErrors() + logger.assertNoErrors() + } } diff --git a/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/internal/parsers/InMemoryParserTest.kt b/paparazzi/src/test/java/app/cash/paparazzi/internal/parsers/InMemoryParserTest.kt similarity index 96% rename from paparazzi/paparazzi/src/test/java/app/cash/paparazzi/internal/parsers/InMemoryParserTest.kt rename to paparazzi/src/test/java/app/cash/paparazzi/internal/parsers/InMemoryParserTest.kt index 9a032e324d..416ed08e41 100644 --- a/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/internal/parsers/InMemoryParserTest.kt +++ b/paparazzi/src/test/java/app/cash/paparazzi/internal/parsers/InMemoryParserTest.kt @@ -1,6 +1,6 @@ package app.cash.paparazzi.internal.parsers -import org.assertj.core.api.Assertions.assertThat +import com.google.common.truth.Truth.assertThat import org.junit.Test import org.xmlpull.v1.XmlPullParserException @@ -55,7 +55,7 @@ class InMemoryParserTest { assertThat(parser.getAttributePrefix(1)).isEqualTo(ANDROID_PREFIX) assertThat(parser.getAttributeValue(0)).isEqualTo("#999999") - assertThat(parser.getAttributeValue(1)).isNotNull // pathData + assertThat(parser.getAttributeValue(1)).isNotNull() // pathData parser.next() // END_TAG = "path" diff --git a/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/internal/parsers/ResourceParserTest.kt b/paparazzi/src/test/java/app/cash/paparazzi/internal/parsers/ResourceParserTest.kt similarity index 97% rename from paparazzi/paparazzi/src/test/java/app/cash/paparazzi/internal/parsers/ResourceParserTest.kt rename to paparazzi/src/test/java/app/cash/paparazzi/internal/parsers/ResourceParserTest.kt index 67402f91ed..f99a38f5dd 100644 --- a/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/internal/parsers/ResourceParserTest.kt +++ b/paparazzi/src/test/java/app/cash/paparazzi/internal/parsers/ResourceParserTest.kt @@ -15,7 +15,7 @@ */ package app.cash.paparazzi.internal.parsers -import org.assertj.core.api.Assertions.assertThat +import com.google.common.truth.Truth.assertThat import org.junit.Test class ResourceParserTest { @@ -53,7 +53,7 @@ class ResourceParserTest { assertThat(namespace).isEqualTo(ANDROID_NAMESPACE) assertThat(prefix).isEqualTo(ANDROID_PREFIX) assertThat(name).isEqualTo(PATH_DATA_ATTR_NAME) - assertThat(value).isNotEmpty // don't care about pathData precision + assertThat(value).isNotEmpty() // don't care about pathData precision } } diff --git a/paparazzi/src/test/java/app/cash/paparazzi/internal/resources/AarSourceResourceRepositoryTest.kt b/paparazzi/src/test/java/app/cash/paparazzi/internal/resources/AarSourceResourceRepositoryTest.kt new file mode 100644 index 0000000000..aee493a6a3 --- /dev/null +++ b/paparazzi/src/test/java/app/cash/paparazzi/internal/resources/AarSourceResourceRepositoryTest.kt @@ -0,0 +1,207 @@ +package app.cash.paparazzi.internal.resources + +import com.android.ide.common.rendering.api.AttrResourceValue +import com.android.ide.common.rendering.api.AttributeFormat +import com.android.ide.common.rendering.api.ResourceNamespace +import com.android.ide.common.rendering.api.StyleResourceValue +import com.android.ide.common.rendering.api.StyleableResourceValue +import com.android.ide.common.resources.ResourceItem +import com.android.resources.ResourceType +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +class AarSourceResourceRepositoryTest { + @get:Rule + val tempDir: TemporaryFolder = TemporaryFolder() + + @Test + fun getAllDeclaredIds_hasRDotTxt() { + // R.txt contains these 3 ids which are actually not defined anywhere else. + // The layout file contains "id_from_layout" but it should not be parsed if R.txt is present. + val repository = makeAarRepositoryFromExplodedAar("my_aar_lib") + assertThat( + repository.getResources(ResourceNamespace.RES_AUTO, ResourceType.ID).keySet() + ).containsExactly("id1", "id2", "id3") + } + + @Test fun getAllDeclaredIds_noRDotTxt() { + // There's no R.txt, so the layout file should be parsed and the two ids found. + val repository = makeAarRepositoryFromExplodedAar("my_aar_lib_noRDotTxt") + assertThat( + repository.getResources(ResourceNamespace.RES_AUTO, ResourceType.ID).keySet() + ).containsExactly( + "btn_title_refresh", + "bug123032845", + "header", + "image", + "imageButton", + "imageView", + "imageView2", + "nonExistent", + "noteArea", + "styledView", + "text2", + "title_refresh_progress" + ) + } + + @Test + fun getAllDeclaredIds_wrongRDotTxt() { + // IDs should come from R.txt, not parsing the layout. + val repository = makeAarRepositoryFromExplodedAar("my_aar_lib_wrongRDotTxt") + assertThat( + repository.getResources(ResourceNamespace.RES_AUTO, ResourceType.ID).keySet() + ).containsExactly("id1", "id2", "id3") + } + + @Test fun getAllDeclaredIds_brokenRDotTxt() { + // We can't parse R.txt, so we fall back to parsing layouts. + val repository = makeAarRepositoryFromExplodedAar("my_aar_lib_brokenRDotTxt") + assertThat( + repository.getResources(ResourceNamespace.RES_AUTO, ResourceType.ID).keySet() + ).containsExactly("id_from_layout") + } + + @Test fun getAllDeclaredIds_unrecognizedTag() { + val repository = makeAarRepositoryFromExplodedAar("unrecognizedTag") + assertThat( + repository.getResources(ResourceNamespace.RES_AUTO, ResourceType.COLOR).keySet() + ).containsExactly("black", "white") + } + + @Test fun multipleValues_wholeResourceDirectory_exploded() { + val repository = makeAarRepositoryFromExplodedAar("my_aar_lib") + checkRepositoryContents(repository) + } + + @Test fun multipleValues_wholeResourceDirectory_unexploded() { + val repository = makeAarRepositoryFromAarArtifact(tempDir.root.toPath(), "my_aar_lib") + checkRepositoryContents(repository) + } + + @Test fun multipleValues_partOfResourceDirectories() { + val repository = makeAarRepositoryFromExplodedAarFiltered( + "my_aar_lib", + "values/strings.xml", + "values-fr/strings.xml" + ) + val items = repository.getResources(ResourceNamespace.RES_AUTO, ResourceType.STRING, "hello") + val helloVariants = getValues(items) + assertThat(helloVariants).containsExactly("bonjour", "hello") + } + + @Test fun libraryNameIsMaintained() { + val repository = makeAarRepositoryFromExplodedAar("my_aar_lib") + assertThat(repository.libraryName).isEqualTo(AAR_LIBRARY_NAME) + for (item in repository.allResources) { + assertThat(item.libraryName).isEqualTo(AAR_LIBRARY_NAME) + } + } + + @Test fun packageName() { + val repository = makeAarRepositoryFromExplodedAar("my_aar_lib") + assertThat(repository.packageName).isEqualTo(AAR_PACKAGE_NAME) + } + + companion object { + private fun getValues(items: List<ResourceItem>): List<String> = + buildList { + items.forEach { item -> + val resourceValue = item.resourceValue + assertThat(resourceValue).isNotNull() + this += resourceValue.value + } + } + + private fun checkRepositoryContents(repository: AarSourceResourceRepository) { + var items = repository.getResources( + namespace = ResourceNamespace.RES_AUTO, + resourceType = ResourceType.STRING, + resourceName = "hello" + ) + val helloVariants = getValues(items) + assertThat(helloVariants).containsExactly("bonjour", "hello", "hola") + + items = repository.getResources( + namespace = ResourceNamespace.RES_AUTO, + resourceType = ResourceType.STYLE, + resourceName = "MyTheme.Dark" + ) + assertThat(items.size).isEqualTo(1) + val styleValue = items[0].resourceValue as StyleResourceValue + assertThat(styleValue.parentStyleName).isEqualTo("android:Theme.Light") + val styleItems = styleValue.definedItems + assertThat(styleItems.size).isEqualTo(2) + val textColor = styleValue.getItem(ResourceNamespace.ANDROID, "textColor") + assertThat(textColor.attrName).isEqualTo("android:textColor") + assertThat(textColor.value).isEqualTo("#999999") + val foo = styleValue.getItem(ResourceNamespace.RES_AUTO, "foo") + assertThat(foo.attrName).isEqualTo("foo") + assertThat(foo.value).isEqualTo("?android:colorForeground") + + items = repository.getResources( + namespace = ResourceNamespace.RES_AUTO, + resourceType = ResourceType.STYLEABLE, + resourceName = "Styleable1" + ) + assertThat(items.size).isEqualTo(1) + var styleableValue = items[0].resourceValue as StyleableResourceValue + var attributes = styleableValue.allAttributes + assertThat(attributes.size).isEqualTo(1) + var attr = attributes[0] + assertThat(attr.name).isEqualTo("some_attr") + assertThat(attr.formats).containsExactly(AttributeFormat.COLOR) + + items = repository.getResources( + namespace = ResourceNamespace.RES_AUTO, + resourceType = ResourceType.STYLEABLE, + resourceName = "Styleable.with.dots" + ) + assertThat(items.size).isEqualTo(1) + styleableValue = items[0].resourceValue as StyleableResourceValue + attributes = styleableValue.allAttributes + assertThat(attributes.size).isEqualTo(1) + attr = attributes[0] + assertThat(attr.name).isEqualTo("some_attr") + assertThat(attr.formats).containsExactly(AttributeFormat.COLOR) + + items = repository.getResources( + namespace = ResourceNamespace.RES_AUTO, + resourceType = ResourceType.ATTR, + resourceName = "some_attr" + ) + assertThat(items.size).isEqualTo(1) + attr = items[0].resourceValue as AttrResourceValue + assertThat(attr.name).isEqualTo("some_attr") + assertThat(attr.formats).containsExactly(AttributeFormat.COLOR) + + items = repository.getResources( + namespace = ResourceNamespace.RES_AUTO, + resourceType = ResourceType.ATTR, + resourceName = "app_attr1" + ) + assertThat(items).isEmpty() + + items = repository.getResources( + namespace = ResourceNamespace.RES_AUTO, + resourceType = ResourceType.ATTR, + resourceName = "app_attr2" + ) + assertThat(items.size).isEqualTo(1) + attr = items[0].resourceValue as AttrResourceValue + assertThat(attr.name).isEqualTo("app_attr2") + assertThat(attr.formats).containsExactly( + AttributeFormat.BOOLEAN, + AttributeFormat.COLOR, + AttributeFormat.DIMENSION, + AttributeFormat.FLOAT, + AttributeFormat.FRACTION, + AttributeFormat.INTEGER, + AttributeFormat.REFERENCE, + AttributeFormat.STRING + ) + } + } +} diff --git a/paparazzi/src/test/java/app/cash/paparazzi/internal/resources/AarTestUtils.kt b/paparazzi/src/test/java/app/cash/paparazzi/internal/resources/AarTestUtils.kt new file mode 100644 index 0000000000..7ec93cff95 --- /dev/null +++ b/paparazzi/src/test/java/app/cash/paparazzi/internal/resources/AarTestUtils.kt @@ -0,0 +1,99 @@ +@file:OptIn(ExperimentalPathApi::class) + +package app.cash.paparazzi.internal.resources + +import com.android.SdkConstants.DOT_AAR +import com.android.ide.common.util.toPathString +import java.nio.file.FileVisitResult.CONTINUE +import java.nio.file.Path +import java.nio.file.Paths +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream +import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.exists +import kotlin.io.path.invariantSeparatorsPathString +import kotlin.io.path.outputStream +import kotlin.io.path.readBytes +import kotlin.io.path.visitFileTree + +internal const val AAR_LIBRARY_NAME = "com.test:test-library:1.0.0" +internal const val AAR_PACKAGE_NAME = "com.test.testlibrary" +internal const val TEST_DATA_DIR = "src/test/resources/aar" + +/** + * Representative of loading an unzipped AAR from the Gradle transforms cache, e.g., + * $GRADLE_USER_HOME/caches/transforms-3/93260cec846aa69823e5e1c7eb771238/transformed/appcompat-1.6.1/res + */ +internal fun makeAarRepositoryFromExplodedAar( + libraryDirName: String +): AarSourceResourceRepository { + return AarSourceResourceRepository.create( + resourceDirectoryOrFile = resolveProjectPath("$TEST_DATA_DIR/$libraryDirName/res"), + libraryName = AAR_LIBRARY_NAME + ) +} + +/** + * Same as [makeAarRepositoryFromExplodedAar], but only selecting certain resource folders + */ +internal fun makeAarRepositoryFromExplodedAarFiltered( + libraryDirName: String, + vararg resources: String +): AarSourceResourceRepository { + val root = resolveProjectPath("$TEST_DATA_DIR/$libraryDirName/res").toPathString() + return AarSourceResourceRepository.create( + resourceFolderRoot = root, + resourceFolderResources = resources.map { resource -> root.resolve(resource) }, + libraryName = AAR_LIBRARY_NAME + ) +} + +/** + * Representative of loading a downloaded AAR from the Gradle cache, e.g., + * $GRADLE_USER_HOME/caches/modules-2/files-2.1/androidx.appcompat/appcompat/1.6.1/6c7577004b7ebbee5ed87d512b578dd20e3c8c31/appcompat-1.6.1.aar + * + * Given a [libraryDirName] pointing to an exploded aar root, create an aar "on-the-fly" with parent [tempDir]. + */ +internal fun makeAarRepositoryFromAarArtifact( + tempDir: Path, + libraryDirName: String +): AarSourceResourceRepository { + val sourceDirectory = resolveProjectPath("$TEST_DATA_DIR/$libraryDirName") + return AarSourceResourceRepository.create( + resourceDirectoryOrFile = createAar(tempDir, sourceDirectory), + libraryName = AAR_LIBRARY_NAME + ) +} + +private fun createAar(tempDir: Path, sourceDirectory: Path): Path { + return tempDir.resolve("${sourceDirectory.fileName}$DOT_AAR").also { aarPath -> + ZipOutputStream(aarPath.outputStream().buffered()).use { zip -> + sourceDirectory.visitFileTree { + onVisitFile { file, _ -> + val relativePath = sourceDirectory.relativize(file).invariantSeparatorsPathString + val entry = ZipEntry(relativePath) + zip.putNextEntry(entry) + zip.write(file.readBytes()) + zip.closeEntry() + return@onVisitFile CONTINUE + } + } + } + } +} + +/** + * Returns the absolute path, given a file or directory relative to the base of the current project, + * + * @throws IllegalArgumentException if the path results in a file that is not found. + */ +internal fun resolveProjectPath(relativePath: String): Path { + val f = projectRoot.resolve(relativePath) + if (f.exists()) { + return f + } + + throw IllegalArgumentException("File \"$relativePath\" not found at \"$projectRoot\"") +} + +private var projectRoot = Paths.get("").toAbsolutePath() diff --git a/paparazzi/src/test/java/app/cash/paparazzi/internal/resources/AppResourceRepositoryTest.kt b/paparazzi/src/test/java/app/cash/paparazzi/internal/resources/AppResourceRepositoryTest.kt new file mode 100644 index 0000000000..12b0c18b3b --- /dev/null +++ b/paparazzi/src/test/java/app/cash/paparazzi/internal/resources/AppResourceRepositoryTest.kt @@ -0,0 +1,33 @@ +package app.cash.paparazzi.internal.resources + +import com.android.ide.common.rendering.api.AttrResourceValue +import com.android.ide.common.rendering.api.AttributeFormat +import com.android.resources.ResourceType +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class AppResourceRepositoryTest { + @Test + fun test() { + val repository = AppResourceRepository.create( + localResourceDirectories = listOf(resolveProjectPath("src/test/resources/folders/res").toFile()), + moduleResourceDirectories = emptyList(), + libraryRepositories = listOf(makeAarRepositoryFromExplodedAar("my_aar_lib")) + ) + + val map = repository.allResources + assertThat(map.size).isEqualTo(45) + + assertThat(map[0].name).isEqualTo("slide_in_from_left") + assertThat(map[0].type).isEqualTo(ResourceType.ANIM) + assertThat(map[1].name).isEqualTo("test_animator") + assertThat(map[1].type).isEqualTo(ResourceType.ANIMATOR) + + assertThat(map[4].name).isEqualTo("some_attr") + assertThat(map[4].type).isEqualTo(ResourceType.ATTR) + assertThat((map[4].resourceValue as AttrResourceValue).formats).isEqualTo(setOf(AttributeFormat.COLOR)) + + assertThat(map[44].name).isEqualTo("test_network_security_config") + assertThat(map[44].type).isEqualTo(ResourceType.XML) + } +} diff --git a/paparazzi/src/test/java/app/cash/paparazzi/internal/resources/CommentTrackingXmlPullParserTest.kt b/paparazzi/src/test/java/app/cash/paparazzi/internal/resources/CommentTrackingXmlPullParserTest.kt new file mode 100644 index 0000000000..9c7195eb8f --- /dev/null +++ b/paparazzi/src/test/java/app/cash/paparazzi/internal/resources/CommentTrackingXmlPullParserTest.kt @@ -0,0 +1,226 @@ +package app.cash.paparazzi.internal.resources + +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import java.io.StringReader + +class CommentTrackingXmlPullParserTest { + @Test + fun test() { + StringReader(SIMPLE_RES).use { + val parser = CommentTrackingXmlPullParser().apply { + setInput(it) + } + + // <resources> + parser.nextToken() + parser.hasAttributes( + name = "resources", + lastComment = null, + attrGroupComment = null + ) + + // new line + parser.nextToken() + parser.hasAttributes( + name = null, + lastComment = null, + attrGroupComment = null + ) + + // <!-- These are the standard attributes that make up a complete theme. --> + parser.nextToken() + parser.hasAttributes( + name = null, + lastComment = "These are the standard attributes that make up a complete theme.", + attrGroupComment = null + ) + + // new line + parser.nextToken() + parser.hasAttributes( + name = null, + lastComment = "These are the standard attributes that make up a complete theme.", + attrGroupComment = null + ) + + // <declare-styleable name="Theme"> + parser.nextToken() + parser.hasAttributes( + name = "declare-styleable", + lastComment = "These are the standard attributes that make up a complete theme.", + attrGroupComment = null + ) + + // new line + parser.nextToken() + parser.hasAttributes( + name = null, + lastComment = "These are the standard attributes that make up a complete theme.", + attrGroupComment = null + ) + + // <!-- ============= --> + parser.nextToken() + parser.hasAttributes( + name = null, + lastComment = "These are the standard attributes that make up a complete theme.", + attrGroupComment = null + ) + + // new line + parser.nextToken() + parser.hasAttributes( + name = null, + lastComment = "These are the standard attributes that make up a complete theme.", + attrGroupComment = null + ) + + // <!-- Button styles --> + parser.nextToken() + parser.hasAttributes( + name = null, + lastComment = "Button styles", + attrGroupComment = null + ) + + // new line + parser.nextToken() + parser.hasAttributes( + name = null, + lastComment = "Button styles", + attrGroupComment = null + ) + + // <!-- ============= --> + parser.nextToken() + parser.hasAttributes( + name = null, + lastComment = "Button styles", + attrGroupComment = null + ) + + // new line + parser.nextToken() + parser.hasAttributes( + name = null, + lastComment = "Button styles", + attrGroupComment = null + ) + + // <eat-comment /> start + parser.nextToken() + parser.hasAttributes( + name = "eat-comment", + lastComment = "Button styles", + attrGroupComment = null + ) + + // <eat-comment /> end + parser.nextToken() + parser.hasAttributes( + name = "eat-comment", + lastComment = null, + attrGroupComment = "Button styles" + ) + + // new line + parser.nextToken() + parser.hasAttributes( + name = null, + lastComment = null, + attrGroupComment = "Button styles" + ) + + // <!-- Normal Button style. --> + parser.nextToken() + parser.hasAttributes( + name = null, + lastComment = "Normal Button style.", + attrGroupComment = "Button styles" + ) + + // new line + parser.nextToken() + parser.hasAttributes( + name = null, + lastComment = "Normal Button style.", + attrGroupComment = "Button styles" + ) + + // <attr name="buttonStyle" format="reference" /> start + parser.nextToken() + parser.hasAttributes( + name = "attr", + lastComment = "Normal Button style.", + attrGroupComment = "Button styles" + ) + + // <attr name="buttonStyle" format="reference" /> end + parser.nextToken() + parser.hasAttributes( + name = "attr", + lastComment = null, + attrGroupComment = "Button styles" + ) + + // new line + parser.nextToken() + parser.hasAttributes( + name = null, + lastComment = null, + attrGroupComment = "Button styles" + ) + + // </declare-styleable> + parser.nextToken() + parser.hasAttributes( + name = "declare-styleable", + lastComment = null, + attrGroupComment = null + ) + + // new line + parser.nextToken() + parser.hasAttributes( + name = null, + lastComment = null, + attrGroupComment = null + ) + + // </resources> + parser.nextToken() + parser.hasAttributes( + name = "resources", + lastComment = null, + attrGroupComment = null + ) + } + } + + private fun CommentTrackingXmlPullParser.hasAttributes( + name: String?, + lastComment: String?, + attrGroupComment: String? + ) { + assertThat(this.name).isEqualTo(name) + assertThat(this.lastComment).isEqualTo(lastComment) + assertThat(attrGroupComment).isEqualTo(attrGroupComment) + } + + companion object { + val SIMPLE_RES = """ + |<resources> + | <!-- These are the standard attributes that make up a complete theme. --> + | <declare-styleable name="Theme"> + | <!-- ============= --> + | <!-- Button styles --> + | <!-- ============= --> + | <eat-comment /> + | <!-- Normal Button style. --> + | <attr name="buttonStyle" format="reference" /> + | </declare-styleable> + |</resources> + """.trimMargin() + } +} diff --git a/paparazzi/src/test/java/app/cash/paparazzi/internal/resources/FrameworkResourceRepositoryTest.kt b/paparazzi/src/test/java/app/cash/paparazzi/internal/resources/FrameworkResourceRepositoryTest.kt new file mode 100644 index 0000000000..1f32c52c7b --- /dev/null +++ b/paparazzi/src/test/java/app/cash/paparazzi/internal/resources/FrameworkResourceRepositoryTest.kt @@ -0,0 +1,185 @@ +package app.cash.paparazzi.internal.resources + +import com.android.ide.common.rendering.api.AttrResourceValue +import com.android.ide.common.rendering.api.AttributeFormat.ENUM +import com.android.ide.common.rendering.api.ResourceNamespace +import com.android.ide.common.resources.ResourceItemWithVisibility +import com.android.ide.common.resources.ResourceRepository +import com.android.resources.ResourceType +import com.android.resources.ResourceType.ATTR +import com.android.resources.ResourceType.ID +import com.android.resources.ResourceVisibility +import com.android.resources.ResourceVisibility.PRIVATE +import com.android.resources.ResourceVisibility.PUBLIC +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import org.junit.Test +import java.nio.file.Path +import java.nio.file.Paths + +class FrameworkResourceRepositoryTest { + @Test + fun fullLoadingFromJar() { + for (languages in listOf(setOf(), setOf("fr", "de"), null)) { + val fromJar = FrameworkResourceRepository.create( + resourceDirectoryOrFile = getFrameworkResJar(), + languagesToLoad = languages, + useCompiled9Patches = false + ) + + checkLanguages(fromJar, languages) + checkContents(fromJar) + } + } + + @Test + fun incrementalLoadingFromJar() { + val withFrench = FrameworkResourceRepository.create( + resourceDirectoryOrFile = getFrameworkResJar(), + languagesToLoad = setOf("fr"), + useCompiled9Patches = false + ) + checkLanguages(withFrench, setOf("fr")) + checkContents(withFrench) + + val withFrenchAndGerman = withFrench.loadMissingLanguages(setOf("de")) + checkLanguages(withFrenchAndGerman, setOf("fr", "de")) + checkContents(withFrenchAndGerman) + } + + @Test + fun useCompiled9Patches() { + val repository = FrameworkResourceRepository.create( + resourceDirectoryOrFile = getFrameworkResJar(), + languagesToLoad = emptySet(), + useCompiled9Patches = true + ) + + val resourceUrl = repository.getResourceUrl("drawable-hdpi/textfield_search_activated_mtrl_alpha.9.png") + assertThat(resourceUrl).isEqualTo("jar://src/test/resources/framework/framework_res.jar!/res/drawable-hdpi/textfield_search_activated_mtrl_alpha.compiled.9.png") + } + + @Test + fun notUseCompiled9Patches() { + val repository = FrameworkResourceRepository.create( + resourceDirectoryOrFile = getFrameworkResJar(), + languagesToLoad = emptySet(), + useCompiled9Patches = false + ) + + val resourceUrl = repository.getResourceUrl("drawable-hdpi/textfield_search_activated_mtrl_alpha.9.png") + assertThat(resourceUrl).isEqualTo("jar://src/test/resources/framework/framework_res.jar!/res/drawable-hdpi/textfield_search_activated_mtrl_alpha.9.png") + } + + private fun getFrameworkResJar(): Path = + Paths.get("src/test/resources/framework/framework_res.jar") + + companion object { + private fun checkLanguages( + repository: FrameworkResourceRepository, + languages: Set<String>? + ) { + if (languages == null) { + assertThat(repository.languageGroups.size).isAtLeast(75) + } else { + assertThat(repository.languageGroups).isEqualTo(languages.union(setOf(""))) + } + } + } + + private fun checkContents(repository: ResourceRepository) { + checkPublicResources(repository) + checkAttributes(repository) + checkIdResources(repository) + } + + private fun checkPublicResources(repository: ResourceRepository) { + val resourceItems = repository.allResources + assertWithMessage("Too few resources: ${resourceItems.size}").that(resourceItems.size) + .isAtLeast(10000) + for (item in resourceItems) { + assertThat(item.namespace).isEqualTo(ResourceNamespace.ANDROID) + } + val expectations = mapOf( + ResourceType.STYLE to 700, + ResourceType.ATTR to 1200, + ResourceType.DRAWABLE to 600, + ResourceType.ID to 60, + ResourceType.LAYOUT to 20 + ) + for (type in ResourceType.values()) { + val publicItems = repository.getPublicResources(ResourceNamespace.ANDROID, type) + val minExpected = expectations[type] + if (minExpected != null) { + assertWithMessage("Too few public resources of type " + type.getName()).that(publicItems.size) + .isAtLeast(minExpected) + } + } + + // Not mentioned in public.xml. + assertVisibility(repository, ResourceType.STRING, "byteShort", PRIVATE) + // Defined at top level. + assertVisibility(repository, ResourceType.STYLE, "Widget.DeviceDefault.Button.Colored", PUBLIC) + // Defined inside a <public-group>. + assertVisibility(repository, ResourceType.ATTR, "packageType", PUBLIC) + // Due to the @hide comment + assertVisibility(repository, ResourceType.DRAWABLE, "ic_info", PRIVATE) + // Due to the naming convention + assertVisibility(repository, ResourceType.ATTR, "__removed2", PRIVATE) + } + + private fun assertVisibility( + repository: ResourceRepository, + type: ResourceType, + name: String, + visibility: ResourceVisibility + ) { + val resources = repository.getResources(ResourceNamespace.ANDROID, type, name) + assertThat(resources).isNotEmpty() + assertThat((resources[0] as ResourceItemWithVisibility).visibility).isEqualTo(visibility) + } + + private fun checkAttributes(repository: ResourceRepository) { + // `typeface` is declared first at top-level and later referenced from within `<declare-styleable>`. + // Make sure the later reference doesn't shadow the original definition. + var attrValue = getAttrValue(repository, "typeface") + assertThat(attrValue).isNotNull() + assertThat(attrValue.formats).containsExactly(ENUM) + assertThat(attrValue.description).isEqualTo("Default text typeface.") + assertThat(attrValue.groupName).isEqualTo("Other non-theme attributes") + var valueMap = attrValue.attributeValues + assertThat(valueMap.size).isEqualTo(4) + assertThat(valueMap).containsEntry("monospace", 3) + assertThat(attrValue.getValueDescription("monospace")).isNull() + + // `appCategory` is defined only in attr_manifest.xml. + attrValue = getAttrValue(repository, "appCategory") + assertThat(attrValue).isNotNull() + assertThat(attrValue.formats).containsExactly(ENUM) + assertThat(attrValue.description).startsWith("Declare the category of this app") + assertThat(attrValue.groupName).isNull() + valueMap = attrValue.attributeValues + assertThat(valueMap.size).isAtLeast(7) + assertThat(valueMap).containsEntry("maps", 6) + assertThat(attrValue.getValueDescription("maps")).contains("navigation") + } + + private fun getAttrValue( + repository: ResourceRepository, + attrName: String + ): AttrResourceValue { + val attrItem = repository.getResources(ResourceNamespace.ANDROID, ATTR, attrName)[0] + return attrItem.resourceValue as AttrResourceValue + } + + private fun checkIdResources(repository: ResourceRepository) { + var items = repository.getResources(ResourceNamespace.ANDROID, ID, "mode_normal") + items = items.filter { it.configuration.isDefault } + assertThat(items).hasSize(1) + + // Check that ID resources defined using @+id syntax in layout XML files are present in the repository. + // The following ID resource is defined by android:id="@+id/radio_power" in layout/power_dialog.xml. + items = repository.getResources(ResourceNamespace.ANDROID, ID, "radio_power") + assertThat(items).hasSize(1) + } +} diff --git a/paparazzi/src/test/java/app/cash/paparazzi/internal/resources/ModuleResourceRepositoryTest.kt b/paparazzi/src/test/java/app/cash/paparazzi/internal/resources/ModuleResourceRepositoryTest.kt new file mode 100644 index 0000000000..ff908efcab --- /dev/null +++ b/paparazzi/src/test/java/app/cash/paparazzi/internal/resources/ModuleResourceRepositoryTest.kt @@ -0,0 +1,27 @@ +package app.cash.paparazzi.internal.resources + +import com.android.ide.common.rendering.api.ResourceNamespace +import com.android.resources.ResourceType +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class ModuleResourceRepositoryTest { + @Test + fun test() { + val repository = ModuleResourceRepository.forMainResources( + namespace = ResourceNamespace.RES_AUTO, + resourceDirectories = listOf(resolveProjectPath("src/test/resources/folders/res").toFile()) + ) + + val map = repository.allResources + assertThat(map.size).isEqualTo(32) + + assertThat(map[0].name).isEqualTo("slide_in_from_left") + assertThat(map[0].type).isEqualTo(ResourceType.ANIM) + assertThat(map[1].name).isEqualTo("test_animator") + assertThat(map[1].type).isEqualTo(ResourceType.ANIMATOR) + + assertThat(map[31].name).isEqualTo("test_network_security_config") + assertThat(map[31].type).isEqualTo(ResourceType.XML) + } +} diff --git a/paparazzi/src/test/java/app/cash/paparazzi/internal/resources/NamespaceResolverTest.kt b/paparazzi/src/test/java/app/cash/paparazzi/internal/resources/NamespaceResolverTest.kt new file mode 100644 index 0000000000..c40b48feec --- /dev/null +++ b/paparazzi/src/test/java/app/cash/paparazzi/internal/resources/NamespaceResolverTest.kt @@ -0,0 +1,79 @@ +package app.cash.paparazzi.internal.resources + +import com.android.SdkConstants.ANDROID_URI +import com.android.SdkConstants.TOOLS_URI +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.kxml2.io.KXmlParser +import org.xmlpull.v1.XmlPullParser +import java.io.StringReader + +class NamespaceResolverTest { + @Test + fun test() { + StringReader(SIMPLE_LAYOUT).use { + val parser = KXmlParser().apply { + setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true) + setInput(it) + } + + var resolver = NamespaceResolver(parser) + assertThat(resolver.namespaceCount).isEqualTo(0) + assertThat(resolver.prefixToUri("newtools")).isNull() + assertThat(resolver.prefixToUri("framework")).isNull() + assertThat(resolver.uriToPrefix(TOOLS_URI)).isNull() + assertThat(resolver.uriToPrefix(ANDROID_URI)).isNull() + + advance(parser) + + resolver = NamespaceResolver(parser) + assertThat(resolver.namespaceCount).isEqualTo(1) + assertThat(resolver.prefixToUri("framework")).isEqualTo(ANDROID_URI) + assertThat(resolver.prefixToUri("newtools")).isNull() + assertThat(resolver.uriToPrefix(ANDROID_URI)).isEqualTo("framework") + assertThat(resolver.uriToPrefix(TOOLS_URI)).isNull() + + advance(parser) + + resolver = NamespaceResolver(parser) + assertThat(resolver.namespaceCount).isEqualTo(2) + assertThat(resolver.prefixToUri("framework")).isEqualTo(ANDROID_URI) + assertThat(resolver.prefixToUri("newtools")).isEqualTo(TOOLS_URI) + assertThat(resolver.uriToPrefix(ANDROID_URI)).isEqualTo("framework") + assertThat(resolver.uriToPrefix(TOOLS_URI)).isEqualTo("newtools") + + advance(parser) + + resolver = NamespaceResolver(parser) + assertThat(resolver.namespaceCount).isEqualTo(0) + assertThat(resolver.prefixToUri("newtools")).isNull() + assertThat(resolver.prefixToUri("framework")).isNull() + assertThat(resolver.uriToPrefix(TOOLS_URI)).isNull() + assertThat(resolver.uriToPrefix(ANDROID_URI)).isNull() + } + } + + private fun advance(parser: KXmlParser) { + var event: Int + do { + event = parser.nextToken() + } while (event != XmlPullParser.END_DOCUMENT && event != XmlPullParser.START_TAG) + } + + companion object { + val SIMPLE_LAYOUT = """ + |<?xml version="1.0" encoding="utf-8"?> + |<LinearLayout xmlns:framework="http://schemas.android.com/apk/res/android" + | framework:orientation="vertical" + | framework:layout_width="fill_parent" + | framework:layout_height="fill_parent"> + | + | <TextView xmlns:newtools="http://schemas.android.com/tools" + | framework:layout_width="fill_parent" + | framework:layout_height="wrap_content" + | newtools:text="Hello World, MyActivity" /> + | + |</LinearLayout> + """.trimMargin() + } +} diff --git a/paparazzi/src/test/java/app/cash/paparazzi/internal/resources/ProjectResourceRepositoryTest.kt b/paparazzi/src/test/java/app/cash/paparazzi/internal/resources/ProjectResourceRepositoryTest.kt new file mode 100644 index 0000000000..f190e2709c --- /dev/null +++ b/paparazzi/src/test/java/app/cash/paparazzi/internal/resources/ProjectResourceRepositoryTest.kt @@ -0,0 +1,27 @@ +package app.cash.paparazzi.internal.resources + +import com.android.resources.ResourceType +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class ProjectResourceRepositoryTest { + @Test + fun test() { + // TODO: need mapOf(package to listOf(resourceDirectory)) for each transitive project module + val repository = ProjectResourceRepository.create( + resourceDirectories = listOf(resolveProjectPath("src/test/resources/folders/res").toFile()), + moduleResourceDirectories = emptyList() + ) + + val map = repository.allResources + assertThat(map.size).isEqualTo(32) + + assertThat(map[0].name).isEqualTo("slide_in_from_left") + assertThat(map[0].type).isEqualTo(ResourceType.ANIM) + assertThat(map[1].name).isEqualTo("test_animator") + assertThat(map[1].type).isEqualTo(ResourceType.ANIMATOR) + + assertThat(map[31].name).isEqualTo("test_network_security_config") + assertThat(map[31].type).isEqualTo(ResourceType.XML) + } +} diff --git a/paparazzi/src/test/java/app/cash/paparazzi/internal/resources/ResourceFolderRepositoryTest.kt b/paparazzi/src/test/java/app/cash/paparazzi/internal/resources/ResourceFolderRepositoryTest.kt new file mode 100644 index 0000000000..c5c33afed8 --- /dev/null +++ b/paparazzi/src/test/java/app/cash/paparazzi/internal/resources/ResourceFolderRepositoryTest.kt @@ -0,0 +1,168 @@ +package app.cash.paparazzi.internal.resources + +import com.android.ide.common.rendering.api.ArrayResourceValue +import com.android.ide.common.rendering.api.AttrResourceValue +import com.android.ide.common.rendering.api.AttributeFormat +import com.android.ide.common.rendering.api.PluralsResourceValue +import com.android.ide.common.rendering.api.ResourceNamespace +import com.android.ide.common.rendering.api.StyleResourceValue +import com.android.ide.common.rendering.api.StyleableResourceValue +import com.android.resources.ResourceType +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class ResourceFolderRepositoryTest { + private val resFolderRoot = resolveProjectPath("src/test/resources/folders/res") + + @Test + fun test() { + val repository = ResourceFolderRepository( + resFolderRoot.toFile(), + ResourceNamespace.TODO() + ) + + val map = repository.allResources + assertThat(map.size).isEqualTo(32) + + // ANIM + assertThat(map[0].name).isEqualTo("slide_in_from_left") + assertThat(map[0].type).isEqualTo(ResourceType.ANIM) + + // ANIMATOR + assertThat(map[1].name).isEqualTo("test_animator") + assertThat(map[1].type).isEqualTo(ResourceType.ANIMATOR) + + // ARRAY + assertThat(map[2].name).isEqualTo("string_array_name") + assertThat(map[2].type).isEqualTo(ResourceType.ARRAY) + with(map[2].resourceValue as ArrayResourceValue) { + assertThat(elementCount).isEqualTo(2) + assertThat(getElement(0)).isEqualTo("First Test String") + assertThat(getElement(1)).isEqualTo("Second Test String") + } + + // ATTR + assertThat(map[3].name).isEqualTo("TestAttr") + assertThat(map[3].type).isEqualTo(ResourceType.ATTR) + assertThat((map[3].resourceValue as AttrResourceValue).formats).isEqualTo(setOf(AttributeFormat.FLOAT)) + assertThat(map[4].name).isEqualTo("TestAttrInt") + assertThat(map[4].type).isEqualTo(ResourceType.ATTR) + assertThat((map[4].resourceValue as AttrResourceValue).formats).isEqualTo(setOf(AttributeFormat.INTEGER)) + + // BOOL + assertThat(map[5].name).isEqualTo("screen_small") + assertThat(map[5].type).isEqualTo(ResourceType.BOOL) + assertThat(map[5].resourceValue.value).isEqualTo(true.toString()) + assertThat(map[6].name).isEqualTo("adjust_view_bounds") + assertThat(map[6].type).isEqualTo(ResourceType.BOOL) + assertThat(map[6].resourceValue.value).isEqualTo(false.toString()) + + // COLOR + assertThat(map[7].name).isEqualTo("test_color") + assertThat(map[7].type).isEqualTo(ResourceType.COLOR) + assertThat(map[7].resourceValue.value).isEqualTo("#ffffffff") + assertThat(map[8].name).isEqualTo("test_color_2") + assertThat(map[8].type).isEqualTo(ResourceType.COLOR) + assertThat(map[8].resourceValue.value).isEqualTo("#00000000") + assertThat(map[9].name).isEqualTo("color_selector") + assertThat(map[9].type).isEqualTo(ResourceType.COLOR) + + // DIMEN + assertThat(map[10].name).isEqualTo("textview_height") + assertThat(map[10].type).isEqualTo(ResourceType.DIMEN) + assertThat(map[10].resourceValue.value).isEqualTo("25dp") + assertThat(map[11].name).isEqualTo("textview_width") + assertThat(map[11].type).isEqualTo(ResourceType.DIMEN) + assertThat(map[11].resourceValue.value).isEqualTo("150dp") + + // DRAWABLE + assertThat(map[12].name).isEqualTo("ic_android_black_24dp") + assertThat(map[12].type).isEqualTo(ResourceType.DRAWABLE) + + // FONT + assertThat(map[13].name).isEqualTo("aclonica") + assertThat(map[13].type).isEqualTo(ResourceType.FONT) + + // ID + assertThat(map[14].name).isEqualTo("test_layout") + assertThat(map[14].type).isEqualTo(ResourceType.ID) + assertThat(map[15].name).isEqualTo("test_view") + assertThat(map[15].type).isEqualTo(ResourceType.ID) + assertThat(map[16].name).isEqualTo("test_menu_1") + assertThat(map[16].type).isEqualTo(ResourceType.ID) + assertThat(map[17].name).isEqualTo("test_menu_2") + assertThat(map[17].type).isEqualTo(ResourceType.ID) + assertThat(map[18].name).isEqualTo("button_ok") + assertThat(map[18].type).isEqualTo(ResourceType.ID) + assertThat(map[19].name).isEqualTo("dialog_exit") + assertThat(map[19].type).isEqualTo(ResourceType.ID) + + // INTEGER + assertThat(map[20].name).isEqualTo("max_speed") + assertThat(map[20].type).isEqualTo(ResourceType.INTEGER) + assertThat(map[20].resourceValue.value).isEqualTo("75") + assertThat(map[21].name).isEqualTo("min_speed") + assertThat(map[21].type).isEqualTo(ResourceType.INTEGER) + assertThat(map[21].resourceValue.value).isEqualTo("5") + + // LAYOUT + assertThat(map[22].name).isEqualTo("test") + assertThat(map[22].type).isEqualTo(ResourceType.LAYOUT) + + // MENU + assertThat(map[23].name).isEqualTo("test_menu") + assertThat(map[23].type).isEqualTo(ResourceType.MENU) + + // MIPMAP + assertThat(map[24].name).isEqualTo("ic_launcher") + assertThat(map[24].type).isEqualTo(ResourceType.MIPMAP) + + // PLURALS + assertThat(map[25].name).isEqualTo("plural_name") + assertThat(map[25].type).isEqualTo(ResourceType.PLURALS) + with(map[25].resourceValue as PluralsResourceValue) { + assertThat(pluralsCount).isEqualTo(2) + assertThat(getQuantity(0)).isEqualTo("zero") + assertThat(getValue(0)).isEqualTo("Nothing") + assertThat(getQuantity(1)).isEqualTo("one") + assertThat(getValue(1)).isEqualTo("One String") + } + + // RAW + assertThat(map[26].name).isEqualTo("test_json") + assertThat(map[26].type).isEqualTo(ResourceType.RAW) + + // STRINGS + assertThat(map[27].name).isEqualTo("string_name") + assertThat(map[27].type).isEqualTo(ResourceType.STRING) + assertThat(map[27].resourceValue.value).isEqualTo("Test String") + assertThat(map[28].name).isEqualTo("string_name_xliff") + assertThat(map[28].type).isEqualTo(ResourceType.STRING) + assertThat(map[28].resourceValue.value).isEqualTo("Test String {0} with suffix") + + // STYLE XML + assertThat(map[29].name).isEqualTo("TestStyle") + assertThat(map[29].type).isEqualTo(ResourceType.STYLE) + with(map[29].resourceValue as StyleResourceValue) { + assertThat(definedItems.size).isEqualTo(2) + assertThat(definedItems.elementAt(0).attrName).isEqualTo("android:scrollbars") + assertThat(definedItems.elementAt(0).value).isEqualTo("horizontal") + assertThat(definedItems.elementAt(1).attrName).isEqualTo("android:marginTop") + assertThat(definedItems.elementAt(1).value).isEqualTo("16dp") + } + + // STYLEABLE + assertThat(map[30].name).isEqualTo("test_styleable") + assertThat(map[30].type).isEqualTo(ResourceType.STYLEABLE) + with(map[30].resourceValue as StyleableResourceValue) { + assertThat(allAttributes.size).isEqualTo(3) + assertThat(allAttributes[0].name).isEqualTo("TestAttr") + assertThat(allAttributes[1].name).isEqualTo("TestAttrInt") + assertThat(allAttributes[2].name).isEqualTo("TestAttrReference") + } + + // XML + assertThat(map[31].name).isEqualTo("test_network_security_config") + assertThat(map[31].type).isEqualTo(ResourceType.XML) + } +} diff --git a/paparazzi/src/test/java/app/cash/paparazzi/internal/resources/ResourceUrlParserTest.kt b/paparazzi/src/test/java/app/cash/paparazzi/internal/resources/ResourceUrlParserTest.kt new file mode 100644 index 0000000000..9cf3ff3ede --- /dev/null +++ b/paparazzi/src/test/java/app/cash/paparazzi/internal/resources/ResourceUrlParserTest.kt @@ -0,0 +1,141 @@ +package app.cash.paparazzi.internal.resources + +import com.android.resources.ResourceType +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class ResourceUrlParserTest { + @Test + fun test() { + val parser = ResourceUrlParser() + + parser.assertResourceUrl("@id/foo") + .hasAttributes( + namespace = null, + resourceType = ResourceType.ID, + resourceName = "foo", + qualifiedResourceName = "foo" + ) + + parser.assertResourceUrl("@+id/foo") + .hasAttributes( + namespace = null, + resourceType = ResourceType.ID, + resourceName = "foo", + qualifiedResourceName = "foo" + ) + + parser.assertResourceUrl("@layout/foo") + .hasAttributes( + namespace = null, + resourceType = ResourceType.LAYOUT, + resourceName = "foo", + qualifiedResourceName = "foo" + ) + + parser.assertResourceUrl("@dimen/foo") + .hasAttributes( + namespace = null, + resourceType = ResourceType.DIMEN, + resourceName = "foo", + qualifiedResourceName = "foo" + ) + + parser.assertResourceUrl("@android:dimen/foo") + .hasAttributes( + namespace = "android", + resourceType = ResourceType.DIMEN, + resourceName = "foo", + qualifiedResourceName = "android:foo" + ) + + parser.assertResourceUrl("?attr/foo") + .hasAttributes( + namespace = null, + resourceType = ResourceType.ATTR, + resourceName = "foo", + qualifiedResourceName = "foo" + ) + + parser.assertResourceUrl("?foo") + .hasAttributes( + namespace = null, + resourceType = null, + resourceName = "foo", + qualifiedResourceName = "foo" + ) + + parser.assertResourceUrl("?android:foo") + .hasAttributes( + namespace = "android", + resourceType = null, + resourceName = "foo", + qualifiedResourceName = "android:foo" + ) + + parser.assertResourceUrl("?androidx:foo") + .hasAttributes( + namespace = "androidx", + resourceType = null, + resourceName = "foo", + qualifiedResourceName = "androidx:foo" + ) + + parser.assertResourceUrl("@my_package:layout/my_name") + .hasAttributes( + namespace = "my_package", + resourceType = ResourceType.LAYOUT, + resourceName = "my_name", + qualifiedResourceName = "my_package:my_name" + ) + + parser.assertResourceUrl("@*my_package:layout/my_name") + .hasAttributes( + namespace = "my_package", + resourceType = ResourceType.LAYOUT, + resourceName = "my_name", + qualifiedResourceName = "my_package:my_name" + ) + + parser.assertResourceUrl("@aapt:_aapt/34") + .hasAttributes( + namespace = "aapt", + resourceType = ResourceType.AAPT, + resourceName = "34", + qualifiedResourceName = "aapt:34" + ) + + parser.assertResourceUrl("@android:style/invalid:reference") + .hasAttributes( + namespace = "android", + resourceType = ResourceType.STYLE, + resourceName = "invalid:reference", + qualifiedResourceName = "android:invalid:reference" + ) + } + + private fun ResourceUrlParser.assertResourceUrl(url: String): ResourceUrlParser { + parseResourceUrl(url) + return this + } + + private fun ResourceUrlParser.hasAttributes( + namespace: String?, + resourceType: ResourceType?, + resourceName: String, + qualifiedResourceName: String + ) { + if (namespace != null) { + assertThat(namespacePrefix).isEqualTo(namespace) + } else { + assertThat(namespacePrefix).isNull() + } + if (resourceType != null) { + assertThat(type).isEqualTo(resourceType.getName()) + } else { + assertThat(type).isNull() + } + assertThat(name).isEqualTo(resourceName) + assertThat(qualifiedName).isEqualTo(qualifiedResourceName) + } +} diff --git a/paparazzi/src/test/java/app/cash/paparazzi/internal/resources/ValueResourceXmlParserTest.kt b/paparazzi/src/test/java/app/cash/paparazzi/internal/resources/ValueResourceXmlParserTest.kt new file mode 100644 index 0000000000..204b0dcf5f --- /dev/null +++ b/paparazzi/src/test/java/app/cash/paparazzi/internal/resources/ValueResourceXmlParserTest.kt @@ -0,0 +1,95 @@ +package app.cash.paparazzi.internal.resources + +import com.android.ide.common.rendering.api.ResourceNamespace +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import java.io.StringReader + +class ValueResourceXmlParserTest { + @Test + fun test() { + StringReader(SIMPLE_LAYOUT).use { + val parser = ValueResourceXmlParser().apply { + setInput(it) + } + + parser.nextToken() + + parser.assertNamespaceResolverCheckFails() + assertThat(parser.namespaceResolverCache).isEmpty() + assertThat(parser.resolverStack).isEmpty() + + parser.nextToken() + + var namespaceResolver = parser.namespaceResolver + assertThat(namespaceResolver).isNotEqualTo(ResourceNamespace.Resolver.EMPTY_RESOLVER) + assertThat((namespaceResolver as NamespaceResolver).namespaceCount).isEqualTo(1) + assertThat(parser.namespaceResolverCache).hasSize(1) + assertThat(parser.resolverStack).hasSize(1) + + parser.nextToken() + + parser.assertNamespaceResolverCheckFails() + assertThat(parser.namespaceResolverCache).hasSize(1) + assertThat(parser.resolverStack).hasSize(1) + + parser.nextToken() + + namespaceResolver = parser.namespaceResolver + assertThat(namespaceResolver).isNotEqualTo(ResourceNamespace.Resolver.EMPTY_RESOLVER) + assertThat((namespaceResolver as NamespaceResolver).namespaceCount).isEqualTo(2) + assertThat(parser.namespaceResolverCache).hasSize(2) + assertThat(parser.resolverStack).hasSize(2) + + parser.nextToken() + + parser.assertNamespaceResolverCheckFails() + assertThat(parser.namespaceResolverCache).hasSize(2) + assertThat(parser.resolverStack).hasSize(1) + + parser.nextToken() + + parser.assertNamespaceResolverCheckFails() + assertThat(parser.namespaceResolverCache).hasSize(2) + assertThat(parser.resolverStack).hasSize(1) + + parser.nextToken() + + parser.assertNamespaceResolverCheckFails() + assertThat(parser.namespaceResolverCache).hasSize(2) + assertThat(parser.resolverStack).hasSize(0) + + parser.nextToken() + + parser.assertNamespaceResolverCheckFails() + assertThat(parser.namespaceResolverCache).hasSize(2) + assertThat(parser.resolverStack).hasSize(0) + } + } + + private fun ValueResourceXmlParser.assertNamespaceResolverCheckFails() { + try { + namespaceResolver + } catch (e: Exception) { + assertThat(e).isInstanceOf(IllegalStateException::class.java) + assertThat(e).hasMessageThat().isEqualTo("Check failed.") + } + } + + companion object { + val SIMPLE_LAYOUT = """ + |<?xml version="1.0" encoding="utf-8"?> + |<LinearLayout xmlns:framework="http://schemas.android.com/apk/res/android" + | framework:orientation="vertical" + | framework:layout_width="fill_parent" + | framework:layout_height="fill_parent"> + | + | <TextView xmlns:newtools="http://schemas.android.com/tools" + | framework:layout_width="fill_parent" + | framework:layout_height="wrap_content" + | newtools:text="Hello World, MyActivity" /> + | + |</LinearLayout> + """.trimMargin() + } +} diff --git a/paparazzi/src/test/resources/aar/my_aar_lib/AndroidManifest.xml b/paparazzi/src/test/resources/aar/my_aar_lib/AndroidManifest.xml new file mode 100644 index 0000000000..7cb7ee75cc --- /dev/null +++ b/paparazzi/src/test/resources/aar/my_aar_lib/AndroidManifest.xml @@ -0,0 +1,19 @@ +<!-- + ~ Copyright (C) 2018 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. + --> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.test.testlibrary"> + <uses-sdk android:minSdkVersion="14" /> +</manifest> diff --git a/paparazzi/src/test/resources/aar/my_aar_lib/R.txt b/paparazzi/src/test/resources/aar/my_aar_lib/R.txt new file mode 100644 index 0000000000..cab6edd9ab --- /dev/null +++ b/paparazzi/src/test/resources/aar/my_aar_lib/R.txt @@ -0,0 +1,16 @@ +int anim anim1 0x7f040000 +int attr attr1 0x7f010000 +int id id1 0x7f0b0000 +int id id2 0x7f0b0001 +int id id3 0x7f0b0002 +int style style1 0x7f070000 +int[] styleable Styleable1 { 0x7f010000 } +int styleable Styleable1_some_attr 0 +int[] styleable Styleable_with_dots { 0x7f010000 } +int styleable Styleable_with_dots_some_attr 0 +int[] styleable Styleable_with_underscore { 0x7f010000, 0x01010002, 0x01010030, 0x7f010068, 0x7f010069 } +int styleable Styleable_with_underscore_app_attr2 3 +int styleable Styleable_with_underscore_app_attr1 0 +int styleable Styleable_with_underscore_android_colorForeground 2 +int styleable Styleable_with_underscore_app_attr3 4 +int styleable Styleable_with_underscore_android_icon 1 diff --git a/paparazzi/src/test/resources/aar/my_aar_lib/res/layout/foo.xml b/paparazzi/src/test/resources/aar/my_aar_lib/res/layout/foo.xml new file mode 100644 index 0000000000..9eac494c72 --- /dev/null +++ b/paparazzi/src/test/resources/aar/my_aar_lib/res/layout/foo.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- + ~ Copyright (C) 2018 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. + --> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/id_from_layout" + android:layout_width="fill_parent" + android:layout_alignParentTop="true" + android:paddingLeft="5dip" + android:paddingRight="5dip"> +</RelativeLayout> diff --git a/paparazzi/src/test/resources/aar/my_aar_lib/res/values-es/strings.xml b/paparazzi/src/test/resources/aar/my_aar_lib/res/values-es/strings.xml new file mode 100644 index 0000000000..3c933333db --- /dev/null +++ b/paparazzi/src/test/resources/aar/my_aar_lib/res/values-es/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="hello">hola</string> +</resources> diff --git a/paparazzi/src/test/resources/aar/my_aar_lib/res/values-fr/strings.xml b/paparazzi/src/test/resources/aar/my_aar_lib/res/values-fr/strings.xml new file mode 100644 index 0000000000..5e805ffe5e --- /dev/null +++ b/paparazzi/src/test/resources/aar/my_aar_lib/res/values-fr/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="hello">bonjour</string> +</resources> diff --git a/paparazzi/src/test/resources/aar/my_aar_lib/res/values/foo.xml b/paparazzi/src/test/resources/aar/my_aar_lib/res/values/foo.xml new file mode 100644 index 0000000000..e44d3f1254 --- /dev/null +++ b/paparazzi/src/test/resources/aar/my_aar_lib/res/values/foo.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2015 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> + <style name="MyTheme.Dark" parent="android:Theme.Light"> + <item name="android:textColor">#999999</item> + <item name="foo">?android:colorForeground</item> + </style> + <attr name="app_attr2" /> + <declare-styleable name="Styleable1"> + <attr name="some_attr" format="color"/> + </declare-styleable> + <declare-styleable name="Styleable.with.dots"> + <attr name="some_attr"/> + </declare-styleable> + <declare-styleable name="Styleable_with_underscore"> + <attr name="app_attr1" /> + <attr name="app_attr2" /> + <attr name="app_attr3" /> + <attr name="android:colorForeground" /> + <attr name="android:icon" /> + </declare-styleable> +</resources> diff --git a/paparazzi/src/test/resources/aar/my_aar_lib/res/values/strings.xml b/paparazzi/src/test/resources/aar/my_aar_lib/res/values/strings.xml new file mode 100644 index 0000000000..63df57e97c --- /dev/null +++ b/paparazzi/src/test/resources/aar/my_aar_lib/res/values/strings.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="hello">hello</string> +</resources> diff --git a/paparazzi/src/test/resources/aar/my_aar_lib_brokenRDotTxt/AndroidManifest.xml b/paparazzi/src/test/resources/aar/my_aar_lib_brokenRDotTxt/AndroidManifest.xml new file mode 100644 index 0000000000..7cb7ee75cc --- /dev/null +++ b/paparazzi/src/test/resources/aar/my_aar_lib_brokenRDotTxt/AndroidManifest.xml @@ -0,0 +1,19 @@ +<!-- + ~ Copyright (C) 2018 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. + --> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.test.testlibrary"> + <uses-sdk android:minSdkVersion="14" /> +</manifest> diff --git a/paparazzi/src/test/resources/aar/my_aar_lib_brokenRDotTxt/R.txt b/paparazzi/src/test/resources/aar/my_aar_lib_brokenRDotTxt/R.txt new file mode 100644 index 0000000000..b5f6745f55 --- /dev/null +++ b/paparazzi/src/test/resources/aar/my_aar_lib_brokenRDotTxt/R.txt @@ -0,0 +1 @@ +Something completely broken and unparseable. diff --git a/paparazzi/src/test/resources/aar/my_aar_lib_brokenRDotTxt/res/layout/foo.xml b/paparazzi/src/test/resources/aar/my_aar_lib_brokenRDotTxt/res/layout/foo.xml new file mode 100644 index 0000000000..9eac494c72 --- /dev/null +++ b/paparazzi/src/test/resources/aar/my_aar_lib_brokenRDotTxt/res/layout/foo.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- + ~ Copyright (C) 2018 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. + --> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/id_from_layout" + android:layout_width="fill_parent" + android:layout_alignParentTop="true" + android:paddingLeft="5dip" + android:paddingRight="5dip"> +</RelativeLayout> diff --git a/paparazzi/src/test/resources/aar/my_aar_lib_noRDotTxt/AndroidManifest.xml b/paparazzi/src/test/resources/aar/my_aar_lib_noRDotTxt/AndroidManifest.xml new file mode 100644 index 0000000000..7cb7ee75cc --- /dev/null +++ b/paparazzi/src/test/resources/aar/my_aar_lib_noRDotTxt/AndroidManifest.xml @@ -0,0 +1,19 @@ +<!-- + ~ Copyright (C) 2018 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. + --> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.test.testlibrary"> + <uses-sdk android:minSdkVersion="14" /> +</manifest> diff --git a/paparazzi/src/test/resources/aar/my_aar_lib_noRDotTxt/res/layout/layout_for_id_scan.xml b/paparazzi/src/test/resources/aar/my_aar_lib_noRDotTxt/res/layout/layout_for_id_scan.xml new file mode 100644 index 0000000000..1704b6713d --- /dev/null +++ b/paparazzi/src/test/resources/aar/my_aar_lib_noRDotTxt/res/layout/layout_for_id_scan.xml @@ -0,0 +1,116 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:orientation="vertical" + android:layout_width="fill_parent" + android:layout_height="fill_parent"> + + <!-- View with an empty id. --> + <ImageView android:id="@+id/" /> + + <RelativeLayout style="@style/TitleBar" android:id="@+id/header"> + <ImageView style="@style/TitleBarLogo" + android:id="@+id/image" + android:contentDescription="@string/description_logo" + android:layout_above="@+id/styledView" + android:layout_alignParentLeft="true" + android:src="@drawable/title_logo" /> + + <View style="@style/TitleBarSpring" + android:layout_alignParentLeft="true" + android:id="@id/styledView"/> + + <ImageView style="@style/TitleBarSeparator" + android:id="@+id/imageView" + android:layout_below="@id/styledView" + android:layout_above="@+id/btn_title_refresh" + android:layout_alignParentLeft="true"/> + + <ImageButton style="@style/TitleBarAction" + android:id="@+id/btn_title_refresh" + android:contentDescription="@string/description_refresh" + android:src="@drawable/ic_title_refresh" + android:layout_width="wrap_content" + android:layout_height="42dp" + android:align_parentLeft="@+id/nonExistent" + android:onClick="onRefreshClick" + android:layout_below="@id/imageView"/> + <ProgressBar style="@style/TitleBarProgressIndicator" + android:id="@+id/title_refresh_progress" + android:layout_width="wrap_content" + android:visibility="visible" + android:layout_below="@+id/btn_title_refresh"/> + + <ImageView style="@style/TitleBarSeparator" + android:id="@+id/imageView2" + android:layout_below="@id/title_refresh_progress" + android:layout_above="@+id/imageButton" /> + + <ImageButton style="@style/TitleBarAction" + android:contentDescription="@string/description_search" + android:src="@drawable/ic_title_search" + android:id="@id/imageButton" + android:layout_width="wrap_content" + android:layout_height="42dp" + android:onClick="onSearchClick" /> + </RelativeLayout> + + <LinearLayout + android:id="@+id/noteArea" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:layout_margin="5dip"> + <EditText + android:id="@android:id/text1" + android:layout_height="fill_parent" + android:hint="@string/note_hint" + android:freezesText="true" + android:gravity="top" android:layout_width="wrap_content" android:layout_weight="1"> + </EditText> + <EditText + android:id="@+id/text2" + android:layout_height="fill_parent" + android:freezesText="true" + android:gravity="top" android:layout_width="wrap_content" android:layout_weight="1"> + <requestFocus /> + </EditText> + </LinearLayout> + + <LinearLayout + android:orientation="horizontal" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + style="@android:style/ButtonBar"> + <Button + android:layout_width="0dip" + android:layout_height="wrap_content" + android:layout_weight="1" + android:onClick="onSaveClick" + android:text="@string/note_save" /> + <Button + android:layout_width="0dip" + android:layout_height="wrap_content" + android:layout_weight="1" + android:onClick="onDiscardClick" + android:text="@string/note_discard" /> + </LinearLayout> + + <android.support.constraint.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="match_parent"> + <FrameLayout + android:layout_width="match_parent" + android:layout_height="0dp" + app:layout_constraintTop_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@+id/bug123032845" /> + <LinearLayout + android:id="@id/bug123032845" + android:layout_width="match_parent" + android:layout_height="64dp" + android:background="@color/colorAccent" + android:orientation="horizontal" + app:layout_constraintBottom_toBottomOf="parent" /> + </android.support.constraint.ConstraintLayout> + +</LinearLayout> diff --git a/paparazzi/src/test/resources/aar/my_aar_lib_wrongRDotTxt/AndroidManifest.xml b/paparazzi/src/test/resources/aar/my_aar_lib_wrongRDotTxt/AndroidManifest.xml new file mode 100644 index 0000000000..7cb7ee75cc --- /dev/null +++ b/paparazzi/src/test/resources/aar/my_aar_lib_wrongRDotTxt/AndroidManifest.xml @@ -0,0 +1,19 @@ +<!-- + ~ Copyright (C) 2018 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. + --> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.test.testlibrary"> + <uses-sdk android:minSdkVersion="14" /> +</manifest> diff --git a/paparazzi/src/test/resources/aar/my_aar_lib_wrongRDotTxt/R.txt b/paparazzi/src/test/resources/aar/my_aar_lib_wrongRDotTxt/R.txt new file mode 100644 index 0000000000..a91115a27d --- /dev/null +++ b/paparazzi/src/test/resources/aar/my_aar_lib_wrongRDotTxt/R.txt @@ -0,0 +1,9 @@ +int[] styleable Styleable_with_underscore { 0x7f010068, 0x7f010069 } +int styleable Styleable_with_underscore_app_attr2 3 +int styleable Styleable_with_underscore_app_attr1 0 +int styleable Styleable_with_underscore_android_colorForeground 2 +int styleable Styleable_with_underscore_app_attr3 4 +int styleable Styleable_with_underscore_android_icon 1 +int id id1 0x7f0b0000 +int id id2 0x7f0b0001 +int id id3 0x7f0b0002 diff --git a/paparazzi/src/test/resources/aar/my_aar_lib_wrongRDotTxt/res/layout/foo.xml b/paparazzi/src/test/resources/aar/my_aar_lib_wrongRDotTxt/res/layout/foo.xml new file mode 100644 index 0000000000..9eac494c72 --- /dev/null +++ b/paparazzi/src/test/resources/aar/my_aar_lib_wrongRDotTxt/res/layout/foo.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- + ~ Copyright (C) 2018 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. + --> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/id_from_layout" + android:layout_width="fill_parent" + android:layout_alignParentTop="true" + android:paddingLeft="5dip" + android:paddingRight="5dip"> +</RelativeLayout> diff --git a/paparazzi/src/test/resources/aar/unrecognizedTag/AndroidManifest.xml b/paparazzi/src/test/resources/aar/unrecognizedTag/AndroidManifest.xml new file mode 100644 index 0000000000..7cb7ee75cc --- /dev/null +++ b/paparazzi/src/test/resources/aar/unrecognizedTag/AndroidManifest.xml @@ -0,0 +1,19 @@ +<!-- + ~ Copyright (C) 2018 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. + --> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.test.testlibrary"> + <uses-sdk android:minSdkVersion="14" /> +</manifest> diff --git a/paparazzi/src/test/resources/aar/unrecognizedTag/res/values/colors.xml b/paparazzi/src/test/resources/aar/unrecognizedTag/res/values/colors.xml new file mode 100644 index 0000000000..d1d3f33561 --- /dev/null +++ b/paparazzi/src/test/resources/aar/unrecognizedTag/res/values/colors.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <color name="black">#FF000000</color> + <unrecognized> + <color name="red">#FFFF0000</color> + <more_unrecognized/> + </unrecognized> + <color name="white">#FFFFFFFF</color> +</resources> diff --git a/paparazzi/src/test/resources/accessibility-new-view.png b/paparazzi/src/test/resources/accessibility-new-view.png new file mode 100644 index 0000000000..86e77911ce Binary files /dev/null and b/paparazzi/src/test/resources/accessibility-new-view.png differ diff --git a/paparazzi/src/test/resources/accessibility.png b/paparazzi/src/test/resources/accessibility.png new file mode 100644 index 0000000000..fa89e4d4e7 Binary files /dev/null and b/paparazzi/src/test/resources/accessibility.png differ diff --git a/paparazzi/paparazzi/src/test/resources/card_chip.xml b/paparazzi/src/test/resources/card_chip.xml similarity index 100% rename from paparazzi/paparazzi/src/test/resources/card_chip.xml rename to paparazzi/src/test/resources/card_chip.xml diff --git a/paparazzi/src/test/resources/folders/res/anim/slide_in_from_left.xml b/paparazzi/src/test/resources/folders/res/anim/slide_in_from_left.xml new file mode 100644 index 0000000000..74b1cc990f --- /dev/null +++ b/paparazzi/src/test/resources/folders/res/anim/slide_in_from_left.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<translate + xmlns:android="http://schemas.android.com/apk/res/android" + android:duration="300" + android:fromXDelta="-100%p" + android:toXDelta="0" + /> diff --git a/paparazzi/src/test/resources/folders/res/animator/test_animator.xml b/paparazzi/src/test/resources/folders/res/animator/test_animator.xml new file mode 100644 index 0000000000..83a1b1dabf --- /dev/null +++ b/paparazzi/src/test/resources/folders/res/animator/test_animator.xml @@ -0,0 +1,7 @@ +<set xmlns:android="http://schemas.android.com/apk/res/android"> + <objectAnimator + android:valueFrom="1.0" + android:valueTo="0.0" + android:propertyName="alpha" + android:duration="0"/> +</set> diff --git a/paparazzi/src/test/resources/folders/res/color/color_selector.xml b/paparazzi/src/test/resources/folders/res/color/color_selector.xml new file mode 100644 index 0000000000..50fd8aa051 --- /dev/null +++ b/paparazzi/src/test/resources/folders/res/color/color_selector.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android" > + <item + android:color="#FFFFFFFF"/> +</selector> diff --git a/paparazzi/src/test/resources/folders/res/drawable/ic_android_black_24dp.xml b/paparazzi/src/test/resources/folders/res/drawable/ic_android_black_24dp.xml new file mode 100644 index 0000000000..fe51230740 --- /dev/null +++ b/paparazzi/src/test/resources/folders/res/drawable/ic_android_black_24dp.xml @@ -0,0 +1,5 @@ +<vector android:height="24dp" android:tint="#000000" + android:viewportHeight="24" android:viewportWidth="24" + android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="#FF000000" android:pathData="M17.6,11.48 L19.44,8.3a0.63,0.63 0,0 0,-1.09 -0.63l-1.88,3.24a11.43,11.43 0,0 0,-8.94 0L5.65,7.67a0.63,0.63 0,0 0,-1.09 0.63L6.4,11.48A10.81,10.81 0,0 0,1 20L23,20A10.81,10.81 0,0 0,17.6 11.48ZM7,17.25A1.25,1.25 0,1 1,8.25 16,1.25 1.25,0 0,1 7,17.25ZM17,17.25A1.25,1.25 0,1 1,18.25 16,1.25 1.25,0 0,1 17,17.25Z"/> +</vector> diff --git a/paparazzi/src/test/resources/folders/res/font/aclonica.xml b/paparazzi/src/test/resources/folders/res/font/aclonica.xml new file mode 100644 index 0000000000..a88a9687e2 --- /dev/null +++ b/paparazzi/src/test/resources/folders/res/font/aclonica.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<font-family xmlns:app="http://schemas.android.com/apk/res-auto" + app:fontProviderAuthority="com.google.android.gms.fonts" + app:fontProviderPackage="com.google.android.gms" + app:fontProviderQuery="Aclonica" + app:fontProviderCerts="@array/com_google_android_gms_fonts_certs"> +</font-family> diff --git a/paparazzi/src/test/resources/folders/res/layout/test.xml b/paparazzi/src/test/resources/folders/res/layout/test.xml new file mode 100644 index 0000000000..ca1770e3cd --- /dev/null +++ b/paparazzi/src/test/resources/folders/res/layout/test.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/test_layout" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + > + + <TextView + android:id="@+id/test_view" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Test" + /> +</LinearLayout> diff --git a/paparazzi/src/test/resources/folders/res/menu/test_menu.xml b/paparazzi/src/test/resources/folders/res/menu/test_menu.xml new file mode 100644 index 0000000000..d724976c01 --- /dev/null +++ b/paparazzi/src/test/resources/folders/res/menu/test_menu.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu + xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/test_menu_1" + android:title="Test Menu 1" + /> + <item + android:id="@+id/test_menu_2" + android:title="Test Menu 2" + /> +</menu> diff --git a/paparazzi/src/test/resources/folders/res/mipmap/ic_launcher.xml b/paparazzi/src/test/resources/folders/res/mipmap/ic_launcher.xml new file mode 100644 index 0000000000..75ed519726 --- /dev/null +++ b/paparazzi/src/test/resources/folders/res/mipmap/ic_launcher.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> + <background android:drawable="@drawable/ic_android_black_24dp.xml" /> + <foreground android:drawable="@drawable/ic_android_black_24dp.xml" /> + <monochrome android:drawable="@drawable/ic_android_black_24dp.xml" /> +</adaptive-icon> diff --git a/paparazzi/src/test/resources/folders/res/raw/test_json.json b/paparazzi/src/test/resources/folders/res/raw/test_json.json new file mode 100644 index 0000000000..600e5f1a72 --- /dev/null +++ b/paparazzi/src/test/resources/folders/res/raw/test_json.json @@ -0,0 +1,3 @@ +{ + "name": "test" +} diff --git a/paparazzi/src/test/resources/folders/res/values/attrs.xml b/paparazzi/src/test/resources/folders/res/values/attrs.xml new file mode 100644 index 0000000000..8c8fae2ae5 --- /dev/null +++ b/paparazzi/src/test/resources/folders/res/values/attrs.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <declare-styleable name="test_styleable"> + <attr name="TestAttr" format="float"/> + <attr name="TestAttrInt" format="integer"/> + <attr name="TestAttrReference"/> + </declare-styleable> +</resources> diff --git a/paparazzi/src/test/resources/folders/res/values/bools.xml b/paparazzi/src/test/resources/folders/res/values/bools.xml new file mode 100644 index 0000000000..6c0736df4a --- /dev/null +++ b/paparazzi/src/test/resources/folders/res/values/bools.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <bool name="screen_small">true</bool> + <bool name="adjust_view_bounds">false</bool> +</resources> diff --git a/paparazzi/src/test/resources/folders/res/values/colors.xml b/paparazzi/src/test/resources/folders/res/values/colors.xml new file mode 100644 index 0000000000..bb10b0a1c1 --- /dev/null +++ b/paparazzi/src/test/resources/folders/res/values/colors.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <color name="test_color">#ffffffff</color> + <color name="test_color_2">#00000000</color> +</resources> diff --git a/paparazzi/src/test/resources/folders/res/values/dimens.xml b/paparazzi/src/test/resources/folders/res/values/dimens.xml new file mode 100644 index 0000000000..d1f7282f59 --- /dev/null +++ b/paparazzi/src/test/resources/folders/res/values/dimens.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <dimen name="textview_height">25dp</dimen> + <dimen name="textview_width">150dp</dimen> +</resources> diff --git a/paparazzi/src/test/resources/folders/res/values/ids.xml b/paparazzi/src/test/resources/folders/res/values/ids.xml new file mode 100644 index 0000000000..5af2a4e9e4 --- /dev/null +++ b/paparazzi/src/test/resources/folders/res/values/ids.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <item type="id" name="button_ok" /> + <item type="id" name="dialog_exit" /> +</resources> diff --git a/paparazzi/src/test/resources/folders/res/values/integers.xml b/paparazzi/src/test/resources/folders/res/values/integers.xml new file mode 100644 index 0000000000..09fe9e4c8c --- /dev/null +++ b/paparazzi/src/test/resources/folders/res/values/integers.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <integer name="max_speed">75</integer> + <integer name="min_speed">5</integer> +</resources> diff --git a/paparazzi/src/test/resources/folders/res/values/strings.xml b/paparazzi/src/test/resources/folders/res/values/strings.xml new file mode 100644 index 0000000000..73b46d7ea8 --- /dev/null +++ b/paparazzi/src/test/resources/folders/res/values/strings.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <plurals name="plural_name"> + <item quantity="zero">Nothing</item> + <item quantity="one">One String</item> + </plurals> + <string name="string_name">Test String</string> + <string name="string_name_xliff">Test String <xliff:g id="number" example="9">{0}</xliff:g> with suffix</string> + <string-array name="string_array_name"> + <item>First Test String</item> + <item>Second Test String</item> + </string-array> +</resources> diff --git a/paparazzi/src/test/resources/folders/res/values/style.xml b/paparazzi/src/test/resources/folders/res/values/style.xml new file mode 100644 index 0000000000..9b38d9367c --- /dev/null +++ b/paparazzi/src/test/resources/folders/res/values/style.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- comment --> + <style name="TestStyle"> + <item name="android:scrollbars">horizontal</item> + <item name="android:marginTop">16dp</item> + </style> +</resources> diff --git a/paparazzi/src/test/resources/folders/res/xml/test_network_security_config.xml b/paparazzi/src/test/resources/folders/res/xml/test_network_security_config.xml new file mode 100644 index 0000000000..6b2473f30a --- /dev/null +++ b/paparazzi/src/test/resources/folders/res/xml/test_network_security_config.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<network-security-config> + <domain-config cleartextTrafficPermitted="false"> + <domain includeSubdomains="true">test.com</domain> + </domain-config> + <base-config cleartextTrafficPermitted="true" /> +</network-security-config> diff --git a/paparazzi/src/test/resources/framework/framework_res.jar b/paparazzi/src/test/resources/framework/framework_res.jar new file mode 100644 index 0000000000..498cb1a765 Binary files /dev/null and b/paparazzi/src/test/resources/framework/framework_res.jar differ diff --git a/paparazzi/paparazzi/src/test/resources/plus_sign.xml b/paparazzi/src/test/resources/plus_sign.xml similarity index 100% rename from paparazzi/paparazzi/src/test/resources/plus_sign.xml rename to paparazzi/src/test/resources/plus_sign.xml diff --git a/paparazzi/src/test/resources/without-layout-params.png b/paparazzi/src/test/resources/without-layout-params.png new file mode 100644 index 0000000000..fa89e4d4e7 Binary files /dev/null and b/paparazzi/src/test/resources/without-layout-params.png differ diff --git a/renovate.json5 b/renovate.json5 new file mode 100644 index 0000000000..5945f2e524 --- /dev/null +++ b/renovate.json5 @@ -0,0 +1,26 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:base" + ], + "packageRules": [ + { + // Compose compiler is tightly coupled to Kotlin version + "groupName": "Kotlin and Compose", + "matchPackagePrefixes": [ + "androidx.compose.compiler", + "org.jetbrains.kotlin:kotlin", + "com.google.devtools.ksp" + ], + }, + { + // Android Gradle Plugin is tightly coupled to its android/platform/tools/base dependencies + // LayoutLib intentionally omitted to be updated independently + "groupName": "Android Tools", + "matchPackagePrefixes": [ + "com.android.tools:", + "com.android.tools.build:", + ], + } + ], +} diff --git a/sample/build.gradle b/sample/build.gradle index 16260abfa4..474a4fcf59 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -3,10 +3,16 @@ apply plugin: 'org.jetbrains.kotlin.android' apply plugin: 'app.cash.paparazzi' android { + namespace 'app.cash.paparazzi.sample' + compileSdk libs.versions.compileSdk.get() as int defaultConfig { minSdk libs.versions.minSdk.get() as int } + compileOptions { + sourceCompatibility = libs.versions.javaTarget.get() + targetCompatibility = libs.versions.javaTarget.get() + } buildFeatures { compose true viewBinding true @@ -18,6 +24,12 @@ android { dependencies { implementation libs.composeUi.material + implementation libs.composeUi.uiTooling testImplementation libs.testParameterInjector } + +// https://github.com/diffplug/spotless/issues/1572 +tasks.withType(com.diffplug.gradle.spotless.SpotlessTask).configureEach { + dependsOn(tasks.withType(Test)) +} diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml deleted file mode 100644 index 15cf1efc32..0000000000 --- a/sample/src/main/AndroidManifest.xml +++ /dev/null @@ -1 +0,0 @@ -<manifest package="app.cash.paparazzi.sample"/> diff --git a/sample/src/main/java/app/cash/paparazzi/sample/ResourcesDemo.kt b/sample/src/main/java/app/cash/paparazzi/sample/ResourcesDemo.kt new file mode 100644 index 0000000000..6546da94c8 --- /dev/null +++ b/sample/src/main/java/app/cash/paparazzi/sample/ResourcesDemo.kt @@ -0,0 +1,122 @@ +package app.cash.paparazzi.sample + +import android.icu.text.MessageFormat +import android.text.Html +import android.widget.TextView +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import app.cash.paparazzi.sample.ResourcesDemoView.Companion.plurals + +const val imageSize = 120f + +@Preview +@Composable +fun ResourcesDemo() { + Column( + modifier = Modifier + .background(Color.White) + .fillMaxSize() + ) { + Image( + modifier = Modifier + .align(alignment = Alignment.CenterHorizontally) + .size(imageSize.dp), + contentScale = ContentScale.FillBounds, + painter = painterResource(id = R.drawable.camera), + contentDescription = "camera" + ) + Image( + modifier = Modifier + .align(alignment = Alignment.CenterHorizontally) + .size(imageSize.dp), + contentScale = ContentScale.FillBounds, + painter = painterResource(id = R.drawable.ic_android_black_24dp), + contentDescription = "android" + ) + + val resources = LocalContext.current.resources + + Text( + text = resources.getBoolean(R.bool.adjust_view_bounds).toString(), + color = Color.Black + ) + Text( + modifier = Modifier + .fillMaxWidth() + .background( + Color(resources.getColor(R.color.keypadGreen, null)) + ), + text = "Color", + color = Color.Black + ) + Text( + text = resources.getString(R.string.string_escaped_chars), + color = Color.Black + ) + Text( + text = "Height: ${resources.getDimension(R.dimen.textview_height)}", + color = Color.Black + ) + Text( + text = "Max speed: ${resources.getInteger(R.integer.max_speed)}", + color = Color.Black + ) + Text( + text = "Plurals:", + color = Color.Black + ) + plurals.forEach { (label, quantity) -> + Row( + modifier = Modifier.fillMaxWidth() + ) { + Text( + modifier = Modifier.padding(start = 4.dp), + text = "$label:", + color = Color.Black + ) + Text( + modifier = Modifier.weight(1f).padding(horizontal = 4.dp), + text = resources.getQuantityString(R.plurals.plural_name, quantity), + color = Color.Black + ) + } + } + Text( + text = resources.getString(R.string.string_name), + color = Color.Black + ) + Text( + text = MessageFormat.format(resources.getString(R.string.string_name_xliff), 5), + color = Color.Black + ) + AndroidView( + factory = { context -> TextView(context) }, + update = { + it.text = + Html.fromHtml(resources.getString(R.string.string_name_html), Html.FROM_HTML_MODE_LEGACY) + it.setTextColor(android.graphics.Color.BLACK) + } + ) + Text( + resources.getStringArray(R.array.string_array_name).joinToString(), + color = Color.Black + ) + } +} diff --git a/sample/src/main/java/app/cash/paparazzi/sample/ResourcesDemoView.kt b/sample/src/main/java/app/cash/paparazzi/sample/ResourcesDemoView.kt new file mode 100644 index 0000000000..015c827dc2 --- /dev/null +++ b/sample/src/main/java/app/cash/paparazzi/sample/ResourcesDemoView.kt @@ -0,0 +1,112 @@ +package app.cash.paparazzi.sample + +import android.content.Context +import android.graphics.Color +import android.icu.text.MessageFormat +import android.text.Html +import android.util.TypedValue +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.annotation.ColorInt +import androidx.annotation.DrawableRes + +class ResourcesDemoView(context: Context) : LinearLayout(context) { + init { + setBackgroundColor(Color.WHITE) + orientation = LinearLayout.VERTICAL + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + addImageView(R.drawable.camera) + addImageView(R.drawable.ic_android_black_24dp) + addTextView(resources.getBoolean(R.bool.adjust_view_bounds).toString()) + addTextView( + text = "Color", + backgroundColor = resources.getColor(R.color.keypadGreen, null) + ) + addTextView(resources.getString(R.string.string_escaped_chars)) + addTextView("Height: ${resources.getDimension(R.dimen.textview_height)}") + addTextView("Max speed: ${context.resources.getInteger(R.integer.max_speed)}") + addTextView("Plurals:") + plurals.forEach { (label, quantity) -> + addView( + LinearLayout(context).apply { + orientation = LinearLayout.HORIZONTAL + addTextView("$label:", width = WRAP_CONTENT, leftMargin = dip(4f)) + addTextView( + context.resources.getQuantityString(R.plurals.plural_name, quantity), + width = WRAP_CONTENT, + weight = 1f, + leftMargin = dip(4f), + rightMargin = dip(4f) + ) + } + ) + } + addTextView(resources.getString(R.string.string_name)) + addTextView(MessageFormat.format(context.resources.getString(R.string.string_name_xliff), 5)) + addTextView( + Html.fromHtml( + resources.getString(R.string.string_name_html), + Html.FROM_HTML_MODE_LEGACY + ) + ) + addTextView(context.resources.getStringArray(R.array.string_array_name).joinToString()) + } + + private fun LinearLayout.addImageView(@DrawableRes drawableRes: Int) { + addView( + ImageView(context).apply { + layoutParams = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + .apply { + gravity = Gravity.CENTER + height = dip(imageSize) + width = dip(imageSize) + } + setImageResource(drawableRes) + } + ) + } + + private fun LinearLayout.addTextView( + text: CharSequence, + @ColorInt backgroundColor: Int = 0, + width: Int = ViewGroup.LayoutParams.MATCH_PARENT, + height: Int = ViewGroup.LayoutParams.WRAP_CONTENT, + weight: Float = 0f, + leftMargin: Int = 0, + rightMargin: Int = 0 + ) { + addView( + TextView(context).apply { + layoutParams = LinearLayout.LayoutParams(width, height, weight).apply { + setMargins(leftMargin, 0, rightMargin, 0) + } + this.text = text + setTextColor(Color.BLACK) + setBackgroundColor(backgroundColor) + } + ) + } + + private fun View.dip(value: Float): Int = + TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + value, + resources.displayMetrics + ).toInt() + + companion object { + val plurals = + mapOf("Zero" to 0, "One" to 1, "Two" to 2, "Few" to 3, "Many" to 11, "Other" to 100) + } +} diff --git a/sample/src/main/res/drawable/ic_android_black_24dp.xml b/sample/src/main/res/drawable/ic_android_black_24dp.xml new file mode 100644 index 0000000000..fe51230740 --- /dev/null +++ b/sample/src/main/res/drawable/ic_android_black_24dp.xml @@ -0,0 +1,5 @@ +<vector android:height="24dp" android:tint="#000000" + android:viewportHeight="24" android:viewportWidth="24" + android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="#FF000000" android:pathData="M17.6,11.48 L19.44,8.3a0.63,0.63 0,0 0,-1.09 -0.63l-1.88,3.24a11.43,11.43 0,0 0,-8.94 0L5.65,7.67a0.63,0.63 0,0 0,-1.09 0.63L6.4,11.48A10.81,10.81 0,0 0,1 20L23,20A10.81,10.81 0,0 0,17.6 11.48ZM7,17.25A1.25,1.25 0,1 1,8.25 16,1.25 1.25,0 0,1 7,17.25ZM17,17.25A1.25,1.25 0,1 1,18.25 16,1.25 1.25,0 0,1 17,17.25Z"/> +</vector> diff --git a/sample/src/main/res/values-ar/strings.xml b/sample/src/main/res/values-ar/strings.xml new file mode 100644 index 0000000000..a1b18cff6d --- /dev/null +++ b/sample/src/main/res/values-ar/strings.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <plurals name="plural_name"> + <!-- https://www.unicode.org/cldr/charts/43/supplemental/language_plural_rules.html#ar --> + <item quantity="zero">0 ولد حضر</item> + <item quantity="one">ولد واحد حضر</item> + <item quantity="two">ولدان حضرا</item> + <item quantity="few">3 أولاد حضروا</item> + <item quantity="many">11 ولدًا حضروا</item> + <item quantity="other">100 ولد حضروا</item> + </plurals> +</resources> diff --git a/sample/src/main/res/values/bools.xml b/sample/src/main/res/values/bools.xml new file mode 100644 index 0000000000..1d181b298c --- /dev/null +++ b/sample/src/main/res/values/bools.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <bool name="adjust_view_bounds">false</bool> +</resources> diff --git a/sample/src/main/res/values/dimens.xml b/sample/src/main/res/values/dimens.xml new file mode 100644 index 0000000000..b1152da04f --- /dev/null +++ b/sample/src/main/res/values/dimens.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <dimen name="textview_height">25dp</dimen> +</resources> diff --git a/sample/src/main/res/values/integers.xml b/sample/src/main/res/values/integers.xml new file mode 100644 index 0000000000..fea2ebe1df --- /dev/null +++ b/sample/src/main/res/values/integers.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <integer name="max_speed">75</integer> +</resources> diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml new file mode 100644 index 0000000000..ba32791b92 --- /dev/null +++ b/sample/src/main/res/values/strings.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <plurals name="plural_name"> + <item quantity="zero">No String</item> + <item quantity="one">One String</item> + <item quantity="two">Two Strings</item> + <item quantity="few">Few Strings</item> + <item quantity="many">Many Strings</item> + <item quantity="other">Other Strings</item> + </plurals> + <string name="string_name" translatable="false">Test String</string> + <string name="string_escaped_chars" translatable="false">\"Test\nString \uD83D\uDE00\uD83D\uDC4C\"</string> + <string name="string_name_xliff" translatable="false">Test String <xliff:g id="number" example="9">{0}</xliff:g> with suffix</string> + <string name="string_name_html" translatable="false">This <b>Test</b> String</string> + <string-array name="string_array_name"> + <item>First Test String</item> + <item>Second Test String</item> + </string-array> +</resources> diff --git a/sample/src/test/java/app/cash/paparazzi/sample/ComposeA11yTest.kt b/sample/src/test/java/app/cash/paparazzi/sample/ComposeA11yTest.kt new file mode 100644 index 0000000000..dc4e719211 --- /dev/null +++ b/sample/src/test/java/app/cash/paparazzi/sample/ComposeA11yTest.kt @@ -0,0 +1,76 @@ +package app.cash.paparazzi.sample + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.selection.toggleable +import androidx.compose.material.Checkbox +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import app.cash.paparazzi.DeviceConfig +import app.cash.paparazzi.Paparazzi +import app.cash.paparazzi.accessibility.AccessibilityRenderExtension +import org.junit.Rule +import org.junit.Test + +class ComposeA11yTest { + @get:Rule + val paparazzi = Paparazzi( + deviceConfig = DeviceConfig.PIXEL.copy(screenWidth = DeviceConfig.PIXEL.screenWidth * 2, softButtons = false), + renderExtensions = setOf(AccessibilityRenderExtension()) + ) + + @Test + fun compositeItems() { + paparazzi.snapshot { + Column { + Row( + Modifier + .toggleable( + value = true, + role = Role.Checkbox, + onValueChange = { } + ) + .fillMaxWidth() + ) { + Text("Option", Modifier.weight(1f)) + Checkbox(checked = true, onCheckedChange = null) + } + Box( + Modifier + .align(Alignment.CenterHorizontally) + .clickable(onClickLabel = "On Click Label") { } + ) + Row(modifier = Modifier.semantics(mergeDescendants = true) {}) { + Image( + imageVector = Icons.Filled.Add, + contentDescription = null // decorative + ) + Column(modifier = Modifier.semantics(mergeDescendants = true) {}) { + Text("Text") + Text( + text = "more text", + modifier = Modifier.semantics { contentDescription = "custom description" } + ) + Column(modifier = Modifier.semantics(mergeDescendants = true) {}) { + Text("Nested text") + Text( + text = "more text", + modifier = Modifier.semantics { contentDescription = "custom description" } + ) + } + } + } + } + } + } +} diff --git a/sample/src/test/java/app/cash/paparazzi/sample/ComposeDialogShrinkTest.kt b/sample/src/test/java/app/cash/paparazzi/sample/ComposeDialogShrinkTest.kt new file mode 100644 index 0000000000..446a4fd497 --- /dev/null +++ b/sample/src/test/java/app/cash/paparazzi/sample/ComposeDialogShrinkTest.kt @@ -0,0 +1,82 @@ +package app.cash.paparazzi.sample + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.AlertDialog +import androidx.compose.material.Button +import androidx.compose.material.Text +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import app.cash.paparazzi.DeviceConfig +import app.cash.paparazzi.Paparazzi +import com.android.ide.common.rendering.api.SessionParams.RenderingMode +import org.junit.Rule +import org.junit.Test + +class ComposeDialogShrinkTest { + @get:Rule + val paparazzi = Paparazzi( + maxPercentDifference = 1.0, + deviceConfig = DeviceConfig.PIXEL_5.copy(softButtons = false), + renderingMode = RenderingMode.SHRINK + ) + + @Test + fun test() { + paparazzi.snapshot { + AlertDialog( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)), + onDismissRequest = {}, + title = { + Text( + modifier = Modifier + .fillMaxWidth(), + textAlign = TextAlign.Center, + text = "Title" + ) + }, + text = { + Text( + modifier = Modifier + .fillMaxWidth(), + textAlign = TextAlign.Center, + text = "Subtitle" + ) + }, + buttons = { + Column( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp + ) + ) { + Button( + modifier = Modifier.fillMaxWidth(), + onClick = {} + ) { + Text("Button 1") + } + Spacer(modifier = Modifier.height(16.dp)) + Button( + modifier = Modifier.fillMaxWidth(), + onClick = {} + ) { + Text("Button 2") + } + } + } + ) + } + } +} diff --git a/sample/src/test/java/app/cash/paparazzi/sample/ResourcesTest.kt b/sample/src/test/java/app/cash/paparazzi/sample/ResourcesTest.kt new file mode 100644 index 0000000000..26a8e925ce --- /dev/null +++ b/sample/src/test/java/app/cash/paparazzi/sample/ResourcesTest.kt @@ -0,0 +1,33 @@ +package app.cash.paparazzi.sample + +import app.cash.paparazzi.DeviceConfig +import app.cash.paparazzi.Paparazzi +import com.google.testing.junit.testparameterinjector.TestParameter +import com.google.testing.junit.testparameterinjector.TestParameterInjector +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(TestParameterInjector::class) +class ResourcesTest( + @TestParameter locale: Locale +) { + @get:Rule + val paparazzi = Paparazzi( + deviceConfig = DeviceConfig.PIXEL_5.copy(locale = locale.tag) + ) + + @Test + fun legacy() { + paparazzi.snapshot(ResourcesDemoView(paparazzi.context)) + } + + @Test + fun compose() { + paparazzi.snapshot { ResourcesDemo() } + } + + enum class Locale(val tag: String?) { + Default(null), AR("ar") + } +} diff --git a/sample/src/test/java/app/cash/paparazzi/sample/TestParameterInjectorTest.kt b/sample/src/test/java/app/cash/paparazzi/sample/TestParameterInjectorTest.kt index 8085af5173..0347548f2a 100644 --- a/sample/src/test/java/app/cash/paparazzi/sample/TestParameterInjectorTest.kt +++ b/sample/src/test/java/app/cash/paparazzi/sample/TestParameterInjectorTest.kt @@ -19,7 +19,7 @@ class TestParameterInjectorTest( ) { NEXUS_4(deviceConfig = DeviceConfig.NEXUS_4), NEXUS_5(deviceConfig = DeviceConfig.NEXUS_5), - NEXUS_5_LAND(deviceConfig = DeviceConfig.NEXUS_5_LAND), + NEXUS_5_LAND(deviceConfig = DeviceConfig.NEXUS_5_LAND) } enum class Theme(val themeName: String) { diff --git a/sample/src/test/java/app/cash/paparazzi/sample/WidgetTest.kt b/sample/src/test/java/app/cash/paparazzi/sample/WidgetTest.kt new file mode 100644 index 0000000000..35c42ac461 --- /dev/null +++ b/sample/src/test/java/app/cash/paparazzi/sample/WidgetTest.kt @@ -0,0 +1,90 @@ +package app.cash.paparazzi.sample + +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.GradientDrawable +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import android.widget.Button +import android.widget.LinearLayout +import android.widget.TextView +import app.cash.paparazzi.DeviceConfig +import app.cash.paparazzi.Paparazzi +import com.android.ide.common.rendering.api.SessionParams +import org.junit.Rule +import org.junit.Test + +class WidgetTest { + @get:Rule + val paparazzi = Paparazzi( + deviceConfig = DeviceConfig.PIXEL_3, + renderingMode = SessionParams.RenderingMode.SHRINK, + showSystemUi = false + ) + + @Test fun default() { + paparazzi.snapshot(buildView(paparazzi.context)) + } + + private fun buildView(context: Context): View { + return LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + layoutParams = ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT) + addView( + TextView(context).apply { + id = 1 + text = "Text View Sample" + } + ) + + addView( + View(context).apply { + id = 2 + layoutParams = LinearLayout.LayoutParams(100, 100) + contentDescription = "Content Description Sample" + } + ) + + addView( + View(context).apply { + id = 3 + layoutParams = LinearLayout.LayoutParams(100, 100).apply { + setMargins(20, 20, 20, 20) + } + contentDescription = "Margin Sample" + } + ) + + addView( + View(context).apply { + id = 4 + layoutParams = LinearLayout.LayoutParams(100, 100).apply { + setMargins(20, 20, 20, 20) + } + foreground = GradientDrawable( + GradientDrawable.Orientation.TL_BR, + intArrayOf(Color.YELLOW, Color.BLUE) + ).apply { + shape = GradientDrawable.OVAL + } + contentDescription = "Foreground Drawable" + } + ) + + addView( + Button(context).apply { + id = 5 + layoutParams = LinearLayout.LayoutParams( + WRAP_CONTENT, + WRAP_CONTENT + ).apply { + gravity = Gravity.CENTER + } + text = "Button Sample" + } + ) + } + } +} diff --git a/settings.gradle b/settings.gradle index 6bf4adc3e6..8702bd6c21 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,12 +1,20 @@ rootProject.name = 'paparazzi-root' -include ':sample' +include ':libs:layoutlib' +include ':libs:native-macarm' +include ':libs:native-macosx' +include ':libs:native-win' +include ':libs:native-linux' +include ':paparazzi' +include ':paparazzi-agent' +include ':paparazzi-gradle-plugin' -includeBuild('paparazzi') { - dependencySubstitution { - substitute module('app.cash.paparazzi:paparazzi') using project(':paparazzi') - substitute module('app.cash.paparazzi:paparazzi-agent') using project(':paparazzi-agent') - } -} +include ':sample' enableFeaturePreview('TYPESAFE_PROJECT_ACCESSORS') + +includeBuild('build-logic') { +// dependencySubstitution { +// substitute module('app.cash.paparazzi:paparazzi-gradle-plugin') using project(':paparazzi-gradle-plugin') +// } +}