diff --git a/.github/ci-gradle.properties b/.github/ci-gradle.properties new file mode 100644 index 000000000..a5a2441fd --- /dev/null +++ b/.github/ci-gradle.properties @@ -0,0 +1,26 @@ +# +# Copyright 2021 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. +# + +org.gradle.daemon=false +org.gradle.parallel=true +org.gradle.jvmargs=-Xmx5120m +org.gradle.workers.max=2 + +kotlin.incremental=false +kotlin.compiler.execution.strategy=in-process + +# Controls KotlinOptions.allWarningsAsErrors. This is used in CI and can be set in local properties. +warningsAsErrors=true diff --git a/.github/workflows/TestConfigurationSample.yaml b/.github/workflows/TestConfigurationSample.yaml new file mode 100644 index 000000000..f4ebc8705 --- /dev/null +++ b/.github/workflows/TestConfigurationSample.yaml @@ -0,0 +1,123 @@ +name: TestConfigurationSample + +on: + push: + branches: + - main + paths: + - 'TestConfigurationSample/**' + pull_request: + paths: + - 'TestConfigurationSample/**' + +env: + SAMPLE_PATH: TestConfigurationSample + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Copy CI gradle.properties + run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties + + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + + - name: Generate cache key + run: ./scripts/checksum.sh $SAMPLE_PATH checksum.txt + + - uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches/modules-* + ~/.gradle/caches/jars-* + ~/.gradle/caches/build-cache-* + key: gradle-${{ hashFiles('checksum.txt') }} + + - name: Build project and run local tests + working-directory: ${{ env.SAMPLE_PATH }} + run: ./gradlew spotlessCheck assembleDebug lintDebug testDebug --stacktrace --no-build-cache --rerun-tasks + + gradleManagedVirtualDevicesTest: + needs: build + runs-on: macos-latest + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Copy CI gradle.properties + run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties + + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + + - name: Generate cache key for Gradle cache + run: ./scripts/checksum.sh $SAMPLE_PATH checksum.txt + + - uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches/modules-* + ~/.gradle/caches/jars-* + ~/.gradle/caches/build-cache-* + key: gradle-${{ hashFiles('checksum.txt') }} + + - name: Cache pixel2api30 system image + uses: actions/cache@v2 + with: + path: | + ~/.android/gradle/avd/dev30_aosp_x86_Pixel_2.* + ~/.android/gradle/avd/dev30_aosp_x86_64_Pixel_2.* + key: pixel2api30 + + - name: Cache pixel2api27 system image + uses: actions/cache@v2 + with: + path: ~/.android/gradle/avd/dev27_aosp_x86_Pixel_2.* + key: pixel2api27 + + - name: Cache nexus9api29 system image + uses: actions/cache@v2 + with: + path: ~/.android/gradle/avd/dev29_aosp_x86_Nexus_9.* + key: nexus9api29 + + - name: Run all tests + working-directory: ${{ env.SAMPLE_PATH }} + run: ./gradlew -Dorg.gradle.workers.max=2 -i pixel2api30DebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.notAnnotation=com.example.android.testing.testconfigurationsample.TestDeviceLargeScreen -Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect" + + - name: Run regression tests + working-directory: ${{ env.SAMPLE_PATH }} + run: ./gradlew -Dorg.gradle.workers.max=2 -i pixel2api27DebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.annotation=com.example.android.testing.testconfigurationsample.TestDeviceApi27 -Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect" + + - name: Run large screen tests + working-directory: ${{ env.SAMPLE_PATH }} + run: ./gradlew -Dorg.gradle.workers.max=2 -i nexus9api29DebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.annotation=com.example.android.testing.testconfigurationsample.TestDeviceLargeScreen -Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect" + + - name: Upload test reports + if: always() + uses: actions/upload-artifact@v2 + with: + name: test-reports + path: ${{ env.SAMPLE_PATH }}/app/build/reports/androidTests + + - name: Upload emulator logs + if: failure() + uses: actions/upload-artifact@v2 + with: + name: emulator-logs + path: | + ${{ env.SAMPLE_PATH }}/app/build/outputs/androidTest-results/managedDevice/** + !${{ env.SAMPLE_PATH }}/**/*:*.xml + !${{ env.SAMPLE_PATH }}/**/*|* diff --git a/TestConfigurationSample/.gitignore b/TestConfigurationSample/.gitignore new file mode 100644 index 000000000..aa724b770 --- /dev/null +++ b/TestConfigurationSample/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/TestConfigurationSample/.google/packaging.yaml b/TestConfigurationSample/.google/packaging.yaml new file mode 100644 index 000000000..d3f279c4f --- /dev/null +++ b/TestConfigurationSample/.google/packaging.yaml @@ -0,0 +1,30 @@ + +# Copyright (C) 2021 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# GOOGLE SAMPLE PACKAGING DATA +# +# This file is used by Google as part of our samples packaging process. +# End users may safely ignore this file. It has no relevance to other systems. +--- +status: PUBLISHED +technologies: [Android] +categories: [Testing] +languages: [Kotlin] +solutions: [Mobile] +github: android/testing-samples +level: INTERMEDIATE +apiRefs: + - android:androidx.compose.Composable +license: apache2 \ No newline at end of file diff --git a/TestConfigurationSample/README.md b/TestConfigurationSample/README.md new file mode 100644 index 000000000..721b7cf73 --- /dev/null +++ b/TestConfigurationSample/README.md @@ -0,0 +1 @@ +# Test Configuration Sample diff --git a/TestConfigurationSample/app/.gitignore b/TestConfigurationSample/app/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/TestConfigurationSample/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/TestConfigurationSample/app/build.gradle b/TestConfigurationSample/app/build.gradle new file mode 100644 index 000000000..b881d3d20 --- /dev/null +++ b/TestConfigurationSample/app/build.gradle @@ -0,0 +1,83 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +android { + compileSdk 31 + buildToolsVersion "30.0.3" + + defaultConfig { + applicationId "com.example.android.testing.testconfigurationsample" + minSdk 23 + targetSdk 31 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary true + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } + buildFeatures { + compose true + } + composeOptions { + kotlinCompilerExtensionVersion compose_version + } + packagingOptions { + resources { + excludes += ['/META-INF/AL2.0', '/META-INF/LGPL2.1'] + } + } + + + testOptions { + devices { + pixel2api30(com.android.build.api.dsl.ManagedVirtualDevice) { + // Use device profiles you typically see in Android Studio + device = "Pixel 2" + apiLevel = 30 + // You can also specify "google" if you require Google Play Services. + systemImageSource = "aosp" + abi = "x86" + } + pixel2api27(com.android.build.api.dsl.ManagedVirtualDevice) { + device = "Pixel 2" + apiLevel = 27 + systemImageSource = "aosp" + abi = "x86" + } + nexus9api29(com.android.build.api.dsl.ManagedVirtualDevice) { + device = "Nexus 9" + apiLevel = 29 + systemImageSource = "aosp" + abi = "x86" + } + } + } +} + +dependencies { + implementation "com.google.android.material:material:$material_version" + implementation "androidx.compose.ui:ui:$compose_version" + implementation "androidx.compose.material:material:$compose_version" + implementation "androidx.compose.ui:ui-tooling:$compose_version" + implementation "androidx.activity:activity-compose:$activity_compose_version" + androidTestImplementation "androidx.test.ext:junit:$junit_version" + androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" +} \ No newline at end of file diff --git a/TestConfigurationSample/app/proguard-rules.pro b/TestConfigurationSample/app/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/TestConfigurationSample/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/TestConfigurationSample/app/src/androidTest/java/com/example/android/testing/testconfigurationsample/MainActivityTests.kt b/TestConfigurationSample/app/src/androidTest/java/com/example/android/testing/testconfigurationsample/MainActivityTests.kt new file mode 100644 index 000000000..e18e15862 --- /dev/null +++ b/TestConfigurationSample/app/src/androidTest/java/com/example/android/testing/testconfigurationsample/MainActivityTests.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.testing.testconfigurationsample + +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class MainActivityTests { + @get:Rule + val composeTestRule = createAndroidComposeRule() + + @Test + fun sampleTest1() { + // Add instrumented tests here + runBlocking { delay(10000) } + assertTrue(true) + } + + @Test + fun sampleTest2() { + // Add instrumented tests here + runBlocking { delay(10000) } + assertTrue(true) + } + + @Test + fun sampleTest3() { + // Add instrumented tests here + runBlocking { delay(10000) } + assertTrue(true) + } + + @Test + fun sampleTest4() { + // Add instrumented tests here + runBlocking { delay(10000) } + assertTrue(true) + } + + /** + * When you find an issue with a specific device or API level, + * you can create an annotation for it and add it to your test. + * In your CI setup you can then run tests with these annotations + * on a specific Gradle Managed Virtual Device. + * + * See the Github Actions setup of this project for an example. + */ + @Test @TestDeviceApi27 + fun regressionTestKnownIssueApi27() { + // Add instrumented tests here + runBlocking { delay(10000) } + assertTrue(true) + } +} diff --git a/TestConfigurationSample/app/src/androidTest/java/com/example/android/testing/testconfigurationsample/TestDeviceAnnotations.kt b/TestConfigurationSample/app/src/androidTest/java/com/example/android/testing/testconfigurationsample/TestDeviceAnnotations.kt new file mode 100644 index 000000000..bb34a7aa6 --- /dev/null +++ b/TestConfigurationSample/app/src/androidTest/java/com/example/android/testing/testconfigurationsample/TestDeviceAnnotations.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.testing.testconfigurationsample + +/** + * Annotate tests with this annotation when testing API 27 regression bugs. + */ +annotation class TestDeviceApi27 + +/** + * Annotate tests with this annotation when testing large-screen specific features. + */ +annotation class TestDeviceLargeScreen diff --git a/TestConfigurationSample/app/src/main/AndroidManifest.xml b/TestConfigurationSample/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..b898d21bf --- /dev/null +++ b/TestConfigurationSample/app/src/main/AndroidManifest.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TestConfigurationSample/app/src/main/java/com/example/android/testing/testconfigurationsample/MainActivity.kt b/TestConfigurationSample/app/src/main/java/com/example/android/testing/testconfigurationsample/MainActivity.kt new file mode 100644 index 000000000..cef565f57 --- /dev/null +++ b/TestConfigurationSample/app/src/main/java/com/example/android/testing/testconfigurationsample/MainActivity.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.testing.testconfigurationsample + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.ui.unit.dp + +/** + * Main entrance for the application. Very basic implementation for testing purposes only. + */ +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + MaterialTheme { + BoxWithConstraints { + Text(if (maxWidth > 500.dp) "Hi Large Screen!" else "Hi Small Screen!") + } + } + } + } +} diff --git a/TestConfigurationSample/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/TestConfigurationSample/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..2b068d114 --- /dev/null +++ b/TestConfigurationSample/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/TestConfigurationSample/app/src/main/res/drawable/ic_launcher_background.xml b/TestConfigurationSample/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..07d5da9cb --- /dev/null +++ b/TestConfigurationSample/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TestConfigurationSample/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/TestConfigurationSample/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..eca70cfe5 --- /dev/null +++ b/TestConfigurationSample/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/TestConfigurationSample/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/TestConfigurationSample/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..eca70cfe5 --- /dev/null +++ b/TestConfigurationSample/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/TestConfigurationSample/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/TestConfigurationSample/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 000000000..c209e78ec Binary files /dev/null and b/TestConfigurationSample/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/TestConfigurationSample/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/TestConfigurationSample/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 000000000..b2dfe3d1b Binary files /dev/null and b/TestConfigurationSample/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/TestConfigurationSample/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/TestConfigurationSample/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 000000000..4f0f1d64e Binary files /dev/null and b/TestConfigurationSample/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/TestConfigurationSample/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/TestConfigurationSample/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 000000000..62b611da0 Binary files /dev/null and b/TestConfigurationSample/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/TestConfigurationSample/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/TestConfigurationSample/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 000000000..948a3070f Binary files /dev/null and b/TestConfigurationSample/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/TestConfigurationSample/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/TestConfigurationSample/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..1b9a6956b Binary files /dev/null and b/TestConfigurationSample/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/TestConfigurationSample/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/TestConfigurationSample/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 000000000..28d4b77f9 Binary files /dev/null and b/TestConfigurationSample/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/TestConfigurationSample/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/TestConfigurationSample/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..9287f5083 Binary files /dev/null and b/TestConfigurationSample/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/TestConfigurationSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/TestConfigurationSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 000000000..aa7d6427e Binary files /dev/null and b/TestConfigurationSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/TestConfigurationSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/TestConfigurationSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..9126ae37c Binary files /dev/null and b/TestConfigurationSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/TestConfigurationSample/app/src/main/res/values-night/themes.xml b/TestConfigurationSample/app/src/main/res/values-night/themes.xml new file mode 100644 index 000000000..a1038fd40 --- /dev/null +++ b/TestConfigurationSample/app/src/main/res/values-night/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/TestConfigurationSample/app/src/main/res/values/colors.xml b/TestConfigurationSample/app/src/main/res/values/colors.xml new file mode 100644 index 000000000..f8c6127d3 --- /dev/null +++ b/TestConfigurationSample/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/TestConfigurationSample/app/src/main/res/values/strings.xml b/TestConfigurationSample/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..a323aa7cf --- /dev/null +++ b/TestConfigurationSample/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + TestConfigurationSample + \ No newline at end of file diff --git a/TestConfigurationSample/app/src/main/res/values/themes.xml b/TestConfigurationSample/app/src/main/res/values/themes.xml new file mode 100644 index 000000000..0fedac1f0 --- /dev/null +++ b/TestConfigurationSample/app/src/main/res/values/themes.xml @@ -0,0 +1,25 @@ + + + + + + +