drop binary heap in test scheduler

The original rationale was avoiding a possible O(n) insertion if
the learning card was due outside the cutoff, but the increased code
complexity doesn't seem worth it, given that learning cards will
rarely grow above 1000.

Also added a currently-disabled test that demonstrates the current undo
handling behaviour is yielding incorrect counts; that will be reworked
in the next commit, and this change will make that easier.
This commit is contained in:
Damien Elmes 2021-05-14 16:02:50 +10:00
parent c41d5ca4bf
commit 9990a10161
6 changed files with 326 additions and 258 deletions

View file

@ -7,7 +7,6 @@ mod preview;
mod relearning; mod relearning;
mod review; mod review;
mod revlog; mod revlog;
mod undo;
use revlog::RevlogEntryPartial; use revlog::RevlogEntryPartial;
@ -375,6 +374,43 @@ impl Collection {
} }
} }
// test helpers
#[cfg(test)]
impl Collection {
pub(crate) fn answer_again(&mut self) {
self.answer(|states| states.again, Rating::Again).unwrap()
}
#[allow(dead_code)]
pub(crate) fn answer_hard(&mut self) {
self.answer(|states| states.hard, Rating::Hard).unwrap()
}
pub(crate) fn answer_good(&mut self) {
self.answer(|states| states.good, Rating::Good).unwrap()
}
pub(crate) fn answer_easy(&mut self) {
self.answer(|states| states.easy, Rating::Easy).unwrap()
}
fn answer<F>(&mut self, get_state: F, rating: Rating) -> Result<()>
where
F: FnOnce(&NextCardStates) -> CardState,
{
let queued = self.next_card()?.unwrap();
self.answer_card(&CardAnswer {
card_id: queued.card.id,
current_state: queued.next_states.current,
new_state: get_state(&queued.next_states),
rating,
answered_at: TimestampMillis::now(),
milliseconds_taken: 0,
})?;
Ok(())
}
}
/// Return a consistent seed for a given card at a given number of reps. /// Return a consistent seed for a given card at a given number of reps.
/// If in test environment, disable fuzzing. /// If in test environment, disable fuzzing.
fn get_fuzz_seed(card: &Card) -> Option<u64> { fn get_fuzz_seed(card: &Card) -> Option<u64> {

View file

@ -1,145 +0,0 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
#[cfg(test)]
mod test {
use crate::{
card::{CardQueue, CardType},
collection::open_test_collection,
deckconfig::LeechAction,
prelude::*,
scheduler::answering::{CardAnswer, Rating},
};
#[test]
fn undo() -> Result<()> {
// add a note
let mut col = open_test_collection();
let nt = col
.get_notetype_by_name("Basic (and reversed card)")?
.unwrap();
let mut note = nt.new_note();
note.set_field(0, "one")?;
note.set_field(1, "two")?;
col.add_note(&mut note, DeckId(1))?;
// turn burying and leech suspension on
let mut conf = col.storage.get_deck_config(DeckConfigId(1))?.unwrap();
conf.inner.bury_new = true;
conf.inner.leech_action = LeechAction::Suspend as i32;
col.storage.update_deck_conf(&conf)?;
// get the first card
let queued = col.next_card()?.unwrap();
let nid = note.id;
let cid = queued.card.id;
let sibling_cid = col.storage.all_card_ids_of_note_in_order(nid)?[1];
let assert_initial_state = |col: &mut Collection| -> Result<()> {
let first = col.storage.get_card(cid)?.unwrap();
assert_eq!(first.queue, CardQueue::New);
let sibling = col.storage.get_card(sibling_cid)?.unwrap();
assert_eq!(sibling.queue, CardQueue::New);
Ok(())
};
assert_initial_state(&mut col)?;
// immediately graduate the first card
col.answer_card(&CardAnswer {
card_id: queued.card.id,
current_state: queued.next_states.current,
new_state: queued.next_states.easy,
rating: Rating::Easy,
answered_at: TimestampMillis::now(),
milliseconds_taken: 0,
})?;
// the sibling will be buried
let sibling = col.storage.get_card(sibling_cid)?.unwrap();
assert_eq!(sibling.queue, CardQueue::SchedBuried);
// make it due now, with 7 lapses. we use the storage layer directly,
// bypassing undo
let mut card = col.storage.get_card(cid)?.unwrap();
assert_eq!(card.ctype, CardType::Review);
card.lapses = 7;
card.due = 0;
col.storage.update_card(&card)?;
// fail it, which should cause it to be marked as a leech
col.clear_study_queues();
let queued = col.next_card()?.unwrap();
dbg!(&queued);
col.answer_card(&CardAnswer {
card_id: queued.card.id,
current_state: queued.next_states.current,
new_state: queued.next_states.again,
rating: Rating::Again,
answered_at: TimestampMillis::now(),
milliseconds_taken: 0,
})?;
let assert_post_review_state = |col: &mut Collection| -> Result<()> {
let card = col.storage.get_card(cid)?.unwrap();
assert_eq!(card.interval, 1);
assert_eq!(card.lapses, 8);
assert_eq!(
col.storage.get_all_revlog_entries(TimestampSecs(0))?.len(),
2
);
let note = col.storage.get_note(nid)?.unwrap();
assert_eq!(note.tags, vec!["leech".to_string()]);
assert_eq!(col.storage.all_tags()?.is_empty(), false);
let deck = col.get_deck(DeckId(1))?.unwrap();
assert_eq!(deck.common.review_studied, 1);
dbg!(&col.next_card()?);
assert_eq!(col.next_card()?.is_some(), false);
Ok(())
};
let assert_pre_review_state = |col: &mut Collection| -> Result<()> {
// the card should have its old state, but a new mtime (which we can't
// easily test without waiting)
let card = col.storage.get_card(cid)?.unwrap();
assert_eq!(card.interval, 4);
assert_eq!(card.lapses, 7);
// the revlog entry should have been removed
assert_eq!(
col.storage.get_all_revlog_entries(TimestampSecs(0))?.len(),
1
);
// the note should no longer be tagged as a leech
let note = col.storage.get_note(nid)?.unwrap();
assert_eq!(note.tags.is_empty(), true);
assert_eq!(col.storage.all_tags()?.is_empty(), true);
let deck = col.get_deck(DeckId(1))?.unwrap();
assert_eq!(deck.common.review_studied, 0);
assert_eq!(col.next_card()?.is_some(), true);
Ok(())
};
// ensure everything is restored on undo/redo
assert_post_review_state(&mut col)?;
col.undo()?;
assert_pre_review_state(&mut col)?;
col.redo()?;
assert_post_review_state(&mut col)?;
col.undo()?;
assert_pre_review_state(&mut col)?;
col.undo()?;
assert_initial_state(&mut col)?;
Ok(())
}
}

View file

@ -6,10 +6,7 @@ pub(crate) mod intersperser;
pub(crate) mod sized_chain; pub(crate) mod sized_chain;
mod sorting; mod sorting;
use std::{ use std::collections::{HashMap, VecDeque};
cmp::Reverse,
collections::{BinaryHeap, HashMap, VecDeque},
};
use intersperser::Intersperser; use intersperser::Intersperser;
use sized_chain::SizedChain; use sized_chain::SizedChain;
@ -114,19 +111,19 @@ impl QueueBuilder {
pub(super) fn build( pub(super) fn build(
mut self, mut self,
top_deck_limits: RemainingLimits, top_deck_limits: RemainingLimits,
learn_ahead_secs: u32, learn_ahead_secs: i64,
selected_deck: DeckId, selected_deck: DeckId,
current_day: u32, current_day: u32,
) -> CardQueues { ) -> CardQueues {
self.sort_new(); self.sort_new();
self.sort_reviews(current_day); self.sort_reviews(current_day);
// split and sort learning // intraday learning
let learn_ahead_secs = learn_ahead_secs as i64; let learning = sort_learning(self.learning);
let (due_learning, later_learning) = split_learning(self.learning, learn_ahead_secs); let cutoff = TimestampSecs::now().adding_secs(learn_ahead_secs);
let learn_count = due_learning.len(); let learn_count = learning.iter().take_while(|e| e.due <= cutoff).count();
// merge day learning in, and cap to parent review count // merge interday learning into main, and cap to parent review count
let main_iter = merge_day_learning( let main_iter = merge_day_learning(
self.review, self.review,
self.day_learning, self.day_learning,
@ -148,8 +145,7 @@ impl QueueBuilder {
}, },
undo: Vec::new(), undo: Vec::new(),
main: main_iter.collect(), main: main_iter.collect(),
due_learning, learning,
later_learning,
learn_ahead_secs, learn_ahead_secs,
selected_deck, selected_deck,
current_day, current_day,
@ -186,34 +182,9 @@ fn merge_new(
} }
} }
/// Split the learning queue into cards due within limit, and cards due later fn sort_learning(mut learning: Vec<DueCard>) -> VecDeque<LearningQueueEntry> {
/// today. Learning does not need to be sorted in advance, as the sorting is learning.sort_unstable_by(|a, b| a.due.cmp(&b.due));
/// done as the heaps/dequeues are built. learning.into_iter().map(LearningQueueEntry::from).collect()
fn split_learning(
learning: Vec<DueCard>,
learn_ahead_secs: i64,
) -> (
VecDeque<LearningQueueEntry>,
BinaryHeap<Reverse<LearningQueueEntry>>,
) {
let cutoff = TimestampSecs(TimestampSecs::now().0 + learn_ahead_secs);
// split learning into now and later
let (mut now, later): (Vec<_>, Vec<_>) = learning
.into_iter()
.map(LearningQueueEntry::from)
.partition(|c| c.due <= cutoff);
// sort due items in ascending order, as we pop the deque from the front
now.sort_unstable_by(|a, b| a.due.cmp(&b.due));
// partition() requires both outputs to be the same, so we need to create the deque
// separately
let now = VecDeque::from(now);
// build the binary min heap
let later: BinaryHeap<_> = later.into_iter().map(Reverse).collect();
(now, later)
} }
impl Collection { impl Collection {
@ -293,7 +264,7 @@ impl Collection {
let queues = queues.build( let queues = queues.build(
selected_deck_limits, selected_deck_limits,
self.learn_ahead_secs(), self.learn_ahead_secs() as i64,
deck_id, deck_id,
timing.days_elapsed, timing.days_elapsed,
); );

View file

@ -1,10 +1,7 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use std::{ use std::{cmp::Ordering, collections::VecDeque};
cmp::{Ordering, Reverse},
collections::VecDeque,
};
use super::CardQueues; use super::CardQueues;
use crate::{prelude::*, scheduler::timing::SchedTimingToday}; use crate::{prelude::*, scheduler::timing::SchedTimingToday};
@ -34,24 +31,16 @@ impl CardQueues {
/// Does not check for newly due cards, as that is already done by /// Does not check for newly due cards, as that is already done by
/// next_learning_entry_due_before_now() /// next_learning_entry_due_before_now()
pub(super) fn next_learning_entry_learning_ahead(&self) -> Option<LearningQueueEntry> { pub(super) fn next_learning_entry_learning_ahead(&self) -> Option<LearningQueueEntry> {
self.due_learning.front().copied() self.learning.front().copied()
} }
pub(super) fn pop_learning_entry(&mut self, id: CardId) -> Option<LearningQueueEntry> { pub(super) fn pop_learning_entry(&mut self, id: CardId) -> Option<LearningQueueEntry> {
if let Some(top) = self.due_learning.front() { if let Some(top) = self.learning.front() {
if top.id == id { if top.id == id {
self.counts.learning -= 1; // under normal circumstances this should not go below 0, but currently
return self.due_learning.pop_front(); // the Python unit tests answer learning cards before they're due
} self.counts.learning = self.counts.learning.saturating_sub(1);
} return self.learning.pop_front();
// fixme: remove this in the future
// the current python unit tests answer learning cards before they're due,
// so for now we also check the head of the later_due queue
if let Some(top) = self.later_learning.peek() {
if top.0.id == id {
// self.counts.learning -= 1;
return self.later_learning.pop().map(|c| c.0);
} }
} }
@ -85,31 +74,28 @@ impl CardQueues {
mut entry: LearningQueueEntry, mut entry: LearningQueueEntry,
timing: SchedTimingToday, timing: SchedTimingToday,
) -> LearningQueueEntry { ) -> LearningQueueEntry {
let learn_ahead_limit = timing.now.adding_secs(self.learn_ahead_secs); let cutoff = timing.now.adding_secs(self.learn_ahead_secs);
if entry.due <= cutoff && self.learning_collapsed() {
if entry.due < learn_ahead_limit { if let Some(next) = self.learning.front() {
if self.learning_collapsed() { // ensure the card is scheduled after the next due card
if let Some(next) = self.due_learning.front() { if next.due >= entry.due {
if next.due >= entry.due { if next.due < cutoff {
// the earliest due card is due later than this one; make this one entry.due = next.due.adding_secs(1)
// due after that one } else {
entry.due = next.due.adding_secs(1); // or outside the cutoff, in cases where the next due
// card is due later than the cutoff
entry.due = cutoff.adding_secs(60);
} }
self.push_due_learning_card(entry);
} else {
// nothing else waiting to review; make this due in a minute
entry.due = learn_ahead_limit.adding_secs(60);
self.later_learning.push(Reverse(entry));
} }
} else { } else {
// not collapsed; can add normally // nothing else waiting to review; make this due a minute after
self.push_due_learning_card(entry); // cutoff
entry.due = cutoff.adding_secs(60);
} }
} else {
// due outside current learn ahead limit, but later today
self.later_learning.push(Reverse(entry));
} }
self.push_learning_card(entry);
entry entry
} }
@ -117,41 +103,39 @@ impl CardQueues {
self.main.is_empty() self.main.is_empty()
} }
/// Adds card, maintaining correct sort order, and increments learning count. /// Adds card, maintaining correct sort order, and increments learning count if
pub(super) fn push_due_learning_card(&mut self, entry: LearningQueueEntry) { /// card is due within cutoff.
self.counts.learning += 1; pub(super) fn push_learning_card(&mut self, entry: LearningQueueEntry) {
let target_idx = let target_idx =
binary_search_by(&self.due_learning, |e| e.due.cmp(&entry.due)).unwrap_or_else(|e| e); binary_search_by(&self.learning, |e| e.due.cmp(&entry.due)).unwrap_or_else(|e| e);
self.due_learning.insert(target_idx, entry); if target_idx < self.counts.learning {
self.counts.learning += 1;
}
self.learning.insert(target_idx, entry);
} }
fn check_for_newly_due_learning_cards(&mut self, cutoff: TimestampSecs) { fn check_for_newly_due_learning_cards(&mut self, cutoff: TimestampSecs) {
while let Some(earliest) = self.later_learning.peek() { self.counts.learning += self
if earliest.0.due > cutoff { .learning
break; .iter()
} .skip(self.counts.learning)
let entry = self.later_learning.pop().unwrap().0; .take_while(|e| e.due <= cutoff)
self.push_due_learning_card(entry); .count();
}
} }
pub(super) fn remove_requeued_learning_card_after_undo(&mut self, id: CardId) { pub(super) fn remove_requeued_learning_card_after_undo(&mut self, id: CardId) {
let due_idx = self let due_idx = self.learning.iter().enumerate().find_map(|(idx, entry)| {
.due_learning if entry.id == id {
.iter() Some(idx)
.enumerate() } else {
.find_map(|(idx, entry)| if entry.id == id { Some(idx) } else { None }); None
}
});
if let Some(idx) = due_idx { if let Some(idx) = due_idx {
self.counts.learning -= 1; // FIXME: if we remove the saturating_sub from pop_learning(), maybe
self.due_learning.remove(idx); // this can go too?
} else { self.counts.learning = self.counts.learning.saturating_sub(1);
// card may be in the later_learning binary heap - we can't remove self.learning.remove(idx);
// it in place, so we have to rebuild it
self.later_learning = self
.later_learning
.drain()
.filter(|e| e.0.id != id)
.collect();
} }
} }
} }

View file

@ -8,10 +8,7 @@ mod limits;
mod main; mod main;
pub(crate) mod undo; pub(crate) mod undo;
use std::{ use std::collections::VecDeque;
cmp::Reverse,
collections::{BinaryHeap, VecDeque},
};
pub(crate) use builder::{DueCard, NewCard}; pub(crate) use builder::{DueCard, NewCard};
pub(crate) use entry::{QueueEntry, QueueEntryKind}; pub(crate) use entry::{QueueEntry, QueueEntryKind};
@ -28,8 +25,7 @@ pub(crate) struct CardQueues {
/// Any undone items take precedence. /// Any undone items take precedence.
undo: Vec<QueueEntry>, undo: Vec<QueueEntry>,
main: VecDeque<MainQueueEntry>, main: VecDeque<MainQueueEntry>,
due_learning: VecDeque<LearningQueueEntry>, learning: VecDeque<LearningQueueEntry>,
later_learning: BinaryHeap<Reverse<LearningQueueEntry>>,
selected_deck: DeckId, selected_deck: DeckId,
current_day: u32, current_day: u32,
learn_ahead_secs: i64, learn_ahead_secs: i64,
@ -208,11 +204,24 @@ impl Collection {
})) }))
} }
} }
}
#[cfg(test)] // test helpers
#[cfg(test)]
impl Collection {
pub(crate) fn next_card(&mut self) -> Result<Option<QueuedCard>> { pub(crate) fn next_card(&mut self) -> Result<Option<QueuedCard>> {
Ok(self Ok(self
.next_cards(1, false)? .next_cards(1, false)?
.map(|mut resp| resp.cards.pop().unwrap())) .map(|mut resp| resp.cards.pop().unwrap()))
} }
pub(crate) fn get_queue_single(&mut self) -> Result<QueuedCards> {
self.next_cards(1, false)?.ok_or(AnkiError::NotFound)
}
pub(crate) fn counts(&mut self) -> [usize; 3] {
self.get_queue_single()
.map(|q| [q.new_count, q.learning_count, q.review_count])
.unwrap_or([0; 3])
}
} }

