diff --git a/.github/workflows/emulator-sample-install.yml b/.github/workflows/emulator-sample-install.yml new file mode 100644 index 000000000..6c03e9c09 --- /dev/null +++ b/.github/workflows/emulator-sample-install.yml @@ -0,0 +1,46 @@ +name: Workflow Sample Install macOS +on: + push: + branches: + - master + pull_request: + branches: + - '*' + +jobs: + build: + runs-on: macos-latest + + steps: + - name: Git checkout + uses: actions/checkout@v2 + + # Zulu Community distribution of OpenJDK + - name: set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + + - name: Generate Gradle checksums + run: | + find buildSrc -name "*.kt" -type f | sort | xargs shasum > gradle-checksums.txt + cat gradle-checksums.txt + - uses: actions/cache@v2 + with: + path: ~/.gradle + key: gradle-${{ hashFiles('gradle-checksums.txt') }}-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }} + restore-keys: | + gradle-${{ hashFiles('gradle-checksums.txt') }}- + + - name: run tests + - uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 29 + ndk: 21.0.6113669 + cmake: 3.10.2.4988404 + script: make runConnectedTests + + # We stop gradle at the end to make sure the cache folders + # don't contain any lock files and are free to be cached. + - name: Make stop + run: make stop diff --git a/Makefile b/Makefile index 541d5ce08..90e53c09a 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: clean compile dryRelease doRelease release update stop +.PHONY: clean compile dryRelease doRelease release update stop runConnectedTests all: clean compile update dryRelease @@ -29,3 +29,7 @@ update: # don't contain any lock files and are free to be cached. stop: ./gradlew --stop + +# run connectedCheck +runConnectedTests: + ./gradlew connectedDebugAndroidTest diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index c33d9e78a..db719b477 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -22,7 +22,6 @@ object Config { object Libs { val appCompat = "androidx.appcompat:appcompat:1.1.0" - val timber = "com.jakewharton.timber:timber:4.7.1" // only bump gson if https://github.com/google/gson/issues/1597 is fixed val gson = "com.google.code.gson:gson:2.8.5" val leakCanary = "com.squareup.leakcanary:leakcanary-android:2.3" @@ -33,15 +32,21 @@ object Config { } object TestLibs { - private val androidxTestVersion = "1.2.0" + private val androidxTestVersion = "1.3.0-rc01" val kotlinTestJunit = "org.jetbrains.kotlin:kotlin-test-junit:$kotlinVersion" val androidxCore = "androidx.test:core:$androidxTestVersion" val androidxRunner = "androidx.test:runner:$androidxTestVersion" - val androidxJunit = "androidx.test.ext:junit:1.1.1" + val androidxJunit = "androidx.test.ext:junit:1.1.2-rc01" + val androidxRules = "androidx.test:rules:$androidxTestVersion" + val androidxCoreKtx = "androidx.test:core-ktx:$androidxTestVersion" + val orchestrator = "androidx.test:orchestrator:$androidxTestVersion" + val androidxJunitKtx = "androidx.test.ext:junit-ktx:1.1.2-rc01" + val espressoCore = "androidx.test.espresso:espresso-core:3.3.0-rc01" val robolectric = "org.robolectric:robolectric:4.3.1" val mockitoKotlin = "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0" val awaitility = "org.awaitility:awaitility-kotlin:4.0.3" + val retrofit = "com.squareup.retrofit2:retrofit:2.9.0" } object QualityPlugins { diff --git a/sentry-sample/build.gradle.kts b/sentry-sample/build.gradle.kts index d7073c252..f0a24f877 100644 --- a/sentry-sample/build.gradle.kts +++ b/sentry-sample/build.gradle.kts @@ -32,6 +32,14 @@ android { setAbiFilters(Config.Android.abiFilters) ndkVersion = Config.Android.ndkVersion } + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + + testInstrumentationRunnerArguments = mapOf("clearPackageData" to "true") + + testOptions { + execution = "ANDROIDX_TEST_ORCHESTRATOR" + } } buildFeatures { @@ -87,6 +95,10 @@ android { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } } dependencies { @@ -101,8 +113,20 @@ dependencies { // } implementation(Config.Libs.appCompat) + compileOnly(Config.CompileOnly.jetbrainsAnnotations) + + androidTestImplementation(Config.TestLibs.espressoCore) + androidTestImplementation(Config.TestLibs.androidxRunner) + androidTestImplementation(Config.TestLibs.androidxJunit) + androidTestImplementation(Config.TestLibs.androidxRules) + androidTestImplementation(Config.TestLibs.androidxCoreKtx) + androidTestImplementation(Config.TestLibs.androidxJunitKtx) + androidTestImplementation(Config.TestLibs.kotlinTestJunit) + androidTestImplementation(Config.TestLibs.orchestrator) + androidTestImplementation(Config.TestLibs.retrofit) // debugging purpose - implementation(Config.Libs.timber) - debugImplementation(Config.Libs.leakCanary) + debugImplementation(Config.Libs.leakCanary) { + exclude(group = "org.jetbrains", module = "annotations") + } } diff --git a/sentry-sample/src/androidTest/java/io/sentry/sample/MainActivityTest.kt b/sentry-sample/src/androidTest/java/io/sentry/sample/MainActivityTest.kt new file mode 100644 index 000000000..875740adb --- /dev/null +++ b/sentry-sample/src/androidTest/java/io/sentry/sample/MainActivityTest.kt @@ -0,0 +1,100 @@ +package io.sentry.sample + +import android.util.Log +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.ext.junit.rules.activityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import io.sentry.core.protocol.SentryId +import java.util.Timer +import java.util.concurrent.CountDownLatch +import kotlin.concurrent.schedule +import okhttp3.ResponseBody +import org.junit.Rule +import org.junit.runner.RunWith +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertTrue +//import kotlin.test.BeforeTest + +@LargeTest +@RunWith(AndroidJUnit4::class) +class MainActivityTest { + + @get:Rule + val rule = activityScenarioRule() + +// @BeforeTest +// fun beforeTest() { +// val token = System.getenv("TEST") +// } + + @Test + fun mainActivityTest() { + onView(withId(R.id.send_message)).perform(click()) + onView(withId(R.id.capture_exception)).perform(click()) + onView(withId(R.id.breadcrumb)).perform(click()) + onView(withId(R.id.native_capture)).perform(click()) + +// val retrofit = Retrofit.Builder() +// .baseUrl("https://sentry.io/") +// .client(OkHttpClient.Builder().addInterceptor { chain -> +// // token from https://sentry.io/settings/account/api/auth-tokens/ +// val request = chain.request().newBuilder().addHeader("Authorization", "Bearer ${TOKEN}}").build() +// chain.proceed(request) +// }.build()) +// .build() +// val service = retrofit.create(SentryService::class.java) + +// val count = CountDownLatch(4) + rule.scenario.onActivity { + val ids = it.ids + assertEquals(4, ids.size) +// val apiCallback = ApiCallback(count) + ids.forEach { id -> + assertNotEquals(SentryId.EMPTY_ID, id) + + // TODO: try 5 times with interval of 1 sec? event might not be processed yet +// service.getEvent(id).enqueue(apiCallback) + } + } + + val count = CountDownLatch(1) + + Timer(true).schedule(5000) { + count.countDown() + } + + // might be possible to replace with https://developer.android.com/training/testing/espresso/idling-resource + // little waiting time for getting events processed +// count.await(1, TimeUnit.MINUTES) + count.await() + } + + internal class ApiCallback(private val count: CountDownLatch) : Callback { + + override fun onFailure(call: Call, t: Throwable) { + Log.e("Sentry", "error: ${t.message}", t) + throw t + } + + override fun onResponse(call: Call, response: Response) { + // TODO: assert its content? right now ony checking if the event exist + if (!response.isSuccessful) { + Log.i("Sentry", response.message()) + Log.e("Sentry", "error: ${response.errorBody()?.string()}") + Log.i("Sentry", "http code: ${response.code()}") + } else { + Log.i("Sentry", "success: ${response.body()?.string()}") + } + assertTrue(response.isSuccessful) + count.countDown() + } + } +} diff --git a/sentry-sample/src/androidTest/java/io/sentry/sample/SentryService.kt b/sentry-sample/src/androidTest/java/io/sentry/sample/SentryService.kt new file mode 100644 index 000000000..434664646 --- /dev/null +++ b/sentry-sample/src/androidTest/java/io/sentry/sample/SentryService.kt @@ -0,0 +1,11 @@ +package io.sentry.sample + +import okhttp3.ResponseBody +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Path + +interface SentryService { + @GET("api/0/projects/sentry-test/android/events/{id}/") + fun getEvent(@Path("id") id: String): Call +} diff --git a/sentry-sample/src/main/cpp/native-sample.cpp b/sentry-sample/src/main/cpp/native-sample.cpp index 5c6b0c374..1a5a50856 100644 --- a/sentry-sample/src/main/cpp/native-sample.cpp +++ b/sentry-sample/src/main/cpp/native-sample.cpp @@ -12,7 +12,7 @@ JNIEXPORT void JNICALL Java_io_sentry_sample_NativeSample_crash(JNIEnv *env, jcl *ptr += 1; } -JNIEXPORT void JNICALL Java_io_sentry_sample_NativeSample_message(JNIEnv *env, jclass cls) { +JNIEXPORT jstring JNICALL Java_io_sentry_sample_NativeSample_message(JNIEnv *env, jclass cls) { __android_log_print(ANDROID_LOG_WARN, TAG, "Sending message."); sentry_value_t event = sentry_value_new_message_event( /* level */ SENTRY_LEVEL_INFO, @@ -20,6 +20,9 @@ JNIEXPORT void JNICALL Java_io_sentry_sample_NativeSample_message(JNIEnv *env, j /* message */ "It works!" ); sentry_capture_event(event); + sentry_value_t event_id = sentry_value_get_by_key(event, "event_id"); + const char *uuid_str = sentry_value_as_string(event_id); + return env->NewStringUTF(uuid_str); } } diff --git a/sentry-sample/src/main/java/io/sentry/sample/MainActivity.java b/sentry-sample/src/main/java/io/sentry/sample/MainActivity.java index 54839356a..9cc6a611e 100644 --- a/sentry-sample/src/main/java/io/sentry/sample/MainActivity.java +++ b/sentry-sample/src/main/java/io/sentry/sample/MainActivity.java @@ -3,59 +3,73 @@ import android.os.Bundle; import androidx.appcompat.app.AppCompatActivity; import io.sentry.core.Sentry; +import io.sentry.core.protocol.SentryId; import io.sentry.core.protocol.User; import io.sentry.sample.databinding.ActivityMainBinding; -import java.util.Collections; -import timber.log.Timber; +import java.util.ArrayList; +import java.util.List; +import org.jetbrains.annotations.TestOnly; public class MainActivity extends AppCompatActivity { + private final List ids = new ArrayList<>(); + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater()); - Timber.i("Sentry.isEnabled() = %s", Sentry.isEnabled()); - binding.crashFromJava.setOnClickListener( view -> { throw new RuntimeException("Uncaught Exception from Java."); }); - binding.sendMessage.setOnClickListener(view -> Sentry.captureMessage("Some message.")); + binding.sendMessage.setOnClickListener( + view -> { + SentryId sentryId = Sentry.captureMessage("Some message."); + ids.add(sentryId); + }); binding.captureException.setOnClickListener( - view -> - Sentry.captureException( - new Exception(new Exception(new Exception("Some exception."))))); + view -> { + SentryId sentryId = + Sentry.captureException( + new Exception(new Exception(new Exception("Some exception.")))); + ids.add(sentryId); + }); binding.breadcrumb.setOnClickListener( view -> { Sentry.addBreadcrumb("Breadcrumb"); Sentry.setExtra("extra", "extra"); - Sentry.setFingerprint(Collections.singletonList("fingerprint")); - Sentry.setTransaction("transaction"); User user = new User(); user.setUsername("username"); Sentry.setUser(user); Sentry.setTag("tag", "tag"); - Sentry.captureException(new Exception("Some exception with scope.")); + SentryId sentryId = + Sentry.captureException(new Exception("Some exception with scope and breadcrumbs.")); + ids.add(sentryId); }); binding.nativeCrash.setOnClickListener(view -> NativeSample.crash()); - binding.nativeCapture.setOnClickListener(view -> NativeSample.message()); + binding.nativeCapture.setOnClickListener( + view -> { + String id = NativeSample.message(); + SentryId sentryId = new SentryId(id); + ids.add(sentryId); + }); binding.anr.setOnClickListener( view -> { - // Try cause ANR by blocking for 2.5 seconds. - // By default the SDK sends an event if blocked by at least 4 seconds. - // The time was configurable (see manifest) to 1 second for demo purposes. + // Try cause ANR by blocking for 10 seconds. + // By default the SDK sends an event if blocked by at least 5 seconds. + // you must keep clicking on the UI, so OS will detect that the UI is not responding. // NOTE: By default it doesn't raise if the debugger is attached. That can also be // configured. try { - Thread.sleep(2500); + Thread.sleep(10000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } @@ -63,4 +77,9 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(binding.getRoot()); } + + @TestOnly + public List getIds() { + return ids; + } } diff --git a/sentry-sample/src/main/java/io/sentry/sample/MyApplication.java b/sentry-sample/src/main/java/io/sentry/sample/MyApplication.java index 61e05ba63..6accab22c 100644 --- a/sentry-sample/src/main/java/io/sentry/sample/MyApplication.java +++ b/sentry-sample/src/main/java/io/sentry/sample/MyApplication.java @@ -2,7 +2,6 @@ import android.app.Application; import android.os.StrictMode; -import timber.log.Timber; // import io.sentry.android.core.SentryAndroid; @@ -14,8 +13,6 @@ public void onCreate() { strictMode(); super.onCreate(); - Timber.plant(new Timber.DebugTree()); - // Example how to initialize the SDK manually which allows access to SentryOptions callbacks. // Make sure you disable the auto init via manifest meta-data: io.sentry.auto-init=false // SentryAndroid.init( diff --git a/sentry-sample/src/main/java/io/sentry/sample/NativeSample.java b/sentry-sample/src/main/java/io/sentry/sample/NativeSample.java index 3d027dfaa..712f99ff7 100644 --- a/sentry-sample/src/main/java/io/sentry/sample/NativeSample.java +++ b/sentry-sample/src/main/java/io/sentry/sample/NativeSample.java @@ -3,7 +3,7 @@ public class NativeSample { public static native void crash(); - public static native void message(); + public static native String message(); static { System.loadLibrary("native-sample"); diff --git a/sentry-sample/src/main/res/layout/activity_main.xml b/sentry-sample/src/main/res/layout/activity_main.xml index ed6a22103..6ad003ae5 100644 --- a/sentry-sample/src/main/res/layout/activity_main.xml +++ b/sentry-sample/src/main/res/layout/activity_main.xml @@ -1,43 +1,51 @@ + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + tools:context=".MainActivity"> -