perf: counts worked out once and kept until something changes

On a copy of a real library (8,067 cards, 20,907 reviews, 20
studysets), every open list read every card (0.8 s), every review
(0.25 s) and replayed schedules after every change, each on its own:
the library, every studyset open on the stack and its topics.

- CardRepository keeps each studyset's cards and every card's stamp
  until a card changes; a read begun before a change keeps nothing.
- ReviewRepository keeps the whole log once many cards' reviews are
  asked for, updated by this device's writes and read anew after a
  sync brings others'.
- StudysetProgressRepository works out every studyset's progress once
  for every list and hands the last to whoever follows it next;
  StudysetListBloc reads it.

A full recount now takes about 65 ms. Home no longer rereads every
revision after each rating.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-17 01:21:16 +02:00
commit 301751607d
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
9 changed files with 836 additions and 258 deletions

View file

@ -25,6 +25,7 @@ import 'package:halkyon_learn_flutter/src/feature/publish/data/publish_repositor
import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_deletions.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_progress_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_tabs.dart';
import 'package:halkyon_learn_flutter/src/feature/tag/data/tag_repository.dart';
@ -65,6 +66,8 @@ PersonDependencies personDependencies({Future<void> Function()? onDispose}) {
);
final pdfSources = PdfSourceRepository(FakePdfSourceGateway(), assets);
final tags = TagRepository(FakeTagGateway());
final reviews = ReviewRepository(FakeReviewGateway());
final settings = SettingsRepository(FakeSettingsGateway());
return PersonDependencies(
sync: SyncService(({required onMerge}) async {}),
studysets: studysets,
@ -73,9 +76,18 @@ PersonDependencies personDependencies({Future<void> Function()? onDispose}) {
paths: paths,
pathProgress: PathProgressRepository(FakePathAnswerGateway()),
pdfSources: pdfSources,
reviews: ReviewRepository(FakeReviewGateway()),
reviews: reviews,
studysetProgress: StudysetProgressRepository(
studysets: studysets,
cards: cards,
notes: notes,
paths: paths,
pdfs: pdfSources,
reviews: reviews,
settings: settings,
),
tags: tags,
settings: SettingsRepository(FakeSettingsGateway()),
settings: settings,
assets: assets,
deletions: StudysetDeletions(studysets, cards, notes, paths, pdfSources),
noteDeletions: NoteDeletions(notes),

View file

@ -591,6 +591,8 @@ void main() {
..add(const CardComposerEventDeleted('new'))
..add(CardComposerEventDeleted(neko));
await until((s) => s.items.isEmpty);
// The list empties at once; the stored card goes after.
await pumpEventQueue();
expect(await cards.cardsOf('japanese'), isEmpty);
expect(gateway.deletions, ['blank $neko', 'delete $neko']);

View file

@ -5,6 +5,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/src/core/content/payload.dart';
import 'package:halkyon_learn_flutter/src/core/content/revision_merge.dart';
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
import 'package:halkyon_learn_flutter/src/feature/card/data/card_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/card/data/card_payload.dart';
import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/card/model/card.dart';
@ -12,6 +13,24 @@ import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
import '../../../support/fake_card_gateway.dart';
/// A [FakeCardGateway] counting how often it reads a studyset's cards.
class _CountingCardGateway() extends FakeCardGateway {
int reads = 0;
int wholeReads = 0;
@override
Future<List<CardRow>> cardsOf(String studysetId) {
reads++;
return super.cardsOf(studysetId);
}
@override
Future<List<CardRevisionRow>> allRevisions() {
wholeReads++;
return super.allRevisions();
}
}
RichContent text(String text) => RichContent({
'document': {'type': 'page', 'note': text},
});
@ -334,6 +353,58 @@ void main() {
expect(await repository.cardsOf('kanji'), hasLength(1));
});
test("reads a studyset's cards once until something changes, here or "
'synced from elsewhere', () async {
final counting = _CountingCardGateway();
final cards = CardRepository(counting);
addTearDown(cards.dispose);
final id = await cards.create('japanese', card('猫', 'cat'));
await cards.create('french', card('chat', 'cat'));
final reads = counting.reads;
expect(fronts(await cards.cardsOf('japanese')), ['猫']);
expect(fronts(await cards.cardsIn(['japanese', 'french'])), ['猫', 'chat']);
expect(fronts(await cards.cardsOf('japanese')), ['猫']);
expect(counting.reads, reads + 2, reason: 'each studyset read once');
await cards.create('japanese', card('犬', 'dog'));
expect(fronts(await cards.cardsOf('japanese')), ['猫', '犬']);
counting.receiveRemoteRevision(
id: 'rev-9000',
cardId: id,
parents: (await cards.cardsOf('japanese')).first.heads,
payload: encodeCardPayload(card('ねこ', 'cat')),
);
await Future<void>.delayed(Duration.zero);
expect(fronts(await cards.cardsOf('japanese')), ['ねこ', '犬']);
});
test('reads every card once until one changes', () async {
final counting = _CountingCardGateway();
final cards = CardRepository(counting);
addTearDown(cards.dispose);
await cards.create('japanese', card('猫', 'cat'));
expect(await cards.allCards(), hasLength(1));
expect(await cards.allCards(), hasLength(1));
expect(counting.wholeReads, 1);
await cards.create('french', card('chat', 'cat'));
expect(await cards.allCards(), hasLength(2));
expect(counting.wholeReads, 2);
});
test('a read begun before a change keeps nothing of it for later', () async {
await repository.create('japanese', card('猫', 'cat'));
final reading = repository.cardsOf('japanese');
await repository.create('japanese', card('犬', 'dog'));
await reading;
expect(fronts(await repository.cardsOf('japanese')), ['猫', '犬']);
});
test('follows edits from other devices', () async {
await repository.create('japanese', card('猫', 'cat'));
final created = (await repository.cardsOf('japanese')).single;

View file

@ -1,4 +1,7 @@
import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/src/feature/study/data/review_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/study/model/card_schedule.dart';
import 'package:halkyon_learn_flutter/src/feature/study/model/rating.dart';
@ -6,6 +9,27 @@ import 'package:halkyon_learn_flutter/src/feature/study/model/review.dart';
import '../../../support/fake_review_gateway.dart';
/// A [FakeReviewGateway] counting how often it reads the whole log, whose
/// [receiveRemote] plays a sync bringing another device's review.
class _CountingReviewGateway() extends FakeReviewGateway {
int wholeReads = 0;
final _remote = StreamController<void>.broadcast();
@override
Future<List<ReviewRow>> allReviews() {
wholeReads++;
return super.allReviews();
}
@override
Stream<void> get remoteChanges => _remote.stream;
void receiveRemote(ReviewRow row) {
rows.add(row);
_remote.add(null);
}
}
void main() {
late FakeReviewGateway gateway;
late ReviewRepository reviews;
@ -35,6 +59,67 @@ void main() {
},
);
test("keeps the whole log once many cards' reviews are asked for, with "
"this device's ratings, undos and fresh starts in it; a sync bringing "
"others' reads it anew", () async {
final counting = _CountingReviewGateway();
final repository = ReviewRepository(counting);
addTearDown(repository.dispose);
final cards = {for (var i = 0; i < 250; i++) 'card-$i'};
final at = DateTime.utc(2026, 10, 5, 9);
final later = at.add(const Duration(minutes: 1));
Future<void> rate(String card, DateTime when) => repository.record(
cardId: card,
rating: Rating.good,
reviewedAt: when,
duration: const Duration(seconds: 3),
);
Future<List<(String, StudyMode)>> kept() async => [
for (final review in await repository.reviewsOf(cards))
(review.cardId, review.mode),
];
await rate('card-0', at);
// A few cards' reviews are read on their own.
expect(await repository.reviewsOf({'card-0'}), hasLength(1));
expect(counting.wholeReads, 0);
expect(await kept(), [('card-0', StudyMode.review)]);
expect(counting.wholeReads, 1);
await rate('card-1', at);
await rate('card-2', later);
await repository.undo(
cardId: 'card-2',
reviewedAt: later,
mode: StudyMode.review,
);
await repository.reset(['card-0'], later);
expect(
await kept(),
unorderedEquals([
('card-0', StudyMode.review),
('card-0', StudyMode.reset),
('card-1', StudyMode.review),
]),
);
expect(await repository.all(), hasLength(3));
expect(counting.wholeReads, 1);
counting.receiveRemote((
id: 'remote',
cardId: 'card-3',
rating: Rating.good.value,
reviewedAt: later,
durationMs: 1000,
mode: StudyMode.review.value,
));
await Future<void>.delayed(Duration.zero);
expect(await repository.all(), hasLength(4));
expect(counting.wholeReads, 2);
});
test('reads only the reviews of the cards asked for', () async {
for (final card in ['a', 'b']) {
await reviews.record(

View file

@ -5,7 +5,6 @@ import 'package:fake_async/fake_async.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_repository.dart';
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
import 'package:halkyon_learn_flutter/src/feature/card/data/card_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_repository.dart';
@ -15,6 +14,7 @@ import 'package:halkyon_learn_flutter/src/feature/learning_path/model/learning_p
import 'package:halkyon_learn_flutter/src/feature/learning_path/model/progress.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/model/step_content.dart';
import 'package:halkyon_learn_flutter/src/feature/pdf_source/data/pdf_source_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/study/data/review_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/study/model/rating.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/bloc/exam_cubit.dart';
@ -34,23 +34,24 @@ import '../../../support/fake_studyset_gateway.dart';
import '../../../support/fake_tag_gateway.dart';
import '../../../support/memory_asset_file_cache.dart';
/// A [FakeCardGateway] counting how often it reads a studyset's cards:
/// reading the plans reads each studied studyset's once, and so does
/// reading the progress with the exam chosen.
class _CountingCardGateway() extends FakeCardGateway {
/// A [FakeReviewGateway] counting how often it reads cards' reviews: the
/// progress with an exam chosen reads them once each time it is read; the
/// plans never do. (The cards themselves come from the card repository's
/// cache while none changes.)
class _CountingReviewGateway() extends FakeReviewGateway {
int reads = 0;
@override
Future<List<CardRow>> cardsOf(String studysetId) {
Future<List<ReviewRow>> reviewsOf(Set<String> cardIds) {
reads++;
return super.cardsOf(studysetId);
return super.reviewsOf(cardIds);
}
}
void main() {
final today = DateTime(2026, 10, 14, 9);
final exam = ExamChoice(plan: 'auto:jlpt:1', date: DateTime(2026, 12, 6));
late _CountingCardGateway cardGateway;
late _CountingReviewGateway reviewGateway;
late StudysetRepository studysets;
late CardRepository cards;
late TagRepository tags;
@ -72,9 +73,9 @@ void main() {
/// JLPT-N5 and 本 untagged, and its cubit once its first reading is done
/// and every change has settled.
void start(FakeAsync async, {ExamChoice? choice}) {
cardGateway = _CountingCardGateway();
reviewGateway = _CountingReviewGateway();
studysets = StudysetRepository(FakeStudysetGateway());
cards = CardRepository(cardGateway);
cards = CardRepository(FakeCardGateway());
paths = LearningPathRepository(FakeLearningPathGateway());
pathProgress = PathProgressRepository(FakePathAnswerGateway());
tags = TagRepository(
@ -82,7 +83,7 @@ void main() {
cardChanges: cards.changes,
pathChanges: paths.changes,
);
reviews = ReviewRepository(FakeReviewGateway());
reviews = ReviewRepository(reviewGateway);
final assets = AssetRepository(
FakeAssetGateway(),
FakeAssetServerGateway(),
@ -129,19 +130,19 @@ void main() {
),
);
test('reads no cards when a review is recorded without an exam '
test('reads no progress when a review is recorded without an exam '
'chosen', () {
fakeAsync((async) {
start(async);
expect(cubit.state.plans, const [
ExamPlan(id: 'auto:jlpt:1', name: 'JLPT N5', tags: ['JLPT-N5']),
]);
final reads = cardGateway.reads;
final reads = reviewGateway.reads;
review(async, neko);
async.elapse(ExamCubit.settle * 2);
expect(cardGateway.reads, reads);
expect(reviewGateway.reads, reads);
unawaited(cubit.close());
async.flushMicrotasks();
}, initialTime: today);
@ -155,23 +156,23 @@ void main() {
run(async, () => cubit.choose(exam.plan, exam.date));
expect(cubit.state.choice, exam);
expect(cubit.state.progress?.newCards, 2);
final reads = cardGateway.reads;
final reads = reviewGateway.reads;
review(async, neko);
async.elapse(const Duration(seconds: 1));
review(async, inu);
async.elapse(const Duration(seconds: 1));
expect(cardGateway.reads, reads, reason: 'still settling');
expect(reviewGateway.reads, reads, reason: 'still settling');
async.elapse(ExamCubit.settle);
// One studyset's cards read once: the progress, not the plans.
expect(cardGateway.reads, reads + 1);
// The progress read once for both ratings.
expect(reviewGateway.reads, reads + 1);
expect(cubit.state.progress?.newCards, 0);
review(async, neko);
unawaited(cubit.close());
async.elapse(ExamCubit.settle * 2);
expect(cardGateway.reads, reads + 1);
expect(reviewGateway.reads, reads + 1);
}, initialTime: today);
});

View file

@ -24,6 +24,7 @@ import 'package:halkyon_learn_flutter/src/feature/study/model/study_progress.dar
import 'package:halkyon_learn_flutter/src/feature/study/model/studyset_study_options.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/bloc/studyset_list_bloc.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_payload.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_progress_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_content.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_tree.dart';
@ -54,6 +55,7 @@ void main() {
late FakePdfSourceGateway pdfGateway;
late PdfSourceRepository pdfs;
late PathProgressRepository pathProgress;
late StudysetProgressRepository progressOfAll;
setUp(() {
gateway = FakeStudysetGateway();
@ -75,9 +77,19 @@ void main() {
MemoryAssetFileCache(),
),
);
progressOfAll = StudysetProgressRepository(
studysets: studysets,
cards: cards,
notes: notes,
paths: paths,
pdfs: pdfs,
reviews: reviews,
settings: settings,
);
});
tearDown(() async {
await bloc.close();
await progressOfAll.dispose();
await studysets.dispose();
await cards.dispose();
await notes.dispose();
@ -102,13 +114,12 @@ void main() {
void start({String? parentId}) {
bloc = StudysetListBloc(
studysets,
progressOfAll,
cards,
notes,
paths,
pdfs,
reviews,
pathProgress,
settings,
parentId: parentId,
)..add(const StudysetListEventStarted());
}
@ -200,7 +211,14 @@ void main() {
(current) => current.withStudy(current.study.copyWith(writeByHand: true)),
);
start();
expect((await listing(['Japanese'])).progress[japanese]?.due, 1);
// Worked out again as the settings change.
await until(
() async => switch (bloc.state) {
StudysetListStateLoaded(:final progress) =>
progress[japanese]?.due == 1,
_ => false,
},
);
});
test('leaves a card buried today out of what is due', () async {

View file

@ -0,0 +1,132 @@
import 'package:clock/clock.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_repository.dart';
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/note/data/note_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/pdf_source/data/pdf_source_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/study/model/rating.dart';
import 'package:halkyon_learn_flutter/src/feature/study/model/study_progress.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_progress_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_content.dart';
import '../../../support/fake_asset_gateway.dart';
import '../../../support/fake_asset_server_gateway.dart';
import '../../../support/fake_card_gateway.dart';
import '../../../support/fake_learning_path_gateway.dart';
import '../../../support/fake_note_gateway.dart';
import '../../../support/fake_pdf_source_gateway.dart';
import '../../../support/fake_review_gateway.dart';
import '../../../support/fake_settings_gateway.dart';
import '../../../support/fake_studyset_gateway.dart';
import '../../../support/memory_asset_file_cache.dart';
void main() {
late StudysetRepository studysets;
late CardRepository cards;
late ReviewRepository reviews;
late StudysetProgressRepository progress;
setUp(() {
studysets = StudysetRepository(FakeStudysetGateway());
cards = CardRepository(FakeCardGateway());
reviews = ReviewRepository(FakeReviewGateway());
final notes = NoteRepository(FakeNoteGateway());
final paths = LearningPathRepository(FakeLearningPathGateway());
final settings = SettingsRepository(FakeSettingsGateway());
final pdfs = PdfSourceRepository(
FakePdfSourceGateway(),
AssetRepository(
FakeAssetGateway(),
FakeAssetServerGateway(),
MemoryAssetFileCache(),
),
);
progress = StudysetProgressRepository(
studysets: studysets,
cards: cards,
notes: notes,
paths: paths,
pdfs: pdfs,
reviews: reviews,
settings: settings,
);
addTearDown(() async {
await progress.dispose();
await studysets.dispose();
await cards.dispose();
await reviews.dispose();
await notes.dispose();
await paths.dispose();
await settings.dispose();
await pdfs.dispose();
});
});
Future<String> card(String studysetId) => cards.create(
studysetId,
CardContent(front: RichContent.plain('猫'), back: RichContent.plain('cat')),
);
/// Everything [progress] hands one following it, as it comes.
List<Map<String, StudyProgress>> follow() {
final seen = <Map<String, StudyProgress>>[];
final subscription = progress.watch().listen(seen.add);
addTearDown(subscription.cancel);
return seen;
}
test('works out every studyset with what is inside it, once for all '
'following it; one following later gets it at once, worked out '
'no more', () async {
final japanese = await studysets.create(StudysetContent.titled('Japanese'));
final kana = await studysets.create(
StudysetContent.titled('Kana'),
parentId: japanese,
);
await card(japanese);
await card(kana);
final first = follow();
await pumpEventQueue();
expect(first, hasLength(1));
expect(first.single[japanese]?.cards, 2);
expect(first.single[kana]?.cards, 1);
final second = follow();
await pumpEventQueue();
expect(second, [first.single]);
expect(first, hasLength(1), reason: 'nothing changed');
});
test('works it out again after a change of cards or reviews, but not when '
'the same studysets are handed again', () async {
final japanese = await studysets.create(StudysetContent.titled('Japanese'));
final neko = await card(japanese);
final seen = follow();
await pumpEventQueue();
expect(seen.last[japanese]?.due, 1);
await studysets.reread();
await pumpEventQueue();
expect(seen, hasLength(1));
await reviews.record(
cardId: neko,
rating: Rating.easy,
reviewedAt: clock.now(),
duration: const Duration(seconds: 2),
);
await pumpEventQueue();
expect(seen.last[japanese]?.due, 0);
await card(japanese);
await pumpEventQueue();
expect(seen.last[japanese]?.cards, 2);
});
}

View file

@ -20,20 +20,23 @@ class FakeReviewGateway() implements ReviewGateway {
Future<List<ReviewRow>> allReviews() async => [...rows];
@override
Future<void> insert(Iterable<ReviewRow> reviews) async {
Future<List<ReviewRow>> insert(Iterable<ReviewRow> reviews) async {
if (failWrites case final error?) {
throw error;
}
for (final review in reviews) {
rows.add((
id: 'review-${(_nextId++).toString().padLeft(4, '0')}',
cardId: review.cardId,
rating: review.rating,
reviewedAt: review.reviewedAt,
durationMs: review.durationMs,
mode: review.mode,
));
}
final stored = [
for (final review in reviews)
(
id: 'review-${(_nextId++).toString().padLeft(4, '0')}',
cardId: review.cardId,
rating: review.rating,
reviewedAt: review.reviewedAt,
durationMs: review.durationMs,
mode: review.mode,
),
];
rows.addAll(stored);
return stored;
}
@override

View file

@ -78,6 +78,7 @@ import 'package:halkyon_learn_flutter/src/feature/study/data/review_gateway.dart
import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_deletions.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_progress_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_tabs.dart';
import 'package:halkyon_learn_flutter/src/feature/tag/data/tag_gateway.dart';
@ -279,6 +280,15 @@ Dependencies testDependencies({
paths,
pdfSources,
);
final studysetProgress = StudysetProgressRepository(
studysets: studysets,
cards: cards,
notes: notes,
paths: paths,
pdfs: pdfSources,
reviews: reviews,
settings: settings,
);
final noteDeletions = NoteDeletions(notes);
final cardDeletions = CardDeletions(cards);
final pathDeletions = LearningPathDeletions(paths);
@ -297,6 +307,7 @@ Dependencies testDependencies({
pathProgress: pathProgress,
pdfSources: pdfSources,
reviews: reviews,
studysetProgress: studysetProgress,
tags: tags,
settings: settings,
assets: assets,
@ -358,6 +369,7 @@ Dependencies testDependencies({
await pathDeletions.dispose();
await stepDeletions.dispose();
await pdfDeletions.dispose();
await studysetProgress.dispose();
await studysets.dispose();
await tags.dispose();
await settings.dispose();