View file

@ -77,3 +77,216 @@ impl CardQueues {
self.undo.push(entry); self.undo.push(entry);
} }
} }
#[cfg(test)]
mod test {
use crate::{
card::{CardQueue, CardType},
collection::open_test_collection,
deckconfig::LeechAction,
prelude::*,
};
fn add_note(col: &mut Collection, with_reverse: bool) -> Result<NoteId> {
let nt = col
.get_notetype_by_name("Basic (and reversed card)")?
.unwrap();
let mut note = nt.new_note();
note.set_field(0, "one")?;
if with_reverse {
note.set_field(1, "two")?;
}
col.add_note(&mut note, DeckId(1))?;
Ok(note.id)
}
#[test]
fn undo() -> Result<()> {
// add a note
let mut col = open_test_collection();
let nid = add_note(&mut col, true)?;
// turn burying and leech suspension on
let mut conf = col.storage.get_deck_config(DeckConfigId(1))?.unwrap();
conf.inner.bury_new = true;
conf.inner.leech_action = LeechAction::Suspend as i32;
col.storage.update_deck_conf(&conf)?;
// get the first card
let queued = col.next_card()?.unwrap();
let cid = queued.card.id;
let sibling_cid = col.storage.all_card_ids_of_note_in_order(nid)?[1];
let assert_initial_state = |col: &mut Collection| -> Result<()> {
let first = col.storage.get_card(cid)?.unwrap();
assert_eq!(first.queue, CardQueue::New);
let sibling = col.storage.get_card(sibling_cid)?.unwrap();
assert_eq!(sibling.queue, CardQueue::New);
Ok(())
};
assert_initial_state(&mut col)?;
// immediately graduate the first card
col.answer_easy();
// the sibling will be buried
let sibling = col.storage.get_card(sibling_cid)?.unwrap();
assert_eq!(sibling.queue, CardQueue::SchedBuried);
// make it due now, with 7 lapses. we use the storage layer directly,
// bypassing undo
let mut card = col.storage.get_card(cid)?.unwrap();
assert_eq!(card.ctype, CardType::Review);
card.lapses = 7;
card.due = 0;
col.storage.update_card(&card)?;
// fail it, which should cause it to be marked as a leech
col.clear_study_queues();
col.answer_again();
let assert_post_review_state = |col: &mut Collection| -> Result<()> {
let card = col.storage.get_card(cid)?.unwrap();
assert_eq!(card.interval, 1);
assert_eq!(card.lapses, 8);
assert_eq!(
col.storage.get_all_revlog_entries(TimestampSecs(0))?.len(),
2
);
let note = col.storage.get_note(nid)?.unwrap();
assert_eq!(note.tags, vec!["leech".to_string()]);
assert_eq!(col.storage.all_tags()?.is_empty(), false);
let deck = col.get_deck(DeckId(1))?.unwrap();
assert_eq!(deck.common.review_studied, 1);
assert_eq!(col.next_card()?.is_some(), false);
Ok(())
};
let assert_pre_review_state = |col: &mut Collection| -> Result<()> {
// the card should have its old state, but a new mtime (which we can't
// easily test without waiting)
let card = col.storage.get_card(cid)?.unwrap();
assert_eq!(card.interval, 4);
assert_eq!(card.lapses, 7);
// the revlog entry should have been removed
assert_eq!(
col.storage.get_all_revlog_entries(TimestampSecs(0))?.len(),
1
);
// the note should no longer be tagged as a leech
let note = col.storage.get_note(nid)?.unwrap();
assert_eq!(note.tags.is_empty(), true);
assert_eq!(col.storage.all_tags()?.is_empty(), true);
let deck = col.get_deck(DeckId(1))?.unwrap();
assert_eq!(deck.common.review_studied, 0);
assert_eq!(col.next_card()?.is_some(), true);
let q = col.get_queue_single()?;
assert_eq!(&[q.new_count, q.learning_count, q.review_count], &[0, 0, 1]);
Ok(())
};
// ensure everything is restored on undo/redo
assert_post_review_state(&mut col)?;
col.undo()?;
assert_pre_review_state(&mut col)?;
col.redo()?;
assert_post_review_state(&mut col)?;
col.undo()?;
assert_pre_review_state(&mut col)?;
col.undo()?;
assert_initial_state(&mut col)?;
Ok(())
}
#[test]
#[ignore = "undo code is currently broken"]
fn undo_counts1() -> Result<()> {
let mut col = open_test_collection();
assert_eq!(col.counts(), [0, 0, 0]);
add_note(&mut col, true)?;
assert_eq!(col.counts(), [2, 0, 0]);
col.answer_good();
assert_eq!(col.counts(), [1, 1, 0]);
col.answer_good();
assert_eq!(col.counts(), [0, 2, 0]);
col.answer_good();
assert_eq!(col.counts(), [0, 1, 0]);
col.answer_good();
assert_eq!(col.counts(), [0, 0, 0]);
// now work backwards
col.undo()?;
assert_eq!(col.counts(), [0, 1, 0]);
col.undo()?;
assert_eq!(col.counts(), [0, 2, 0]);
col.undo()?;
assert_eq!(col.counts(), [1, 1, 0]);
col.undo()?;
assert_eq!(col.counts(), [2, 0, 0]);
col.undo()?;
assert_eq!(col.counts(), [0, 0, 0]);
Ok(())
}
#[test]
fn undo_counts2() -> Result<()> {
let mut col = open_test_collection();
assert_eq!(col.counts(), [0, 0, 0]);
add_note(&mut col, true)?;
assert_eq!(col.counts(), [2, 0, 0]);
col.answer_again();
assert_eq!(col.counts(), [1, 1, 0]);
col.answer_again();
assert_eq!(col.counts(), [0, 2, 0]);
col.answer_again();
assert_eq!(col.counts(), [0, 2, 0]);
col.answer_again();
assert_eq!(col.counts(), [0, 2, 0]);
col.answer_good();
assert_eq!(col.counts(), [0, 2, 0]);
col.answer_easy();
assert_eq!(col.counts(), [0, 1, 0]);
col.answer_good();
// last card, due in a minute
assert_eq!(col.counts(), [0, 0, 0]);
Ok(())
}
#[test]
fn undo_counts_relearn() -> Result<()> {
let mut col = open_test_collection();
add_note(&mut col, true)?;
col.storage
.db
.execute_batch("update cards set due=0,queue=2,type=2")?;
assert_eq!(col.counts(), [0, 0, 2]);
col.answer_again();
assert_eq!(col.counts(), [0, 1, 1]);
col.answer_again();
assert_eq!(col.counts(), [0, 2, 0]);
col.answer_easy();
assert_eq!(col.counts(), [0, 1, 0]);
col.answer_easy();
assert_eq!(col.counts(), [0, 0, 0]);
Ok(())
}
}