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:
parent
de6af3efab
commit
301751607d
9 changed files with 836 additions and 258 deletions
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue