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

Catch errors on content scan #150

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
12 changes: 7 additions & 5 deletions lib/localization/arb/intl_de.arb
Original file line number Diff line number Diff line change
Expand Up @@ -317,9 +317,11 @@
"playerInterfaceColorStyleThemeBackgroundColor": "Hintergrundfarbe des Farbthemas",
"resolveConflict": "Konflikt auflößen",
"conflictExplanation": "Das System hat andere Lieblingstitel als Sweyer, bitte wähle welche als Favorit markiert bleiben sollen. Es werden nur die Lieder angezeigt, die im Konflikt stehen.",
"debugTextScaleFactor": "Text scale factor",
"debugShowDebugOverlay": "Show debug overlay",
"debugShowPerformanceOverlay": "Show performance overlay",
"debugShowCheckerboardRasterCacheImages": "Show checkerboard raster cache images",
"debugShowSemanticsDebugger": "Show semantics debugger"
"debugTextScaleFactor": "Textskalierungsfaktor",
"debugShowDebugOverlay": "Debug-Einblendung anzeigen",
"debugShowPerformanceOverlay": "Leistungs-Einblendung anzeigen",
"debugShowCheckerboardRasterCacheImages": "Schachbrett-Raster-Cache-Bilder anzeigen",
"debugShowSemanticsDebugger": "Semantik-Debugger anzeigen",
"failedToInitialize": "Die Musiksuche ist fehlgeschlagen",
"retry": "Nochmal versuchen"
}
4 changes: 3 additions & 1 deletion lib/localization/arb/intl_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -321,5 +321,7 @@
"debugShowDebugOverlay": "Show debug overlay",
"debugShowPerformanceOverlay": "Show performance overlay",
"debugShowCheckerboardRasterCacheImages": "Show checkerboard raster cache images",
"debugShowSemanticsDebugger": "Show semantics debugger"
"debugShowSemanticsDebugger": "Show semantics debugger",
"failedToInitialize": "Failed to search for music",
"retry": "Retry"
}
12 changes: 7 additions & 5 deletions lib/localization/arb/intl_ru.arb
Original file line number Diff line number Diff line change
Expand Up @@ -266,9 +266,11 @@
"playerInterfaceColorStyleThemeBackgroundColor": "Цвет фона из темы",
"resolveConflict": "Разрешить конфликт",
"conflictExplanation": "Избранные треки в системе отличаются от Sweyer, выберите, какие из них следует оставить в качестве избранных. Показаны только конфликтующие треки.",
"debugTextScaleFactor": "Text scale factor",
"debugShowDebugOverlay": "Show debug overlay",
"debugShowPerformanceOverlay": "Show performance overlay",
"debugShowCheckerboardRasterCacheImages": "Show checkerboard raster cache images",
"debugShowSemanticsDebugger": "Show semantics debugger"
"debugTextScaleFactor": "Масштаб текста",
"debugShowDebugOverlay": "Показать оверлей отладки",
"debugShowPerformanceOverlay": "Показать оверлей производительности",
"debugShowCheckerboardRasterCacheImages": "Показать изображения кеша растровых данных в виде шахматной доски",
"debugShowSemanticsDebugger": "Показать отладчик семантики",
"failedToInitialize": "Не удалось найти музыку",
"retry": "Попробовать снова"
}
4 changes: 3 additions & 1 deletion lib/localization/arb/intl_tr.arb
Original file line number Diff line number Diff line change
Expand Up @@ -321,5 +321,7 @@
"debugShowDebugOverlay": "Hata ayıklama bölümünü göster",
"debugShowPerformanceOverlay": "Performans bölümünü göster",
"debugShowCheckerboardRasterCacheImages": "Dama tahtası tarama önbelleği fotoğraflarını göster",
"debugShowSemanticsDebugger": "Anlambilim hata ayıklayıcısını göster"
"debugShowSemanticsDebugger": "Anlambilim hata ayıklayıcısını göster",
"failedToInitialize": "Müzik araması başarısız oldu",
"retry": "Tekrar dene"
}
45 changes: 28 additions & 17 deletions lib/logic/player/content.dart
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,10 @@ class ContentControl extends Control {
bool get initializing => _initializeCompleter != null;
Completer<void>? _initializeCompleter;

/// Whether there was an error during initialization.
bool get failedToInitialize => _failedToInitialize;
bool _failedToInitialize = false;

/// The main data app initialization function, initializes all queues.
/// Also handles no-permissions situations.
@override
Expand All @@ -232,23 +236,30 @@ class ContentControl extends Control {
if (Permissions.instance.granted) {
// TODO: prevent initializing if already initialized
_initializeCompleter = Completer();
emitContentChange(); // update UI to show "Searching songs" screen
_restoreSorts();
await Future.any([
_initializeCompleter!.future,
Future.wait([
for (final contentType in ContentType.values)
refetch(contentType, updateQueues: false, emitChangeEvent: false),
]),
]);
if (!_empty && _initializeCompleter != null && !_initializeCompleter!.isCompleted) {
// _initQuickActions();
await QueueControl.instance.init();
PlaybackControl.instance.init();
await MusicPlayer.instance.init();
await FavoritesControl.instance.init();
PlayerInterfaceColorStyleControl.instance.init();
AppWidgetControl.instance.init();
_failedToInitialize = false;
try {
emitContentChange(); // update UI to show "Searching songs" screen
_restoreSorts();
await Future.any([
_initializeCompleter!.future,
Future.wait([
for (final contentType in ContentType.values)
refetch(contentType, updateQueues: false, emitChangeEvent: false),
]),
]);
if (!_empty && _initializeCompleter != null && !_initializeCompleter!.isCompleted) {
// _initQuickActions();
await QueueControl.instance.init();
PlaybackControl.instance.init();
await MusicPlayer.instance.init();
await FavoritesControl.instance.init();
PlayerInterfaceColorStyleControl.instance.init();
AppWidgetControl.instance.init();
}
} catch (error, stack) {
_failedToInitialize = true;
FirebaseCrashlytics.instance.recordError(error, stack, reason: 'initializing ContentControl', fatal: true);
dispose();
}
_initializeCompleter = null;
}
Expand Down
30 changes: 30 additions & 0 deletions lib/routes/home_route/home_route.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ class _InitialRouteState extends State<InitialRoute> {
builder: (context, value, child) {
if (ContentControl.instance.stateNullable == null) {
_animateNotMainUi();
if (ContentControl.instance.failedToInitialize) {
return const _InitializationFailedScreen();
}
return const _SongsEmptyScreen();
} else {
return StreamBuilder(
Expand Down Expand Up @@ -168,6 +171,33 @@ class _SearchingSongsScreen extends StatelessWidget {
}
}

/// Screen displayed when initialization of the ContentControl failed.
class _InitializationFailedScreen extends StatelessWidget {
const _InitializationFailedScreen();

@override
Widget build(BuildContext context) {
final l10n = getl10n(context);
return CenterContentScreen(
text: l10n.failedToInitialize,
widget: ConstrainedBox(
constraints: const BoxConstraints(
minHeight: 40.0,
minWidth: 130.0,
),
child: AppButton(
text: l10n.retry,
onPressed: _handleRetryRequest,
),
),
);
}

Future<void> _handleRetryRequest() async {
await ContentControl.instance.init();
}
}

/// Screen displayed when no songs had been found
class _SongsEmptyScreen extends StatefulWidget {
const _SongsEmptyScreen();
Expand Down
55 changes: 22 additions & 33 deletions test/fakes/fake_sweyer_plugin_platform.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:collection/collection.dart';
import 'dart:collection';

import 'package:flutter/services.dart';
import 'package:sweyer_plugin/sweyer_plugin.dart';
import '../test.dart';
Expand All @@ -24,6 +25,9 @@ class FavoriteLogEntry {
int get hashCode => Object.hash(Object.hashAllUnordered(songs), setFavorite);
}

/// A function that produces raw content data.
typedef RawContentFactory = Iterable<Map<String, dynamic>> Function();

class FakeSweyerPluginPlatform extends SweyerPluginPlatform {
FakeSweyerPluginPlatform(TestWidgetsFlutterBinding binding) {
instance = this;
Expand All @@ -37,15 +41,14 @@ class FakeSweyerPluginPlatform extends SweyerPluginPlatform {
}
static late FakeSweyerPluginPlatform instance;

List<Song>? songs;
List<Album>? albums;
List<Playlist>? playlists;
List<Artist>? artists;

List<Map<String, dynamic>>? rawSongs;
List<Map<String, dynamic>>? rawAlbums;
List<Map<String, dynamic>>? rawPlaylists;
List<Map<String, dynamic>>? rawArtists;
set songs(List<Song> value) => songsFactory = () => value.map((song) => song.toMap());
RawContentFactory songsFactory = () => [songWith().toMap()];
set albums(List<Album> value) => albumsFactory = () => value.map((album) => album.toMap());
RawContentFactory albumsFactory = () => [albumWith().toMap()];
set playlists(List<Playlist> value) => playlistsFactory = () => value.map((playlist) => playlist.toMap());
RawContentFactory playlistsFactory = () => [playlistWith().toMap()];
set artists(List<Artist> value) => artistsFactory = () => value.map((artist) => artist.toMap());
RawContentFactory artistsFactory = () => [artistWith().toMap()];

@override
Future<void> createPlaylist(String name) async {}
Expand All @@ -64,8 +67,10 @@ class FakeSweyerPluginPlatform extends SweyerPluginPlatform {
required List<int> songIds,
required int playlistId,
}) async {
final playlist = playlists!.firstWhere((playlist) => playlist.id == playlistId);
playlist.songIds.insertAll(index, songIds);
final playlists = playlistsFactory().toList();
final playlist = playlists.firstWhere((playlist) => playlist['id'] == playlistId);
(playlist.putIfAbsent('songIds', () => []) as List).insertAll(index, songIds);
playlistsFactory = () => playlists;
}

@override
Expand Down Expand Up @@ -101,20 +106,12 @@ class FakeSweyerPluginPlatform extends SweyerPluginPlatform {

@override
Future<Iterable<Map<String, dynamic>>> retrieveAlbums() async {
return CombinedIterableView([
rawAlbums,
albums?.map((album) => album.toMap()),
if (rawAlbums == null && albums == null) [albumWith().toMap()]
].whereNotNull());
return albumsFactory();
}

@override
Future<Iterable<Map<String, dynamic>>> retrieveArtists() async {
return CombinedIterableView([
rawArtists,
artists?.map((artist) => artist.toMap()),
if (rawArtists == null && artists == null) [artistWith().toMap()]
].whereNotNull());
return artistsFactory();
}

@override
Expand All @@ -124,20 +121,12 @@ class FakeSweyerPluginPlatform extends SweyerPluginPlatform {

@override
Future<Iterable<Map<String, dynamic>>> retrievePlaylists() async {
return CombinedIterableView([
rawPlaylists,
playlists?.map((playlist) => playlist.toMap()),
if (rawPlaylists == null && playlists == null) [playlistWith().toMap()]
].whereNotNull());
return playlistsFactory();
}

@override
Future<Iterable<Map<String, dynamic>>> retrieveSongs() async {
return CombinedIterableView([
rawSongs,
songs?.map((song) => song.toMap()),
if (rawSongs == null && songs == null) [songWith().toMap()]
].whereNotNull());
return songsFactory();
}

/// The log of all recorded [setSongsFavorite] calls.
Expand All @@ -151,5 +140,5 @@ class FakeSweyerPluginPlatform extends SweyerPluginPlatform {
}

/// Get a song by its [id].
Song? _songById(int id) => songs?.firstWhere((song) => song.id == id);
Song? _songById(int id) => ContentControl.instance.state.allSongs.songs.firstWhere((song) => song.id == id);
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions test/golden/routes_golden_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ void main() {
}, (WidgetTester tester) async {
await tester.pumpAndSettle();
});

testAppGoldens('init_error_screen', setUp: () {
registerAppSetup(() {
CrashlyticsObserver(TestWidgetsFlutterBinding.ensureInitialized(), throwFatalErrors: false);
FakeSweyerPluginPlatform.instance.songsFactory = () => throw TypeError();
});
}, (WidgetTester tester) async {
await tester.pumpAndSettle();
});
});

group('tabs_route', () {
Expand Down
18 changes: 10 additions & 8 deletions test/logic/models/content_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ void main() {
late CrashlyticsObserver crashlyticsObserver;
registerAppSetup(() {
crashlyticsObserver = CrashlyticsObserver(TestWidgetsFlutterBinding.ensureInitialized());
FakeSweyerPluginPlatform.instance.rawAlbums = validAlbums.map((element) => element.$1).toList();
FakeSweyerPluginPlatform.instance.albumsFactory =
() => validAlbums.map((element) => element.$1).toList();
});
TestWidgetsFlutterBinding.ensureInitialized().runAppTestWithoutUi(() {
expect(
Expand Down Expand Up @@ -142,7 +143,8 @@ void main() {
late CrashlyticsObserver crashlyticsObserver;
registerAppSetup(() {
crashlyticsObserver = CrashlyticsObserver(TestWidgetsFlutterBinding.ensureInitialized());
FakeSweyerPluginPlatform.instance.rawArtists = validArtists.map((element) => element.$1).toList();
FakeSweyerPluginPlatform.instance.artistsFactory =
() => validArtists.map((element) => element.$1).toList();
});
TestWidgetsFlutterBinding.ensureInitialized().runAppTestWithoutUi(() {
expect(
Expand Down Expand Up @@ -182,7 +184,7 @@ void main() {
late CrashlyticsObserver crashlyticsObserver;
registerAppSetup(() {
crashlyticsObserver = CrashlyticsObserver(TestWidgetsFlutterBinding.ensureInitialized());
FakeSweyerPluginPlatform.instance.rawPlaylists = validPlaylists.map((element) => element.$1).toList();
FakeSweyerPluginPlatform.instance.playlistsFactory = () => validPlaylists.map((element) => element.$1).toList();
});
TestWidgetsFlutterBinding.ensureInitialized().runAppTestWithoutUi(() {
expect(
Expand Down Expand Up @@ -253,7 +255,7 @@ void main() {
late CrashlyticsObserver crashlyticsObserver;
registerAppSetup(() {
crashlyticsObserver = CrashlyticsObserver(TestWidgetsFlutterBinding.ensureInitialized());
FakeSweyerPluginPlatform.instance.rawSongs = validSongs.map((element) => element.$1).toList();
FakeSweyerPluginPlatform.instance.songsFactory = () => validSongs.map((element) => element.$1).toList();
});
TestWidgetsFlutterBinding.ensureInitialized().runAppTestWithoutUi(() {
expect(
Expand All @@ -279,7 +281,7 @@ void main() {
late CrashlyticsObserver crashlyticsObserver;
registerAppSetup(() {
crashlyticsObserver = CrashlyticsObserver(TestWidgetsFlutterBinding.ensureInitialized());
FakeSweyerPluginPlatform.instance.rawAlbums = invalidAlbums;
FakeSweyerPluginPlatform.instance.albumsFactory = () => invalidAlbums;
});
TestWidgetsFlutterBinding.ensureInitialized().runAppTestWithoutUi(() {
expect(
Expand Down Expand Up @@ -307,7 +309,7 @@ void main() {
late CrashlyticsObserver crashlyticsObserver;
registerAppSetup(() {
crashlyticsObserver = CrashlyticsObserver(TestWidgetsFlutterBinding.ensureInitialized());
FakeSweyerPluginPlatform.instance.rawArtists = invalidArtists;
FakeSweyerPluginPlatform.instance.artistsFactory = () => invalidArtists;
});
TestWidgetsFlutterBinding.ensureInitialized().runAppTestWithoutUi(() {
expect(
Expand Down Expand Up @@ -337,7 +339,7 @@ void main() {
late CrashlyticsObserver crashlyticsObserver;
registerAppSetup(() {
crashlyticsObserver = CrashlyticsObserver(TestWidgetsFlutterBinding.ensureInitialized());
FakeSweyerPluginPlatform.instance.rawPlaylists = invalidPlaylists;
FakeSweyerPluginPlatform.instance.playlistsFactory = () => invalidPlaylists;
});
TestWidgetsFlutterBinding.ensureInitialized().runAppTestWithoutUi(() {
expect(
Expand Down Expand Up @@ -377,7 +379,7 @@ void main() {
late CrashlyticsObserver crashlyticsObserver;
registerAppSetup(() {
crashlyticsObserver = CrashlyticsObserver(TestWidgetsFlutterBinding.ensureInitialized());
FakeSweyerPluginPlatform.instance.rawSongs = invalidSongs;
FakeSweyerPluginPlatform.instance.songsFactory = () => invalidSongs;
});
TestWidgetsFlutterBinding.ensureInitialized().runAppTestWithoutUi(() {
expect(
Expand Down
39 changes: 39 additions & 0 deletions test/routes/home_route_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,45 @@ void main() {
});
});

testWidgets('error screen - shows when searching for tracks fails', (WidgetTester tester) async {
late CrashlyticsObserver crashlyticsObserver;
var songsFactoryCallCount = 0;
registerAppSetup(() {
crashlyticsObserver = CrashlyticsObserver(tester.binding, throwFatalErrors: false);
FakeSweyerPluginPlatform.instance.songsFactory = () {
songsFactoryCallCount += 1;
if (songsFactoryCallCount <= 2) {
throw TypeError();
}
return [songWith().toMap()];
};
});
await tester.runAppTest(() async {
// Expect to find the error screen
await tester.pumpAndSettle();
expect(find.text(l10n.failedToInitialize), findsOneWidget);
expect(songsFactoryCallCount, 1);
expect(crashlyticsObserver.fatalErrorCount, 1);
expect(find.ancestor(of: find.text(l10n.retry), matching: find.byType(AppButton)), findsOneWidget);

// Expect to find the error screen again after a retry
await tester.tap(find.text(l10n.retry));
await tester.pumpAndSettle();
expect(songsFactoryCallCount, 2);
expect(crashlyticsObserver.fatalErrorCount, 2);
expect(find.ancestor(of: find.text(l10n.retry), matching: find.byType(AppButton)), findsOneWidget);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could add another test that it actually can display songs after a successful retry?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


// Expect to correctly load the content on the third try
await tester.tap(find.text(l10n.retry));
await tester.pumpAndSettle();
expect(songsFactoryCallCount, 3);
expect(crashlyticsObserver.fatalErrorCount, 2);
expect(find.text(l10n.retry), findsNothing);
expect(find.byType(Home), findsOneWidget);
expect(ContentControl.instance.state.allSongs.songs, [songWith()]);
});
});

testWidgets('home screen - shows when permissions are granted and not searching for tracks',
(WidgetTester tester) async {
late AppWidgetChannelObserver appWidgetChannelObserver;
Expand Down