Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test(integration): enable e2e #2476

Merged
merged 26 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 15 additions & 30 deletions .github/workflows/flutter_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ on:
- "flutter/**"

env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_AUTH_TOKEN_E2E: ${{ secrets.SENTRY_AUTH_TOKEN_E2E }}

jobs:
vaind marked this conversation as resolved.
Show resolved Hide resolved
cancel-previous-workflow:
Expand All @@ -25,7 +25,7 @@ jobs:
access_token: ${{ github.token }}

test-android:
runs-on: macos-13
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
Expand All @@ -38,6 +38,12 @@ jobs:
- name: checkout
uses: actions/checkout@v4

- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm

- uses: actions/setup-java@v4
with:
distribution: "adopt"
Expand All @@ -56,28 +62,7 @@ jobs:
- name: Gradle cache
uses: gradle/gradle-build-action@ac2d340dc04d9e1113182899e983b5400c17cda1 # [email protected]

- name: AVD cache
uses: actions/cache@v4
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: avd-31

- name: create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d #[email protected]
with:
working-directory: ./flutter/example
api-level: 31
profile: Nexus 6
arch: x86_64
force-avd-creation: false
avd-name: macOS-avd-x86_64-31
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: echo 'Generated AVD snapshot for caching.'
# TODO: fix emulator caching, in ubuntu-latest emulator won't boot: https://github.com/ReactiveCircus/android-emulator-runner/issues/278

- name: build apk
working-directory: ./flutter/example/android
Expand All @@ -91,8 +76,8 @@ jobs:
profile: Nexus 6
arch: x86_64
force-avd-creation: false
avd-name: macOS-avd-x86_64-31
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
avd-name: avd-x86_64-31
emulator-options: -no-snapshot-save -no-window -accel on -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: ./gradlew testDebugUnitTest

Expand All @@ -104,10 +89,10 @@ jobs:
profile: Nexus 6
arch: x86_64
force-avd-creation: false
avd-name: macOS-avd-x86_64-31
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
avd-name: avd-x86_64-31
emulator-options: -no-snapshot-save -no-window -accel on -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: flutter test integration_test/all.dart --verbose
script: flutter test integration_test/all.dart --dart-define SENTRY_AUTH_TOKEN_E2E=$SENTRY_AUTH_TOKEN_E2E --verbose

cocoa:
name: "${{ matrix.target }} | ${{ matrix.sdk }}"
Expand Down Expand Up @@ -158,7 +143,7 @@ jobs:
- name: run integration test
# Disable flutter integration tests for iOS for now (https://github.com/getsentry/sentry-dart/issues/1605#issuecomment-1695809346)
if: ${{ matrix.target != 'ios' }}
run: flutter test -d "${{ steps.device.outputs.name }}" integration_test/all.dart --verbose
run: flutter test -d "${{ steps.device.outputs.name }}" integration_test/all.dart --dart-define SENTRY_AUTH_TOKEN_E2E=$SENTRY_AUTH_TOKEN_E2E --verbose

- name: run native test
# We only have the native unit test package in the iOS xcodeproj at the moment.
Expand Down
139 changes: 76 additions & 63 deletions flutter/example/integration_test/integration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,19 @@ import 'dart:convert';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart';
import 'package:integration_test/integration_test.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:sentry_flutter_example/main.dart';

import 'utils.dart';

void main() {
// const org = 'sentry-sdks';
// const slug = 'sentry-flutter';
// const authToken = String.fromEnvironment('SENTRY_AUTH_TOKEN');
const org = 'sentry-sdks';
const slug = 'sentry-flutter';
const authToken = String.fromEnvironment('SENTRY_AUTH_TOKEN_E2E');
const fakeDsn = 'https://[email protected]/1234567';

TestWidgetsFlutterBinding.ensureInitialized();
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

tearDown(() async {
await Sentry.close();
Expand Down Expand Up @@ -98,7 +101,7 @@ void main() {

// ignore: deprecated_member_use_from_same_package
// ignore: deprecated_member_use
final associatedEventId = await Sentry.captureMessage("Associated");
final associatedEventId = await Sentry.captureMessage('Associated');
final feedback = SentryFeedback(
message: 'message',
contactEmail: '[email protected]',
Expand Down Expand Up @@ -162,66 +165,76 @@ void main() {
await transaction.finish();
});

// group('e2e', () {
// var output = find.byKey(const Key('output'));
// late Fixture fixture;
//
// setUp(() {
// fixture = Fixture();
// });
//
// testWidgets('captureException', (tester) async {
// await setupSentryAndApp(tester,
// dsn: exampleDsn, beforeSendCallback: fixture.beforeSend);
//
// await tester.tap(find.text('captureException'));
// await tester.pumpAndSettle();
//
// final text = output.evaluate().single.widget as Text;
// final id = text.data!;
//
// final uri = Uri.parse(
// 'https://sentry.io/api/0/projects/$org/$slug/events/$id/',
// );
// expect(authToken, isNotEmpty);
//
// final event = await fixture.poll(uri, authToken);
// expect(event, isNotNull);
//
// final sentEvent = fixture.sentEvent;
// expect(sentEvent, isNotNull);
//
// final tags = event!["tags"] as List<dynamic>;
//
// expect(sentEvent!.eventId.toString(), event["id"]);
// expect("_Exception: Exception: captureException", event["title"]);
// expect(sentEvent.release, event["release"]["version"]);
// expect(
// 2,
// (tags.firstWhere((e) => e["value"] == sentEvent.environment) as Map)
// .length);
// expect(sentEvent.fingerprint, event["fingerprint"] ?? []);
// expect(
// 2,
// (tags.firstWhere((e) => e["value"] == SentryLevel.error.name) as Map)
// .length);
// expect(sentEvent.logger, event["logger"]);
//
// final dist = tags.firstWhere((element) => element['key'] == 'dist');
// expect('1', dist['value']);
//
// final environment =
// tags.firstWhere((element) => element['key'] == 'environment');
// expect('integration', environment['value']);
// });
// });
group('e2e', () {
var output = find.byKey(const Key('output'));
late Fixture fixture;

setUp(() {
fixture = Fixture();
});

testWidgets('captureException', (tester) async {
late Uri uri;

await restoreFlutterOnErrorAfter(() async {
buenaflor marked this conversation as resolved.
Show resolved Hide resolved
await setupSentryAndApp(tester,
dsn: exampleDsn, beforeSendCallback: fixture.beforeSend);

await tester.tap(find.text('captureException'));
await tester.pumpAndSettle();

final text = output.evaluate().single.widget as Text;
final id = text.data!;

uri = Uri.parse(
'https://sentry.io/api/0/projects/$org/$slug/events/$id/',
);
});

expect(authToken, isNotEmpty);

final event = await fixture.poll(uri, authToken);
expect(event, isNotNull);

final sentEvents = fixture.sentEvents
.where((el) => el!.eventId.toString() == event!['id']);
expect(
sentEvents.length, 1); // one button click should only send one error
final sentEvent = sentEvents.first;

final tags = event!['tags'] as List<dynamic>;

print('event id: ${event['id']}');
print('event title: ${event['title']}');
expect(sentEvent!.eventId.toString(), event['id']);
expect('_Exception: Exception: captureException', event['title']);
expect(sentEvent.release, event['release']['version']);
expect(
2,
(tags.firstWhere((e) => e['value'] == sentEvent.environment) as Map)
.length);
expect(sentEvent.fingerprint, event['fingerprint'] ?? []);
expect(
2,
(tags.firstWhere((e) => e['value'] == SentryLevel.error.name) as Map)
.length);
expect(sentEvent.logger, event['logger']);

final dist = tags.firstWhere((element) => element['key'] == 'dist');
expect('1', dist['value']);

final environment =
tags.firstWhere((element) => element['key'] == 'environment');
expect('integration', environment['value']);
});
});
}

class Fixture {
SentryEvent? sentEvent;
List<SentryEvent?> sentEvents = [];

FutureOr<SentryEvent?> beforeSend(SentryEvent event, Hint hint) async {
sentEvent = event;
sentEvents.add(event);
return event;
}

Expand All @@ -237,16 +250,16 @@ class Fixture {

while (retries < maxRetries) {
try {
print("Trying to fetch $url [try $retries/$maxRetries]");
print('Trying to fetch $url [try $retries/$maxRetries]');
final response = await client.get(
url,
headers: <String, String>{'Authorization': 'Bearer $authToken'},
);
print("Response status code: ${response.statusCode}");
print('Response status code: ${response.statusCode}');
if (response.statusCode == 200) {
return jsonDecode(utf8.decode(response.bodyBytes));
} else if (response.statusCode == 401) {
print("Cannot fetch $url - invalid auth token.");
print('Cannot fetch $url - invalid auth token.');
break;
}
} catch (e) {
Expand Down
22 changes: 22 additions & 0 deletions flutter/example/integration_test/utils.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import 'dart:async';

import 'package:flutter/cupertino.dart';

/// Restores Flutter's `FlutterError.onError` to its original state after executing a function.
///
/// `testWidgets` and `SentryFlutter.init` automatically override `FlutterError.onError`.
/// If `FlutterError.onError` is not restored to its original state and an assertion fails
/// Flutter will complain and throw an error.
///
/// This function ensures `FlutterError.onError` is restored to its initial state after `fn` runs.
/// It must be called **after** the function executes but **before** any assertions.
FutureOr<void> restoreFlutterOnErrorAfter(FutureOr<void> Function() fn) async {
final originalOnError = FlutterError.onError;
await fn();
final overriddenOnError = FlutterError.onError;

FlutterError.onError = (FlutterErrorDetails details) {
if (overriddenOnError != originalOnError) overriddenOnError?.call(details);
originalOnError?.call(details);
};
}
2 changes: 2 additions & 0 deletions flutter/lib/src/screenshot/widget_filter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -247,9 +247,11 @@ extension on Element {
@internal
extension Opaqueness on Color {
@pragma('vm:prefer-inline')
// ignore: deprecated_member_use
bool get isOpaque => alpha == 0xff;

@pragma('vm:prefer-inline')
// ignore: deprecated_member_use
Color asOpaque() => isOpaque ? this : Color.fromARGB(0xff, red, green, blue);
buenaflor marked this conversation as resolved.
Show resolved Hide resolved
}

Expand Down
Loading