Merge branch 'master' into ref

This commit is contained in:
Rai 2019-12-25 23:10:28 +01:00 committed by GitHub
commit 020fa0b2f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 2655 additions and 743 deletions

View file

@ -20,11 +20,13 @@ jobs:
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 12 node-version: 12
- name: Install dependencies - name: Set up Protoc
run: | uses: Arduino/actions/setup-protoc@master
sudo apt install portaudio19-dev
pip install -r requirements.qt
- name: Run checks - name: Run checks
run: | run: |
make check sudo apt install portaudio19-dev
python${{ matrix.python-version }} -m venv ~/pyenv
. ~/pyenv/bin/activate
pip install -r requirements.qt
pip install --upgrade setuptools pip
make check RUSTARGS=""

5
.gitignore vendored
View file

@ -3,15 +3,18 @@
*\# *\#
*~ *~
.*.swp .*.swp
.DS_Store
.build .build
.coverage .coverage
.DS_Store
.mypy_cache .mypy_cache
.pytype .pytype
__pycache__ __pycache__
anki/buildhash.py anki/buildhash.py
anki/backend_pb2.*
aqt/forms aqt/forms
locale locale
rs/ankirs/src/backend_proto.rs
rs/target
tools/runanki.system tools/runanki.system
ts/node_modules ts/node_modules
web/deckbrowser.js web/deckbrowser.js

View file

@ -1,5 +1,5 @@
[settings] [settings]
skip=aqt/forms skip=aqt/forms,anki/backend_pb2.py,backend_pb2.pyi
multi_line_output=3 multi_line_output=3
include_trailing_comma=True include_trailing_comma=True
force_grid_wrap=0 force_grid_wrap=0

165
Makefile
View file

@ -1,13 +1,14 @@
PREFIX := /usr PREFIX := /usr
SHELL := bash SHELL := bash
.SHELLFLAGS := -eu -o pipefail -c .SHELLFLAGS := -eu -o pipefail -c
.ONESHELL:
.DELETE_ON_ERROR: .DELETE_ON_ERROR:
MAKEFLAGS += --warn-undefined-variables MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules MAKEFLAGS += --no-builtin-rules
RUNARGS := RUNARGS :=
.SUFFIXES: .SUFFIXES:
BLACKARGS := -t py36 anki aqt BLACKARGS := -t py36 anki aqt tests
RUSTARGS := --release --strip
ISORTARGS := anki aqt tests
$(shell mkdir -p .build) $(shell mkdir -p .build)
@ -40,6 +41,8 @@ install:
-xdg-mime default anki.desktop application/x-apkg -xdg-mime default anki.desktop application/x-apkg
@echo @echo
@echo "Install complete." @echo "Install complete."
# fixme: _ankirs.so needs to be copied into system python env or
# 'maturin build' used
uninstall: uninstall:
rm -rf ${DESTDIR}${PREFIX}/share/anki rm -rf ${DESTDIR}${PREFIX}/share/anki
@ -55,20 +58,48 @@ uninstall:
# Prerequisites # Prerequisites
###################### ######################
RUNREQS := .build/pyrunreqs .build/jsreqs RUNREQS := .build/py-run-deps .build/ts-deps
.build/pyrunreqs: requirements.txt # Python prerequisites
######################
.build/py-run-deps: requirements.txt
pip install -r $< pip install -r $<
touch $@ @touch $@
.build/pycheckreqs: requirements.check .build/pyrunreqs .build/py-check-reqs: requirements.check .build/py-run-deps
pip install -r $< pip install -r $<
./tools/typecheck-setup.sh ./tools/typecheck-setup.sh
touch $@ @touch $@
.build/jsreqs: ts/package.json # TS prerequisites
######################
.build/ts-deps: ts/package.json
(cd ts && npm i) (cd ts && npm i)
touch $@ @touch $@
# Rust prerequisites
######################
.build/rust-deps: .build/py-run-deps
pip install maturin
@touch $@
RUST_TOOLCHAIN := $(shell cat rs/rust-toolchain)
.build/rs-fmt-deps:
rustup component add rustfmt-preview --toolchain $(RUST_TOOLCHAIN)
@touch $@
.build/rs-clippy-deps:
rustup component add clippy-preview --toolchain $(RUST_TOOLCHAIN)
@touch $@
# Protobuf
######################
PROTODEPS := $(wildcard proto/*.proto)
# Typescript source # Typescript source
###################### ######################
@ -76,18 +107,31 @@ RUNREQS := .build/pyrunreqs .build/jsreqs
TSDEPS := $(wildcard ts/src/*.ts) TSDEPS := $(wildcard ts/src/*.ts)
JSDEPS := $(patsubst ts/src/%.ts, web/%.js, $(TSDEPS)) JSDEPS := $(patsubst ts/src/%.ts, web/%.js, $(TSDEPS))
# Rust source
######################
RSDEPS := $(shell find rs -type f | egrep -v 'target|/\.|proto.rs')
# Building # Building
###################### ######################
BUILDDEPS := .build/ui .build/js BUILDDEPS := .build/ui .build/js .build/rs .build/py-proto
.build/ui: $(RUNREQS) $(shell find designer -type f) .build/ui: $(RUNREQS) $(shell find designer -type f)
./tools/build_ui.sh ./tools/build_ui.sh
touch $@ @touch $@
.build/js: .build/jsreqs $(TSDEPS) .build/js: .build/ts-deps $(TSDEPS)
(cd ts && npm run build) (cd ts && npm run build)
touch $@ @touch $@
.build/rs: .build/rust-deps $(RUNREQS) $(RSDEPS) $(PROTODEPS)
(cd rs/pymod && maturin develop $(RUSTARGS))
@touch $@
.build/py-proto: $(RUNREQS) $(PROTODEPS)
protoc --proto_path=proto --python_out=anki --mypy_out=anki proto/backend.proto
@touch $@
.PHONY: build clean .PHONY: build clean
@ -97,6 +141,7 @@ build: $(BUILDDEPS)
clean: clean:
rm -rf .build rm -rf .build
rm -rf $(JSDEPS) rm -rf $(JSDEPS)
rm -rf rs/target
# Running # Running
###################### ######################
@ -109,61 +154,89 @@ run: build
###################### ######################
.PHONY: check .PHONY: check
check: mypy pyimports pyfmt pytest pylint checkpretty check: rs-test rs-fmt rs-clippy py-mypy py-test py-fmt py-imports py-lint ts-fmt
.PHONY: fix
fix: fix-py-fmt fix-py-imports fix-rs-fmt fix-ts-fmt
# Checking python # Checking python
###################### ######################
PYCHECKDEPS := $(BUILDDEPS) .build/pycheckreqs $(shell find anki aqt -name '*.py' | grep -v buildhash.py) PYCHECKDEPS := $(BUILDDEPS) .build/py-check-reqs $(shell find anki aqt -name '*.py' | grep -v buildhash.py)
PYTESTDEPS := $(wildcard tests/*.py)
.build/mypy: $(PYCHECKDEPS) .build/py-mypy: $(PYCHECKDEPS)
mypy anki aqt mypy anki aqt
touch $@ @touch $@
.build/pytest: $(PYCHECKDEPS) $(wildcard tests/*.py) .build/py-test: $(PYCHECKDEPS) $(PYTESTDEPS)
./tools/tests.sh ./tools/tests.sh
touch $@ @touch $@
.build/pylint: $(PYCHECKDEPS) .build/py-lint: $(PYCHECKDEPS)
pylint -j 0 --rcfile=.pylintrc -f colorized --extension-pkg-whitelist=PyQt5 anki aqt pylint -j 0 --rcfile=.pylintrc -f colorized --extension-pkg-whitelist=PyQt5,_ankirs anki aqt
touch $@ @touch $@
.build/pyimports: $(PYCHECKDEPS) .build/py-imports: $(PYCHECKDEPS) $(PYTESTDEPS)
isort anki aqt --check # if this fails, run 'make fixpyimports' isort $(ISORTARGS) --check # if this fails, run 'make fix-py-imports'
touch $@ @touch $@
.build/pyfmt: $(PYCHECKDEPS) .build/py-fmt: $(PYCHECKDEPS) $(PYTESTDEPS)
black --check $(BLACKARGS) # if this fails, run 'make fixpyfmt' black --check $(BLACKARGS) # if this fails, run 'make fix-py-fmt'
touch $@ @touch $@
.PHONY: mypy pytest pylint pyimports pyfmt .PHONY: py-mypy py-test py-lint py-imports py-fmt
mypy: .build/mypy py-mypy: .build/py-mypy
pytest: .build/pytest py-test: .build/py-test
pylint: .build/pylint py-lint: .build/py-lint
pyimports: .build/pyimports py-imports: .build/py-imports
pyfmt: .build/pyfmt py-fmt: .build/py-fmt
.PHONY: fixpyimports fixpyfmt .PHONY: fix-py-imports fix-py-fmt
fixpyimports: fix-py-imports:
isort anki aqt isort $(ISORTARGS)
fixpyfmt: fix-py-fmt:
black $(BLACKARGS) anki aqt black $(BLACKARGS) anki aqt
# Checking rust
######################
.build/rs-test: $(RSDEPS)
(cd rs/ankirs && cargo test)
@touch $@
.build/rs-fmt: .build/rs-fmt-deps $(RSDEPS)
(cd rs && cargo fmt -- --check) # if this fails, run 'make fix-rs-fmt'
@touch $@
.build/rs-clippy: .build/rs-clippy-deps $(RSDEPS)
(cd rs && cargo clippy -- -D warnings)
@touch $@
.PHONY: rs-test rs-fmt fix-rs-fmt rs-clippy
rs-test: .build/rs-test
rs-fmt: .build/rs-fmt
rs-clippy: .build/rs-clippy
fix-rs-fmt:
(cd rs && cargo fmt)
# Checking typescript # Checking typescript
###################### ######################
TSCHECKDEPS := $(BUILDDEPS) $(TSDEPS) TSCHECKDEPS := $(BUILDDEPS) $(TSDEPS)
.build/checkpretty: $(TSCHECKDEPS) .build/ts-fmt: $(TSCHECKDEPS)
(cd ts && npm run check-pretty) # if this fails, run 'make pretty' (cd ts && npm run check-pretty) # if this fails, run 'make fix-ts-fmt'
touch $@ @touch $@
.build/pretty: $(TSCHECKDEPS) .PHONY: fix-ts-fmt ts-fmt
ts-fmt: .build/ts-fmt
fix-ts-fmt:
(cd ts && npm run pretty) (cd ts && npm run pretty)
touch $@
.PHONY: pretty checkpretty
pretty: .build/pretty
checkpretty: .build/checkpretty

View file

@ -18,6 +18,16 @@ To start, make sure you have the following installed:
- mpv - mpv
- lame - lame
- npm - npm
- your platform's C compiler, eg gcc, Xcode or Visual Studio 2017.
- GNU make
- protoc v3 (https://github.com/protocolbuffers/protobuf/releases)
- rustup (https://rustup.rs/)
- pip 19+
Next, build a Python virtual environment and activate it:
$ python3 -m venv ~/pyenv
$ . ~/pyenv/bin/activate
If the distro you are using has PyQt5 installed, make sure you have the PyQt5 If the distro you are using has PyQt5 installed, make sure you have the PyQt5
WebEngine module and development tools (eg pyqt5-dev-tools) installed as well. WebEngine module and development tools (eg pyqt5-dev-tools) installed as well.
@ -49,11 +59,11 @@ Mac users
You can use homebrew to install some dependencies: You can use homebrew to install some dependencies:
$ brew install python mpv lame portaudio $ brew install python mpv lame portaudio protobuf npm rustup-init
Windows users Windows users
-------------- --------------
The build scripts have not been tested on Windows, and you'll find things The build process uses a GNU makefile, so you'll either need to run
easiest if you build Anki using WSL. GNU make via WSL (https://docs.microsoft.com/en-us/windows/wsl/install-win10)
https://docs.microsoft.com/en-us/windows/wsl/install-win10 or Cygwin, or manually execute the build steps.

72
anki/backend.py Normal file
View file

@ -0,0 +1,72 @@
# pylint: skip-file
from typing import Dict, List
import _ankirs # pytype: disable=import-error
import anki.backend_pb2 as pb
from .types import AllTemplateReqs
class BackendException(Exception):
def __str__(self) -> str:
err: pb.BackendError = self.args[0] # pylint: disable=unsubscriptable-object
kind = err.WhichOneof("value")
if kind == "invalid_input":
return f"invalid input: {err.invalid_input.info}"
elif kind == "template_parse":
return f"template parse: {err.template_parse.info}"
else:
return f"unhandled error: {err}"
def proto_template_reqs_to_legacy(
reqs: List[pb.TemplateRequirement],
) -> AllTemplateReqs:
legacy_reqs = []
for (idx, req) in enumerate(reqs):
kind = req.WhichOneof("value")
# fixme: sorting is for the unit tests - should check if any
# code depends on the order
if kind == "any":
legacy_reqs.append((idx, "any", sorted(req.any.ords)))
elif kind == "all":
legacy_reqs.append((idx, "all", sorted(req.all.ords)))
else:
l: List[int] = []
legacy_reqs.append((idx, "none", l))
return legacy_reqs
class Backend:
def __init__(self):
self._backend = _ankirs.Backend()
def _run_command(self, input: pb.BackendInput) -> pb.BackendOutput:
input_bytes = input.SerializeToString()
output_bytes = self._backend.command(input_bytes)
output = pb.BackendOutput()
output.ParseFromString(output_bytes)
kind = output.WhichOneof("value")
if kind == "error":
raise BackendException(output.error)
else:
return output
def plus_one(self, num: int) -> int:
input = pb.BackendInput(plus_one=pb.PlusOneIn(num=num))
output = self._run_command(input)
return output.plus_one.num
def template_requirements(
self, template_fronts: List[str], field_map: Dict[str, int]
) -> AllTemplateReqs:
input = pb.BackendInput(
template_requirements=pb.TemplateRequirementsIn(
template_front=template_fronts, field_names_to_ordinals=field_map
)
)
output = self._run_command(input).template_requirements
reqs: List[pb.TemplateRequirement] = output.requirements # type: ignore
return proto_template_reqs_to_legacy(reqs)

View file

@ -19,6 +19,7 @@ import anki.find
import anki.latex # sets up hook import anki.latex # sets up hook
import anki.notes import anki.notes
import anki.template import anki.template
from anki.backend import Backend
from anki.cards import Card from anki.cards import Card
from anki.consts import * from anki.consts import *
from anki.db import DB from anki.db import DB
@ -84,8 +85,12 @@ class _Collection:
ls: int ls: int
conf: Dict[str, Any] conf: Dict[str, Any]
_undo: List[Any] _undo: List[Any]
backend: Backend
def __init__(self, db: DB, server: bool = False, log: bool = False) -> None: def __init__(
self, db: DB, backend: Backend, server: bool = False, log: bool = False
) -> None:
self.backend = backend
self._debugLog = log self._debugLog = log
self.db = db self.db = db
self.path = db._path self.path = db._path

View file

@ -8,6 +8,7 @@ import operator
import unicodedata import unicodedata
from typing import Any, Dict, List, Optional, Set, Tuple, Union from typing import Any, Dict, List, Optional, Set, Tuple, Union
import anki # pylint: disable=unused-import
from anki.consts import * from anki.consts import *
from anki.errors import DeckRenameError from anki.errors import DeckRenameError
from anki.hooks import runHook from anki.hooks import runHook
@ -98,7 +99,7 @@ class DeckManager:
# Registry save/load # Registry save/load
############################################################# #############################################################
def __init__(self, col) -> None: def __init__(self, col: "anki.storage._Collection") -> None:
self.col = col self.col = col
self.decks = {} self.decks = {}
self.dconf = {} self.dconf = {}

View file

@ -608,9 +608,7 @@ def findDupes(col, fieldName, search="") -> List[Tuple[Any, List]]:
# empty does not count as duplicate # empty does not count as duplicate
if not val: if not val:
continue continue
if val not in vals: vals.setdefault(val, []).append(nid)
vals[val] = []
vals[val].append(nid)
if len(vals[val]) == 2: if len(vals[val]) == 2:
dupes.append((val, vals[val])) dupes.append((val, vals[val]))
return dupes return dupes

View file

@ -556,6 +556,9 @@ select id from notes where mid = ?)"""
########################################################################## ##########################################################################
def _updateRequired(self, m: NoteType) -> None: def _updateRequired(self, m: NoteType) -> None:
self._updateRequiredNew(m)
def _updateRequiredLegacy(self, m: NoteType) -> None:
if m["type"] == MODEL_CLOZE: if m["type"] == MODEL_CLOZE:
# nothing to do # nothing to do
return return
@ -566,6 +569,14 @@ select id from notes where mid = ?)"""
req.append([t["ord"], ret[0], ret[1]]) req.append([t["ord"], ret[0], ret[1]])
m["req"] = req m["req"] = req
def _updateRequiredNew(self, m: NoteType) -> None:
fronts = [t["qfmt"] for t in m["tmpls"]]
field_map = {}
for (idx, fld) in enumerate(m["flds"]):
field_map[fld["name"]] = idx
reqs = self.col.backend.template_requirements(fronts, field_map)
m["req"] = [list(l) for l in reqs]
def _reqForTemplate( def _reqForTemplate(
self, m: NoteType, flds: List[str], t: Template self, m: NoteType, flds: List[str], t: Template
) -> Tuple[Union[str, List[int]], ...]: ) -> Tuple[Union[str, List[int]], ...]:

View file

@ -4,6 +4,7 @@
from typing import Any, List, Optional, Tuple from typing import Any, List, Optional, Tuple
import anki # pylint: disable=unused-import
from anki.utils import ( from anki.utils import (
fieldChecksum, fieldChecksum,
guid64, guid64,
@ -19,7 +20,10 @@ class Note:
tags: List[str] tags: List[str]
def __init__( def __init__(
self, col, model: Optional[Any] = None, id: Optional[int] = None self,
col: "anki.storage._Collection",
model: Optional[Any] = None,
id: Optional[int] = None,
) -> None: ) -> None:
assert not (model and id) assert not (model and id)
self.col = col self.col = col

View file

@ -8,6 +8,7 @@ import os
import re import re
from typing import Any, Dict, Tuple from typing import Any, Dict, Tuple
from anki.backend import Backend
from anki.collection import _Collection from anki.collection import _Collection
from anki.consts import * from anki.consts import *
from anki.db import DB from anki.db import DB
@ -26,6 +27,10 @@ def Collection(
path: str, lock: bool = True, server: bool = False, log: bool = False path: str, lock: bool = True, server: bool = False, log: bool = False
) -> _Collection: ) -> _Collection:
"Open a new or existing collection. Path must be unicode." "Open a new or existing collection. Path must be unicode."
backend = Backend()
# fixme: this call is temporarily here to ensure the brige is working
# on all platforms, and should be removed in a future beta
assert backend.plus_one(5) == 6
assert path.endswith(".anki2") assert path.endswith(".anki2")
path = os.path.abspath(path) path = os.path.abspath(path)
create = not os.path.exists(path) create = not os.path.exists(path)
@ -46,7 +51,7 @@ def Collection(
db.execute("pragma journal_mode = wal") db.execute("pragma journal_mode = wal")
db.setAutocommit(False) db.setAutocommit(False)
# add db to col and do any remaining upgrades # add db to col and do any remaining upgrades
col = _Collection(db, server, log) col = _Collection(db, backend=backend, server=server, log=log)
if ver < SCHEMA_VERSION: if ver < SCHEMA_VERSION:
_upgrade(col, ver) _upgrade(col, ver)
elif ver > SCHEMA_VERSION: elif ver > SCHEMA_VERSION:
@ -60,7 +65,11 @@ def Collection(
addBasicModel(col) addBasicModel(col)
col.save() col.save()
if lock: if lock:
col.lock() try:
col.lock()
except:
col.db.close()
raise
return col return col

View file

@ -14,6 +14,7 @@ import json
import re import re
from typing import Callable, Dict, List, Tuple from typing import Callable, Dict, List, Tuple
import anki # pylint: disable=unused-import
from anki.hooks import runHook from anki.hooks import runHook
from anki.utils import ids2str, intTime from anki.utils import ids2str, intTime
@ -23,7 +24,7 @@ class TagManager:
# Registry save/load # Registry save/load
############################################################# #############################################################
def __init__(self, col) -> None: def __init__(self, col: "anki.storage._Collection") -> None:
self.col = col self.col = col
self.tags: Dict[str, int] = {} self.tags: Dict[str, int] = {}

View file

@ -210,14 +210,15 @@ class Template:
return "{unknown field %s}" % tag_name return "{unknown field %s}" % tag_name
return txt return txt
@classmethod
def clozeText(self, txt: str, ord: str, type: str) -> str: def clozeText(self, txt: str, ord: str, type: str) -> str:
"""Processes the given Cloze deletion within the given template.""" """Processe the given Cloze deletion within the given template."""
reg = clozeReg reg = clozeReg
currentRegex = clozeReg % ord currentRegex = clozeReg % ord
if not re.search(currentRegex, txt): if not re.search(currentRegex, txt):
# No Cloze deletion was found in txt. # No Cloze deletion was found in txt.
return "" return ""
txt = self._removeFormattingFromMathjax(txt, ord) txt = cls._removeFormattingFromMathjax(txt, ord)
def repl(m): def repl(m):
# replace chosen cloze with type # replace chosen cloze with type
@ -237,7 +238,8 @@ class Template:
# and display other clozes normally # and display other clozes normally
return re.sub(reg % r"\d+", "\\2", txt) return re.sub(reg % r"\d+", "\\2", txt)
def _removeFormattingFromMathjax(self, txt, ord) -> str: @classmethod
def _removeFormattingFromMathjax(cls, txt, ord) -> str:
"""Marks all clozes within MathJax to prevent formatting them. """Marks all clozes within MathJax to prevent formatting them.
Active Cloze deletions within MathJax should not be wrapped inside Active Cloze deletions within MathJax should not be wrapped inside

View file

@ -1,4 +1,4 @@
from typing import Any, Dict, Tuple, Union from typing import Any, Dict, List, Tuple, Union
# Model attributes are stored in a dict keyed by strings. This type alias # Model attributes are stored in a dict keyed by strings. This type alias
# provides more descriptive function signatures than just 'Dict[str, Any]' # provides more descriptive function signatures than just 'Dict[str, Any]'
@ -31,3 +31,8 @@ QAData = Tuple[
# Corresponds to 'cardFlags' column. TODO: document # Corresponds to 'cardFlags' column. TODO: document
int, int,
] ]
TemplateRequirementType = str # Union["all", "any", "none"]
# template ordinal, type, list of field ordinals
TemplateRequiredFieldOrds = Tuple[int, TemplateRequirementType, List[int]]
AllTemplateReqs = List[TemplateRequiredFieldOrds]

View file

@ -332,11 +332,19 @@ def _run(argv=None, exec=True):
opts, args = parseArgs(argv) opts, args = parseArgs(argv)
# profile manager # profile manager
pm = ProfileManager(opts.base) pm = None
pmLoadResult = pm.setupMeta() try:
pm = ProfileManager(opts.base)
pmLoadResult = pm.setupMeta()
except:
# will handle below
pass
# gl workarounds if pm:
setupGL(pm) # gl workarounds
setupGL(pm)
# apply user-provided scale factor
os.environ["QT_SCALE_FACTOR"] = str(pm.uiScale())
# opt in to full hidpi support? # opt in to full hidpi support?
if not os.environ.get("ANKI_NOHIGHDPI"): if not os.environ.get("ANKI_NOHIGHDPI"):
@ -348,9 +356,6 @@ def _run(argv=None, exec=True):
if os.environ.get("ANKI_SOFTWAREOPENGL"): if os.environ.get("ANKI_SOFTWAREOPENGL"):
QCoreApplication.setAttribute(Qt.AA_UseSoftwareOpenGL) QCoreApplication.setAttribute(Qt.AA_UseSoftwareOpenGL)
# apply user-provided scale factor
os.environ["QT_SCALE_FACTOR"] = str(pm.uiScale())
# create the app # create the app
QCoreApplication.setApplicationName("Anki") QCoreApplication.setApplicationName("Anki")
QGuiApplication.setDesktopFileName("anki.desktop") QGuiApplication.setDesktopFileName("anki.desktop")
@ -359,6 +364,16 @@ def _run(argv=None, exec=True):
# we've signaled the primary instance, so we should close # we've signaled the primary instance, so we should close
return return
if not pm:
QMessageBox.critical(
None,
"Error",
"""\
Anki could not create its data folder. Please see the File Locations \
section of the manual, and ensure that location is not read-only.""",
)
return
# disable icons on mac; this must be done before window created # disable icons on mac; this must be done before window created
if isMac: if isMac:
app.setAttribute(Qt.AA_DontShowIconsInMenus) app.setAttribute(Qt.AA_DontShowIconsInMenus)
@ -392,12 +407,14 @@ environment points to a valid, writable folder.""",
) )
return return
if pmLoadResult.firstTime:
pm.setDefaultLang()
if pmLoadResult.loadError: if pmLoadResult.loadError:
QMessageBox.warning( QMessageBox.warning(
None, None,
"Preferences Corrupt", "Preferences Corrupt",
"""\ """Anki's prefs21.db file was corrupt and has been recreated. If you were using multiple \
Anki's prefs21.db file was corrupt and has been recreated. If you were using multiple \
profiles, please add them back using the same names to recover your cards.""", profiles, please add them back using the same names to recover your cards.""",
) )

View file

@ -148,6 +148,7 @@ system. It's free and open source."
"David Bailey", "David Bailey",
"Arman High", "Arman High",
"Arthur Milchior", "Arthur Milchior",
"Rai (Michael Pokorny)",
) )
) )

View file

@ -97,22 +97,7 @@ class ProfileManager:
###################################################################### ######################################################################
def ensureBaseExists(self): def ensureBaseExists(self):
try: self._ensureExists(self.base)
self._ensureExists(self.base)
except:
# can't translate, as lang not initialized, and qt may not be
print("unable to create base folder")
QMessageBox.critical(
None,
"Error",
"""\
Anki could not create the folder %s. Please ensure that location is not \
read-only and you have permission to write to it. If you cannot fix this \
issue, please see the documentation for information on running Anki from \
a flash drive."""
% self.base,
)
raise
# Folder migration # Folder migration
###################################################################### ######################################################################
@ -385,7 +370,6 @@ create table if not exists profiles
"insert or replace into profiles values ('_global', ?)", "insert or replace into profiles values ('_global', ?)",
self._pickle(metaConf), self._pickle(metaConf),
) )
self._setDefaultLang()
return result return result
def _ensureProfile(self): def _ensureProfile(self):
@ -409,7 +393,7 @@ please see:
###################################################################### ######################################################################
# On first run, allow the user to choose the default language # On first run, allow the user to choose the default language
def _setDefaultLang(self): def setDefaultLang(self):
# create dialog # create dialog
class NoCloseDiag(QDialog): class NoCloseDiag(QDialog):
def reject(self): def reject(self):
@ -452,7 +436,7 @@ please see:
None, "Anki", en % name, QMessageBox.Yes | QMessageBox.No, QMessageBox.No None, "Anki", en % name, QMessageBox.Yes | QMessageBox.No, QMessageBox.No
) )
if r != QMessageBox.Yes: if r != QMessageBox.Yes:
return self._setDefaultLang() return self.setDefaultLang()
self.setLang(code) self.setLang(code)
def setLang(self, code): def setLang(self, code):

View file

@ -416,11 +416,16 @@ body {{ zoom: {}; background: {}; {} }}
self.evalWithCallback("$(document.body).height()", self._onHeight) self.evalWithCallback("$(document.body).height()", self._onHeight)
def _onHeight(self, qvar): def _onHeight(self, qvar):
from aqt import mw
if qvar is None: if qvar is None:
from aqt import mw
mw.progress.timer(1000, mw.reset, False) mw.progress.timer(1000, mw.reset, False)
return return
height = math.ceil(qvar * self.zoomFactor()) scaleFactor = self.zoomFactor()
if scaleFactor == 1:
scaleFactor = mw.pm.uiScale()
height = math.ceil(qvar * scaleFactor)
self.setFixedHeight(height) self.setFixedHeight(height)

View file

@ -34,3 +34,5 @@ ignore_missing_imports = True
ignore_missing_imports = True ignore_missing_imports = True
[mypy-jsonschema.*] [mypy-jsonschema.*]
ignore_missing_imports = True ignore_missing_imports = True
[mypy-_ankirs]
ignore_missing_imports = True

68
proto/backend.proto Normal file
View file

@ -0,0 +1,68 @@
syntax = "proto3";
package backend_proto;
message Empty {}
message BackendInput {
oneof value {
PlusOneIn plus_one = 2;
TemplateRequirementsIn template_requirements = 3;
}
}
message BackendOutput {
oneof value {
BackendError error = 1;
PlusOneOut plus_one = 2;
TemplateRequirementsOut template_requirements = 3;
}
}
message BackendError {
oneof value {
InvalidInputError invalid_input = 1;
TemplateParseError template_parse = 2;
}
}
message InvalidInputError {
string info = 1;
}
message PlusOneIn {
int32 num = 1;
}
message PlusOneOut {
int32 num = 1;
}
message TemplateParseError {
string info = 1;
}
message TemplateRequirementsIn {
repeated string template_front = 1;
map<string, uint32> field_names_to_ordinals = 2;
}
message TemplateRequirementsOut {
repeated TemplateRequirement requirements = 1;
}
message TemplateRequirement {
oneof value {
TemplateRequirementAll all = 1;
TemplateRequirementAny any = 2;
Empty none = 3;
}
}
message TemplateRequirementAll {
repeated uint32 ords = 1;
}
message TemplateRequirementAny {
repeated uint32 ords = 1;
}

View file

@ -1,6 +1,5 @@
nose nose2
mock mock
mypy==0.750
# fixme: when isort 5.0 is released, switch to pypy # fixme: when isort 5.0 is released, switch to pypy
git+https://github.com/dae/isort#egg=isort git+https://github.com/dae/isort#egg=isort
# fixme: when pylint supports isort 5.0, switch to pypy # fixme: when pylint supports isort 5.0, switch to pypy

View file

@ -8,3 +8,7 @@ jsonschema
psutil; sys_platform == "win32" psutil; sys_platform == "win32"
distro; sys_platform != "win32" and sys_platform != "darwin" distro; sys_platform != "win32" and sys_platform != "darwin"
typing typing
protobuf
mypy==0.750
mypy_protobuf

783
rs/Cargo.lock generated Normal file
View file

@ -0,0 +1,783 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "aho-corasick"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58fb5e95d83b38284460a5fda7d6470aa0b8844d283a0b614b8535e880800d2d"
dependencies = [
"memchr",
]
[[package]]
name = "ankirs"
version = "0.1.0"
dependencies = [
"bytes",
"failure",
"nom",
"prost",
"prost-build",
]
[[package]]
name = "arrayvec"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9"
dependencies = [
"nodrop",
]
[[package]]
name = "autocfg"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2"
[[package]]
name = "backtrace"
version = "0.3.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "924c76597f0d9ca25d762c25a4d369d51267536465dc5064bdf0eb073ed477ea"
dependencies = [
"backtrace-sys",
"cfg-if",
"libc",
"rustc-demangle",
]
[[package]]
name = "backtrace-sys"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d6575f128516de27e3ce99689419835fce9643a9b215a14d2b5b685be018491"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "byteorder"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5"
[[package]]
name = "bytes"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c"
dependencies = [
"byteorder",
"iovec",
]
[[package]]
name = "c2-chacha"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "214238caa1bf3a496ec3392968969cab8549f96ff30652c9e56885329315f6bb"
dependencies = [
"ppv-lite86",
]
[[package]]
name = "cc"
version = "1.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52a465a666ca3d838ebbf08b241383421412fe7ebb463527bba275526d89f76"
[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "ctor"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd8ce37ad4184ab2ce004c33bf6379185d3b1c95801cab51026bd271bf68eedc"
dependencies = [
"quote 1.0.2",
"syn 1.0.11",
]
[[package]]
name = "either"
version = "1.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3"
[[package]]
name = "failure"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8273f13c977665c5db7eb2b99ae520952fe5ac831ae4cd09d80c4c7042b5ed9"
dependencies = [
"backtrace",
"failure_derive",
]
[[package]]
name = "failure_derive"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bc225b78e0391e4b8683440bf2e63c2deeeb2ce5189eab46e2b68c6d3725d08"
dependencies = [
"proc-macro2 1.0.6",
"quote 1.0.2",
"syn 1.0.11",
"synstructure",
]
[[package]]
name = "fixedbitset"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86d4de0081402f5e88cdac65c8dcdcc73118c1a7a465e2a05f0da05843a8ea33"
[[package]]
name = "getrandom"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7db7ca94ed4cd01190ceee0d8a8052f08a247aa1b469a7f68c6a3b71afcf407"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "ghost"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a36606a68532b5640dc86bb1f33c64b45c4682aad4c50f3937b317ea387f3d6"
dependencies = [
"proc-macro2 1.0.6",
"quote 1.0.2",
"syn 1.0.11",
]
[[package]]
name = "heck"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "indoc"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9553c1e16c114b8b77ebeb329e5f2876eed62a8d51178c8bc6bff0d65f98f8"
dependencies = [
"indoc-impl",
"proc-macro-hack",
]
[[package]]
name = "indoc-impl"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b714fc08d0961716390977cdff1536234415ac37b509e34e5a983def8340fb75"
dependencies = [
"proc-macro-hack",
"proc-macro2 1.0.6",
"quote 1.0.2",
"syn 1.0.11",
"unindent",
]
[[package]]
name = "inventory"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4cece20baea71d9f3435e7bbe9adf4765f091c5fe404975f844006964a71299"
dependencies = [
"ctor",
"ghost",
"inventory-impl",
]
[[package]]
name = "inventory-impl"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2869bf972e998977b1cb87e60df70341d48e48dca0823f534feb91ea44adaf9"
dependencies = [
"proc-macro2 1.0.6",
"quote 1.0.2",
"syn 1.0.11",
]
[[package]]
name = "iovec"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
dependencies = [
"libc",
]
[[package]]
name = "itertools"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f"
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lexical-core"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304bccb228c4b020f3a4835d247df0a02a7c4686098d4167762cfbbe4c5cb14"
dependencies = [
"arrayvec",
"cfg-if",
"rustc_version",
"ryu",
"static_assertions",
]
[[package]]
name = "libc"
version = "0.2.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558"
[[package]]
name = "log"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7"
dependencies = [
"cfg-if",
]
[[package]]
name = "memchr"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88579771288728879b57485cc7d6b07d648c9f0141eb955f8ab7f9d45394468e"
[[package]]
name = "multimap"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eb04b9f127583ed176e163fb9ec6f3e793b87e21deedd5734a69386a18a0151"
[[package]]
name = "nodrop"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
[[package]]
name = "nom"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c618b63422da4401283884e6668d39f819a106ef51f5f59b81add00075da35ca"
dependencies = [
"lexical-core",
"memchr",
"version_check 0.1.5",
]
[[package]]
name = "num-traits"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c81ffc11c212fa327657cb19dd85eb7419e163b5b076bede2bdb5c974c07e4"
dependencies = [
"autocfg",
]
[[package]]
name = "paste"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "423a519e1c6e828f1e73b720f9d9ed2fa643dce8a7737fb43235ce0b41eeaa49"
dependencies = [
"paste-impl",
"proc-macro-hack",
]
[[package]]
name = "paste-impl"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4214c9e912ef61bf42b81ba9a47e8aad1b2ffaf739ab162bf96d1e011f54e6c5"
dependencies = [
"proc-macro-hack",
"proc-macro2 1.0.6",
"quote 1.0.2",
"syn 1.0.11",
]
[[package]]
name = "petgraph"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3659d1ee90221741f65dd128d9998311b0e40c5d3c23a62445938214abce4f"
dependencies = [
"fixedbitset",
]
[[package]]
name = "ppv-lite86"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b"
[[package]]
name = "proc-macro-hack"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecd45702f76d6d3c75a80564378ae228a85f0b59d2f3ed43c91b4a69eb2ebfc5"
dependencies = [
"proc-macro2 1.0.6",
"quote 1.0.2",
"syn 1.0.11",
]
[[package]]
name = "proc-macro2"
version = "0.4.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759"
dependencies = [
"unicode-xid 0.1.0",
]
[[package]]
name = "proc-macro2"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9e470a8dc4aeae2dee2f335e8f533e2d4b347e1434e5671afc49b054592f27"
dependencies = [
"unicode-xid 0.2.0",
]
[[package]]
name = "prost"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96d14b1c185652833d24aaad41c5832b0be5616a590227c1fbff57c616754b23"
dependencies = [
"byteorder",
"bytes",
"prost-derive",
]
[[package]]
name = "prost-build"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb788126ea840817128183f8f603dce02cb7aea25c2a0b764359d8e20010702e"
dependencies = [
"bytes",
"heck",
"itertools",
"log",
"multimap",
"petgraph",
"prost",
"prost-types",
"tempfile",
"which",
]
[[package]]
name = "prost-derive"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e7dc378b94ac374644181a2247cebf59a6ec1c88b49ac77f3a94b86b79d0e11"
dependencies = [
"failure",
"itertools",
"proc-macro2 0.4.30",
"quote 0.6.13",
"syn 0.15.44",
]
[[package]]
name = "prost-types"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1de482a366941c8d56d19b650fac09ca08508f2a696119ee7513ad590c8bac6f"
dependencies = [
"bytes",
"prost",
]
[[package]]
name = "pymod"
version = "0.1.0"
dependencies = [
"ankirs",
"pyo3",
]
[[package]]
name = "pyo3"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f9df1468dddf8a59ec799cf3b930bb75ec09deabe875ba953e06c51d1077136"
dependencies = [
"indoc",
"inventory",
"lazy_static",
"libc",
"num-traits",
"paste",
"pyo3cls",
"regex",
"serde",
"serde_json",
"spin",
"unindent",
"version_check 0.9.1",
]
[[package]]
name = "pyo3-derive-backend"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f6e56fb3e97b344a8f87d036f94578399402c6b75949de6270cd07928f790b1"
dependencies = [
"proc-macro2 1.0.6",
"quote 1.0.2",
"syn 1.0.11",
]
[[package]]
name = "pyo3cls"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97452dcdf5941627ebc5c06664a07821fc7fc88d7515f02178193a8ebe316468"
dependencies = [
"proc-macro2 1.0.6",
"pyo3-derive-backend",
"quote 1.0.2",
"syn 1.0.11",
]
[[package]]
name = "quote"
version = "0.6.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1"
dependencies = [
"proc-macro2 0.4.30",
]
[[package]]
name = "quote"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe"
dependencies = [
"proc-macro2 1.0.6",
]
[[package]]
name = "rand"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae1b169243eaf61759b8475a998f0a385e42042370f3a7dbaf35246eacc8412"
dependencies = [
"getrandom",
"libc",
"rand_chacha",
"rand_core",
"rand_hc",
]
[[package]]
name = "rand_chacha"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03a2a90da8c7523f554344f921aa97283eadf6ac484a6d2a7d0212fa7f8d6853"
dependencies = [
"c2-chacha",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
dependencies = [
"getrandom",
]
[[package]]
name = "rand_hc"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
dependencies = [
"rand_core",
]
[[package]]
name = "redox_syscall"
version = "0.1.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84"
[[package]]
name = "regex"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc220bd33bdce8f093101afe22a037b8eb0e5af33592e6a9caafff0d4cb81cbd"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
"thread_local",
]
[[package]]
name = "regex-syntax"
version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11a7e20d1cce64ef2fed88b66d347f88bd9babb82845b2b858f3edbf59a4f716"
[[package]]
name = "remove_dir_all"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e"
dependencies = [
"winapi",
]
[[package]]
name = "rustc-demangle"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783"
[[package]]
name = "rustc_version"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
dependencies = [
"semver",
]
[[package]]
name = "ryu"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8"
[[package]]
name = "semver"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
dependencies = [
"semver-parser",
]
[[package]]
name = "semver-parser"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "serde"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "414115f25f818d7dfccec8ee535d76949ae78584fc4f79a6f45a904bf8ab4449"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "128f9e303a5a29922045a830221b8f78ec74a5f544944f3d5984f8ec3895ef64"
dependencies = [
"proc-macro2 1.0.6",
"quote 1.0.2",
"syn 1.0.11",
]
[[package]]
name = "serde_json"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c575e0cc52bdd09b47f330f646cf59afc586e9c4e3ccd6fc1f625b8ea1dad7"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "static_assertions"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f3eb36b47e512f8f1c9e3d10c2c1965bc992bd9cdb024fa581e2194501c83d3"
[[package]]
name = "syn"
version = "0.15.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5"
dependencies = [
"proc-macro2 0.4.30",
"quote 0.6.13",
"unicode-xid 0.1.0",
]
[[package]]
name = "syn"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dff0acdb207ae2fe6d5976617f887eb1e35a2ba52c13c7234c790960cdad9238"
dependencies = [
"proc-macro2 1.0.6",
"quote 1.0.2",
"unicode-xid 0.2.0",
]
[[package]]
name = "synstructure"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545"
dependencies = [
"proc-macro2 1.0.6",
"quote 1.0.2",
"syn 1.0.11",
"unicode-xid 0.2.0",
]
[[package]]
name = "tempfile"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9"
dependencies = [
"cfg-if",
"libc",
"rand",
"redox_syscall",
"remove_dir_all",
"winapi",
]
[[package]]
name = "thread_local"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b"
dependencies = [
"lazy_static",
]
[[package]]
name = "unicode-segmentation"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0"
[[package]]
name = "unicode-xid"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
[[package]]
name = "unicode-xid"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
[[package]]
name = "unindent"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63f18aa3b0e35fed5a0048f029558b1518095ffe2a0a31fb87c93dece93a4993"
[[package]]
name = "version_check"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
[[package]]
name = "version_check"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce"
[[package]]
name = "wasi"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b89c3ce4ce14bdc6fb6beaf9ec7928ca331de5df7e5ea278375642a2f478570d"
[[package]]
name = "which"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b57acb10231b9493c8472b20cb57317d0679a49e0bdbee44b3b803a6473af164"
dependencies = [
"failure",
"libc",
]
[[package]]
name = "winapi"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

6
rs/Cargo.toml Normal file
View file

@ -0,0 +1,6 @@
[workspace]
members = ["ankirs", "pymod"]
[profile.release]
lto = true
codegen-units = 1

14
rs/ankirs/Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[package]
name = "ankirs"
version = "0.1.0"
edition = "2018"
authors = ["Ankitects Pty Ltd and contributors"]
[dependencies]
nom = "5.0.1"
failure = "0.1.6"
prost = "0.5.0"
bytes = "0.4"
[build-dependencies]
prost-build = "0.5.0"

7
rs/ankirs/build.rs Normal file
View file

@ -0,0 +1,7 @@
use prost_build;
fn main() {
// avoid default OUT_DIR for now, for code completion
std::env::set_var("OUT_DIR", "src");
prost_build::compile_protos(&["../../proto/backend.proto"], &["../../proto/"]).unwrap();
}

139
rs/ankirs/src/backend.rs Normal file
View file

@ -0,0 +1,139 @@
use crate::backend_proto as pt;
use crate::backend_proto::backend_input::Value;
use crate::err::{AnkiError, Result};
use crate::template::{FieldMap, FieldRequirements, ParsedTemplate};
use prost::Message;
use std::collections::HashSet;
pub struct Backend {}
impl Default for Backend {
fn default() -> Self {
Backend {}
}
}
/// Convert an Anki error to a protobuf error.
impl std::convert::From<AnkiError> for pt::BackendError {
fn from(err: AnkiError) -> Self {
use pt::backend_error::Value as V;
let value = match err {
AnkiError::InvalidInput { info } => V::InvalidInput(pt::InvalidInputError { info }),
AnkiError::TemplateParseError { info } => {
V::TemplateParse(pt::TemplateParseError { info })
}
};
pt::BackendError { value: Some(value) }
}
}
// Convert an Anki error to a protobuf output.
impl std::convert::From<AnkiError> for pt::backend_output::Value {
fn from(err: AnkiError) -> Self {
pt::backend_output::Value::Error(err.into())
}
}
impl Backend {
pub fn new() -> Backend {
Backend::default()
}
/// Decode a request, process it, and return the encoded result.
pub fn run_command_bytes(&mut self, req: &[u8]) -> Vec<u8> {
let mut buf = vec![];
let req = match pt::BackendInput::decode(req) {
Ok(req) => req,
Err(_e) => {
// unable to decode
let err = AnkiError::invalid_input("couldn't decode backend request");
let output = pt::BackendOutput {
value: Some(err.into()),
};
output.encode(&mut buf).expect("encode failed");
return buf;
}
};
let resp = self.run_command(req);
resp.encode(&mut buf).expect("encode failed");
buf
}
fn run_command(&self, input: pt::BackendInput) -> pt::BackendOutput {
let oval = if let Some(ival) = input.value {
match self.run_command_inner(ival) {
Ok(output) => output,
Err(err) => err.into(),
}
} else {
AnkiError::invalid_input("unrecognized backend input value").into()
};
pt::BackendOutput { value: Some(oval) }
}
fn run_command_inner(
&self,
ival: pt::backend_input::Value,
) -> Result<pt::backend_output::Value> {
use pt::backend_output::Value as OValue;
Ok(match ival {
Value::TemplateRequirements(input) => {
OValue::TemplateRequirements(self.template_requirements(input)?)
}
Value::PlusOne(input) => OValue::PlusOne(self.plus_one(input)?),
})
}
fn plus_one(&self, input: pt::PlusOneIn) -> Result<pt::PlusOneOut> {
let num = input.num + 1;
Ok(pt::PlusOneOut { num })
}
fn template_requirements(
&self,
input: pt::TemplateRequirementsIn,
) -> Result<pt::TemplateRequirementsOut> {
let map: FieldMap = input
.field_names_to_ordinals
.iter()
.map(|(name, ord)| (name.as_str(), *ord as u16))
.collect();
// map each provided template into a requirements list
use crate::backend_proto::template_requirement::Value;
let all_reqs = input
.template_front
.into_iter()
.map(|template| {
if let Ok(tmpl) = ParsedTemplate::from_text(&template) {
// convert the rust structure into a protobuf one
let val = match tmpl.requirements(&map) {
FieldRequirements::Any(ords) => Value::Any(pt::TemplateRequirementAny {
ords: ords_hash_to_set(ords),
}),
FieldRequirements::All(ords) => Value::All(pt::TemplateRequirementAll {
ords: ords_hash_to_set(ords),
}),
FieldRequirements::None => Value::None(pt::Empty {}),
};
Ok(pt::TemplateRequirement { value: Some(val) })
} else {
// template parsing failures make card unsatisfiable
Ok(pt::TemplateRequirement {
value: Some(Value::None(pt::Empty {})),
})
}
})
.collect::<Result<Vec<_>>>()?;
Ok(pt::TemplateRequirementsOut {
requirements: all_reqs,
})
}
}
fn ords_hash_to_set(ords: HashSet<u16>) -> Vec<u32> {
ords.iter().map(|ord| *ord as u32).collect()
}

23
rs/ankirs/src/err.rs Normal file
View file

@ -0,0 +1,23 @@
pub use failure::{Error, Fail};
pub type Result<T> = std::result::Result<T, AnkiError>;
#[derive(Debug, Fail)]
pub enum AnkiError {
#[fail(display = "invalid input: {}", info)]
InvalidInput { info: String },
#[fail(display = "invalid card template: {}", info)]
TemplateParseError { info: String },
}
// error helpers
impl AnkiError {
pub(crate) fn parse<S: Into<String>>(s: S) -> AnkiError {
AnkiError::TemplateParseError { info: s.into() }
}
pub(crate) fn invalid_input<S: Into<String>>(s: S) -> AnkiError {
AnkiError::InvalidInput { info: s.into() }
}
}

5
rs/ankirs/src/lib.rs Normal file
View file

@ -0,0 +1,5 @@
mod backend_proto;
pub mod backend;
pub mod err;
pub mod template;

360
rs/ankirs/src/template.rs Normal file
View file

@ -0,0 +1,360 @@
use crate::err::{AnkiError, Result};
use nom;
use nom::branch::alt;
use nom::bytes::complete::tag;
use nom::error::ErrorKind;
use nom::sequence::delimited;
use std::collections::{HashMap, HashSet};
pub type FieldMap<'a> = HashMap<&'a str, u16>;
// Lexing
//----------------------------------------
#[derive(Debug)]
pub enum Token<'a> {
Text(&'a str),
Replacement(&'a str),
OpenConditional(&'a str),
OpenNegated(&'a str),
CloseConditional(&'a str),
}
/// a span of text, terminated by {{, }} or end of string
pub(crate) fn text_until_handlebars(s: &str) -> nom::IResult<&str, &str> {
let end = s.len();
let limited_end = end
.min(s.find("{{").unwrap_or(end))
.min(s.find("}}").unwrap_or(end));
let (output, input) = s.split_at(limited_end);
if output.is_empty() {
Err(nom::Err::Error((input, ErrorKind::TakeUntil)))
} else {
Ok((input, output))
}
}
/// text outside handlebars
fn text_token(s: &str) -> nom::IResult<&str, Token> {
text_until_handlebars(s).map(|(input, output)| (input, Token::Text(output)))
}
/// text wrapped in handlebars
fn handle_token(s: &str) -> nom::IResult<&str, Token> {
delimited(tag("{{"), text_until_handlebars, tag("}}"))(s)
.map(|(input, output)| (input, classify_handle(output)))
}
/// classify handle based on leading character
fn classify_handle(s: &str) -> Token {
let start = s.trim();
if start.len() < 2 {
return Token::Replacement(start);
}
if start.starts_with('#') {
Token::OpenConditional(&start[1..].trim_start())
} else if start.starts_with('/') {
Token::CloseConditional(&start[1..].trim_start())
} else if start.starts_with('^') {
Token::OpenNegated(&start[1..].trim_start())
} else {
Token::Replacement(start)
}
}
fn next_token(input: &str) -> nom::IResult<&str, Token> {
alt((handle_token, text_token))(input)
}
fn tokens(template: &str) -> impl Iterator<Item = Result<Token>> {
let mut data = template;
std::iter::from_fn(move || {
if data.is_empty() {
return None;
}
match next_token(data) {
Ok((i, o)) => {
data = i;
Some(Ok(o))
}
Err(e) => Some(Err(AnkiError::parse(format!("{:?}", e)))),
}
})
}
// Parsing
//----------------------------------------
#[derive(Debug, PartialEq)]
enum ParsedNode<'a> {
Text(&'a str),
Replacement {
key: &'a str,
filters: Vec<&'a str>,
},
Conditional {
key: &'a str,
children: Vec<ParsedNode<'a>>,
},
NegatedConditional {
key: &'a str,
children: Vec<ParsedNode<'a>>,
},
}
#[derive(Debug)]
pub struct ParsedTemplate<'a>(Vec<ParsedNode<'a>>);
impl ParsedTemplate<'_> {
pub fn from_text(template: &str) -> Result<ParsedTemplate> {
let mut iter = tokens(template);
Ok(Self(parse_inner(&mut iter, None)?))
}
}
fn parse_inner<'a, I: Iterator<Item = Result<Token<'a>>>>(
iter: &mut I,
open_tag: Option<&'a str>,
) -> Result<Vec<ParsedNode<'a>>> {
let mut nodes = vec![];
while let Some(token) = iter.next() {
use Token::*;
nodes.push(match token? {
Text(t) => ParsedNode::Text(t),
Replacement(t) => {
let mut it = t.rsplit(':');
ParsedNode::Replacement {
key: it.next().unwrap(),
filters: it.collect(),
}
}
OpenConditional(t) => ParsedNode::Conditional {
key: t,
children: parse_inner(iter, Some(t))?,
},
OpenNegated(t) => ParsedNode::NegatedConditional {
key: t,
children: parse_inner(iter, Some(t))?,
},
CloseConditional(t) => {
if let Some(open) = open_tag {
if open == t {
// matching closing tag, move back to parent
return Ok(nodes);
}
}
return Err(AnkiError::parse(format!(
"unbalanced closing tag: {:?} / {}",
open_tag, t
)));
}
});
}
if let Some(open) = open_tag {
Err(AnkiError::parse(format!("unclosed conditional {}", open)))
} else {
Ok(nodes)
}
}
// Checking if template is empty
//----------------------------------------
impl ParsedTemplate<'_> {
/// true if provided fields are sufficient to render the template
pub fn renders_with_fields(&self, nonempty_fields: &HashSet<&str>) -> bool {
!template_is_empty(nonempty_fields, &self.0)
}
}
fn template_is_empty<'a>(nonempty_fields: &HashSet<&str>, nodes: &[ParsedNode<'a>]) -> bool {
use ParsedNode::*;
for node in nodes {
match node {
// ignore normal text
Text(_) => (),
Replacement { key, filters } => {
// Anki doesn't consider a type: reference as a required field
if filters.contains(&"type") {
continue;
}
if nonempty_fields.contains(*key) {
// a single replacement is enough
return false;
}
}
Conditional { key, children } => {
if !nonempty_fields.contains(*key) {
continue;
}
if !template_is_empty(nonempty_fields, children) {
return false;
}
}
NegatedConditional { .. } => {
// negated conditionals ignored when determining card generation
continue;
}
}
}
true
}
// Compatibility with old Anki versions
//----------------------------------------
#[derive(Debug, Clone, PartialEq)]
pub enum FieldRequirements {
Any(HashSet<u16>),
All(HashSet<u16>),
None,
}
impl ParsedTemplate<'_> {
/// Return fields required by template.
///
/// This is not able to represent negated expressions or combinations of
/// Any and All, and is provided only for the sake of backwards
/// compatibility.
pub fn requirements(&self, field_map: &FieldMap) -> FieldRequirements {
let mut nonempty: HashSet<_> = Default::default();
let mut ords = HashSet::new();
for (name, ord) in field_map {
nonempty.clear();
nonempty.insert(*name);
if self.renders_with_fields(&nonempty) {
ords.insert(*ord);
}
}
if !ords.is_empty() {
return FieldRequirements::Any(ords);
}
nonempty.extend(field_map.keys());
ords.extend(field_map.values().copied());
for (name, ord) in field_map {
// can we remove this field and still render?
nonempty.remove(name);
if self.renders_with_fields(&nonempty) {
ords.remove(ord);
}
nonempty.insert(*name);
}
if !ords.is_empty() && self.renders_with_fields(&nonempty) {
FieldRequirements::All(ords)
} else {
FieldRequirements::None
}
}
}
// Tests
//---------------------------------------
#[cfg(test)]
mod test {
use super::{FieldMap, ParsedNode::*, ParsedTemplate as PT};
use crate::template::FieldRequirements;
use std::collections::HashSet;
use std::iter::FromIterator;
#[test]
fn test_parsing() {
let tmpl = PT::from_text("foo {{bar}} {{#baz}} quux {{/baz}}").unwrap();
assert_eq!(
tmpl.0,
vec![
Text("foo "),
Replacement {
key: "bar",
filters: vec![]
},
Text(" "),
Conditional {
key: "baz",
children: vec![Text(" quux ")]
}
]
);
let tmpl = PT::from_text("{{^baz}}{{/baz}}").unwrap();
assert_eq!(
tmpl.0,
vec![NegatedConditional {
key: "baz",
children: vec![]
}]
);
PT::from_text("{{#mis}}{{/matched}}").unwrap_err();
PT::from_text("{{/matched}}").unwrap_err();
PT::from_text("{{#mis}}").unwrap_err();
// whitespace
assert_eq!(
PT::from_text("{{ tag }}").unwrap().0,
vec![Replacement {
key: "tag",
filters: vec![]
}]
);
}
#[test]
fn test_nonempty() {
let fields = HashSet::from_iter(vec!["1", "3"].into_iter());
let mut tmpl = PT::from_text("{{2}}{{1}}").unwrap();
assert_eq!(tmpl.renders_with_fields(&fields), true);
tmpl = PT::from_text("{{2}}{{type:cloze:1}}").unwrap();
assert_eq!(tmpl.renders_with_fields(&fields), false);
tmpl = PT::from_text("{{2}}{{4}}").unwrap();
assert_eq!(tmpl.renders_with_fields(&fields), false);
tmpl = PT::from_text("{{#3}}{{^2}}{{1}}{{/2}}{{/3}}").unwrap();
assert_eq!(tmpl.renders_with_fields(&fields), false);
}
#[test]
fn test_requirements() {
let field_map: FieldMap = vec!["a", "b"]
.iter()
.enumerate()
.map(|(a, b)| (*b, a as u16))
.collect();
let mut tmpl = PT::from_text("{{a}}{{b}}").unwrap();
assert_eq!(
tmpl.requirements(&field_map),
FieldRequirements::Any(HashSet::from_iter(vec![0, 1].into_iter()))
);
tmpl = PT::from_text("{{#a}}{{b}}{{/a}}").unwrap();
assert_eq!(
tmpl.requirements(&field_map),
FieldRequirements::All(HashSet::from_iter(vec![0, 1].into_iter()))
);
tmpl = PT::from_text("{{c}}").unwrap();
assert_eq!(tmpl.requirements(&field_map), FieldRequirements::None);
tmpl = PT::from_text("{{^a}}{{b}}{{/a}}").unwrap();
assert_eq!(tmpl.requirements(&field_map), FieldRequirements::None);
tmpl = PT::from_text("{{#a}}{{#b}}{{a}}{{/b}}{{/a}}").unwrap();
assert_eq!(
tmpl.requirements(&field_map),
FieldRequirements::All(HashSet::from_iter(vec![0, 1].into_iter()))
);
tmpl = PT::from_text("{{a}}{{type:b}}").unwrap();
assert_eq!(
tmpl.requirements(&field_map),
FieldRequirements::Any(HashSet::from_iter(vec![0].into_iter()))
);
}
}

16
rs/pymod/Cargo.toml Normal file
View file

@ -0,0 +1,16 @@
[package]
name = "pymod"
version = "0.1.0"
edition = "2018"
authors = ["Ankitects Pty Ltd and contributors"]
[dependencies]
ankirs = { path = "../ankirs" }
[dependencies.pyo3]
version = "0.8.0"
features = ["extension-module"]
[lib]
name = "_ankirs"
crate-type = ["cdylib"]

33
rs/pymod/src/lib.rs Normal file
View file

@ -0,0 +1,33 @@
use ankirs::backend::Backend as RustBackend;
use pyo3::prelude::*;
use pyo3::types::PyBytes;
#[pyclass]
struct Backend {
backend: RustBackend,
}
#[pymethods]
impl Backend {
#[new]
fn init(obj: &PyRawObject) {
obj.init({
Backend {
backend: Default::default(),
}
});
}
fn command(&mut self, py: Python, input: &PyBytes) -> PyResult<PyObject> {
let out_bytes = self.backend.run_command_bytes(input.as_bytes());
let out_obj = PyBytes::new(py, &out_bytes);
Ok(out_obj.into())
}
}
#[pymodule]
fn _ankirs(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_class::<Backend>()?;
Ok(())
}

1
rs/rust-toolchain Normal file
View file

@ -0,0 +1 @@
nightly-2019-12-15

1
rs/rustfmt.toml Normal file
View file

@ -0,0 +1 @@
ignore = ["backend_proto.rs"]

View file

@ -1,6 +1,10 @@
import tempfile, os, shutil import os
import shutil
import tempfile
from anki import Collection as aopen from anki import Collection as aopen
def assertException(exception, func): def assertException(exception, func):
found = False found = False
try: try:
@ -25,6 +29,7 @@ def getEmptyCol():
col = aopen(nam) col = aopen(nam)
return col return col
getEmptyCol.master = "" getEmptyCol.master = ""
# Fallback for when the DB needs options passed in. # Fallback for when the DB needs options passed in.
@ -34,10 +39,12 @@ def getEmptyDeckWith(**kwargs):
os.unlink(nam) os.unlink(nam)
return aopen(nam, **kwargs) return aopen(nam, **kwargs)
def getUpgradeDeckPath(name="anki12.anki"): def getUpgradeDeckPath(name="anki12.anki"):
src = os.path.join(testDir, "support", name) src = os.path.join(testDir, "support", name)
(fd, dst) = tempfile.mkstemp(suffix=".anki2") (fd, dst) = tempfile.mkstemp(suffix=".anki2")
shutil.copy(src, dst) shutil.copy(src, dst)
return dst return dst
testDir = os.path.dirname(__file__) testDir = os.path.dirname(__file__)

View file

@ -1,73 +1,57 @@
import os.path import os.path
from nose.tools import assert_equals
from mock import MagicMock
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from zipfile import ZipFile from zipfile import ZipFile
from mock import MagicMock
from nose2.tools.such import helper
from aqt.addons import AddonManager from aqt.addons import AddonManager
def test_readMinimalManifest(): def test_readMinimalManifest():
assertReadManifest( assertReadManifest(
'{"package": "yes", "name": "no"}', '{"package": "yes", "name": "no"}', {"package": "yes", "name": "no"}
{"package": "yes", "name": "no"}
) )
def test_readExtraKeys(): def test_readExtraKeys():
assertReadManifest( assertReadManifest(
'{"package": "a", "name": "b", "mod": 3, "conflicts": ["d", "e"]}', '{"package": "a", "name": "b", "mod": 3, "conflicts": ["d", "e"]}',
{"package": "a", "name": "b", "mod": 3, "conflicts": ["d", "e"]} {"package": "a", "name": "b", "mod": 3, "conflicts": ["d", "e"]},
) )
def test_invalidManifest(): def test_invalidManifest():
assertReadManifest( assertReadManifest('{"one": 1}', {})
'{"one": 1}',
{}
)
def test_mustHaveName(): def test_mustHaveName():
assertReadManifest( assertReadManifest('{"package": "something"}', {})
'{"package": "something"}',
{}
)
def test_mustHavePackage(): def test_mustHavePackage():
assertReadManifest( assertReadManifest('{"name": "something"}', {})
'{"name": "something"}',
{}
)
def test_invalidJson(): def test_invalidJson():
assertReadManifest( assertReadManifest("this is not a JSON dictionary", {})
'this is not a JSON dictionary',
{}
)
def test_missingManifest(): def test_missingManifest():
assertReadManifest( assertReadManifest(
'{"package": "what", "name": "ever"}', '{"package": "what", "name": "ever"}', {}, nameInZip="not-manifest.bin"
{},
nameInZip="not-manifest.bin"
) )
def test_ignoreExtraKeys(): def test_ignoreExtraKeys():
assertReadManifest( assertReadManifest(
'{"package": "a", "name": "b", "game": "c"}', '{"package": "a", "name": "b", "game": "c"}', {"package": "a", "name": "b"}
{"package": "a", "name": "b"}
) )
def test_conflictsMustBeStrings(): def test_conflictsMustBeStrings():
assertReadManifest( assertReadManifest(
'{"package": "a", "name": "b", "conflicts": ["c", 4, {"d": "e"}]}', '{"package": "a", "name": "b", "conflicts": ["c", 4, {"d": "e"}]}', {}
{}
) )
@ -80,4 +64,4 @@ def assertReadManifest(contents, expectedManifest, nameInZip="manifest.json"):
adm = AddonManager(MagicMock()) adm = AddonManager(MagicMock())
with ZipFile(zfn, "r") as zfile: with ZipFile(zfn, "r") as zfile:
assert_equals(adm.readManifestFile(zfile), expectedManifest) helper.assertEquals(adm.readManifestFile(zfile), expectedManifest)

View file

@ -2,11 +2,12 @@
from tests.shared import getEmptyCol from tests.shared import getEmptyCol
def test_previewCards(): def test_previewCards():
deck = getEmptyCol() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = '1' f["Front"] = "1"
f['Back'] = '2' f["Back"] = "2"
# non-empty and active # non-empty and active
cards = deck.previewCards(f, 0) cards = deck.previewCards(f, 0)
assert len(cards) == 1 assert len(cards) == 1
@ -22,11 +23,12 @@ def test_previewCards():
# make sure we haven't accidentally added cards to the db # make sure we haven't accidentally added cards to the db
assert deck.cardCount() == 1 assert deck.cardCount() == 1
def test_delete(): def test_delete():
deck = getEmptyCol() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = '1' f["Front"] = "1"
f['Back'] = '2' f["Back"] = "2"
deck.addNote(f) deck.addNote(f)
cid = f.cards()[0].id cid = f.cards()[0].id
deck.reset() deck.reset()
@ -38,62 +40,65 @@ def test_delete():
assert deck.db.scalar("select count() from cards") == 0 assert deck.db.scalar("select count() from cards") == 0
assert deck.db.scalar("select count() from graves") == 2 assert deck.db.scalar("select count() from graves") == 2
def test_misc(): def test_misc():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = '1' f["Front"] = "1"
f['Back'] = '2' f["Back"] = "2"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
id = d.models.current()['id'] id = d.models.current()["id"]
assert c.template()['ord'] == 0 assert c.template()["ord"] == 0
def test_genrem(): def test_genrem():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = '1' f["Front"] = "1"
f['Back'] = '' f["Back"] = ""
d.addNote(f) d.addNote(f)
assert len(f.cards()) == 1 assert len(f.cards()) == 1
m = d.models.current() m = d.models.current()
mm = d.models mm = d.models
# adding a new template should automatically create cards # adding a new template should automatically create cards
t = mm.newTemplate("rev") t = mm.newTemplate("rev")
t['qfmt'] = '{{Front}}' t["qfmt"] = "{{Front}}"
t['afmt'] = "" t["afmt"] = ""
mm.addTemplate(m, t) mm.addTemplate(m, t)
mm.save(m, templates=True) mm.save(m, templates=True)
assert len(f.cards()) == 2 assert len(f.cards()) == 2
# if the template is changed to remove cards, they'll be removed # if the template is changed to remove cards, they'll be removed
t['qfmt'] = "{{Back}}" t["qfmt"] = "{{Back}}"
mm.save(m, templates=True) mm.save(m, templates=True)
d.remCards(d.emptyCids()) d.remCards(d.emptyCids())
assert len(f.cards()) == 1 assert len(f.cards()) == 1
# if we add to the note, a card should be automatically generated # if we add to the note, a card should be automatically generated
f.load() f.load()
f['Back'] = "1" f["Back"] = "1"
f.flush() f.flush()
assert len(f.cards()) == 2 assert len(f.cards()) == 2
def test_gendeck(): def test_gendeck():
d = getEmptyCol() d = getEmptyCol()
cloze = d.models.byName("Cloze") cloze = d.models.byName("Cloze")
d.models.setCurrent(cloze) d.models.setCurrent(cloze)
f = d.newNote() f = d.newNote()
f['Text'] = '{{c1::one}}' f["Text"] = "{{c1::one}}"
d.addNote(f) d.addNote(f)
assert d.cardCount() == 1 assert d.cardCount() == 1
assert f.cards()[0].did == 1 assert f.cards()[0].did == 1
# set the model to a new default deck # set the model to a new default deck
newId = d.decks.id("new") newId = d.decks.id("new")
cloze['did'] = newId cloze["did"] = newId
d.models.save(cloze, updateReqs=False) d.models.save(cloze, updateReqs=False)
# a newly generated card should share the first card's deck # a newly generated card should share the first card's deck
f['Text'] += '{{c2::two}}' f["Text"] += "{{c2::two}}"
f.flush() f.flush()
assert f.cards()[1].did == 1 assert f.cards()[1].did == 1
# and same with multiple cards # and same with multiple cards
f['Text'] += '{{c3::three}}' f["Text"] += "{{c3::three}}"
f.flush() f.flush()
assert f.cards()[2].did == 1 assert f.cards()[2].did == 1
# if one of the cards is in a different deck, it should revert to the # if one of the cards is in a different deck, it should revert to the
@ -101,9 +106,6 @@ def test_gendeck():
c = f.cards()[1] c = f.cards()[1]
c.did = newId c.did = newId
c.flush() c.flush()
f['Text'] += '{{c4::four}}' f["Text"] += "{{c4::four}}"
f.flush() f.flush()
assert f.cards()[3].did == newId assert f.cards()[3].did == newId

View file

@ -1,16 +1,15 @@
# coding: utf-8 # coding: utf-8
import os, tempfile import os
from tests.shared import assertException, getEmptyCol import tempfile
from anki.stdmodels import addBasicModel, models
from anki import Collection as aopen from anki import Collection as aopen
from anki.stdmodels import addBasicModel, models
from anki.utils import isWin
from tests.shared import assertException, getEmptyCol
newPath = None
newMod = None
def test_create_open(): def test_create_open():
global newPath, newMod
(fd, path) = tempfile.mkstemp(suffix=".anki2", prefix="test_attachNew") (fd, path) = tempfile.mkstemp(suffix=".anki2", prefix="test_attachNew")
try: try:
os.close(fd) os.close(fd)
@ -30,27 +29,32 @@ def test_create_open():
deck.close() deck.close()
# non-writeable dir # non-writeable dir
assertException(Exception, if isWin:
lambda: aopen("/attachroot.anki2")) dir = "c:\root.anki2"
else:
dir = "/attachroot.anki2"
assertException(Exception, lambda: aopen(dir))
# reuse tmp file from before, test non-writeable file # reuse tmp file from before, test non-writeable file
os.chmod(newPath, 0) os.chmod(newPath, 0)
assertException(Exception, assertException(Exception, lambda: aopen(newPath))
lambda: aopen(newPath))
os.chmod(newPath, 0o666) os.chmod(newPath, 0o666)
os.unlink(newPath) os.unlink(newPath)
def test_noteAddDelete(): def test_noteAddDelete():
deck = getEmptyCol() deck = getEmptyCol()
# add a note # add a note
f = deck.newNote() f = deck.newNote()
f['Front'] = "one"; f['Back'] = "two" f["Front"] = "one"
f["Back"] = "two"
n = deck.addNote(f) n = deck.addNote(f)
assert n == 1 assert n == 1
# test multiple cards - add another template # test multiple cards - add another template
m = deck.models.current(); mm = deck.models m = deck.models.current()
mm = deck.models
t = mm.newTemplate("Reverse") t = mm.newTemplate("Reverse")
t['qfmt'] = "{{Back}}" t["qfmt"] = "{{Back}}"
t['afmt'] = "{{Front}}" t["afmt"] = "{{Front}}"
mm.addTemplate(m, t) mm.addTemplate(m, t)
mm.save(m) mm.save(m)
# the default save doesn't generate cards # the default save doesn't generate cards
@ -61,7 +65,8 @@ def test_noteAddDelete():
assert deck.cardCount() == 2 assert deck.cardCount() == 2
# creating new notes should use both cards # creating new notes should use both cards
f = deck.newNote() f = deck.newNote()
f['Front'] = "three"; f['Back'] = "four" f["Front"] = "three"
f["Back"] = "four"
n = deck.addNote(f) n = deck.addNote(f)
assert n == 2 assert n == 2
assert deck.cardCount() == 4 assert deck.cardCount() == 4
@ -72,36 +77,39 @@ def test_noteAddDelete():
assert not f.dupeOrEmpty() assert not f.dupeOrEmpty()
# now let's make a duplicate # now let's make a duplicate
f2 = deck.newNote() f2 = deck.newNote()
f2['Front'] = "one"; f2['Back'] = "" f2["Front"] = "one"
f2["Back"] = ""
assert f2.dupeOrEmpty() assert f2.dupeOrEmpty()
# empty first field should not be permitted either # empty first field should not be permitted either
f2['Front'] = " " f2["Front"] = " "
assert f2.dupeOrEmpty() assert f2.dupeOrEmpty()
def test_fieldChecksum(): def test_fieldChecksum():
deck = getEmptyCol() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = "new"; f['Back'] = "new2" f["Front"] = "new"
f["Back"] = "new2"
deck.addNote(f) deck.addNote(f)
assert deck.db.scalar( assert deck.db.scalar("select csum from notes") == int("c2a6b03f", 16)
"select csum from notes") == int("c2a6b03f", 16)
# changing the val should change the checksum # changing the val should change the checksum
f['Front'] = "newx" f["Front"] = "newx"
f.flush() f.flush()
assert deck.db.scalar( assert deck.db.scalar("select csum from notes") == int("302811ae", 16)
"select csum from notes") == int("302811ae", 16)
def test_addDelTags(): def test_addDelTags():
deck = getEmptyCol() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = "1" f["Front"] = "1"
deck.addNote(f) deck.addNote(f)
f2 = deck.newNote() f2 = deck.newNote()
f2['Front'] = "2" f2["Front"] = "2"
deck.addNote(f2) deck.addNote(f2)
# adding for a given id # adding for a given id
deck.tags.bulkAdd([f.id], "foo") deck.tags.bulkAdd([f.id], "foo")
f.load(); f2.load() f.load()
f2.load()
assert "foo" in f.tags assert "foo" in f.tags
assert "foo" not in f2.tags assert "foo" not in f2.tags
# should be canonified # should be canonified
@ -110,6 +118,7 @@ def test_addDelTags():
assert f.tags[0] == "aaa" assert f.tags[0] == "aaa"
assert len(f.tags) == 2 assert len(f.tags) == 2
def test_timestamps(): def test_timestamps():
deck = getEmptyCol() deck = getEmptyCol()
assert len(deck.models.models) == len(models) assert len(deck.models.models) == len(models)
@ -117,23 +126,24 @@ def test_timestamps():
addBasicModel(deck) addBasicModel(deck)
assert len(deck.models.models) == 100 + len(models) assert len(deck.models.models) == 100 + len(models)
def test_furigana(): def test_furigana():
deck = getEmptyCol() deck = getEmptyCol()
mm = deck.models mm = deck.models
m = mm.current() m = mm.current()
# filter should work # filter should work
m['tmpls'][0]['qfmt'] = '{{kana:Front}}' m["tmpls"][0]["qfmt"] = "{{kana:Front}}"
mm.save(m) mm.save(m)
n = deck.newNote() n = deck.newNote()
n['Front'] = 'foo[abc]' n["Front"] = "foo[abc]"
deck.addNote(n) deck.addNote(n)
c = n.cards()[0] c = n.cards()[0]
assert c.q().endswith("abc") assert c.q().endswith("abc")
# and should avoid sound # and should avoid sound
n['Front'] = 'foo[sound:abc.mp3]' n["Front"] = "foo[sound:abc.mp3]"
n.flush() n.flush()
assert "sound:" in c.q(reload=True) assert "sound:" in c.q(reload=True)
# it shouldn't throw an error while people are editing # it shouldn't throw an error while people are editing
m['tmpls'][0]['qfmt'] = '{{kana:}}' m["tmpls"][0]["qfmt"] = "{{kana:}}"
mm.save(m) mm.save(m)
c.q(reload=True) c.q(reload=True)

View file

@ -3,6 +3,7 @@
from anki.errors import DeckRenameError from anki.errors import DeckRenameError
from tests.shared import assertException, getEmptyCol from tests.shared import assertException, getEmptyCol
def test_basic(): def test_basic():
deck = getEmptyCol() deck = getEmptyCol()
# we start with a standard deck # we start with a standard deck
@ -34,21 +35,22 @@ def test_basic():
# parents with a different case should be handled correctly # parents with a different case should be handled correctly
deck.decks.id("ONE") deck.decks.id("ONE")
m = deck.models.current() m = deck.models.current()
m['did'] = deck.decks.id("one::two") m["did"] = deck.decks.id("one::two")
deck.models.save(m, updateReqs=False) deck.models.save(m, updateReqs=False)
n = deck.newNote() n = deck.newNote()
n['Front'] = "abc" n["Front"] = "abc"
deck.addNote(n) deck.addNote(n)
# this will error if child and parent case don't match # this will error if child and parent case don't match
deck.sched.deckDueList() deck.sched.deckDueList()
def test_remove(): def test_remove():
deck = getEmptyCol() deck = getEmptyCol()
# create a new deck, and add a note/card to it # create a new deck, and add a note/card to it
g1 = deck.decks.id("g1") g1 = deck.decks.id("g1")
f = deck.newNote() f = deck.newNote()
f['Front'] = "1" f["Front"] = "1"
f.model()['did'] = g1 f.model()["did"] = g1
deck.addNote(f) deck.addNote(f)
c = f.cards()[0] c = f.cards()[0]
assert c.did == g1 assert c.did == g1
@ -62,12 +64,14 @@ def test_remove():
assert deck.decks.name(c.did) == "[no deck]" assert deck.decks.name(c.did) == "[no deck]"
# let's create another deck and explicitly set the card to it # let's create another deck and explicitly set the card to it
g2 = deck.decks.id("g2") g2 = deck.decks.id("g2")
c.did = g2; c.flush() c.did = g2
c.flush()
# this time we'll delete the card/note too # this time we'll delete the card/note too
deck.decks.rem(g2, cardsToo=True) deck.decks.rem(g2, cardsToo=True)
assert deck.cardCount() == 0 assert deck.cardCount() == 0
assert deck.noteCount() == 0 assert deck.noteCount() == 0
def test_rename(): def test_rename():
d = getEmptyCol() d = getEmptyCol()
id = d.decks.id("hello::world") id = d.decks.id("hello::world")
@ -80,8 +84,7 @@ def test_rename():
# create another deck # create another deck
id = d.decks.id("tmp") id = d.decks.id("tmp")
# we can't rename it if it conflicts # we can't rename it if it conflicts
assertException( assertException(Exception, lambda: d.decks.rename(d.decks.get(id), "foo"))
Exception, lambda: d.decks.rename(d.decks.get(id), "foo"))
# when renaming, the children should be renamed too # when renaming, the children should be renamed too
d.decks.id("one::two::three") d.decks.id("one::two::three")
id = d.decks.id("one") id = d.decks.id("one")
@ -102,62 +105,66 @@ def test_rename():
assertException(DeckRenameError, lambda: d.decks.rename(child, "PARENT::child")) assertException(DeckRenameError, lambda: d.decks.rename(child, "PARENT::child"))
def test_renameForDragAndDrop(): def test_renameForDragAndDrop():
d = getEmptyCol() d = getEmptyCol()
def deckNames(): def deckNames():
return [ name for name in sorted(d.decks.allNames()) if name != 'Default' ] return [name for name in sorted(d.decks.allNames()) if name != "Default"]
languages_did = d.decks.id('Languages') languages_did = d.decks.id("Languages")
chinese_did = d.decks.id('Chinese') chinese_did = d.decks.id("Chinese")
hsk_did = d.decks.id('Chinese::HSK') hsk_did = d.decks.id("Chinese::HSK")
# Renaming also renames children # Renaming also renames children
d.decks.renameForDragAndDrop(chinese_did, languages_did) d.decks.renameForDragAndDrop(chinese_did, languages_did)
assert deckNames() == [ 'Languages', 'Languages::Chinese', 'Languages::Chinese::HSK' ] assert deckNames() == ["Languages", "Languages::Chinese", "Languages::Chinese::HSK"]
# Dragging a deck onto itself is a no-op # Dragging a deck onto itself is a no-op
d.decks.renameForDragAndDrop(languages_did, languages_did) d.decks.renameForDragAndDrop(languages_did, languages_did)
assert deckNames() == [ 'Languages', 'Languages::Chinese', 'Languages::Chinese::HSK' ] assert deckNames() == ["Languages", "Languages::Chinese", "Languages::Chinese::HSK"]
# Dragging a deck onto its parent is a no-op # Dragging a deck onto its parent is a no-op
d.decks.renameForDragAndDrop(hsk_did, chinese_did) d.decks.renameForDragAndDrop(hsk_did, chinese_did)
assert deckNames() == [ 'Languages', 'Languages::Chinese', 'Languages::Chinese::HSK' ] assert deckNames() == ["Languages", "Languages::Chinese", "Languages::Chinese::HSK"]
# Dragging a deck onto a descendant is a no-op # Dragging a deck onto a descendant is a no-op
d.decks.renameForDragAndDrop(languages_did, hsk_did) d.decks.renameForDragAndDrop(languages_did, hsk_did)
assert deckNames() == [ 'Languages', 'Languages::Chinese', 'Languages::Chinese::HSK' ] assert deckNames() == ["Languages", "Languages::Chinese", "Languages::Chinese::HSK"]
# Can drag a grandchild onto its grandparent. It becomes a child # Can drag a grandchild onto its grandparent. It becomes a child
d.decks.renameForDragAndDrop(hsk_did, languages_did) d.decks.renameForDragAndDrop(hsk_did, languages_did)
assert deckNames() == [ 'Languages', 'Languages::Chinese', 'Languages::HSK' ] assert deckNames() == ["Languages", "Languages::Chinese", "Languages::HSK"]
# Can drag a deck onto its sibling # Can drag a deck onto its sibling
d.decks.renameForDragAndDrop(hsk_did, chinese_did) d.decks.renameForDragAndDrop(hsk_did, chinese_did)
assert deckNames() == [ 'Languages', 'Languages::Chinese', 'Languages::Chinese::HSK' ] assert deckNames() == ["Languages", "Languages::Chinese", "Languages::Chinese::HSK"]
# Can drag a deck back to the top level # Can drag a deck back to the top level
d.decks.renameForDragAndDrop(chinese_did, None) d.decks.renameForDragAndDrop(chinese_did, None)
assert deckNames() == [ 'Chinese', 'Chinese::HSK', 'Languages' ] assert deckNames() == ["Chinese", "Chinese::HSK", "Languages"]
# Dragging a top level deck to the top level is a no-op # Dragging a top level deck to the top level is a no-op
d.decks.renameForDragAndDrop(chinese_did, None) d.decks.renameForDragAndDrop(chinese_did, None)
assert deckNames() == [ 'Chinese', 'Chinese::HSK', 'Languages' ] assert deckNames() == ["Chinese", "Chinese::HSK", "Languages"]
# can't drack a deck where sibling have same name # can't drack a deck where sibling have same name
new_hsk_did = d.decks.id("HSK") new_hsk_did = d.decks.id("HSK")
assertException(DeckRenameError, lambda: d.decks.renameForDragAndDrop(new_hsk_did, chinese_did)) assertException(
DeckRenameError, lambda: d.decks.renameForDragAndDrop(new_hsk_did, chinese_did)
)
d.decks.rem(new_hsk_did) d.decks.rem(new_hsk_did)
# can't drack a deck where sibling have same name different case # can't drack a deck where sibling have same name different case
new_hsk_did = d.decks.id("hsk") new_hsk_did = d.decks.id("hsk")
assertException(DeckRenameError, lambda: d.decks.renameForDragAndDrop(new_hsk_did, chinese_did)) assertException(
DeckRenameError, lambda: d.decks.renameForDragAndDrop(new_hsk_did, chinese_did)
)
d.decks.rem(new_hsk_did) d.decks.rem(new_hsk_did)
# '' is a convenient alias for the top level DID # '' is a convenient alias for the top level DID
d.decks.renameForDragAndDrop(hsk_did, '') d.decks.renameForDragAndDrop(hsk_did, "")
assert deckNames() == [ 'Chinese', 'HSK', 'Languages' ] assert deckNames() == ["Chinese", "HSK", "Languages"]
def test_check(): def test_check():
d = getEmptyCol() d = getEmptyCol()

View file

@ -1,37 +1,48 @@
# coding: utf-8 # coding: utf-8
import nose, os, tempfile import os
import tempfile
from nose2.tools.decorators import with_setup
from anki import Collection as aopen from anki import Collection as aopen
from anki.exporting import * from anki.exporting import *
from anki.importing import Anki2Importer from anki.importing import Anki2Importer
from .shared import getEmptyCol from .shared import getEmptyCol
deck = None deck = None
ds = None ds = None
testDir = os.path.dirname(__file__) testDir = os.path.dirname(__file__)
def setup1(): def setup1():
global deck global deck
deck = getEmptyCol() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = "foo"; f['Back'] = "bar<br>"; f.tags = ["tag", "tag2"] f["Front"] = "foo"
f["Back"] = "bar<br>"
f.tags = ["tag", "tag2"]
deck.addNote(f) deck.addNote(f)
# with a different deck # with a different deck
f = deck.newNote() f = deck.newNote()
f['Front'] = "baz"; f['Back'] = "qux" f["Front"] = "baz"
f.model()['did'] = deck.decks.id("new deck") f["Back"] = "qux"
f.model()["did"] = deck.decks.id("new deck")
deck.addNote(f) deck.addNote(f)
########################################################################## ##########################################################################
@nose.with_setup(setup1)
@with_setup(setup1)
def test_export_anki(): def test_export_anki():
# create a new deck with its own conf to test conf copying # create a new deck with its own conf to test conf copying
did = deck.decks.id("test") did = deck.decks.id("test")
dobj = deck.decks.get(did) dobj = deck.decks.get(did)
confId = deck.decks.confId("newconf") confId = deck.decks.confId("newconf")
conf = deck.decks.getConf(confId) conf = deck.decks.getConf(confId)
conf['new']['perDay'] = 5 conf["new"]["perDay"] = 5
deck.decks.save(conf) deck.decks.save(conf)
deck.decks.setConf(dobj, confId) deck.decks.setConf(dobj, confId)
# export # export
@ -43,7 +54,7 @@ def test_export_anki():
e.exportInto(newname) e.exportInto(newname)
# exporting should not have changed conf for original deck # exporting should not have changed conf for original deck
conf = deck.decks.confForDid(did) conf = deck.decks.confForDid(did)
assert conf['id'] != 1 assert conf["id"] != 1
# connect to new deck # connect to new deck
d2 = aopen(newname) d2 = aopen(newname)
assert d2.cardCount() == 2 assert d2.cardCount() == 2
@ -51,10 +62,10 @@ def test_export_anki():
did = d2.decks.id("test", create=False) did = d2.decks.id("test", create=False)
assert did assert did
conf2 = d2.decks.confForDid(did) conf2 = d2.decks.confForDid(did)
assert conf2['new']['perDay'] == 20 assert conf2["new"]["perDay"] == 20
dobj = d2.decks.get(did) dobj = d2.decks.get(did)
# conf should be 1 # conf should be 1
assert dobj['conf'] == 1 assert dobj["conf"] == 1
# try again, limited to a deck # try again, limited to a deck
fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".anki2") fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".anki2")
newname = str(newname) newname = str(newname)
@ -65,13 +76,14 @@ def test_export_anki():
d2 = aopen(newname) d2 = aopen(newname)
assert d2.cardCount() == 1 assert d2.cardCount() == 1
@nose.with_setup(setup1)
@with_setup(setup1)
def test_export_ankipkg(): def test_export_ankipkg():
# add a test file to the media folder # add a test file to the media folder
with open(os.path.join(deck.media.dir(), "今日.mp3"), "w") as f: with open(os.path.join(deck.media.dir(), "今日.mp3"), "w") as f:
f.write("test") f.write("test")
n = deck.newNote() n = deck.newNote()
n['Front'] = '[sound:今日.mp3]' n["Front"] = "[sound:今日.mp3]"
deck.addNote(n) deck.addNote(n)
e = AnkiPackageExporter(deck) e = AnkiPackageExporter(deck)
fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".apkg") fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".apkg")
@ -80,13 +92,14 @@ def test_export_ankipkg():
os.unlink(newname) os.unlink(newname)
e.exportInto(newname) e.exportInto(newname)
@nose.with_setup(setup1)
@with_setup(setup1)
def test_export_anki_due(): def test_export_anki_due():
deck = getEmptyCol() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = "foo" f["Front"] = "foo"
deck.addNote(f) deck.addNote(f)
deck.crt -= 86400*10 deck.crt -= 86400 * 10
deck.sched.reset() deck.sched.reset()
c = deck.sched.getCard() c = deck.sched.getCard()
deck.sched.answerCard(c, 3) deck.sched.answerCard(c, 3)
@ -112,7 +125,8 @@ def test_export_anki_due():
deck2.sched.reset() deck2.sched.reset()
assert c.due - deck2.sched.today == 1 assert c.due - deck2.sched.today == 1
# @nose.with_setup(setup1)
# @with_setup(setup1)
# def test_export_textcard(): # def test_export_textcard():
# e = TextCardExporter(deck) # e = TextCardExporter(deck)
# f = unicode(tempfile.mkstemp(prefix="ankitest")[1]) # f = unicode(tempfile.mkstemp(prefix="ankitest")[1])
@ -121,7 +135,8 @@ def test_export_anki_due():
# e.includeTags = True # e.includeTags = True
# e.exportInto(f) # e.exportInto(f)
@nose.with_setup(setup1)
@with_setup(setup1)
def test_export_textnote(): def test_export_textnote():
e = TextNoteExporter(deck) e = TextNoteExporter(deck)
fd, f = tempfile.mkstemp(prefix="ankitest") fd, f = tempfile.mkstemp(prefix="ankitest")
@ -135,5 +150,6 @@ def test_export_textnote():
e.exportInto(f) e.exportInto(f)
assert open(f).readline() == "foo\tbar\n" assert open(f).readline() == "foo\tbar\n"
def test_exporters(): def test_exporters():
assert "*.apkg" in str(exporters()) assert "*.apkg" in str(exporters())

View file

@ -1,9 +1,10 @@
# coding: utf-8 # coding: utf-8
from nose.tools import assert_raises from nose2.tools.such import helper
from anki.find import Finder from anki.find import Finder
from tests.shared import getEmptyCol from tests.shared import getEmptyCol
def test_parse(): def test_parse():
f = Finder(None) f = Finder(None)
assert f._tokenize("hello world") == ["hello", "world"] assert f._tokenize("hello world") == ["hello", "world"]
@ -12,43 +13,54 @@ def test_parse():
assert f._tokenize("one --two") == ["one", "-", "two"] assert f._tokenize("one --two") == ["one", "-", "two"]
assert f._tokenize("one - two") == ["one", "-", "two"] assert f._tokenize("one - two") == ["one", "-", "two"]
assert f._tokenize("one or -two") == ["one", "or", "-", "two"] assert f._tokenize("one or -two") == ["one", "or", "-", "two"]
assert f._tokenize("'hello \"world\"'") == ["hello \"world\""] assert f._tokenize("'hello \"world\"'") == ['hello "world"']
assert f._tokenize('"hello world"') == ["hello world"] assert f._tokenize('"hello world"') == ["hello world"]
assert f._tokenize("one (two or ( three or four))") == [ assert f._tokenize("one (two or ( three or four))") == [
"one", "(", "two", "or", "(", "three", "or", "four", "one",
")", ")"] "(",
"two",
"or",
"(",
"three",
"or",
"four",
")",
")",
]
assert f._tokenize("embedded'string") == ["embedded'string"] assert f._tokenize("embedded'string") == ["embedded'string"]
assert f._tokenize("deck:'two words'") == ["deck:two words"] assert f._tokenize("deck:'two words'") == ["deck:two words"]
def test_findCards(): def test_findCards():
deck = getEmptyCol() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = 'dog' f["Front"] = "dog"
f['Back'] = 'cat' f["Back"] = "cat"
f.tags.append("monkey animal_1 * %") f.tags.append("monkey animal_1 * %")
f1id = f.id f1id = f.id
deck.addNote(f) deck.addNote(f)
firstCardId = f.cards()[0].id firstCardId = f.cards()[0].id
f = deck.newNote() f = deck.newNote()
f['Front'] = 'goats are fun' f["Front"] = "goats are fun"
f['Back'] = 'sheep' f["Back"] = "sheep"
f.tags.append("sheep goat horse animal11") f.tags.append("sheep goat horse animal11")
deck.addNote(f) deck.addNote(f)
f2id = f.id f2id = f.id
f = deck.newNote() f = deck.newNote()
f['Front'] = 'cat' f["Front"] = "cat"
f['Back'] = 'sheep' f["Back"] = "sheep"
deck.addNote(f) deck.addNote(f)
catCard = f.cards()[0] catCard = f.cards()[0]
m = deck.models.current(); mm = deck.models m = deck.models.current()
mm = deck.models
t = mm.newTemplate("Reverse") t = mm.newTemplate("Reverse")
t['qfmt'] = "{{Back}}" t["qfmt"] = "{{Back}}"
t['afmt'] = "{{Front}}" t["afmt"] = "{{Front}}"
mm.addTemplate(m, t) mm.addTemplate(m, t)
mm.save(m) mm.save(m)
f = deck.newNote() f = deck.newNote()
f['Front'] = 'test' f["Front"] = "test"
f['Back'] = 'foo bar' f["Back"] = "foo bar"
deck.addNote(f) deck.addNote(f)
latestCardIds = [c.id for c in f.cards()] latestCardIds = [c.id for c in f.cards()]
# tag searches # tag searches
@ -66,9 +78,7 @@ def test_findCards():
assert len(deck.findCards("tag:sheep -tag:monkey")) == 1 assert len(deck.findCards("tag:sheep -tag:monkey")) == 1
assert len(deck.findCards("-tag:sheep")) == 4 assert len(deck.findCards("-tag:sheep")) == 4
deck.tags.bulkAdd(deck.db.list("select id from notes"), "foo bar") deck.tags.bulkAdd(deck.db.list("select id from notes"), "foo bar")
assert (len(deck.findCards("tag:foo")) == assert len(deck.findCards("tag:foo")) == len(deck.findCards("tag:bar")) == 5
len(deck.findCards("tag:bar")) ==
5)
deck.tags.bulkRem(deck.db.list("select id from notes"), "foo") deck.tags.bulkRem(deck.db.list("select id from notes"), "foo")
assert len(deck.findCards("tag:foo")) == 0 assert len(deck.findCards("tag:foo")) == 0
assert len(deck.findCards("tag:bar")) == 5 assert len(deck.findCards("tag:bar")) == 5
@ -86,7 +96,8 @@ def test_findCards():
c.flush() c.flush()
assert deck.findCards("is:review") == [c.id] assert deck.findCards("is:review") == [c.id]
assert deck.findCards("is:due") == [] assert deck.findCards("is:due") == []
c.due = 0; c.queue = 2 c.due = 0
c.queue = 2
c.flush() c.flush()
assert deck.findCards("is:due") == [c.id] assert deck.findCards("is:due") == [c.id]
assert len(deck.findCards("-is:due")) == 4 assert len(deck.findCards("-is:due")) == 4
@ -97,10 +108,10 @@ def test_findCards():
assert deck.findCards("is:suspended") == [c.id] assert deck.findCards("is:suspended") == [c.id]
# nids # nids
assert deck.findCards("nid:54321") == [] assert deck.findCards("nid:54321") == []
assert len(deck.findCards("nid:%d"%f.id)) == 2 assert len(deck.findCards("nid:%d" % f.id)) == 2
assert len(deck.findCards("nid:%d,%d" % (f1id, f2id))) == 2 assert len(deck.findCards("nid:%d,%d" % (f1id, f2id))) == 2
# templates # templates
with assert_raises(Exception): with helper.assertRaises(Exception):
deck.findCards("card:foo") deck.findCards("card:foo")
assert len(deck.findCards("'card:card 1'")) == 4 assert len(deck.findCards("'card:card 1'")) == 4
assert len(deck.findCards("card:reverse")) == 1 assert len(deck.findCards("card:reverse")) == 1
@ -115,16 +126,16 @@ def test_findCards():
assert len(deck.findCards("front:do")) == 0 assert len(deck.findCards("front:do")) == 0
assert len(deck.findCards("front:*")) == 5 assert len(deck.findCards("front:*")) == 5
# ordering # ordering
deck.conf['sortType'] = "noteCrt" deck.conf["sortType"] = "noteCrt"
assert deck.findCards("front:*", order=True)[-1] in latestCardIds assert deck.findCards("front:*", order=True)[-1] in latestCardIds
assert deck.findCards("", order=True)[-1] in latestCardIds assert deck.findCards("", order=True)[-1] in latestCardIds
deck.conf['sortType'] = "noteFld" deck.conf["sortType"] = "noteFld"
assert deck.findCards("", order=True)[0] == catCard.id assert deck.findCards("", order=True)[0] == catCard.id
assert deck.findCards("", order=True)[-1] in latestCardIds assert deck.findCards("", order=True)[-1] in latestCardIds
deck.conf['sortType'] = "cardMod" deck.conf["sortType"] = "cardMod"
assert deck.findCards("", order=True)[-1] in latestCardIds assert deck.findCards("", order=True)[-1] in latestCardIds
assert deck.findCards("", order=True)[0] == firstCardId assert deck.findCards("", order=True)[0] == firstCardId
deck.conf['sortBackwards'] = True deck.conf["sortBackwards"] = True
assert deck.findCards("", order=True)[0] in latestCardIds assert deck.findCards("", order=True)[0] in latestCardIds
# model # model
assert len(deck.findCards("note:basic")) == 5 assert len(deck.findCards("note:basic")) == 5
@ -136,29 +147,30 @@ def test_findCards():
assert len(deck.findCards("-deck:foo")) == 5 assert len(deck.findCards("-deck:foo")) == 5
assert len(deck.findCards("deck:def*")) == 5 assert len(deck.findCards("deck:def*")) == 5
assert len(deck.findCards("deck:*EFAULT")) == 5 assert len(deck.findCards("deck:*EFAULT")) == 5
with assert_raises(Exception): with helper.assertRaises(Exception):
deck.findCards("deck:*cefault") deck.findCards("deck:*cefault")
# full search # full search
f = deck.newNote() f = deck.newNote()
f['Front'] = 'hello<b>world</b>' f["Front"] = "hello<b>world</b>"
f['Back'] = 'abc' f["Back"] = "abc"
deck.addNote(f) deck.addNote(f)
# as it's the sort field, it matches # as it's the sort field, it matches
assert len(deck.findCards("helloworld")) == 2 assert len(deck.findCards("helloworld")) == 2
#assert len(deck.findCards("helloworld", full=True)) == 2 # assert len(deck.findCards("helloworld", full=True)) == 2
# if we put it on the back, it won't # if we put it on the back, it won't
(f['Front'], f['Back']) = (f['Back'], f['Front']) (f["Front"], f["Back"]) = (f["Back"], f["Front"])
f.flush() f.flush()
assert len(deck.findCards("helloworld")) == 0 assert len(deck.findCards("helloworld")) == 0
#assert len(deck.findCards("helloworld", full=True)) == 2 # assert len(deck.findCards("helloworld", full=True)) == 2
#assert len(deck.findCards("back:helloworld", full=True)) == 2 # assert len(deck.findCards("back:helloworld", full=True)) == 2
# searching for an invalid special tag should not error # searching for an invalid special tag should not error
with assert_raises(Exception): with helper.assertRaises(Exception):
len(deck.findCards("is:invalid")) len(deck.findCards("is:invalid"))
# should be able to limit to parent deck, no children # should be able to limit to parent deck, no children
id = deck.db.scalar("select id from cards limit 1") id = deck.db.scalar("select id from cards limit 1")
deck.db.execute("update cards set did = ? where id = ?", deck.db.execute(
deck.decks.id("Default::Child"), id) "update cards set did = ? where id = ?", deck.decks.id("Default::Child"), id
)
assert len(deck.findCards("deck:default")) == 7 assert len(deck.findCards("deck:default")) == 7
assert len(deck.findCards("deck:default::child")) == 1 assert len(deck.findCards("deck:default::child")) == 1
assert len(deck.findCards("deck:default -deck:default::*")) == 6 assert len(deck.findCards("deck:default -deck:default::*")) == 6
@ -166,7 +178,9 @@ def test_findCards():
id = deck.db.scalar("select id from cards limit 1") id = deck.db.scalar("select id from cards limit 1")
deck.db.execute( deck.db.execute(
"update cards set queue=2, ivl=10, reps=20, due=30, factor=2200 " "update cards set queue=2, ivl=10, reps=20, due=30, factor=2200 "
"where id = ?", id) "where id = ?",
id,
)
assert len(deck.findCards("prop:ivl>5")) == 1 assert len(deck.findCards("prop:ivl>5")) == 1
assert len(deck.findCards("prop:ivl<5")) > 1 assert len(deck.findCards("prop:ivl<5")) > 1
assert len(deck.findCards("prop:ivl>=5")) == 1 assert len(deck.findCards("prop:ivl>=5")) == 1
@ -205,8 +219,8 @@ def test_findCards():
# empty field # empty field
assert len(deck.findCards("front:")) == 0 assert len(deck.findCards("front:")) == 0
f = deck.newNote() f = deck.newNote()
f['Front'] = '' f["Front"] = ""
f['Back'] = 'abc2' f["Back"] = "abc2"
assert deck.addNote(f) == 1 assert deck.addNote(f) == 1
assert len(deck.findCards("front:")) == 1 assert len(deck.findCards("front:")) == 1
# OR searches and nesting # OR searches and nesting
@ -220,60 +234,67 @@ def test_findCards():
assert len(deck.findCards("(()")) == 0 assert len(deck.findCards("(()")) == 0
# added # added
assert len(deck.findCards("added:0")) == 0 assert len(deck.findCards("added:0")) == 0
deck.db.execute("update cards set id = id - 86400*1000 where id = ?", deck.db.execute("update cards set id = id - 86400*1000 where id = ?", id)
id)
assert len(deck.findCards("added:1")) == deck.cardCount() - 1 assert len(deck.findCards("added:1")) == deck.cardCount() - 1
assert len(deck.findCards("added:2")) == deck.cardCount() assert len(deck.findCards("added:2")) == deck.cardCount()
# flag # flag
with assert_raises(Exception): with helper.assertRaises(Exception):
deck.findCards("flag:01") deck.findCards("flag:01")
with assert_raises(Exception): with helper.assertRaises(Exception):
deck.findCards("flag:12") deck.findCards("flag:12")
def test_findReplace(): def test_findReplace():
deck = getEmptyCol() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = 'foo' f["Front"] = "foo"
f['Back'] = 'bar' f["Back"] = "bar"
deck.addNote(f) deck.addNote(f)
f2 = deck.newNote() f2 = deck.newNote()
f2['Front'] = 'baz' f2["Front"] = "baz"
f2['Back'] = 'foo' f2["Back"] = "foo"
deck.addNote(f2) deck.addNote(f2)
nids = [f.id, f2.id] nids = [f.id, f2.id]
# should do nothing # should do nothing
assert deck.findReplace(nids, "abc", "123") == 0 assert deck.findReplace(nids, "abc", "123") == 0
# global replace # global replace
assert deck.findReplace(nids, "foo", "qux") == 2 assert deck.findReplace(nids, "foo", "qux") == 2
f.load(); assert f['Front'] == "qux" f.load()
f2.load(); assert f2['Back'] == "qux" assert f["Front"] == "qux"
f2.load()
assert f2["Back"] == "qux"
# single field replace # single field replace
assert deck.findReplace(nids, "qux", "foo", field="Front") == 1 assert deck.findReplace(nids, "qux", "foo", field="Front") == 1
f.load(); assert f['Front'] == "foo" f.load()
f2.load(); assert f2['Back'] == "qux" assert f["Front"] == "foo"
f2.load()
assert f2["Back"] == "qux"
# regex replace # regex replace
assert deck.findReplace(nids, "B.r", "reg") == 0 assert deck.findReplace(nids, "B.r", "reg") == 0
f.load(); assert f['Back'] != "reg" f.load()
assert f["Back"] != "reg"
assert deck.findReplace(nids, "B.r", "reg", regex=True) == 1 assert deck.findReplace(nids, "B.r", "reg", regex=True) == 1
f.load(); assert f['Back'] == "reg" f.load()
assert f["Back"] == "reg"
def test_findDupes(): def test_findDupes():
deck = getEmptyCol() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = 'foo' f["Front"] = "foo"
f['Back'] = 'bar' f["Back"] = "bar"
deck.addNote(f) deck.addNote(f)
f2 = deck.newNote() f2 = deck.newNote()
f2['Front'] = 'baz' f2["Front"] = "baz"
f2['Back'] = 'bar' f2["Back"] = "bar"
deck.addNote(f2) deck.addNote(f2)
f3 = deck.newNote() f3 = deck.newNote()
f3['Front'] = 'quux' f3["Front"] = "quux"
f3['Back'] = 'bar' f3["Back"] = "bar"
deck.addNote(f3) deck.addNote(f3)
f4 = deck.newNote() f4 = deck.newNote()
f4['Front'] = 'quuux' f4["Front"] = "quuux"
f4['Back'] = 'nope' f4["Back"] = "nope"
deck.addNote(f4) deck.addNote(f4)
r = deck.findDupes("Back") r = deck.findDupes("Back")
assert r[0][0] == "bar" assert r[0][0] == "bar"

View file

@ -1,9 +1,11 @@
from tests.shared import assertException, getEmptyCol from tests.shared import assertException, getEmptyCol
def test_flags(): def test_flags():
col = getEmptyCol() col = getEmptyCol()
n = col.newNote() n = col.newNote()
n['Front'] = "one"; n['Back'] = "two" n["Front"] = "one"
n["Back"] = "two"
cnt = col.addNote(n) cnt = col.addNote(n)
c = n.cards()[0] c = n.cards()[0]
# make sure higher bits are preserved # make sure higher bits are preserved

View file

@ -1,22 +1,29 @@
# coding: utf-8 # coding: utf-8
import os import os
from tests.shared import getUpgradeDeckPath, getEmptyCol
from anki.importing import (
Anki2Importer,
AnkiPackageImporter,
MnemosyneImporter,
SupermemoXmlImporter,
TextImporter,
)
from anki.utils import ids2str from anki.utils import ids2str
from anki.importing import Anki2Importer, TextImporter, \ from tests.shared import getEmptyCol, getUpgradeDeckPath
SupermemoXmlImporter, MnemosyneImporter, AnkiPackageImporter
testDir = os.path.dirname(__file__) testDir = os.path.dirname(__file__)
srcNotes=None srcNotes = None
srcCards=None srcCards = None
def test_anki2_mediadupes(): def test_anki2_mediadupes():
tmp = getEmptyCol() tmp = getEmptyCol()
# add a note that references a sound # add a note that references a sound
n = tmp.newNote() n = tmp.newNote()
n['Front'] = "[sound:foo.mp3]" n["Front"] = "[sound:foo.mp3]"
mid = n.model()['id'] mid = n.model()["id"]
tmp.addNote(n) tmp.addNote(n)
# add that sound to media folder # add that sound to media folder
with open(os.path.join(tmp.media.dir(), "foo.mp3"), "w") as f: with open(os.path.join(tmp.media.dir(), "foo.mp3"), "w") as f:
@ -41,8 +48,7 @@ def test_anki2_mediadupes():
f.write("bar") f.write("bar")
imp = Anki2Importer(empty, tmp.path) imp = Anki2Importer(empty, tmp.path)
imp.run() imp.run()
assert sorted(os.listdir(empty.media.dir())) == [ assert sorted(os.listdir(empty.media.dir())) == ["foo.mp3", "foo_%s.mp3" % mid]
"foo.mp3", "foo_%s.mp3" % mid]
n = empty.getNote(empty.db.scalar("select id from notes")) n = empty.getNote(empty.db.scalar("select id from notes"))
assert "_" in n.fields[0] assert "_" in n.fields[0]
# if the localized media file already exists, we rewrite the note and # if the localized media file already exists, we rewrite the note and
@ -52,25 +58,24 @@ def test_anki2_mediadupes():
f.write("bar") f.write("bar")
imp = Anki2Importer(empty, tmp.path) imp = Anki2Importer(empty, tmp.path)
imp.run() imp.run()
assert sorted(os.listdir(empty.media.dir())) == [ assert sorted(os.listdir(empty.media.dir())) == ["foo.mp3", "foo_%s.mp3" % mid]
"foo.mp3", "foo_%s.mp3" % mid] assert sorted(os.listdir(empty.media.dir())) == ["foo.mp3", "foo_%s.mp3" % mid]
assert sorted(os.listdir(empty.media.dir())) == [
"foo.mp3", "foo_%s.mp3" % mid]
n = empty.getNote(empty.db.scalar("select id from notes")) n = empty.getNote(empty.db.scalar("select id from notes"))
assert "_" in n.fields[0] assert "_" in n.fields[0]
def test_apkg(): def test_apkg():
tmp = getEmptyCol() tmp = getEmptyCol()
apkg = str(os.path.join(testDir, "support/media.apkg")) apkg = str(os.path.join(testDir, "support/media.apkg"))
imp = AnkiPackageImporter(tmp, apkg) imp = AnkiPackageImporter(tmp, apkg)
assert os.listdir(tmp.media.dir()) == [] assert os.listdir(tmp.media.dir()) == []
imp.run() imp.run()
assert os.listdir(tmp.media.dir()) == ['foo.wav'] assert os.listdir(tmp.media.dir()) == ["foo.wav"]
# importing again should be idempotent in terms of media # importing again should be idempotent in terms of media
tmp.remCards(tmp.db.list("select id from cards")) tmp.remCards(tmp.db.list("select id from cards"))
imp = AnkiPackageImporter(tmp, apkg) imp = AnkiPackageImporter(tmp, apkg)
imp.run() imp.run()
assert os.listdir(tmp.media.dir()) == ['foo.wav'] assert os.listdir(tmp.media.dir()) == ["foo.wav"]
# but if the local file has different data, it will rename # but if the local file has different data, it will rename
tmp.remCards(tmp.db.list("select id from cards")) tmp.remCards(tmp.db.list("select id from cards"))
with open(os.path.join(tmp.media.dir(), "foo.wav"), "w") as f: with open(os.path.join(tmp.media.dir(), "foo.wav"), "w") as f:
@ -79,6 +84,7 @@ def test_apkg():
imp.run() imp.run()
assert len(os.listdir(tmp.media.dir())) == 2 assert len(os.listdir(tmp.media.dir())) == 2
def test_anki2_diffmodel_templates(): def test_anki2_diffmodel_templates():
# different from the above as this one tests only the template text being # different from the above as this one tests only the template text being
# changed, not the number of cards/fields # changed, not the number of cards/fields
@ -94,11 +100,12 @@ def test_anki2_diffmodel_templates():
imp.dupeOnSchemaChange = True imp.dupeOnSchemaChange = True
imp.run() imp.run()
# collection should contain the note we imported # collection should contain the note we imported
assert(dst.noteCount() == 1) assert dst.noteCount() == 1
# the front template should contain the text added in the 2nd package # the front template should contain the text added in the 2nd package
tcid = dst.findCards("")[0] # only 1 note in collection tcid = dst.findCards("")[0] # only 1 note in collection
tnote = dst.getCard(tcid).note() tnote = dst.getCard(tcid).note()
assert("Changed Front Template" in dst.findTemplates(tnote)[0]['qfmt']) assert "Changed Front Template" in dst.findTemplates(tnote)[0]["qfmt"]
def test_anki2_updates(): def test_anki2_updates():
# create a new empty deck # create a new empty deck
@ -127,6 +134,7 @@ def test_anki2_updates():
assert dst.noteCount() == 1 assert dst.noteCount() == 1
assert dst.db.scalar("select flds from notes").startswith("goodbye") assert dst.db.scalar("select flds from notes").startswith("goodbye")
def test_csv(): def test_csv():
deck = getEmptyCol() deck = getEmptyCol()
file = str(os.path.join(testDir, "support/text-2fields.txt")) file = str(os.path.join(testDir, "support/text-2fields.txt"))
@ -147,7 +155,7 @@ def test_csv():
n.flush() n.flush()
i.run() i.run()
n.load() n.load()
assert n.tags == ['test'] assert n.tags == ["test"]
# if add-only mode, count will be 0 # if add-only mode, count will be 0
i.importMode = 1 i.importMode = 1
i.run() i.run()
@ -161,6 +169,7 @@ def test_csv():
assert deck.cardCount() == 11 assert deck.cardCount() == 11
deck.close() deck.close()
def test_csv2(): def test_csv2():
deck = getEmptyCol() deck = getEmptyCol()
mm = deck.models mm = deck.models
@ -169,9 +178,9 @@ def test_csv2():
mm.addField(m, f) mm.addField(m, f)
mm.save(m) mm.save(m)
n = deck.newNote() n = deck.newNote()
n['Front'] = "1" n["Front"] = "1"
n['Back'] = "2" n["Back"] = "2"
n['Three'] = "3" n["Three"] = "3"
deck.addNote(n) deck.addNote(n)
# an update with unmapped fields should not clobber those fields # an update with unmapped fields should not clobber those fields
file = str(os.path.join(testDir, "support/text-update.txt")) file = str(os.path.join(testDir, "support/text-update.txt"))
@ -179,16 +188,17 @@ def test_csv2():
i.initMapping() i.initMapping()
i.run() i.run()
n.load() n.load()
assert n['Front'] == "1" assert n["Front"] == "1"
assert n['Back'] == "x" assert n["Back"] == "x"
assert n['Three'] == "3" assert n["Three"] == "3"
deck.close() deck.close()
def test_supermemo_xml_01_unicode(): def test_supermemo_xml_01_unicode():
deck = getEmptyCol() deck = getEmptyCol()
file = str(os.path.join(testDir, "support/supermemo1.xml")) file = str(os.path.join(testDir, "support/supermemo1.xml"))
i = SupermemoXmlImporter(deck, file) i = SupermemoXmlImporter(deck, file)
#i.META.logToStdOutput = True # i.META.logToStdOutput = True
i.run() i.run()
assert i.total == 1 assert i.total == 1
cid = deck.db.scalar("select id from cards") cid = deck.db.scalar("select id from cards")
@ -198,6 +208,7 @@ def test_supermemo_xml_01_unicode():
assert c.reps == 7 assert c.reps == 7
deck.close() deck.close()
def test_mnemo(): def test_mnemo():
deck = getEmptyCol() deck = getEmptyCol()
file = str(os.path.join(testDir, "support/mnemo.db")) file = str(os.path.join(testDir, "support/mnemo.db"))

View file

@ -1,20 +1,21 @@
# coding: utf-8 # coding: utf-8
import os import os
import shutil import shutil
from tests.shared import getEmptyCol
from anki.utils import stripHTML from anki.utils import stripHTML
from tests.shared import getEmptyCol
def test_latex(): def test_latex():
d = getEmptyCol() d = getEmptyCol()
# change latex cmd to simulate broken build # change latex cmd to simulate broken build
import anki.latex import anki.latex
anki.latex.pngCommands[0][0] = "nolatex" anki.latex.pngCommands[0][0] = "nolatex"
# add a note with latex # add a note with latex
f = d.newNote() f = d.newNote()
f['Front'] = "[latex]hello[/latex]" f["Front"] = "[latex]hello[/latex]"
d.addNote(f) d.addNote(f)
# but since latex couldn't run, there's nothing there # but since latex couldn't run, there's nothing there
assert len(os.listdir(d.media.dir())) == 0 assert len(os.listdir(d.media.dir())) == 0
@ -34,13 +35,13 @@ def test_latex():
assert ".png" in f.cards()[0].q() assert ".png" in f.cards()[0].q()
# adding new notes should cause generation on question display # adding new notes should cause generation on question display
f = d.newNote() f = d.newNote()
f['Front'] = "[latex]world[/latex]" f["Front"] = "[latex]world[/latex]"
d.addNote(f) d.addNote(f)
f.cards()[0].q() f.cards()[0].q()
assert len(os.listdir(d.media.dir())) == 2 assert len(os.listdir(d.media.dir())) == 2
# another note with the same media should reuse # another note with the same media should reuse
f = d.newNote() f = d.newNote()
f['Front'] = " [latex]world[/latex]" f["Front"] = " [latex]world[/latex]"
d.addNote(f) d.addNote(f)
assert len(os.listdir(d.media.dir())) == 2 assert len(os.listdir(d.media.dir())) == 2
oldcard = f.cards()[0] oldcard = f.cards()[0]
@ -49,7 +50,7 @@ def test_latex():
# missing media will show the latex # missing media will show the latex
anki.latex.build = False anki.latex.build = False
f = d.newNote() f = d.newNote()
f['Front'] = "[latex]foo[/latex]" f["Front"] = "[latex]foo[/latex]"
d.addNote(f) d.addNote(f)
assert len(os.listdir(d.media.dir())) == 2 assert len(os.listdir(d.media.dir())) == 2
assert stripHTML(f.cards()[0].q()) == "[latex]foo[/latex]" assert stripHTML(f.cards()[0].q()) == "[latex]foo[/latex]"
@ -86,10 +87,11 @@ def test_latex():
(result, msg) = _test_includes_bad_command("\\emph") (result, msg) = _test_includes_bad_command("\\emph")
assert not result, msg assert not result, msg
def _test_includes_bad_command(bad): def _test_includes_bad_command(bad):
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = '[latex]%s[/latex]' % bad f["Front"] = "[latex]%s[/latex]" % bad
d.addNote(f) d.addNote(f)
q = f.cards()[0].q() q = f.cards()[0].q()
return ("'%s' is not allowed on cards" % bad in q, "Card content: %s" % q) return ("'%s' is not allowed on cards" % bad in q, "Card content: %s" % q)

View file

@ -1,7 +1,7 @@
# coding: utf-8 # coding: utf-8
import tempfile
import os import os
import tempfile
import time import time
from .shared import getEmptyCol, testDir from .shared import getEmptyCol, testDir
@ -23,6 +23,7 @@ def test_add():
f.write("world") f.write("world")
assert d.media.addFile(path) == "foo (1).jpg" assert d.media.addFile(path) == "foo (1).jpg"
def test_strings(): def test_strings():
d = getEmptyCol() d = getEmptyCol()
mf = d.media.filesInStr mf = d.media.filesInStr
@ -31,12 +32,16 @@ def test_strings():
assert mf(mid, "aoeu<img src='foo.jpg'>ao") == ["foo.jpg"] assert mf(mid, "aoeu<img src='foo.jpg'>ao") == ["foo.jpg"]
assert mf(mid, "aoeu<img src='foo.jpg' style='test'>ao") == ["foo.jpg"] assert mf(mid, "aoeu<img src='foo.jpg' style='test'>ao") == ["foo.jpg"]
assert mf(mid, "aoeu<img src='foo.jpg'><img src=\"bar.jpg\">ao") == [ assert mf(mid, "aoeu<img src='foo.jpg'><img src=\"bar.jpg\">ao") == [
"foo.jpg", "bar.jpg"] "foo.jpg",
"bar.jpg",
]
assert mf(mid, "aoeu<img src=foo.jpg style=bar>ao") == ["foo.jpg"] assert mf(mid, "aoeu<img src=foo.jpg style=bar>ao") == ["foo.jpg"]
assert mf(mid, "<img src=one><img src=two>") == ["one", "two"] assert mf(mid, "<img src=one><img src=two>") == ["one", "two"]
assert mf(mid, "aoeu<img src=\"foo.jpg\">ao") == ["foo.jpg"] assert mf(mid, 'aoeu<img src="foo.jpg">ao') == ["foo.jpg"]
assert mf(mid, "aoeu<img src=\"foo.jpg\"><img class=yo src=fo>ao") == [ assert mf(mid, 'aoeu<img src="foo.jpg"><img class=yo src=fo>ao') == [
"foo.jpg", "fo"] "foo.jpg",
"fo",
]
assert mf(mid, "aou[sound:foo.mp3]aou") == ["foo.mp3"] assert mf(mid, "aou[sound:foo.mp3]aou") == ["foo.mp3"]
sp = d.media.strip sp = d.media.strip
assert sp("aoeu") == "aoeu" assert sp("aoeu") == "aoeu"
@ -47,6 +52,7 @@ def test_strings():
assert es("<img src='http://foo.com'>") == "<img src='http://foo.com'>" assert es("<img src='http://foo.com'>") == "<img src='http://foo.com'>"
assert es('<img src="foo bar.jpg">') == '<img src="foo%20bar.jpg">' assert es('<img src="foo bar.jpg">') == '<img src="foo%20bar.jpg">'
def test_deckIntegration(): def test_deckIntegration():
d = getEmptyCol() d = getEmptyCol()
# create a media dir # create a media dir
@ -56,11 +62,13 @@ def test_deckIntegration():
d.media.addFile(file) d.media.addFile(file)
# add a note which references it # add a note which references it
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "<img src='fake.png'>" f["Front"] = "one"
f["Back"] = "<img src='fake.png'>"
d.addNote(f) d.addNote(f)
# and one which references a non-existent file # and one which references a non-existent file
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "<img src='fake2.png'>" f["Front"] = "one"
f["Back"] = "<img src='fake2.png'>"
d.addNote(f) d.addNote(f)
# and add another file which isn't used # and add another file which isn't used
with open(os.path.join(d.media.dir(), "foo.jpg"), "w") as f: with open(os.path.join(d.media.dir(), "foo.jpg"), "w") as f:
@ -70,12 +78,16 @@ def test_deckIntegration():
assert ret[0] == ["fake2.png"] assert ret[0] == ["fake2.png"]
assert ret[1] == ["foo.jpg"] assert ret[1] == ["foo.jpg"]
def test_changes(): def test_changes():
d = getEmptyCol() d = getEmptyCol()
def added(): def added():
return d.media.db.execute("select fname from media where csum is not null") return d.media.db.execute("select fname from media where csum is not null")
def removed(): def removed():
return d.media.db.execute("select fname from media where csum is null") return d.media.db.execute("select fname from media where csum is null")
assert not list(added()) assert not list(added())
assert not list(removed()) assert not list(removed())
# add a file # add a file
@ -97,26 +109,27 @@ def test_changes():
assert not list(removed()) assert not list(removed())
# but if we add another file, it will # but if we add another file, it will
time.sleep(1) time.sleep(1)
with open(path+"2", "w") as f: with open(path + "2", "w") as f:
f.write("yo") f.write("yo")
d.media.findChanges() d.media.findChanges()
assert len(list(added())) == 2 assert len(list(added())) == 2
assert not list(removed()) assert not list(removed())
# deletions should get noticed too # deletions should get noticed too
time.sleep(1) time.sleep(1)
os.unlink(path+"2") os.unlink(path + "2")
d.media.findChanges() d.media.findChanges()
assert len(list(added())) == 1 assert len(list(added())) == 1
assert len(list(removed())) == 1 assert len(list(removed())) == 1
def test_illegal(): def test_illegal():
d = getEmptyCol() d = getEmptyCol()
aString = "a:b|cd\\e/f\0g*h" aString = "a:b|cd\\e/f\0g*h"
good = "abcdefgh" good = "abcdefgh"
assert d.media.stripIllegal(aString) == good assert d.media.stripIllegal(aString) == good
for c in aString: for c in aString:
bad = d.media.hasIllegal("somestring"+c+"morestring") bad = d.media.hasIllegal("somestring" + c + "morestring")
if bad: if bad:
assert(c not in good) assert c not in good
else: else:
assert(c in good) assert c in good

View file

@ -1,43 +1,47 @@
# coding: utf-8 # coding: utf-8
import time
from tests.shared import getEmptyCol
from anki.consts import MODEL_CLOZE
from anki.utils import stripHTML, joinFields
import anki.template import anki.template
from anki.consts import MODEL_CLOZE
from anki.utils import isWin, joinFields, stripHTML
from tests.shared import getEmptyCol
def test_modelDelete(): def test_modelDelete():
deck = getEmptyCol() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = '1' f["Front"] = "1"
f['Back'] = '2' f["Back"] = "2"
deck.addNote(f) deck.addNote(f)
assert deck.cardCount() == 1 assert deck.cardCount() == 1
deck.models.rem(deck.models.current()) deck.models.rem(deck.models.current())
assert deck.cardCount() == 0 assert deck.cardCount() == 0
def test_modelCopy(): def test_modelCopy():
deck = getEmptyCol() deck = getEmptyCol()
m = deck.models.current() m = deck.models.current()
m2 = deck.models.copy(m) m2 = deck.models.copy(m)
assert m2['name'] == "Basic copy" assert m2["name"] == "Basic copy"
assert m2['id'] != m['id'] assert m2["id"] != m["id"]
assert len(m2['flds']) == 2 assert len(m2["flds"]) == 2
assert len(m['flds']) == 2 assert len(m["flds"]) == 2
assert len(m2['flds']) == len(m['flds']) assert len(m2["flds"]) == len(m["flds"])
assert len(m['tmpls']) == 1 assert len(m["tmpls"]) == 1
assert len(m2['tmpls']) == 1 assert len(m2["tmpls"]) == 1
assert deck.models.scmhash(m) == deck.models.scmhash(m2) assert deck.models.scmhash(m) == deck.models.scmhash(m2)
def test_fields(): def test_fields():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = '1' f["Front"] = "1"
f['Back'] = '2' f["Back"] = "2"
d.addNote(f) d.addNote(f)
m = d.models.current() m = d.models.current()
# make sure renaming a field updates the templates # make sure renaming a field updates the templates
d.models.renameField(m, m['flds'][0], "NewFront") d.models.renameField(m, m["flds"][0], "NewFront")
assert "{{NewFront}}" in m['tmpls'][0]['qfmt'] assert "{{NewFront}}" in m["tmpls"][0]["qfmt"]
h = d.models.scmhash(m) h = d.models.scmhash(m)
# add a field # add a field
f = d.models.newField("foo") f = d.models.newField("foo")
@ -46,44 +50,46 @@ def test_fields():
assert d.models.scmhash(m) != h assert d.models.scmhash(m) != h
# rename it # rename it
d.models.renameField(m, f, "bar") d.models.renameField(m, f, "bar")
assert d.getNote(d.models.nids(m)[0])['bar'] == '' assert d.getNote(d.models.nids(m)[0])["bar"] == ""
# delete back # delete back
d.models.remField(m, m['flds'][1]) d.models.remField(m, m["flds"][1])
assert d.getNote(d.models.nids(m)[0]).fields == ["1", ""] assert d.getNote(d.models.nids(m)[0]).fields == ["1", ""]
# move 0 -> 1 # move 0 -> 1
d.models.moveField(m, m['flds'][0], 1) d.models.moveField(m, m["flds"][0], 1)
assert d.getNote(d.models.nids(m)[0]).fields == ["", "1"] assert d.getNote(d.models.nids(m)[0]).fields == ["", "1"]
# move 1 -> 0 # move 1 -> 0
d.models.moveField(m, m['flds'][1], 0) d.models.moveField(m, m["flds"][1], 0)
assert d.getNote(d.models.nids(m)[0]).fields == ["1", ""] assert d.getNote(d.models.nids(m)[0]).fields == ["1", ""]
# add another and put in middle # add another and put in middle
f = d.models.newField("baz") f = d.models.newField("baz")
d.models.addField(m, f) d.models.addField(m, f)
f = d.getNote(d.models.nids(m)[0]) f = d.getNote(d.models.nids(m)[0])
f['baz'] = "2" f["baz"] = "2"
f.flush() f.flush()
assert d.getNote(d.models.nids(m)[0]).fields == ["1", "", "2"] assert d.getNote(d.models.nids(m)[0]).fields == ["1", "", "2"]
# move 2 -> 1 # move 2 -> 1
d.models.moveField(m, m['flds'][2], 1) d.models.moveField(m, m["flds"][2], 1)
assert d.getNote(d.models.nids(m)[0]).fields == ["1", "2", ""] assert d.getNote(d.models.nids(m)[0]).fields == ["1", "2", ""]
# move 0 -> 2 # move 0 -> 2
d.models.moveField(m, m['flds'][0], 2) d.models.moveField(m, m["flds"][0], 2)
assert d.getNote(d.models.nids(m)[0]).fields == ["2", "", "1"] assert d.getNote(d.models.nids(m)[0]).fields == ["2", "", "1"]
# move 0 -> 1 # move 0 -> 1
d.models.moveField(m, m['flds'][0], 1) d.models.moveField(m, m["flds"][0], 1)
assert d.getNote(d.models.nids(m)[0]).fields == ["", "2", "1"] assert d.getNote(d.models.nids(m)[0]).fields == ["", "2", "1"]
def test_templates(): def test_templates():
d = getEmptyCol() d = getEmptyCol()
m = d.models.current(); mm = d.models m = d.models.current()
mm = d.models
t = mm.newTemplate("Reverse") t = mm.newTemplate("Reverse")
t['qfmt'] = "{{Back}}" t["qfmt"] = "{{Back}}"
t['afmt'] = "{{Front}}" t["afmt"] = "{{Front}}"
mm.addTemplate(m, t) mm.addTemplate(m, t)
mm.save(m) mm.save(m)
f = d.newNote() f = d.newNote()
f['Front'] = '1' f["Front"] = "1"
f['Back'] = '2' f["Back"] = "2"
d.addNote(f) d.addNote(f)
assert d.cardCount() == 2 assert d.cardCount() == 2
(c, c2) = f.cards() (c, c2) = f.cards()
@ -92,11 +98,12 @@ def test_templates():
assert c2.ord == 1 assert c2.ord == 1
# switch templates # switch templates
d.models.moveTemplate(m, c.template(), 1) d.models.moveTemplate(m, c.template(), 1)
c.load(); c2.load() c.load()
c2.load()
assert c.ord == 1 assert c.ord == 1
assert c2.ord == 0 assert c2.ord == 0
# removing a template should delete its cards # removing a template should delete its cards
assert d.models.remTemplate(m, m['tmpls'][0]) assert d.models.remTemplate(m, m["tmpls"][0])
assert d.cardCount() == 1 assert d.cardCount() == 1
# and should have updated the other cards' ordinals # and should have updated the other cards' ordinals
c = f.cards()[0] c = f.cards()[0]
@ -105,64 +112,67 @@ def test_templates():
# it shouldn't be possible to orphan notes by removing templates # it shouldn't be possible to orphan notes by removing templates
t = mm.newTemplate("template name") t = mm.newTemplate("template name")
mm.addTemplate(m, t) mm.addTemplate(m, t)
assert not d.models.remTemplate(m, m['tmpls'][0]) assert not d.models.remTemplate(m, m["tmpls"][0])
def test_cloze_ordinals(): def test_cloze_ordinals():
d = getEmptyCol() d = getEmptyCol()
d.models.setCurrent(d.models.byName("Cloze")) d.models.setCurrent(d.models.byName("Cloze"))
m = d.models.current(); mm = d.models m = d.models.current()
mm = d.models
#We replace the default Cloze template
# We replace the default Cloze template
t = mm.newTemplate("ChainedCloze") t = mm.newTemplate("ChainedCloze")
t['qfmt'] = "{{text:cloze:Text}}" t["qfmt"] = "{{text:cloze:Text}}"
t['afmt'] = "{{text:cloze:Text}}" t["afmt"] = "{{text:cloze:Text}}"
mm.addTemplate(m, t) mm.addTemplate(m, t)
mm.save(m) mm.save(m)
d.models.remTemplate(m, m['tmpls'][0]) d.models.remTemplate(m, m["tmpls"][0])
f = d.newNote() f = d.newNote()
f['Text'] = '{{c1::firstQ::firstA}}{{c2::secondQ::secondA}}' f["Text"] = "{{c1::firstQ::firstA}}{{c2::secondQ::secondA}}"
d.addNote(f) d.addNote(f)
assert d.cardCount() == 2 assert d.cardCount() == 2
(c, c2) = f.cards() (c, c2) = f.cards()
# first card should have first ord # first card should have first ord
assert c.ord == 0 assert c.ord == 0
assert c2.ord == 1 assert c2.ord == 1
def test_text(): def test_text():
d = getEmptyCol() d = getEmptyCol()
m = d.models.current() m = d.models.current()
m['tmpls'][0]['qfmt'] = "{{text:Front}}" m["tmpls"][0]["qfmt"] = "{{text:Front}}"
d.models.save(m) d.models.save(m)
f = d.newNote() f = d.newNote()
f['Front'] = 'hello<b>world' f["Front"] = "hello<b>world"
d.addNote(f) d.addNote(f)
assert "helloworld" in f.cards()[0].q() assert "helloworld" in f.cards()[0].q()
def test_cloze(): def test_cloze():
d = getEmptyCol() d = getEmptyCol()
d.models.setCurrent(d.models.byName("Cloze")) d.models.setCurrent(d.models.byName("Cloze"))
f = d.newNote() f = d.newNote()
assert f.model()['name'] == "Cloze" assert f.model()["name"] == "Cloze"
# a cloze model with no clozes is not empty # a cloze model with no clozes is not empty
f['Text'] = 'nothing' f["Text"] = "nothing"
assert d.addNote(f) assert d.addNote(f)
# try with one cloze # try with one cloze
f = d.newNote() f = d.newNote()
f['Text'] = "hello {{c1::world}}" f["Text"] = "hello {{c1::world}}"
assert d.addNote(f) == 1 assert d.addNote(f) == 1
assert "hello <span class=cloze>[...]</span>" in f.cards()[0].q() assert "hello <span class=cloze>[...]</span>" in f.cards()[0].q()
assert "hello <span class=cloze>world</span>" in f.cards()[0].a() assert "hello <span class=cloze>world</span>" in f.cards()[0].a()
# and with a comment # and with a comment
f = d.newNote() f = d.newNote()
f['Text'] = "hello {{c1::world::typical}}" f["Text"] = "hello {{c1::world::typical}}"
assert d.addNote(f) == 1 assert d.addNote(f) == 1
assert "<span class=cloze>[typical]</span>" in f.cards()[0].q() assert "<span class=cloze>[typical]</span>" in f.cards()[0].q()
assert "<span class=cloze>world</span>" in f.cards()[0].a() assert "<span class=cloze>world</span>" in f.cards()[0].a()
# and with 2 clozes # and with 2 clozes
f = d.newNote() f = d.newNote()
f['Text'] = "hello {{c1::world}} {{c2::bar}}" f["Text"] = "hello {{c1::world}} {{c2::bar}}"
assert d.addNote(f) == 2 assert d.addNote(f) == 2
(c1, c2) = f.cards() (c1, c2) = f.cards()
assert "<span class=cloze>[...]</span> bar" in c1.q() assert "<span class=cloze>[...]</span> bar" in c1.q()
@ -172,25 +182,27 @@ def test_cloze():
# if there are multiple answers for a single cloze, they are given in a # if there are multiple answers for a single cloze, they are given in a
# list # list
f = d.newNote() f = d.newNote()
f['Text'] = "a {{c1::b}} {{c1::c}}" f["Text"] = "a {{c1::b}} {{c1::c}}"
assert d.addNote(f) == 1 assert d.addNote(f) == 1
assert "<span class=cloze>b</span> <span class=cloze>c</span>" in ( assert "<span class=cloze>b</span> <span class=cloze>c</span>" in (f.cards()[0].a())
f.cards()[0].a())
# if we add another cloze, a card should be generated # if we add another cloze, a card should be generated
cnt = d.cardCount() cnt = d.cardCount()
f['Text'] = "{{c2::hello}} {{c1::foo}}" f["Text"] = "{{c2::hello}} {{c1::foo}}"
f.flush() f.flush()
assert d.cardCount() == cnt + 1 assert d.cardCount() == cnt + 1
# 0 or negative indices are not supported # 0 or negative indices are not supported
f['Text'] += "{{c0::zero}} {{c-1:foo}}" f["Text"] += "{{c0::zero}} {{c-1:foo}}"
f.flush() f.flush()
assert len(f.cards()) == 2 assert len(f.cards()) == 2
def test_cloze_mathjax(): def test_cloze_mathjax():
d = getEmptyCol() d = getEmptyCol()
d.models.setCurrent(d.models.byName("Cloze")) d.models.setCurrent(d.models.byName("Cloze"))
f = d.newNote() f = d.newNote()
f['Text'] = r'{{c1::ok}} \(2^2\) {{c2::not ok}} \(2^{{c3::2}}\) \(x^3\) {{c4::blah}} {{c5::text with \(x^2\) jax}}' f[
"Text"
] = r"{{c1::ok}} \(2^2\) {{c2::not ok}} \(2^{{c3::2}}\) \(x^3\) {{c4::blah}} {{c5::text with \(x^2\) jax}}"
assert d.addNote(f) assert d.addNote(f)
assert len(f.cards()) == 5 assert len(f.cards()) == 5
assert "class=cloze" in f.cards()[0].q() assert "class=cloze" in f.cards()[0].q()
@ -200,56 +212,70 @@ def test_cloze_mathjax():
assert "class=cloze" in f.cards()[4].q() assert "class=cloze" in f.cards()[4].q()
f = d.newNote() f = d.newNote()
f['Text'] = r'\(a\) {{c1::b}} \[ {{c1::c}} \]' f["Text"] = r"\(a\) {{c1::b}} \[ {{c1::c}} \]"
assert d.addNote(f) assert d.addNote(f)
assert len(f.cards()) == 1 assert len(f.cards()) == 1
assert f.cards()[0].q().endswith('\(a\) <span class=cloze>[...]</span> \[ [...] \]') assert f.cards()[0].q().endswith("\(a\) <span class=cloze>[...]</span> \[ [...] \]")
def test_chained_mods(): def test_chained_mods():
d = getEmptyCol() d = getEmptyCol()
d.models.setCurrent(d.models.byName("Cloze")) d.models.setCurrent(d.models.byName("Cloze"))
m = d.models.current(); mm = d.models m = d.models.current()
mm = d.models
#We replace the default Cloze template
# We replace the default Cloze template
t = mm.newTemplate("ChainedCloze") t = mm.newTemplate("ChainedCloze")
t['qfmt'] = "{{cloze:text:Text}}" t["qfmt"] = "{{cloze:text:Text}}"
t['afmt'] = "{{cloze:text:Text}}" t["afmt"] = "{{cloze:text:Text}}"
mm.addTemplate(m, t) mm.addTemplate(m, t)
mm.save(m) mm.save(m)
d.models.remTemplate(m, m['tmpls'][0]) d.models.remTemplate(m, m["tmpls"][0])
f = d.newNote() f = d.newNote()
q1 = '<span style=\"color:red\">phrase</span>' q1 = '<span style="color:red">phrase</span>'
a1 = '<b>sentence</b>' a1 = "<b>sentence</b>"
q2 = '<span style=\"color:red\">en chaine</span>' q2 = '<span style="color:red">en chaine</span>'
a2 = '<i>chained</i>' a2 = "<i>chained</i>"
f['Text'] = "This {{c1::%s::%s}} demonstrates {{c1::%s::%s}} clozes." % (q1,a1,q2,a2) f["Text"] = "This {{c1::%s::%s}} demonstrates {{c1::%s::%s}} clozes." % (
q1,
a1,
q2,
a2,
)
assert d.addNote(f) == 1 assert d.addNote(f) == 1
assert "This <span class=cloze>[sentence]</span> demonstrates <span class=cloze>[chained]</span> clozes." in f.cards()[0].q() assert (
assert "This <span class=cloze>phrase</span> demonstrates <span class=cloze>en chaine</span> clozes." in f.cards()[0].a() "This <span class=cloze>[sentence]</span> demonstrates <span class=cloze>[chained]</span> clozes."
in f.cards()[0].q()
)
assert (
"This <span class=cloze>phrase</span> demonstrates <span class=cloze>en chaine</span> clozes."
in f.cards()[0].a()
)
def test_modelChange(): def test_modelChange():
deck = getEmptyCol() deck = getEmptyCol()
basic = deck.models.byName("Basic") basic = deck.models.byName("Basic")
cloze = deck.models.byName("Cloze") cloze = deck.models.byName("Cloze")
# enable second template and add a note # enable second template and add a note
m = deck.models.current(); mm = deck.models m = deck.models.current()
mm = deck.models
t = mm.newTemplate("Reverse") t = mm.newTemplate("Reverse")
t['qfmt'] = "{{Back}}" t["qfmt"] = "{{Back}}"
t['afmt'] = "{{Front}}" t["afmt"] = "{{Front}}"
mm.addTemplate(m, t) mm.addTemplate(m, t)
mm.save(m) mm.save(m)
f = deck.newNote() f = deck.newNote()
f['Front'] = 'f' f["Front"] = "f"
f['Back'] = 'b123' f["Back"] = "b123"
deck.addNote(f) deck.addNote(f)
# switch fields # switch fields
map = {0: 1, 1: 0} map = {0: 1, 1: 0}
deck.models.change(basic, [f.id], basic, map, None) deck.models.change(basic, [f.id], basic, map, None)
f.load() f.load()
assert f['Front'] == 'b123' assert f["Front"] == "b123"
assert f['Back'] == 'f' assert f["Back"] == "f"
# switch cards # switch cards
c0 = f.cards()[0] c0 = f.cards()[0]
c1 = f.cards()[1] c1 = f.cards()[1]
@ -258,7 +284,9 @@ def test_modelChange():
assert c0.ord == 0 assert c0.ord == 0
assert c1.ord == 1 assert c1.ord == 1
deck.models.change(basic, [f.id], basic, None, map) deck.models.change(basic, [f.id], basic, None, map)
f.load(); c0.load(); c1.load() f.load()
c0.load()
c1.load()
assert "f" in c0.q() assert "f" in c0.q()
assert "b123" in c1.q() assert "b123" in c1.q()
assert c0.ord == 1 assert c0.ord == 1
@ -267,6 +295,9 @@ def test_modelChange():
assert f.cards()[0].id == c1.id assert f.cards()[0].id == c1.id
# delete first card # delete first card
map = {0: None, 1: 1} map = {0: None, 1: 1}
if isWin:
# The low precision timer on Windows reveals a race condition
time.sleep(0.05)
deck.models.change(basic, [f.id], basic, None, map) deck.models.change(basic, [f.id], basic, None, map)
f.load() f.load()
c0.load() c0.load()
@ -279,30 +310,31 @@ def test_modelChange():
# but we have two cards, as a new one was generated # but we have two cards, as a new one was generated
assert len(f.cards()) == 2 assert len(f.cards()) == 2
# an unmapped field becomes blank # an unmapped field becomes blank
assert f['Front'] == 'b123' assert f["Front"] == "b123"
assert f['Back'] == 'f' assert f["Back"] == "f"
deck.models.change(basic, [f.id], basic, map, None) deck.models.change(basic, [f.id], basic, map, None)
f.load() f.load()
assert f['Front'] == '' assert f["Front"] == ""
assert f['Back'] == 'f' assert f["Back"] == "f"
# another note to try model conversion # another note to try model conversion
f = deck.newNote() f = deck.newNote()
f['Front'] = 'f2' f["Front"] = "f2"
f['Back'] = 'b2' f["Back"] = "b2"
deck.addNote(f) deck.addNote(f)
assert deck.models.useCount(basic) == 2 assert deck.models.useCount(basic) == 2
assert deck.models.useCount(cloze) == 0 assert deck.models.useCount(cloze) == 0
map = {0: 0, 1: 1} map = {0: 0, 1: 1}
deck.models.change(basic, [f.id], cloze, map, map) deck.models.change(basic, [f.id], cloze, map, map)
f.load() f.load()
assert f['Text'] == "f2" assert f["Text"] == "f2"
assert len(f.cards()) == 2 assert len(f.cards()) == 2
# back the other way, with deletion of second ord # back the other way, with deletion of second ord
deck.models.remTemplate(basic, basic['tmpls'][1]) deck.models.remTemplate(basic, basic["tmpls"][1])
assert deck.db.scalar("select count() from cards where nid = ?", f.id) == 2 assert deck.db.scalar("select count() from cards where nid = ?", f.id) == 2
deck.models.change(cloze, [f.id], basic, map, map) deck.models.change(cloze, [f.id], basic, map, map)
assert deck.db.scalar("select count() from cards where nid = ?", f.id) == 1 assert deck.db.scalar("select count() from cards where nid = ?", f.id) == 1
def test_templates(): def test_templates():
d = dict(Foo="x", Bar="y") d = dict(Foo="x", Bar="y")
assert anki.template.render("{{Foo}}", d) == "x" assert anki.template.render("{{Foo}}", d) == "x"
@ -311,68 +343,83 @@ def test_templates():
assert anki.template.render("{{#Bar}}{{#Foo}}{{Foo}}{{/Foo}}{{/Bar}}", d) == "x" assert anki.template.render("{{#Bar}}{{#Foo}}{{Foo}}{{/Foo}}{{/Bar}}", d) == "x"
assert anki.template.render("{{#Baz}}{{#Foo}}{{Foo}}{{/Foo}}{{/Baz}}", d) == "" assert anki.template.render("{{#Baz}}{{#Foo}}{{Foo}}{{/Foo}}{{/Baz}}", d) == ""
def test_availOrds(): def test_availOrds():
d = getEmptyCol() d = getEmptyCol()
m = d.models.current(); mm = d.models m = d.models.current()
t = m['tmpls'][0] mm = d.models
t = m["tmpls"][0]
f = d.newNote() f = d.newNote()
f['Front'] = "1" f["Front"] = "1"
# simple templates # simple templates
assert mm.availOrds(m, joinFields(f.fields)) == [0] assert mm.availOrds(m, joinFields(f.fields)) == [0]
t['qfmt'] = "{{Back}}" t["qfmt"] = "{{Back}}"
mm.save(m, templates=True) mm.save(m, templates=True)
assert not mm.availOrds(m, joinFields(f.fields)) assert not mm.availOrds(m, joinFields(f.fields))
# AND # AND
t['qfmt'] = "{{#Front}}{{#Back}}{{Front}}{{/Back}}{{/Front}}" t["qfmt"] = "{{#Front}}{{#Back}}{{Front}}{{/Back}}{{/Front}}"
mm.save(m, templates=True) mm.save(m, templates=True)
assert not mm.availOrds(m, joinFields(f.fields)) assert not mm.availOrds(m, joinFields(f.fields))
t['qfmt'] = "{{#Front}}\n{{#Back}}\n{{Front}}\n{{/Back}}\n{{/Front}}" t["qfmt"] = "{{#Front}}\n{{#Back}}\n{{Front}}\n{{/Back}}\n{{/Front}}"
mm.save(m, templates=True) mm.save(m, templates=True)
assert not mm.availOrds(m, joinFields(f.fields)) assert not mm.availOrds(m, joinFields(f.fields))
# OR # OR
t['qfmt'] = "{{Front}}\n{{Back}}" t["qfmt"] = "{{Front}}\n{{Back}}"
mm.save(m, templates=True) mm.save(m, templates=True)
assert mm.availOrds(m, joinFields(f.fields)) == [0] assert mm.availOrds(m, joinFields(f.fields)) == [0]
t['Front'] = "" t["Front"] = ""
t['Back'] = "1" t["Back"] = "1"
assert mm.availOrds(m, joinFields(f.fields)) == [0] assert mm.availOrds(m, joinFields(f.fields)) == [0]
def test_req(): def test_req():
def reqSize(model): def reqSize(model):
if model['type'] == MODEL_CLOZE: if model["type"] == MODEL_CLOZE:
return return
assert (len(model['tmpls']) == len(model['req'])) assert len(model["tmpls"]) == len(model["req"])
d = getEmptyCol() d = getEmptyCol()
mm = d.models mm = d.models
basic = mm.byName("Basic") basic = mm.byName("Basic")
assert 'req' in basic assert "req" in basic
reqSize(basic) reqSize(basic)
assert basic['req'][0] == [0, 'all', [0]] r = basic["req"][0]
assert r[0] == 0
assert r[1] in ("any", "all")
assert r[2] == [0]
opt = mm.byName("Basic (optional reversed card)") opt = mm.byName("Basic (optional reversed card)")
reqSize(opt) reqSize(opt)
assert opt['req'][0] == [0, 'all', [0]] r = opt["req"][0]
assert opt['req'][1] == [1, 'all', [1, 2]] assert r[1] in ("any", "all")
#testing any assert r[2] == [0]
opt['tmpls'][1]['qfmt'] = "{{Back}}{{Add Reverse}}" assert opt["req"][1] == [1, "all", [1, 2]]
# testing any
opt["tmpls"][1]["qfmt"] = "{{Back}}{{Add Reverse}}"
mm.save(opt, templates=True) mm.save(opt, templates=True)
assert opt['req'][1] == [1, 'any', [1, 2]] assert opt["req"][1] == [1, "any", [1, 2]]
#testing None # testing None
opt['tmpls'][1]['qfmt'] = "{{^Add Reverse}}{{Back}}{{/Add Reverse}}" opt["tmpls"][1]["qfmt"] = "{{^Add Reverse}}{{Back}}{{/Add Reverse}}"
mm.save(opt, templates=True) mm.save(opt, templates=True)
assert opt['req'][1] == [1, 'none', []] assert opt["req"][1] == [1, "none", []]
def test_updatereqs_performance(): opt = mm.byName("Basic (type in the answer)")
import time reqSize(opt)
d = getEmptyCol() r = opt["req"][0]
mm = d.models assert r[1] in ("any", "all")
m = mm.byName("Basic") assert r[2] == [0]
for i in range(100):
fld = mm.newField(f"field{i}")
mm.addField(m, fld) # def test_updatereqs_performance():
tmpl = mm.newTemplate(f"template{i}") # import time
tmpl['qfmt'] = "{{field%s}}" % i # d = getEmptyCol()
mm.addTemplate(m, tmpl) # mm = d.models
t = time.time() # m = mm.byName("Basic")
mm.save(m, templates=True) # for i in range(100):
print("took", (time.time()-t)*100) # fld = mm.newField(f"field{i}")
# mm.addField(m, fld)
# tmpl = mm.newTemplate(f"template{i}")
# tmpl['qfmt'] = "{{field%s}}" % i
# mm.addTemplate(m, tmpl)
# t = time.time()
# mm.save(m, templates=True)
# print("took", (time.time()-t)*100)

View file

@ -1,39 +1,45 @@
# coding: utf-8 # coding: utf-8
import time
import copy import copy
import time
from anki.consts import STARTING_FACTOR from anki.consts import STARTING_FACTOR
from tests.shared import getEmptyCol as getEmptyColOrig
from anki.utils import intTime
from anki.hooks import addHook from anki.hooks import addHook
from anki.utils import intTime
from tests.shared import getEmptyCol as getEmptyColOrig
def getEmptyCol(): def getEmptyCol():
col = getEmptyColOrig() col = getEmptyColOrig()
col.changeSchedulerVer(1) col.changeSchedulerVer(1)
return col return col
def test_clock(): def test_clock():
d = getEmptyCol() d = getEmptyCol()
if (d.sched.dayCutoff - intTime()) < 10*60: if (d.sched.dayCutoff - intTime()) < 10 * 60:
raise Exception("Unit tests will fail around the day rollover.") raise Exception("Unit tests will fail around the day rollover.")
def checkRevIvl(d, c, targetIvl): def checkRevIvl(d, c, targetIvl):
min, max = d.sched._fuzzIvlRange(targetIvl) min, max = d.sched._fuzzIvlRange(targetIvl)
return min <= c.ivl <= max return min <= c.ivl <= max
def test_basics(): def test_basics():
d = getEmptyCol() d = getEmptyCol()
d.reset() d.reset()
assert not d.sched.getCard() assert not d.sched.getCard()
def test_new(): def test_new():
d = getEmptyCol() d = getEmptyCol()
d.reset() d.reset()
assert d.sched.newCount == 0 assert d.sched.newCount == 0
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "two" f["Front"] = "one"
f["Back"] = "two"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
assert d.sched.newCount == 1 assert d.sched.newCount == 1
@ -71,15 +77,16 @@ def test_new():
# assert qs[n] in c.q() # assert qs[n] in c.q()
# d.sched.answerCard(c, 2) # d.sched.answerCard(c, 2)
def test_newLimits(): def test_newLimits():
d = getEmptyCol() d = getEmptyCol()
# add some notes # add some notes
g2 = d.decks.id("Default::foo") g2 = d.decks.id("Default::foo")
for i in range(30): for i in range(30):
f = d.newNote() f = d.newNote()
f['Front'] = str(i) f["Front"] = str(i)
if i > 4: if i > 4:
f.model()['did'] = g2 f.model()["did"] = g2
d.addNote(f) d.addNote(f)
# give the child deck a different configuration # give the child deck a different configuration
c2 = d.decks.confId("new conf") c2 = d.decks.confId("new conf")
@ -92,33 +99,36 @@ def test_newLimits():
assert c.did == 1 assert c.did == 1
# limit the parent to 10 cards, meaning we get 10 in total # limit the parent to 10 cards, meaning we get 10 in total
conf1 = d.decks.confForDid(1) conf1 = d.decks.confForDid(1)
conf1['new']['perDay'] = 10 conf1["new"]["perDay"] = 10
d.reset() d.reset()
assert d.sched.newCount == 10 assert d.sched.newCount == 10
# if we limit child to 4, we should get 9 # if we limit child to 4, we should get 9
conf2 = d.decks.confForDid(g2) conf2 = d.decks.confForDid(g2)
conf2['new']['perDay'] = 4 conf2["new"]["perDay"] = 4
d.reset() d.reset()
assert d.sched.newCount == 9 assert d.sched.newCount == 9
def test_newBoxes(): def test_newBoxes():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
c = d.sched.getCard() c = d.sched.getCard()
d.sched._cardConf(c)['new']['delays'] = [1,2,3,4,5] d.sched._cardConf(c)["new"]["delays"] = [1, 2, 3, 4, 5]
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
# should handle gracefully # should handle gracefully
d.sched._cardConf(c)['new']['delays'] = [1] d.sched._cardConf(c)["new"]["delays"] = [1]
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
def test_learn(): def test_learn():
d = getEmptyCol() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "two" f["Front"] = "one"
f["Back"] = "two"
f = d.addNote(f) f = d.addNote(f)
# set as a learn card and rebuild queues # set as a learn card and rebuild queues
d.db.execute("update cards set queue=0, type=0") d.db.execute("update cards set queue=0, type=0")
@ -126,12 +136,12 @@ def test_learn():
# sched.getCard should return it, since it's due in the past # sched.getCard should return it, since it's due in the past
c = d.sched.getCard() c = d.sched.getCard()
assert c assert c
d.sched._cardConf(c)['new']['delays'] = [0.5, 3, 10] d.sched._cardConf(c)["new"]["delays"] = [0.5, 3, 10]
# fail it # fail it
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
# it should have three reps left to graduation # it should have three reps left to graduation
assert c.left%1000 == 3 assert c.left % 1000 == 3
assert c.left//1000 == 3 assert c.left // 1000 == 3
# it should by due in 30 seconds # it should by due in 30 seconds
t = round(c.due - time.time()) t = round(c.due - time.time())
assert t >= 25 and t <= 40 assert t >= 25 and t <= 40
@ -139,8 +149,8 @@ def test_learn():
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
# it should by due in 3 minutes # it should by due in 3 minutes
assert round(c.due - time.time()) in (179, 180) assert round(c.due - time.time()) in (179, 180)
assert c.left%1000 == 2 assert c.left % 1000 == 2
assert c.left//1000 == 2 assert c.left // 1000 == 2
# check log is accurate # check log is accurate
log = d.db.first("select * from revlog order by id desc") log = d.db.first("select * from revlog order by id desc")
assert log[3] == 2 assert log[3] == 2
@ -150,8 +160,8 @@ def test_learn():
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
# it should by due in 10 minutes # it should by due in 10 minutes
assert round(c.due - time.time()) in (599, 600) assert round(c.due - time.time()) in (599, 600)
assert c.left%1000 == 1 assert c.left % 1000 == 1
assert c.left//1000 == 1 assert c.left // 1000 == 1
# the next pass should graduate the card # the next pass should graduate the card
assert c.queue == 1 assert c.queue == 1
assert c.type == 1 assert c.type == 1
@ -159,7 +169,7 @@ def test_learn():
assert c.queue == 2 assert c.queue == 2
assert c.type == 2 assert c.type == 2
# should be due tomorrow, with an interval of 1 # should be due tomorrow, with an interval of 1
assert c.due == d.sched.today+1 assert c.due == d.sched.today + 1
assert c.ivl == 1 assert c.ivl == 1
# or normal removal # or normal removal
c.type = 0 c.type = 0
@ -188,14 +198,15 @@ def test_learn():
assert c.queue == 2 assert c.queue == 2
assert c.due == 321 assert c.due == 321
def test_learn_collapsed(): def test_learn_collapsed():
d = getEmptyCol() d = getEmptyCol()
# add 2 notes # add 2 notes
f = d.newNote() f = d.newNote()
f['Front'] = "1" f["Front"] = "1"
f = d.addNote(f) f = d.addNote(f)
f = d.newNote() f = d.newNote()
f['Front'] = "2" f["Front"] = "2"
f = d.addNote(f) f = d.addNote(f)
# set as a learn card and rebuild queues # set as a learn card and rebuild queues
d.db.execute("update cards set queue=0, type=0") d.db.execute("update cards set queue=0, type=0")
@ -214,27 +225,28 @@ def test_learn_collapsed():
c = d.sched.getCard() c = d.sched.getCard()
assert not c.q().endswith("2") assert not c.q().endswith("2")
def test_learn_day(): def test_learn_day():
d = getEmptyCol() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
f = d.addNote(f) f = d.addNote(f)
d.sched.reset() d.sched.reset()
c = d.sched.getCard() c = d.sched.getCard()
d.sched._cardConf(c)['new']['delays'] = [1, 10, 1440, 2880] d.sched._cardConf(c)["new"]["delays"] = [1, 10, 1440, 2880]
# pass it # pass it
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
# two reps to graduate, 1 more today # two reps to graduate, 1 more today
assert c.left%1000 == 3 assert c.left % 1000 == 3
assert c.left//1000 == 1 assert c.left // 1000 == 1
assert d.sched.counts() == (0, 1, 0) assert d.sched.counts() == (0, 1, 0)
c = d.sched.getCard() c = d.sched.getCard()
ni = d.sched.nextIvl ni = d.sched.nextIvl
assert ni(c, 2) == 86400 assert ni(c, 2) == 86400
# answering it will place it in queue 3 # answering it will place it in queue 3
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
assert c.due == d.sched.today+1 assert c.due == d.sched.today + 1
assert c.queue == 3 assert c.queue == 3
assert not d.sched.getCard() assert not d.sched.getCard()
# for testing, move it back a day # for testing, move it back a day
@ -244,7 +256,7 @@ def test_learn_day():
assert d.sched.counts() == (0, 1, 0) assert d.sched.counts() == (0, 1, 0)
c = d.sched.getCard() c = d.sched.getCard()
# nextIvl should work # nextIvl should work
assert ni(c, 2) == 86400*2 assert ni(c, 2) == 86400 * 2
# if we fail it, it should be back in the correct queue # if we fail it, it should be back in the correct queue
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
assert c.queue == 1 assert c.queue == 1
@ -266,17 +278,19 @@ def test_learn_day():
c.flush() c.flush()
d.reset() d.reset()
assert d.sched.counts() == (0, 0, 1) assert d.sched.counts() == (0, 0, 1)
d.sched._cardConf(c)['lapse']['delays'] = [1440] d.sched._cardConf(c)["lapse"]["delays"] = [1440]
c = d.sched.getCard() c = d.sched.getCard()
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
assert c.queue == 3 assert c.queue == 3
assert d.sched.counts() == (0, 0, 0) assert d.sched.counts() == (0, 0, 0)
def test_reviews(): def test_reviews():
d = getEmptyCol() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "two" f["Front"] = "one"
f["Back"] = "two"
d.addNote(f) d.addNote(f)
# set the card up as a review card, due 8 days ago # set the card up as a review card, due 8 days ago
c = f.cards()[0] c = f.cards()[0]
@ -295,7 +309,7 @@ def test_reviews():
################################################## ##################################################
# different delay to new # different delay to new
d.reset() d.reset()
d.sched._cardConf(c)['lapse']['delays'] = [2, 20] d.sched._cardConf(c)["lapse"]["delays"] = [2, 20]
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
assert c.queue == 1 assert c.queue == 1
# it should be due tomorrow, with an interval of 1 # it should be due tomorrow, with an interval of 1
@ -313,7 +327,7 @@ def test_reviews():
# check ests. # check ests.
ni = d.sched.nextIvl ni = d.sched.nextIvl
assert ni(c, 1) == 120 assert ni(c, 1) == 120
assert ni(c, 2) == 20*60 assert ni(c, 2) == 20 * 60
# try again with an ease of 2 instead # try again with an ease of 2 instead
################################################## ##################################################
c = copy.copy(cardcopy) c = copy.copy(cardcopy)
@ -355,8 +369,10 @@ def test_reviews():
c.flush() c.flush()
# steup hook # steup hook
hooked = [] hooked = []
def onLeech(card): def onLeech(card):
hooked.append(1) hooked.append(1)
addHook("leech", onLeech) addHook("leech", onLeech)
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
assert hooked assert hooked
@ -364,10 +380,11 @@ def test_reviews():
c.load() c.load()
assert c.queue == -1 assert c.queue == -1
def test_button_spacing(): def test_button_spacing():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
# 1 day ivl review card due now # 1 day ivl review card due now
c = f.cards()[0] c = f.cards()[0]
@ -384,13 +401,14 @@ def test_button_spacing():
assert ni(c, 3) == "3 days" assert ni(c, 3) == "3 days"
assert ni(c, 4) == "4 days" assert ni(c, 4) == "4 days"
def test_overdue_lapse(): def test_overdue_lapse():
# disabled in commit 3069729776990980f34c25be66410e947e9d51a2 # disabled in commit 3069729776990980f34c25be66410e947e9d51a2
return return
d = getEmptyCol() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
# simulate a review that was lapsed and is now due for its normal review # simulate a review that was lapsed and is now due for its normal review
c = f.cards()[0] c = f.cards()[0]
@ -419,13 +437,15 @@ def test_overdue_lapse():
d.sched.reset() d.sched.reset()
assert d.sched.counts() == (0, 0, 1) assert d.sched.counts() == (0, 0, 1)
def test_finished(): def test_finished():
d = getEmptyCol() d = getEmptyCol()
# nothing due # nothing due
assert "Congratulations" in d.sched.finishedMsg() assert "Congratulations" in d.sched.finishedMsg()
assert "limit" not in d.sched.finishedMsg() assert "limit" not in d.sched.finishedMsg()
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "two" f["Front"] = "one"
f["Back"] = "two"
d.addNote(f) d.addNote(f)
# have a new card # have a new card
assert "new cards available" in d.sched.finishedMsg() assert "new cards available" in d.sched.finishedMsg()
@ -438,44 +458,46 @@ def test_finished():
assert "Congratulations" in d.sched.finishedMsg() assert "Congratulations" in d.sched.finishedMsg()
assert "limit" not in d.sched.finishedMsg() assert "limit" not in d.sched.finishedMsg()
def test_nextIvl(): def test_nextIvl():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "two" f["Front"] = "one"
f["Back"] = "two"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
conf = d.decks.confForDid(1) conf = d.decks.confForDid(1)
conf['new']['delays'] = [0.5, 3, 10] conf["new"]["delays"] = [0.5, 3, 10]
conf['lapse']['delays'] = [1, 5, 9] conf["lapse"]["delays"] = [1, 5, 9]
c = d.sched.getCard() c = d.sched.getCard()
# new cards # new cards
################################################## ##################################################
ni = d.sched.nextIvl ni = d.sched.nextIvl
assert ni(c, 1) == 30 assert ni(c, 1) == 30
assert ni(c, 2) == 180 assert ni(c, 2) == 180
assert ni(c, 3) == 4*86400 assert ni(c, 3) == 4 * 86400
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
# cards in learning # cards in learning
################################################## ##################################################
assert ni(c, 1) == 30 assert ni(c, 1) == 30
assert ni(c, 2) == 180 assert ni(c, 2) == 180
assert ni(c, 3) == 4*86400 assert ni(c, 3) == 4 * 86400
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
assert ni(c, 1) == 30 assert ni(c, 1) == 30
assert ni(c, 2) == 600 assert ni(c, 2) == 600
assert ni(c, 3) == 4*86400 assert ni(c, 3) == 4 * 86400
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
# normal graduation is tomorrow # normal graduation is tomorrow
assert ni(c, 2) == 1*86400 assert ni(c, 2) == 1 * 86400
assert ni(c, 3) == 4*86400 assert ni(c, 3) == 4 * 86400
# lapsed cards # lapsed cards
################################################## ##################################################
c.type = 2 c.type = 2
c.ivl = 100 c.ivl = 100
c.factor = STARTING_FACTOR c.factor = STARTING_FACTOR
assert ni(c, 1) == 60 assert ni(c, 1) == 60
assert ni(c, 2) == 100*86400 assert ni(c, 2) == 100 * 86400
assert ni(c, 3) == 100*86400 assert ni(c, 3) == 100 * 86400
# review cards # review cards
################################################## ##################################################
c.queue = 2 c.queue = 2
@ -484,8 +506,8 @@ def test_nextIvl():
# failing it should put it at 60s # failing it should put it at 60s
assert ni(c, 1) == 60 assert ni(c, 1) == 60
# or 1 day if relearn is false # or 1 day if relearn is false
d.sched._cardConf(c)['lapse']['delays']=[] d.sched._cardConf(c)["lapse"]["delays"] = []
assert ni(c, 1) == 1*86400 assert ni(c, 1) == 1 * 86400
# (* 100 1.2 86400)10368000.0 # (* 100 1.2 86400)10368000.0
assert ni(c, 2) == 10368000 assert ni(c, 2) == 10368000
# (* 100 2.5 86400)21600000.0 # (* 100 2.5 86400)21600000.0
@ -494,10 +516,11 @@ def test_nextIvl():
assert ni(c, 4) == 28080000 assert ni(c, 4) == 28080000
assert d.sched.nextIvlStr(c, 4) == "10.8 months" assert d.sched.nextIvlStr(c, 4) == "10.8 months"
def test_misc(): def test_misc():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
# burying # burying
@ -508,10 +531,11 @@ def test_misc():
d.reset() d.reset()
assert d.sched.getCard() assert d.sched.getCard()
def test_suspend(): def test_suspend():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
# suspending # suspending
@ -525,7 +549,11 @@ def test_suspend():
d.reset() d.reset()
assert d.sched.getCard() assert d.sched.getCard()
# should cope with rev cards being relearnt # should cope with rev cards being relearnt
c.due = 0; c.ivl = 100; c.type = 2; c.queue = 2; c.flush() c.due = 0
c.ivl = 100
c.type = 2
c.queue = 2
c.flush()
d.reset() d.reset()
c = d.sched.getCard() c = d.sched.getCard()
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
@ -551,10 +579,11 @@ def test_suspend():
assert c.due == 1 assert c.due == 1
assert c.did == 1 assert c.did == 1
def test_cram(): def test_cram():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
c.ivl = 100 c.ivl = 100
@ -566,7 +595,7 @@ def test_cram():
c.startTimer() c.startTimer()
c.flush() c.flush()
d.reset() d.reset()
assert d.sched.counts() == (0,0,0) assert d.sched.counts() == (0, 0, 0)
cardcopy = copy.copy(c) cardcopy = copy.copy(c)
# create a dynamic deck and refresh it # create a dynamic deck and refresh it
did = d.decks.newDyn("Cram") did = d.decks.newDyn("Cram")
@ -575,18 +604,18 @@ def test_cram():
# should appear as new in the deck list # should appear as new in the deck list
assert sorted(d.sched.deckDueList())[0][4] == 1 assert sorted(d.sched.deckDueList())[0][4] == 1
# and should appear in the counts # and should appear in the counts
assert d.sched.counts() == (1,0,0) assert d.sched.counts() == (1, 0, 0)
# grab it and check estimates # grab it and check estimates
c = d.sched.getCard() c = d.sched.getCard()
assert d.sched.answerButtons(c) == 2 assert d.sched.answerButtons(c) == 2
assert d.sched.nextIvl(c, 1) == 600 assert d.sched.nextIvl(c, 1) == 600
assert d.sched.nextIvl(c, 2) == 138*60*60*24 assert d.sched.nextIvl(c, 2) == 138 * 60 * 60 * 24
cram = d.decks.get(did) cram = d.decks.get(did)
cram['delays'] = [1, 10] cram["delays"] = [1, 10]
assert d.sched.answerButtons(c) == 3 assert d.sched.answerButtons(c) == 3
assert d.sched.nextIvl(c, 1) == 60 assert d.sched.nextIvl(c, 1) == 60
assert d.sched.nextIvl(c, 2) == 600 assert d.sched.nextIvl(c, 2) == 600
assert d.sched.nextIvl(c, 3) == 138*60*60*24 assert d.sched.nextIvl(c, 3) == 138 * 60 * 60 * 24
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
# elapsed time was 75 days # elapsed time was 75 days
# factor = 2.5+1.2/2 = 1.85 # factor = 2.5+1.2/2 = 1.85
@ -595,12 +624,11 @@ def test_cram():
assert c.odue == 138 assert c.odue == 138
assert c.queue == 1 assert c.queue == 1
# should be logged as a cram rep # should be logged as a cram rep
assert d.db.scalar( assert d.db.scalar("select type from revlog order by id desc limit 1") == 3
"select type from revlog order by id desc limit 1") == 3
# check ivls again # check ivls again
assert d.sched.nextIvl(c, 1) == 60 assert d.sched.nextIvl(c, 1) == 60
assert d.sched.nextIvl(c, 2) == 138*60*60*24 assert d.sched.nextIvl(c, 2) == 138 * 60 * 60 * 24
assert d.sched.nextIvl(c, 3) == 138*60*60*24 assert d.sched.nextIvl(c, 3) == 138 * 60 * 60 * 24
# when it graduates, due is updated # when it graduates, due is updated
c = d.sched.getCard() c = d.sched.getCard()
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
@ -616,7 +644,7 @@ def test_cram():
# check ivls again - passing should be idempotent # check ivls again - passing should be idempotent
assert d.sched.nextIvl(c, 1) == 60 assert d.sched.nextIvl(c, 1) == 60
assert d.sched.nextIvl(c, 2) == 600 assert d.sched.nextIvl(c, 2) == 600
assert d.sched.nextIvl(c, 3) == 138*60*60*24 assert d.sched.nextIvl(c, 3) == 138 * 60 * 60 * 24
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
assert c.ivl == 138 assert c.ivl == 138
assert c.odue == 138 assert c.odue == 138
@ -630,20 +658,20 @@ def test_cram():
assert len(d.sched.deckDueList()) == 1 assert len(d.sched.deckDueList()) == 1
c.load() c.load()
assert c.ivl == 1 assert c.ivl == 1
assert c.due == d.sched.today+1 assert c.due == d.sched.today + 1
# make it due # make it due
d.reset() d.reset()
assert d.sched.counts() == (0,0,0) assert d.sched.counts() == (0, 0, 0)
c.due = -5 c.due = -5
c.ivl = 100 c.ivl = 100
c.flush() c.flush()
d.reset() d.reset()
assert d.sched.counts() == (0,0,1) assert d.sched.counts() == (0, 0, 1)
# cram again # cram again
did = d.decks.newDyn("Cram") did = d.decks.newDyn("Cram")
d.sched.rebuildDyn(did) d.sched.rebuildDyn(did)
d.reset() d.reset()
assert d.sched.counts() == (0,0,1) assert d.sched.counts() == (0, 0, 1)
c.load() c.load()
assert d.sched.answerButtons(c) == 4 assert d.sched.answerButtons(c) == 4
# add a sibling so we can test minSpace, etc # add a sibling so we can test minSpace, etc
@ -661,10 +689,11 @@ def test_cram():
# it should have been moved back to the original deck # it should have been moved back to the original deck
assert c.did == 1 assert c.did == 1
def test_cram_rem(): def test_cram_rem():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
oldDue = f.cards()[0].due oldDue = f.cards()[0].due
did = d.decks.newDyn("Cram") did = d.decks.newDyn("Cram")
@ -681,16 +710,17 @@ def test_cram_rem():
assert c.type == c.queue == 0 assert c.type == c.queue == 0
assert c.due == oldDue assert c.due == oldDue
def test_cram_resched(): def test_cram_resched():
# add card # add card
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
# cram deck # cram deck
did = d.decks.newDyn("Cram") did = d.decks.newDyn("Cram")
cram = d.decks.get(did) cram = d.decks.get(did)
cram['resched'] = False cram["resched"] = False
d.sched.rebuildDyn(did) d.sched.rebuildDyn(did)
d.reset() d.reset()
# graduate should return it to new # graduate should return it to new
@ -786,22 +816,25 @@ def test_cram_resched():
# d.sched.answerCard(c, 2) # d.sched.answerCard(c, 2)
# print c.__dict__ # print c.__dict__
def test_ordcycle(): def test_ordcycle():
d = getEmptyCol() d = getEmptyCol()
# add two more templates and set second active # add two more templates and set second active
m = d.models.current(); mm = d.models m = d.models.current()
mm = d.models
t = mm.newTemplate("Reverse") t = mm.newTemplate("Reverse")
t['qfmt'] = "{{Back}}" t["qfmt"] = "{{Back}}"
t['afmt'] = "{{Front}}" t["afmt"] = "{{Front}}"
mm.addTemplate(m, t) mm.addTemplate(m, t)
t = mm.newTemplate("f2") t = mm.newTemplate("f2")
t['qfmt'] = "{{Front}}" t["qfmt"] = "{{Front}}"
t['afmt'] = "{{Back}}" t["afmt"] = "{{Back}}"
mm.addTemplate(m, t) mm.addTemplate(m, t)
mm.save(m) mm.save(m)
# create a new note; it should have 3 cards # create a new note; it should have 3 cards
f = d.newNote() f = d.newNote()
f['Front'] = "1"; f['Back'] = "1" f["Front"] = "1"
f["Back"] = "1"
d.addNote(f) d.addNote(f)
assert d.cardCount() == 3 assert d.cardCount() == 3
d.reset() d.reset()
@ -810,10 +843,12 @@ def test_ordcycle():
assert d.sched.getCard().ord == 1 assert d.sched.getCard().ord == 1
assert d.sched.getCard().ord == 2 assert d.sched.getCard().ord == 2
def test_counts_idx(): def test_counts_idx():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "two" f["Front"] = "one"
f["Back"] = "two"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
assert d.sched.counts() == (1, 0, 0) assert d.sched.counts() == (1, 0, 0)
@ -832,10 +867,11 @@ def test_counts_idx():
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
assert d.sched.counts() == (0, 2, 0) assert d.sched.counts() == (0, 2, 0)
def test_repCounts(): def test_repCounts():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
# lrnReps should be accurate on pass/fail # lrnReps should be accurate on pass/fail
@ -853,7 +889,7 @@ def test_repCounts():
d.sched.answerCard(d.sched.getCard(), 2) d.sched.answerCard(d.sched.getCard(), 2)
assert d.sched.counts() == (0, 0, 0) assert d.sched.counts() == (0, 0, 0)
f = d.newNote() f = d.newNote()
f['Front'] = "two" f["Front"] = "two"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
# initial pass should be correct too # initial pass should be correct too
@ -865,14 +901,14 @@ def test_repCounts():
assert d.sched.counts() == (0, 0, 0) assert d.sched.counts() == (0, 0, 0)
# immediate graduate should work # immediate graduate should work
f = d.newNote() f = d.newNote()
f['Front'] = "three" f["Front"] = "three"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
d.sched.answerCard(d.sched.getCard(), 3) d.sched.answerCard(d.sched.getCard(), 3)
assert d.sched.counts() == (0, 0, 0) assert d.sched.counts() == (0, 0, 0)
# and failing a review should too # and failing a review should too
f = d.newNote() f = d.newNote()
f['Front'] = "three" f["Front"] = "three"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
c.type = 2 c.type = 2
@ -884,12 +920,13 @@ def test_repCounts():
d.sched.answerCard(d.sched.getCard(), 1) d.sched.answerCard(d.sched.getCard(), 1)
assert d.sched.counts() == (0, 1, 0) assert d.sched.counts() == (0, 1, 0)
def test_timing(): def test_timing():
d = getEmptyCol() d = getEmptyCol()
# add a few review cards, due today # add a few review cards, due today
for i in range(5): for i in range(5):
f = d.newNote() f = d.newNote()
f['Front'] = "num"+str(i) f["Front"] = "num" + str(i)
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
c.type = 2 c.type = 2
@ -900,7 +937,7 @@ def test_timing():
d.reset() d.reset()
c = d.sched.getCard() c = d.sched.getCard()
# set a a fail delay of 1 second so we don't have to wait # set a a fail delay of 1 second so we don't have to wait
d.sched._cardConf(c)['lapse']['delays'][0] = 1/60.0 d.sched._cardConf(c)["lapse"]["delays"][0] = 1 / 60.0
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
# the next card should be another review # the next card should be another review
c = d.sched.getCard() c = d.sched.getCard()
@ -910,11 +947,12 @@ def test_timing():
c = d.sched.getCard() c = d.sched.getCard()
assert c.queue == 1 assert c.queue == 1
def test_collapse(): def test_collapse():
d = getEmptyCol() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
# test collapsing # test collapsing
@ -924,16 +962,17 @@ def test_collapse():
d.sched.answerCard(c, 3) d.sched.answerCard(c, 3)
assert not d.sched.getCard() assert not d.sched.getCard()
def test_deckDue(): def test_deckDue():
d = getEmptyCol() d = getEmptyCol()
# add a note with default deck # add a note with default deck
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
# and one that's a child # and one that's a child
f = d.newNote() f = d.newNote()
f['Front'] = "two" f["Front"] = "two"
default1 = f.model()['did'] = d.decks.id("Default::1") default1 = f.model()["did"] = d.decks.id("Default::1")
d.addNote(f) d.addNote(f)
# make it a review card # make it a review card
c = f.cards()[0] c = f.cards()[0]
@ -942,13 +981,13 @@ def test_deckDue():
c.flush() c.flush()
# add one more with a new deck # add one more with a new deck
f = d.newNote() f = d.newNote()
f['Front'] = "two" f["Front"] = "two"
foobar = f.model()['did'] = d.decks.id("foo::bar") foobar = f.model()["did"] = d.decks.id("foo::bar")
d.addNote(f) d.addNote(f)
# and one that's a sibling # and one that's a sibling
f = d.newNote() f = d.newNote()
f['Front'] = "three" f["Front"] = "three"
foobaz = f.model()['did'] = d.decks.id("foo::baz") foobaz = f.model()["did"] = d.decks.id("foo::baz")
d.addNote(f) d.addNote(f)
d.reset() d.reset()
assert len(d.decks.decks) == 5 assert len(d.decks.decks) == 5
@ -970,10 +1009,12 @@ def test_deckDue():
assert tree[0][5][0][2] == 1 assert tree[0][5][0][2] == 1
assert tree[0][5][0][4] == 0 assert tree[0][5][0][4] == 0
# code should not fail if a card has an invalid deck # code should not fail if a card has an invalid deck
c.did = 12345; c.flush() c.did = 12345
c.flush()
d.sched.deckDueList() d.sched.deckDueList()
d.sched.deckDueTree() d.sched.deckDueTree()
def test_deckTree(): def test_deckTree():
d = getEmptyCol() d = getEmptyCol()
d.decks.id("new::b::c") d.decks.id("new::b::c")
@ -983,75 +1024,80 @@ def test_deckTree():
names.remove("new") names.remove("new")
assert "new" not in names assert "new" not in names
def test_deckFlow(): def test_deckFlow():
d = getEmptyCol() d = getEmptyCol()
# add a note with default deck # add a note with default deck
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
# and one that's a child # and one that's a child
f = d.newNote() f = d.newNote()
f['Front'] = "two" f["Front"] = "two"
default1 = f.model()['did'] = d.decks.id("Default::2") default1 = f.model()["did"] = d.decks.id("Default::2")
d.addNote(f) d.addNote(f)
# and another that's higher up # and another that's higher up
f = d.newNote() f = d.newNote()
f['Front'] = "three" f["Front"] = "three"
default1 = f.model()['did'] = d.decks.id("Default::1") default1 = f.model()["did"] = d.decks.id("Default::1")
d.addNote(f) d.addNote(f)
# should get top level one first, then ::1, then ::2 # should get top level one first, then ::1, then ::2
d.reset() d.reset()
assert d.sched.counts() == (3,0,0) assert d.sched.counts() == (3, 0, 0)
for i in "one", "three", "two": for i in "one", "three", "two":
c = d.sched.getCard() c = d.sched.getCard()
assert c.note()['Front'] == i assert c.note()["Front"] == i
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
def test_reorder(): def test_reorder():
d = getEmptyCol() d = getEmptyCol()
# add a note with default deck # add a note with default deck
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
f2 = d.newNote() f2 = d.newNote()
f2['Front'] = "two" f2["Front"] = "two"
d.addNote(f2) d.addNote(f2)
assert f2.cards()[0].due == 2 assert f2.cards()[0].due == 2
found=False found = False
# 50/50 chance of being reordered # 50/50 chance of being reordered
for i in range(20): for i in range(20):
d.sched.randomizeCards(1) d.sched.randomizeCards(1)
if f.cards()[0].due != f.id: if f.cards()[0].due != f.id:
found=True found = True
break break
assert found assert found
d.sched.orderCards(1) d.sched.orderCards(1)
assert f.cards()[0].due == 1 assert f.cards()[0].due == 1
# shifting # shifting
f3 = d.newNote() f3 = d.newNote()
f3['Front'] = "three" f3["Front"] = "three"
d.addNote(f3) d.addNote(f3)
f4 = d.newNote() f4 = d.newNote()
f4['Front'] = "four" f4["Front"] = "four"
d.addNote(f4) d.addNote(f4)
assert f.cards()[0].due == 1 assert f.cards()[0].due == 1
assert f2.cards()[0].due == 2 assert f2.cards()[0].due == 2
assert f3.cards()[0].due == 3 assert f3.cards()[0].due == 3
assert f4.cards()[0].due == 4 assert f4.cards()[0].due == 4
d.sched.sortCards([ d.sched.sortCards([f3.cards()[0].id, f4.cards()[0].id], start=1, shift=True)
f3.cards()[0].id, f4.cards()[0].id], start=1, shift=True)
assert f.cards()[0].due == 3 assert f.cards()[0].due == 3
assert f2.cards()[0].due == 4 assert f2.cards()[0].due == 4
assert f3.cards()[0].due == 1 assert f3.cards()[0].due == 1
assert f4.cards()[0].due == 2 assert f4.cards()[0].due == 2
def test_forget(): def test_forget():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
c.queue = 2; c.type = 2; c.ivl = 100; c.due = 0 c.queue = 2
c.type = 2
c.ivl = 100
c.due = 0
c.flush() c.flush()
d.reset() d.reset()
assert d.sched.counts() == (0, 0, 1) assert d.sched.counts() == (0, 0, 1)
@ -1059,10 +1105,11 @@ def test_forget():
d.reset() d.reset()
assert d.sched.counts() == (1, 0, 0) assert d.sched.counts() == (1, 0, 0)
def test_resched(): def test_resched():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
d.sched.reschedCards([c.id], 0, 0) d.sched.reschedCards([c.id], 0, 0)
@ -1072,14 +1119,15 @@ def test_resched():
assert c.queue == c.type == 2 assert c.queue == c.type == 2
d.sched.reschedCards([c.id], 1, 1) d.sched.reschedCards([c.id], 1, 1)
c.load() c.load()
assert c.due == d.sched.today+1 assert c.due == d.sched.today + 1
assert c.ivl == +1 assert c.ivl == +1
def test_norelearn(): def test_norelearn():
d = getEmptyCol() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
c.type = 2 c.type = 2
@ -1093,13 +1141,15 @@ def test_norelearn():
c.flush() c.flush()
d.reset() d.reset()
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
d.sched._cardConf(c)['lapse']['delays'] = [] d.sched._cardConf(c)["lapse"]["delays"] = []
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
def test_failmult(): def test_failmult():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "two" f["Front"] = "one"
f["Back"] = "two"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
c.type = 2 c.type = 2
@ -1111,7 +1161,7 @@ def test_failmult():
c.lapses = 1 c.lapses = 1
c.startTimer() c.startTimer()
c.flush() c.flush()
d.sched._cardConf(c)['lapse']['mult'] = 0.5 d.sched._cardConf(c)["lapse"]["mult"] = 0.5
c = d.sched.getCard() c = d.sched.getCard()
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
assert c.ivl == 50 assert c.ivl == 50

View file

@ -1,34 +1,49 @@
# coding: utf-8 # coding: utf-8
import time
import copy import copy
import time
from anki.consts import STARTING_FACTOR from anki.consts import STARTING_FACTOR
from tests.shared import getEmptyCol
from anki.utils import intTime
from anki.hooks import addHook from anki.hooks import addHook
from anki.utils import intTime
from tests.shared import getEmptyCol
# Between 2-4AM, shift the time back so test assumptions hold.
lt = time.localtime()
if lt.tm_hour > 2 and lt.tm_hour < 4:
orig_time = time.time
def adjusted_time():
return orig_time() - 60 * 60 * 2
time.time = adjusted_time
def test_clock(): def test_clock():
d = getEmptyCol() d = getEmptyCol()
if (d.sched.dayCutoff - intTime()) < 10*60: if (d.sched.dayCutoff - intTime()) < 10 * 60:
raise Exception("Unit tests will fail around the day rollover.") raise Exception("Unit tests will fail around the day rollover.")
def checkRevIvl(d, c, targetIvl): def checkRevIvl(d, c, targetIvl):
min, max = d.sched._fuzzIvlRange(targetIvl) min, max = d.sched._fuzzIvlRange(targetIvl)
return min <= c.ivl <= max return min <= c.ivl <= max
def test_basics(): def test_basics():
d = getEmptyCol() d = getEmptyCol()
d.reset() d.reset()
assert not d.sched.getCard() assert not d.sched.getCard()
def test_new(): def test_new():
d = getEmptyCol() d = getEmptyCol()
d.reset() d.reset()
assert d.sched.newCount == 0 assert d.sched.newCount == 0
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "two" f["Front"] = "one"
f["Back"] = "two"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
assert d.sched.newCount == 1 assert d.sched.newCount == 1
@ -66,15 +81,16 @@ def test_new():
# assert qs[n] in c.q() # assert qs[n] in c.q()
# d.sched.answerCard(c, 2) # d.sched.answerCard(c, 2)
def test_newLimits(): def test_newLimits():
d = getEmptyCol() d = getEmptyCol()
# add some notes # add some notes
g2 = d.decks.id("Default::foo") g2 = d.decks.id("Default::foo")
for i in range(30): for i in range(30):
f = d.newNote() f = d.newNote()
f['Front'] = str(i) f["Front"] = str(i)
if i > 4: if i > 4:
f.model()['did'] = g2 f.model()["did"] = g2
d.addNote(f) d.addNote(f)
# give the child deck a different configuration # give the child deck a different configuration
c2 = d.decks.confId("new conf") c2 = d.decks.confId("new conf")
@ -87,33 +103,36 @@ def test_newLimits():
assert c.did == 1 assert c.did == 1
# limit the parent to 10 cards, meaning we get 10 in total # limit the parent to 10 cards, meaning we get 10 in total
conf1 = d.decks.confForDid(1) conf1 = d.decks.confForDid(1)
conf1['new']['perDay'] = 10 conf1["new"]["perDay"] = 10
d.reset() d.reset()
assert d.sched.newCount == 10 assert d.sched.newCount == 10
# if we limit child to 4, we should get 9 # if we limit child to 4, we should get 9
conf2 = d.decks.confForDid(g2) conf2 = d.decks.confForDid(g2)
conf2['new']['perDay'] = 4 conf2["new"]["perDay"] = 4
d.reset() d.reset()
assert d.sched.newCount == 9 assert d.sched.newCount == 9
def test_newBoxes(): def test_newBoxes():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
c = d.sched.getCard() c = d.sched.getCard()
d.sched._cardConf(c)['new']['delays'] = [1,2,3,4,5] d.sched._cardConf(c)["new"]["delays"] = [1, 2, 3, 4, 5]
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
# should handle gracefully # should handle gracefully
d.sched._cardConf(c)['new']['delays'] = [1] d.sched._cardConf(c)["new"]["delays"] = [1]
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
def test_learn(): def test_learn():
d = getEmptyCol() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "two" f["Front"] = "one"
f["Back"] = "two"
f = d.addNote(f) f = d.addNote(f)
# set as a learn card and rebuild queues # set as a learn card and rebuild queues
d.db.execute("update cards set queue=0, type=0") d.db.execute("update cards set queue=0, type=0")
@ -121,12 +140,12 @@ def test_learn():
# sched.getCard should return it, since it's due in the past # sched.getCard should return it, since it's due in the past
c = d.sched.getCard() c = d.sched.getCard()
assert c assert c
d.sched._cardConf(c)['new']['delays'] = [0.5, 3, 10] d.sched._cardConf(c)["new"]["delays"] = [0.5, 3, 10]
# fail it # fail it
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
# it should have three reps left to graduation # it should have three reps left to graduation
assert c.left%1000 == 3 assert c.left % 1000 == 3
assert c.left//1000 == 3 assert c.left // 1000 == 3
# it should by due in 30 seconds # it should by due in 30 seconds
t = round(c.due - time.time()) t = round(c.due - time.time())
assert t >= 25 and t <= 40 assert t >= 25 and t <= 40
@ -134,9 +153,9 @@ def test_learn():
d.sched.answerCard(c, 3) d.sched.answerCard(c, 3)
# it should by due in 3 minutes # it should by due in 3 minutes
dueIn = c.due - time.time() dueIn = c.due - time.time()
assert 179 <= dueIn <= 180*1.25 assert 179 <= dueIn <= 180 * 1.25
assert c.left%1000 == 2 assert c.left % 1000 == 2
assert c.left//1000 == 2 assert c.left // 1000 == 2
# check log is accurate # check log is accurate
log = d.db.first("select * from revlog order by id desc") log = d.db.first("select * from revlog order by id desc")
assert log[3] == 3 assert log[3] == 3
@ -146,9 +165,9 @@ def test_learn():
d.sched.answerCard(c, 3) d.sched.answerCard(c, 3)
# it should by due in 10 minutes # it should by due in 10 minutes
dueIn = c.due - time.time() dueIn = c.due - time.time()
assert 599 <= dueIn <= 600*1.25 assert 599 <= dueIn <= 600 * 1.25
assert c.left%1000 == 1 assert c.left % 1000 == 1
assert c.left//1000 == 1 assert c.left // 1000 == 1
# the next pass should graduate the card # the next pass should graduate the card
assert c.queue == 1 assert c.queue == 1
assert c.type == 1 assert c.type == 1
@ -156,7 +175,7 @@ def test_learn():
assert c.queue == 2 assert c.queue == 2
assert c.type == 2 assert c.type == 2
# should be due tomorrow, with an interval of 1 # should be due tomorrow, with an interval of 1
assert c.due == d.sched.today+1 assert c.due == d.sched.today + 1
assert c.ivl == 1 assert c.ivl == 1
# or normal removal # or normal removal
c.type = 0 c.type = 0
@ -168,10 +187,11 @@ def test_learn():
# revlog should have been updated each time # revlog should have been updated each time
assert d.db.scalar("select count() from revlog where type = 0") == 5 assert d.db.scalar("select count() from revlog where type = 0") == 5
def test_relearn(): def test_relearn():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
c.ivl = 100 c.ivl = 100
@ -193,10 +213,11 @@ def test_relearn():
assert c.ivl == 2 assert c.ivl == 2
assert c.due == d.sched.today + c.ivl assert c.due == d.sched.today + c.ivl
def test_relearn_no_steps(): def test_relearn_no_steps():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
c.ivl = 100 c.ivl = 100
@ -205,7 +226,7 @@ def test_relearn_no_steps():
c.flush() c.flush()
conf = d.decks.confForDid(1) conf = d.decks.confForDid(1)
conf['lapse']['delays'] = [] conf["lapse"]["delays"] = []
d.decks.save(conf) d.decks.save(conf)
# fail the card # fail the card
@ -214,14 +235,15 @@ def test_relearn_no_steps():
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
assert c.type == c.queue == 2 assert c.type == c.queue == 2
def test_learn_collapsed(): def test_learn_collapsed():
d = getEmptyCol() d = getEmptyCol()
# add 2 notes # add 2 notes
f = d.newNote() f = d.newNote()
f['Front'] = "1" f["Front"] = "1"
f = d.addNote(f) f = d.addNote(f)
f = d.newNote() f = d.newNote()
f['Front'] = "2" f["Front"] = "2"
f = d.addNote(f) f = d.addNote(f)
# set as a learn card and rebuild queues # set as a learn card and rebuild queues
d.db.execute("update cards set queue=0, type=0") d.db.execute("update cards set queue=0, type=0")
@ -240,27 +262,28 @@ def test_learn_collapsed():
c = d.sched.getCard() c = d.sched.getCard()
assert not c.q().endswith("2") assert not c.q().endswith("2")
def test_learn_day(): def test_learn_day():
d = getEmptyCol() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
f = d.addNote(f) f = d.addNote(f)
d.sched.reset() d.sched.reset()
c = d.sched.getCard() c = d.sched.getCard()
d.sched._cardConf(c)['new']['delays'] = [1, 10, 1440, 2880] d.sched._cardConf(c)["new"]["delays"] = [1, 10, 1440, 2880]
# pass it # pass it
d.sched.answerCard(c, 3) d.sched.answerCard(c, 3)
# two reps to graduate, 1 more today # two reps to graduate, 1 more today
assert c.left%1000 == 3 assert c.left % 1000 == 3
assert c.left//1000 == 1 assert c.left // 1000 == 1
assert d.sched.counts() == (0, 1, 0) assert d.sched.counts() == (0, 1, 0)
c = d.sched.getCard() c = d.sched.getCard()
ni = d.sched.nextIvl ni = d.sched.nextIvl
assert ni(c, 3) == 86400 assert ni(c, 3) == 86400
# answering it will place it in queue 3 # answering it will place it in queue 3
d.sched.answerCard(c, 3) d.sched.answerCard(c, 3)
assert c.due == d.sched.today+1 assert c.due == d.sched.today + 1
assert c.queue == 3 assert c.queue == 3
assert not d.sched.getCard() assert not d.sched.getCard()
# for testing, move it back a day # for testing, move it back a day
@ -270,7 +293,7 @@ def test_learn_day():
assert d.sched.counts() == (0, 1, 0) assert d.sched.counts() == (0, 1, 0)
c = d.sched.getCard() c = d.sched.getCard()
# nextIvl should work # nextIvl should work
assert ni(c, 3) == 86400*2 assert ni(c, 3) == 86400 * 2
# if we fail it, it should be back in the correct queue # if we fail it, it should be back in the correct queue
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
assert c.queue == 1 assert c.queue == 1
@ -292,17 +315,19 @@ def test_learn_day():
c.flush() c.flush()
d.reset() d.reset()
assert d.sched.counts() == (0, 0, 1) assert d.sched.counts() == (0, 0, 1)
d.sched._cardConf(c)['lapse']['delays'] = [1440] d.sched._cardConf(c)["lapse"]["delays"] = [1440]
c = d.sched.getCard() c = d.sched.getCard()
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
assert c.queue == 3 assert c.queue == 3
assert d.sched.counts() == (0, 0, 0) assert d.sched.counts() == (0, 0, 0)
def test_reviews(): def test_reviews():
d = getEmptyCol() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "two" f["Front"] = "one"
f["Back"] = "two"
d.addNote(f) d.addNote(f)
# set the card up as a review card, due 8 days ago # set the card up as a review card, due 8 days ago
c = f.cards()[0] c = f.cards()[0]
@ -359,8 +384,10 @@ def test_reviews():
c.flush() c.flush()
# steup hook # steup hook
hooked = [] hooked = []
def onLeech(card): def onLeech(card):
hooked.append(1) hooked.append(1)
addHook("leech", onLeech) addHook("leech", onLeech)
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
assert hooked assert hooked
@ -368,6 +395,7 @@ def test_reviews():
c.load() c.load()
assert c.queue == -1 assert c.queue == -1
def test_review_limits(): def test_review_limits():
d = getEmptyCol() d = getEmptyCol()
@ -377,21 +405,22 @@ def test_review_limits():
pconf = d.decks.getConf(d.decks.confId("parentConf")) pconf = d.decks.getConf(d.decks.confId("parentConf"))
cconf = d.decks.getConf(d.decks.confId("childConf")) cconf = d.decks.getConf(d.decks.confId("childConf"))
pconf['rev']['perDay'] = 5 pconf["rev"]["perDay"] = 5
d.decks.updateConf(pconf) d.decks.updateConf(pconf)
d.decks.setConf(parent, pconf['id']) d.decks.setConf(parent, pconf["id"])
cconf['rev']['perDay'] = 10 cconf["rev"]["perDay"] = 10
d.decks.updateConf(cconf) d.decks.updateConf(cconf)
d.decks.setConf(child, cconf['id']) d.decks.setConf(child, cconf["id"])
m = d.models.current() m = d.models.current()
m['did'] = child['id'] m["did"] = child["id"]
d.models.save(m, updateReqs=False) d.models.save(m, updateReqs=False)
# add some cards # add some cards
for i in range(20): for i in range(20):
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "two" f["Front"] = "one"
f["Back"] = "two"
d.addNote(f) d.addNote(f)
# make them reviews # make them reviews
@ -402,11 +431,11 @@ def test_review_limits():
tree = d.sched.deckDueTree() tree = d.sched.deckDueTree()
# (('Default', 1, 0, 0, 0, ()), ('parent', 1514457677462, 5, 0, 0, (('child', 1514457677463, 5, 0, 0, ()),))) # (('Default', 1, 0, 0, 0, ()), ('parent', 1514457677462, 5, 0, 0, (('child', 1514457677463, 5, 0, 0, ()),)))
assert tree[1][2] == 5 # parent assert tree[1][2] == 5 # parent
assert tree[1][5][0][2] == 5 # child assert tree[1][5][0][2] == 5 # child
# .counts() should match # .counts() should match
d.decks.select(child['id']) d.decks.select(child["id"])
d.sched.reset() d.sched.reset()
assert d.sched.counts() == (0, 0, 5) assert d.sched.counts() == (0, 0, 5)
@ -416,24 +445,25 @@ def test_review_limits():
assert d.sched.counts() == (0, 0, 4) assert d.sched.counts() == (0, 0, 4)
tree = d.sched.deckDueTree() tree = d.sched.deckDueTree()
assert tree[1][2] == 4 # parent assert tree[1][2] == 4 # parent
assert tree[1][5][0][2] == 4 # child assert tree[1][5][0][2] == 4 # child
# switch limits # switch limits
d.decks.setConf(parent, cconf['id']) d.decks.setConf(parent, cconf["id"])
d.decks.setConf(child, pconf['id']) d.decks.setConf(child, pconf["id"])
d.decks.select(parent['id']) d.decks.select(parent["id"])
d.sched.reset() d.sched.reset()
# child limits do not affect the parent # child limits do not affect the parent
tree = d.sched.deckDueTree() tree = d.sched.deckDueTree()
assert tree[1][2] == 9 # parent assert tree[1][2] == 9 # parent
assert tree[1][5][0][2] == 4 # child assert tree[1][5][0][2] == 4 # child
def test_button_spacing(): def test_button_spacing():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
# 1 day ivl review card due now # 1 day ivl review card due now
c = f.cards()[0] c = f.cards()[0]
@ -452,16 +482,17 @@ def test_button_spacing():
# if hard factor is <= 1, then hard may not increase # if hard factor is <= 1, then hard may not increase
conf = d.decks.confForDid(1) conf = d.decks.confForDid(1)
conf['rev']['hardFactor'] = 1 conf["rev"]["hardFactor"] = 1
assert ni(c, 2) == "1 day" assert ni(c, 2) == "1 day"
def test_overdue_lapse(): def test_overdue_lapse():
# disabled in commit 3069729776990980f34c25be66410e947e9d51a2 # disabled in commit 3069729776990980f34c25be66410e947e9d51a2
return return
d = getEmptyCol() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
# simulate a review that was lapsed and is now due for its normal review # simulate a review that was lapsed and is now due for its normal review
c = f.cards()[0] c = f.cards()[0]
@ -490,13 +521,15 @@ def test_overdue_lapse():
d.sched.reset() d.sched.reset()
assert d.sched.counts() == (0, 0, 1) assert d.sched.counts() == (0, 0, 1)
def test_finished(): def test_finished():
d = getEmptyCol() d = getEmptyCol()
# nothing due # nothing due
assert "Congratulations" in d.sched.finishedMsg() assert "Congratulations" in d.sched.finishedMsg()
assert "limit" not in d.sched.finishedMsg() assert "limit" not in d.sched.finishedMsg()
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "two" f["Front"] = "one"
f["Back"] = "two"
d.addNote(f) d.addNote(f)
# have a new card # have a new card
assert "new cards available" in d.sched.finishedMsg() assert "new cards available" in d.sched.finishedMsg()
@ -509,47 +542,49 @@ def test_finished():
assert "Congratulations" in d.sched.finishedMsg() assert "Congratulations" in d.sched.finishedMsg()
assert "limit" not in d.sched.finishedMsg() assert "limit" not in d.sched.finishedMsg()
def test_nextIvl(): def test_nextIvl():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "two" f["Front"] = "one"
f["Back"] = "two"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
conf = d.decks.confForDid(1) conf = d.decks.confForDid(1)
conf['new']['delays'] = [0.5, 3, 10] conf["new"]["delays"] = [0.5, 3, 10]
conf['lapse']['delays'] = [1, 5, 9] conf["lapse"]["delays"] = [1, 5, 9]
c = d.sched.getCard() c = d.sched.getCard()
# new cards # new cards
################################################## ##################################################
ni = d.sched.nextIvl ni = d.sched.nextIvl
assert ni(c, 1) == 30 assert ni(c, 1) == 30
assert ni(c, 2) == (30+180)//2 assert ni(c, 2) == (30 + 180) // 2
assert ni(c, 3) == 180 assert ni(c, 3) == 180
assert ni(c, 4) == 4*86400 assert ni(c, 4) == 4 * 86400
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
# cards in learning # cards in learning
################################################## ##################################################
assert ni(c, 1) == 30 assert ni(c, 1) == 30
assert ni(c, 2) == (30+180)//2 assert ni(c, 2) == (30 + 180) // 2
assert ni(c, 3) == 180 assert ni(c, 3) == 180
assert ni(c, 4) == 4*86400 assert ni(c, 4) == 4 * 86400
d.sched.answerCard(c, 3) d.sched.answerCard(c, 3)
assert ni(c, 1) == 30 assert ni(c, 1) == 30
assert ni(c, 2) == (180+600)//2 assert ni(c, 2) == (180 + 600) // 2
assert ni(c, 3) == 600 assert ni(c, 3) == 600
assert ni(c, 4) == 4*86400 assert ni(c, 4) == 4 * 86400
d.sched.answerCard(c, 3) d.sched.answerCard(c, 3)
# normal graduation is tomorrow # normal graduation is tomorrow
assert ni(c, 3) == 1*86400 assert ni(c, 3) == 1 * 86400
assert ni(c, 4) == 4*86400 assert ni(c, 4) == 4 * 86400
# lapsed cards # lapsed cards
################################################## ##################################################
c.type = 2 c.type = 2
c.ivl = 100 c.ivl = 100
c.factor = STARTING_FACTOR c.factor = STARTING_FACTOR
assert ni(c, 1) == 60 assert ni(c, 1) == 60
assert ni(c, 3) == 100*86400 assert ni(c, 3) == 100 * 86400
assert ni(c, 4) == 101*86400 assert ni(c, 4) == 101 * 86400
# review cards # review cards
################################################## ##################################################
c.queue = 2 c.queue = 2
@ -558,8 +593,8 @@ def test_nextIvl():
# failing it should put it at 60s # failing it should put it at 60s
assert ni(c, 1) == 60 assert ni(c, 1) == 60
# or 1 day if relearn is false # or 1 day if relearn is false
d.sched._cardConf(c)['lapse']['delays']=[] d.sched._cardConf(c)["lapse"]["delays"] = []
assert ni(c, 1) == 1*86400 assert ni(c, 1) == 1 * 86400
# (* 100 1.2 86400)10368000.0 # (* 100 1.2 86400)10368000.0
assert ni(c, 2) == 10368000 assert ni(c, 2) == 10368000
# (* 100 2.5 86400)21600000.0 # (* 100 2.5 86400)21600000.0
@ -568,14 +603,15 @@ def test_nextIvl():
assert ni(c, 4) == 28080000 assert ni(c, 4) == 28080000
assert d.sched.nextIvlStr(c, 4) == "10.8 months" assert d.sched.nextIvlStr(c, 4) == "10.8 months"
def test_bury(): def test_bury():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
f = d.newNote() f = d.newNote()
f['Front'] = "two" f["Front"] = "two"
d.addNote(f) d.addNote(f)
c2 = f.cards()[0] c2 = f.cards()[0]
# burying # burying
@ -590,11 +626,14 @@ def test_bury():
assert not d.sched.getCard() assert not d.sched.getCard()
d.sched.unburyCardsForDeck(type="manual") d.sched.unburyCardsForDeck(type="manual")
c.load(); assert c.queue == 0 c.load()
c2.load(); assert c2.queue == -2 assert c.queue == 0
c2.load()
assert c2.queue == -2
d.sched.unburyCardsForDeck(type="siblings") d.sched.unburyCardsForDeck(type="siblings")
c2.load(); assert c2.queue == 0 c2.load()
assert c2.queue == 0
d.sched.buryCards([c.id, c2.id]) d.sched.buryCards([c.id, c2.id])
d.sched.unburyCardsForDeck(type="all") d.sched.unburyCardsForDeck(type="all")
@ -603,10 +642,11 @@ def test_bury():
assert d.sched.counts() == (2, 0, 0) assert d.sched.counts() == (2, 0, 0)
def test_suspend(): def test_suspend():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
# suspending # suspending
@ -620,7 +660,11 @@ def test_suspend():
d.reset() d.reset()
assert d.sched.getCard() assert d.sched.getCard()
# should cope with rev cards being relearnt # should cope with rev cards being relearnt
c.due = 0; c.ivl = 100; c.type = 2; c.queue = 2; c.flush() c.due = 0
c.ivl = 100
c.type = 2
c.queue = 2
c.flush()
d.reset() d.reset()
c = d.sched.getCard() c = d.sched.getCard()
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
@ -648,10 +692,11 @@ def test_suspend():
assert c.did != 1 assert c.did != 1
assert c.odue == 1 assert c.odue == 1
def test_filt_reviewing_early_normal(): def test_filt_reviewing_early_normal():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
c.ivl = 100 c.ivl = 100
@ -663,7 +708,7 @@ def test_filt_reviewing_early_normal():
c.startTimer() c.startTimer()
c.flush() c.flush()
d.reset() d.reset()
assert d.sched.counts() == (0,0,0) assert d.sched.counts() == (0, 0, 0)
# create a dynamic deck and refresh it # create a dynamic deck and refresh it
did = d.decks.newDyn("Cram") did = d.decks.newDyn("Cram")
d.sched.rebuildDyn(did) d.sched.rebuildDyn(did)
@ -671,14 +716,14 @@ def test_filt_reviewing_early_normal():
# should appear as normal in the deck list # should appear as normal in the deck list
assert sorted(d.sched.deckDueList())[0][2] == 1 assert sorted(d.sched.deckDueList())[0][2] == 1
# and should appear in the counts # and should appear in the counts
assert d.sched.counts() == (0,0,1) assert d.sched.counts() == (0, 0, 1)
# grab it and check estimates # grab it and check estimates
c = d.sched.getCard() c = d.sched.getCard()
assert d.sched.answerButtons(c) == 4 assert d.sched.answerButtons(c) == 4
assert d.sched.nextIvl(c, 1) == 600 assert d.sched.nextIvl(c, 1) == 600
assert d.sched.nextIvl(c, 2) == int(75*1.2)*86400 assert d.sched.nextIvl(c, 2) == int(75 * 1.2) * 86400
assert d.sched.nextIvl(c, 3) == int(75*2.5)*86400 assert d.sched.nextIvl(c, 3) == int(75 * 2.5) * 86400
assert d.sched.nextIvl(c, 4) == int(75*2.5*1.15)*86400 assert d.sched.nextIvl(c, 4) == int(75 * 2.5 * 1.15) * 86400
# answer 'good' # answer 'good'
d.sched.answerCard(c, 3) d.sched.answerCard(c, 3)
@ -688,8 +733,7 @@ def test_filt_reviewing_early_normal():
# should not be in learning # should not be in learning
assert c.queue == 2 assert c.queue == 2
# should be logged as a cram rep # should be logged as a cram rep
assert d.db.scalar( assert d.db.scalar("select type from revlog order by id desc limit 1") == 3
"select type from revlog order by id desc limit 1") == 3
# due in 75 days, so it's been waiting 25 days # due in 75 days, so it's been waiting 25 days
c.ivl = 100 c.ivl = 100
@ -699,20 +743,21 @@ def test_filt_reviewing_early_normal():
d.reset() d.reset()
c = d.sched.getCard() c = d.sched.getCard()
assert d.sched.nextIvl(c, 2) == 60*86400 assert d.sched.nextIvl(c, 2) == 60 * 86400
assert d.sched.nextIvl(c, 3) == 100*86400 assert d.sched.nextIvl(c, 3) == 100 * 86400
assert d.sched.nextIvl(c, 4) == 114*86400 assert d.sched.nextIvl(c, 4) == 114 * 86400
def test_filt_keep_lrn_state(): def test_filt_keep_lrn_state():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
# fail the card outside filtered deck # fail the card outside filtered deck
c = d.sched.getCard() c = d.sched.getCard()
d.sched._cardConf(c)['new']['delays'] = [1, 10, 61] d.sched._cardConf(c)["new"]["delays"] = [1, 10, 61]
d.decks.save() d.decks.save()
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
@ -736,30 +781,31 @@ def test_filt_keep_lrn_state():
# should be able to advance learning steps # should be able to advance learning steps
d.sched.answerCard(c, 3) d.sched.answerCard(c, 3)
# should be due at least an hour in the future # should be due at least an hour in the future
assert c.due - intTime() > 60*60 assert c.due - intTime() > 60 * 60
# emptying the deck preserves learning state # emptying the deck preserves learning state
d.sched.emptyDyn(did) d.sched.emptyDyn(did)
c.load() c.load()
assert c.type == c.queue == 1 assert c.type == c.queue == 1
assert c.left == 1001 assert c.left == 1001
assert c.due - intTime() > 60*60 assert c.due - intTime() > 60 * 60
def test_preview(): def test_preview():
# add cards # add cards
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
orig = copy.copy(c) orig = copy.copy(c)
f2 = d.newNote() f2 = d.newNote()
f2['Front'] = "two" f2["Front"] = "two"
d.addNote(f2) d.addNote(f2)
# cram deck # cram deck
did = d.decks.newDyn("Cram") did = d.decks.newDyn("Cram")
cram = d.decks.get(did) cram = d.decks.get(did)
cram['resched'] = False cram["resched"] = False
d.sched.rebuildDyn(did) d.sched.rebuildDyn(did)
d.reset() d.reset()
# grab the first card # grab the first card
@ -793,22 +839,25 @@ def test_preview():
assert c.reps == 0 assert c.reps == 0
assert c.type == 0 assert c.type == 0
def test_ordcycle(): def test_ordcycle():
d = getEmptyCol() d = getEmptyCol()
# add two more templates and set second active # add two more templates and set second active
m = d.models.current(); mm = d.models m = d.models.current()
mm = d.models
t = mm.newTemplate("Reverse") t = mm.newTemplate("Reverse")
t['qfmt'] = "{{Back}}" t["qfmt"] = "{{Back}}"
t['afmt'] = "{{Front}}" t["afmt"] = "{{Front}}"
mm.addTemplate(m, t) mm.addTemplate(m, t)
t = mm.newTemplate("f2") t = mm.newTemplate("f2")
t['qfmt'] = "{{Front}}" t["qfmt"] = "{{Front}}"
t['afmt'] = "{{Back}}" t["afmt"] = "{{Back}}"
mm.addTemplate(m, t) mm.addTemplate(m, t)
mm.save(m) mm.save(m)
# create a new note; it should have 3 cards # create a new note; it should have 3 cards
f = d.newNote() f = d.newNote()
f['Front'] = "1"; f['Back'] = "1" f["Front"] = "1"
f["Back"] = "1"
d.addNote(f) d.addNote(f)
assert d.cardCount() == 3 assert d.cardCount() == 3
d.reset() d.reset()
@ -817,10 +866,12 @@ def test_ordcycle():
assert d.sched.getCard().ord == 1 assert d.sched.getCard().ord == 1
assert d.sched.getCard().ord == 2 assert d.sched.getCard().ord == 2
def test_counts_idx(): def test_counts_idx():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "two" f["Front"] = "one"
f["Back"] = "two"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
assert d.sched.counts() == (1, 0, 0) assert d.sched.counts() == (1, 0, 0)
@ -839,10 +890,11 @@ def test_counts_idx():
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
assert d.sched.counts() == (0, 1, 0) assert d.sched.counts() == (0, 1, 0)
def test_repCounts(): def test_repCounts():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
# lrnReps should be accurate on pass/fail # lrnReps should be accurate on pass/fail
@ -860,7 +912,7 @@ def test_repCounts():
d.sched.answerCard(d.sched.getCard(), 3) d.sched.answerCard(d.sched.getCard(), 3)
assert d.sched.counts() == (0, 0, 0) assert d.sched.counts() == (0, 0, 0)
f = d.newNote() f = d.newNote()
f['Front'] = "two" f["Front"] = "two"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
# initial pass should be correct too # initial pass should be correct too
@ -872,14 +924,14 @@ def test_repCounts():
assert d.sched.counts() == (0, 0, 0) assert d.sched.counts() == (0, 0, 0)
# immediate graduate should work # immediate graduate should work
f = d.newNote() f = d.newNote()
f['Front'] = "three" f["Front"] = "three"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
d.sched.answerCard(d.sched.getCard(), 4) d.sched.answerCard(d.sched.getCard(), 4)
assert d.sched.counts() == (0, 0, 0) assert d.sched.counts() == (0, 0, 0)
# and failing a review should too # and failing a review should too
f = d.newNote() f = d.newNote()
f['Front'] = "three" f["Front"] = "three"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
c.type = 2 c.type = 2
@ -891,12 +943,13 @@ def test_repCounts():
d.sched.answerCard(d.sched.getCard(), 1) d.sched.answerCard(d.sched.getCard(), 1)
assert d.sched.counts() == (0, 1, 0) assert d.sched.counts() == (0, 1, 0)
def test_timing(): def test_timing():
d = getEmptyCol() d = getEmptyCol()
# add a few review cards, due today # add a few review cards, due today
for i in range(5): for i in range(5):
f = d.newNote() f = d.newNote()
f['Front'] = "num"+str(i) f["Front"] = "num" + str(i)
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
c.type = 2 c.type = 2
@ -917,11 +970,12 @@ def test_timing():
c = d.sched.getCard() c = d.sched.getCard()
assert c.queue == 1 assert c.queue == 1
def test_collapse(): def test_collapse():
d = getEmptyCol() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
# test collapsing # test collapsing
@ -931,16 +985,17 @@ def test_collapse():
d.sched.answerCard(c, 4) d.sched.answerCard(c, 4)
assert not d.sched.getCard() assert not d.sched.getCard()
def test_deckDue(): def test_deckDue():
d = getEmptyCol() d = getEmptyCol()
# add a note with default deck # add a note with default deck
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
# and one that's a child # and one that's a child
f = d.newNote() f = d.newNote()
f['Front'] = "two" f["Front"] = "two"
default1 = f.model()['did'] = d.decks.id("Default::1") default1 = f.model()["did"] = d.decks.id("Default::1")
d.addNote(f) d.addNote(f)
# make it a review card # make it a review card
c = f.cards()[0] c = f.cards()[0]
@ -949,13 +1004,13 @@ def test_deckDue():
c.flush() c.flush()
# add one more with a new deck # add one more with a new deck
f = d.newNote() f = d.newNote()
f['Front'] = "two" f["Front"] = "two"
foobar = f.model()['did'] = d.decks.id("foo::bar") foobar = f.model()["did"] = d.decks.id("foo::bar")
d.addNote(f) d.addNote(f)
# and one that's a sibling # and one that's a sibling
f = d.newNote() f = d.newNote()
f['Front'] = "three" f["Front"] = "three"
foobaz = f.model()['did'] = d.decks.id("foo::baz") foobaz = f.model()["did"] = d.decks.id("foo::baz")
d.addNote(f) d.addNote(f)
d.reset() d.reset()
assert len(d.decks.decks) == 5 assert len(d.decks.decks) == 5
@ -977,10 +1032,12 @@ def test_deckDue():
assert tree[0][5][0][2] == 1 assert tree[0][5][0][2] == 1
assert tree[0][5][0][4] == 0 assert tree[0][5][0][4] == 0
# code should not fail if a card has an invalid deck # code should not fail if a card has an invalid deck
c.did = 12345; c.flush() c.did = 12345
c.flush()
d.sched.deckDueList() d.sched.deckDueList()
d.sched.deckDueTree() d.sched.deckDueTree()
def test_deckTree(): def test_deckTree():
d = getEmptyCol() d = getEmptyCol()
d.decks.id("new::b::c") d.decks.id("new::b::c")
@ -990,75 +1047,80 @@ def test_deckTree():
names.remove("new") names.remove("new")
assert "new" not in names assert "new" not in names
def test_deckFlow(): def test_deckFlow():
d = getEmptyCol() d = getEmptyCol()
# add a note with default deck # add a note with default deck
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
# and one that's a child # and one that's a child
f = d.newNote() f = d.newNote()
f['Front'] = "two" f["Front"] = "two"
default1 = f.model()['did'] = d.decks.id("Default::2") default1 = f.model()["did"] = d.decks.id("Default::2")
d.addNote(f) d.addNote(f)
# and another that's higher up # and another that's higher up
f = d.newNote() f = d.newNote()
f['Front'] = "three" f["Front"] = "three"
default1 = f.model()['did'] = d.decks.id("Default::1") default1 = f.model()["did"] = d.decks.id("Default::1")
d.addNote(f) d.addNote(f)
# should get top level one first, then ::1, then ::2 # should get top level one first, then ::1, then ::2
d.reset() d.reset()
assert d.sched.counts() == (3,0,0) assert d.sched.counts() == (3, 0, 0)
for i in "one", "three", "two": for i in "one", "three", "two":
c = d.sched.getCard() c = d.sched.getCard()
assert c.note()['Front'] == i assert c.note()["Front"] == i
d.sched.answerCard(c, 3) d.sched.answerCard(c, 3)
def test_reorder(): def test_reorder():
d = getEmptyCol() d = getEmptyCol()
# add a note with default deck # add a note with default deck
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
f2 = d.newNote() f2 = d.newNote()
f2['Front'] = "two" f2["Front"] = "two"
d.addNote(f2) d.addNote(f2)
assert f2.cards()[0].due == 2 assert f2.cards()[0].due == 2
found=False found = False
# 50/50 chance of being reordered # 50/50 chance of being reordered
for i in range(20): for i in range(20):
d.sched.randomizeCards(1) d.sched.randomizeCards(1)
if f.cards()[0].due != f.id: if f.cards()[0].due != f.id:
found=True found = True
break break
assert found assert found
d.sched.orderCards(1) d.sched.orderCards(1)
assert f.cards()[0].due == 1 assert f.cards()[0].due == 1
# shifting # shifting
f3 = d.newNote() f3 = d.newNote()
f3['Front'] = "three" f3["Front"] = "three"
d.addNote(f3) d.addNote(f3)
f4 = d.newNote() f4 = d.newNote()
f4['Front'] = "four" f4["Front"] = "four"
d.addNote(f4) d.addNote(f4)
assert f.cards()[0].due == 1 assert f.cards()[0].due == 1
assert f2.cards()[0].due == 2 assert f2.cards()[0].due == 2
assert f3.cards()[0].due == 3 assert f3.cards()[0].due == 3
assert f4.cards()[0].due == 4 assert f4.cards()[0].due == 4
d.sched.sortCards([ d.sched.sortCards([f3.cards()[0].id, f4.cards()[0].id], start=1, shift=True)
f3.cards()[0].id, f4.cards()[0].id], start=1, shift=True)
assert f.cards()[0].due == 3 assert f.cards()[0].due == 3
assert f2.cards()[0].due == 4 assert f2.cards()[0].due == 4
assert f3.cards()[0].due == 1 assert f3.cards()[0].due == 1
assert f4.cards()[0].due == 2 assert f4.cards()[0].due == 2
def test_forget(): def test_forget():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
c.queue = 2; c.type = 2; c.ivl = 100; c.due = 0 c.queue = 2
c.type = 2
c.ivl = 100
c.due = 0
c.flush() c.flush()
d.reset() d.reset()
assert d.sched.counts() == (0, 0, 1) assert d.sched.counts() == (0, 0, 1)
@ -1066,10 +1128,11 @@ def test_forget():
d.reset() d.reset()
assert d.sched.counts() == (1, 0, 0) assert d.sched.counts() == (1, 0, 0)
def test_resched(): def test_resched():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
d.sched.reschedCards([c.id], 0, 0) d.sched.reschedCards([c.id], 0, 0)
@ -1079,14 +1142,15 @@ def test_resched():
assert c.queue == c.type == 2 assert c.queue == c.type == 2
d.sched.reschedCards([c.id], 1, 1) d.sched.reschedCards([c.id], 1, 1)
c.load() c.load()
assert c.due == d.sched.today+1 assert c.due == d.sched.today + 1
assert c.ivl == +1 assert c.ivl == +1
def test_norelearn(): def test_norelearn():
d = getEmptyCol() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
c.type = 2 c.type = 2
@ -1100,13 +1164,15 @@ def test_norelearn():
c.flush() c.flush()
d.reset() d.reset()
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
d.sched._cardConf(c)['lapse']['delays'] = [] d.sched._cardConf(c)["lapse"]["delays"] = []
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
def test_failmult(): def test_failmult():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "two" f["Front"] = "one"
f["Back"] = "two"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
c.type = 2 c.type = 2
@ -1118,19 +1184,20 @@ def test_failmult():
c.lapses = 1 c.lapses = 1
c.startTimer() c.startTimer()
c.flush() c.flush()
d.sched._cardConf(c)['lapse']['mult'] = 0.5 d.sched._cardConf(c)["lapse"]["mult"] = 0.5
c = d.sched.getCard() c = d.sched.getCard()
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
assert c.ivl == 50 assert c.ivl == 50
d.sched.answerCard(c, 1) d.sched.answerCard(c, 1)
assert c.ivl == 25 assert c.ivl == 25
def test_moveVersions(): def test_moveVersions():
col = getEmptyCol() col = getEmptyCol()
col.changeSchedulerVer(1) col.changeSchedulerVer(1)
n = col.newNote() n = col.newNote()
n['Front'] = "one" n["Front"] = "one"
col.addNote(n) col.addNote(n)
# make it a learning card # make it a learning card
@ -1168,8 +1235,10 @@ def test_moveVersions():
col.changeSchedulerVer(2) col.changeSchedulerVer(2)
# card with 100 day interval, answering again # card with 100 day interval, answering again
col.sched.reschedCards([c.id], 100, 100) col.sched.reschedCards([c.id], 100, 100)
c.load(); c.due = 0; c.flush() c.load()
col.sched._cardConf(c)['lapse']['mult'] = 0.5 c.due = 0
c.flush()
col.sched._cardConf(c)["lapse"]["mult"] = 0.5
col.sched.reset() col.sched.reset()
c = col.sched.getCard() c = col.sched.getCard()
col.sched.answerCard(c, 1) col.sched.answerCard(c, 1)
@ -1178,6 +1247,7 @@ def test_moveVersions():
c.load() c.load()
assert c.due == 50 assert c.due == 50
# cards with a due date earlier than the collection should retain # cards with a due date earlier than the collection should retain
# their due date when removed # their due date when removed
def test_negativeDueFilter(): def test_negativeDueFilter():
@ -1185,7 +1255,8 @@ def test_negativeDueFilter():
# card due prior to collection date # card due prior to collection date
f = d.newNote() f = d.newNote()
f['Front'] = "one"; f['Back'] = "two" f["Front"] = "one"
f["Back"] = "two"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
c.due = -5 c.due = -5
@ -1201,4 +1272,3 @@ def test_negativeDueFilter():
c.load() c.load()
assert c.due == -5 assert c.due == -5

View file

@ -1,12 +1,15 @@
# coding: utf-8 # coding: utf-8
import os import os
from tests.shared import getEmptyCol import tempfile
from tests.shared import getEmptyCol
def test_stats(): def test_stats():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = "foo" f["Front"] = "foo"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
# card stats # card stats
@ -17,15 +20,20 @@ def test_stats():
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
assert d.cardStats(c) assert d.cardStats(c)
def test_graphs_empty(): def test_graphs_empty():
d = getEmptyCol() d = getEmptyCol()
assert d.stats().report() assert d.stats().report()
def test_graphs(): def test_graphs():
from anki import Collection as aopen from anki import Collection as aopen
d = aopen(os.path.expanduser("~/test.anki2"))
dir = tempfile.gettempdir()
d = aopen(os.path.join(dir, "test.anki2"))
g = d.stats() g = d.stats()
rep = g.report() rep = g.report()
with open(os.path.expanduser("~/test.html"), "w") as f: with open(os.path.join(dir, "test.html"), "w") as f:
f.write(rep) f.write(rep)
return return

View file

@ -2,15 +2,18 @@ from anki.template import Template
def test_remove_formatting_from_mathjax(): def test_remove_formatting_from_mathjax():
t = Template('') t = Template("")
assert t._removeFormattingFromMathjax(r'\(2^{{c3::2}}\)', 3) == r'\(2^{{C3::2}}\)' assert t._removeFormattingFromMathjax(r"\(2^{{c3::2}}\)", 3) == r"\(2^{{C3::2}}\)"
txt = (r'{{c1::ok}} \(2^2\) {{c2::not ok}} \(2^{{c3::2}}\) \(x^3\) ' txt = (
r'{{c4::blah}} {{c5::text with \(x^2\) jax}}') r"{{c1::ok}} \(2^2\) {{c2::not ok}} \(2^{{c3::2}}\) \(x^3\) "
r"{{c4::blah}} {{c5::text with \(x^2\) jax}}"
)
# Cloze 2 is not in MathJax, so it should not get protected against # Cloze 2 is not in MathJax, so it should not get protected against
# formatting. # formatting.
assert t._removeFormattingFromMathjax(txt, 2) == txt assert t._removeFormattingFromMathjax(txt, 2) == txt
txt = r'\(a\) {{c1::b}} \[ {{c1::c}} \]' txt = r"\(a\) {{c1::b}} \[ {{c1::c}} \]"
assert t._removeFormattingFromMathjax(txt, 1) == ( assert t._removeFormattingFromMathjax(txt, 1) == (
r'\(a\) {{c1::b}} \[ {{C1::c}} \]') r"\(a\) {{c1::b}} \[ {{C1::c}} \]"
)

View file

@ -1,8 +1,10 @@
# coding: utf-8 # coding: utf-8
import time import time
from tests.shared import getEmptyCol
from anki.consts import * from anki.consts import *
from tests.shared import getEmptyCol
def test_op(): def test_op():
d = getEmptyCol() d = getEmptyCol()
@ -10,7 +12,7 @@ def test_op():
assert not d.undoName() assert not d.undoName()
# let's adjust a study option # let's adjust a study option
d.save("studyopts") d.save("studyopts")
d.conf['abc'] = 5 d.conf["abc"] = 5
# it should be listed as undoable # it should be listed as undoable
assert d.undoName() == "studyopts" assert d.undoName() == "studyopts"
# with about 5 minutes until it's clobbered # with about 5 minutes until it's clobbered
@ -18,7 +20,7 @@ def test_op():
# undoing should restore the old value # undoing should restore the old value
d.undo() d.undo()
assert not d.undoName() assert not d.undoName()
assert 'abc' not in d.conf assert "abc" not in d.conf
# an (auto)save will clear the undo # an (auto)save will clear the undo
d.save("foo") d.save("foo")
assert d.undoName() == "foo" assert d.undoName() == "foo"
@ -27,7 +29,7 @@ def test_op():
# and a review will, too # and a review will, too
d.save("add") d.save("add")
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
assert d.undoName() == "add" assert d.undoName() == "add"
@ -35,11 +37,12 @@ def test_op():
d.sched.answerCard(c, 2) d.sched.answerCard(c, 2)
assert d.undoName() == "Review" assert d.undoName() == "Review"
def test_review(): def test_review():
d = getEmptyCol() d = getEmptyCol()
d.conf['counts'] = COUNT_REMAINING d.conf["counts"] = COUNT_REMAINING
f = d.newNote() f = d.newNote()
f['Front'] = "one" f["Front"] = "one"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
assert not d.undoName() assert not d.undoName()
@ -62,7 +65,7 @@ def test_review():
assert not d.undoName() assert not d.undoName()
# we should be able to undo multiple answers too # we should be able to undo multiple answers too
f = d.newNote() f = d.newNote()
f['Front'] = "two" f["Front"] = "two"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
assert d.sched.counts() == (2, 0, 0) assert d.sched.counts() == (2, 0, 0)
@ -85,5 +88,3 @@ def test_review():
assert d.undoName() == "foo" assert d.undoName() == "foo"
d.undo() d.undo()
assert not d.undoName() assert not d.undoName()

View file

@ -2,6 +2,7 @@
from anki.utils import fmtTimeSpan from anki.utils import fmtTimeSpan
def test_fmtTimeSpan(): def test_fmtTimeSpan():
assert fmtTimeSpan(5) == "5 seconds" assert fmtTimeSpan(5) == "5 seconds"
assert fmtTimeSpan(5, inTime=True) == "in 5 seconds" assert fmtTimeSpan(5, inTime=True) == "in 5 seconds"

View file

@ -10,11 +10,7 @@ set -e
BIN="$(cd "`dirname "$0"`"; pwd)" BIN="$(cd "`dirname "$0"`"; pwd)"
export PYTHONPATH=${BIN}/..:${PYTHONPATH} export PYTHONPATH=${BIN}/..:${PYTHONPATH}
# favour nosetests3 if available nose="python -m nose2 --plugin=nose2.plugins.mp -N 16"
nose=nosetests
if which nosetests3 >/dev/null 2>&1; then
nose=nosetests3
fi
dir=. dir=.
@ -24,7 +20,4 @@ else
lim="tests.test_$1" lim="tests.test_$1"
fi fi
if [ x$coverage != x ]; then (cd $dir && $nose $lim $args)
args="--with-coverage"
fi
(cd $dir && $nose -s --processes=16 --process-timeout=300 $lim $args --cover-package=anki)

4
ts/package-lock.json generated
View file

@ -10,7 +10,7 @@
"integrity": "sha512-Lz4BAJihoFw5nRzKvg4nawXPzutkv7wmfQ5121avptaSIXlDNJCUuxZxX/G+9EVidZGuO0UBlk+YjKbwRKJigg==", "integrity": "sha512-Lz4BAJihoFw5nRzKvg4nawXPzutkv7wmfQ5121avptaSIXlDNJCUuxZxX/G+9EVidZGuO0UBlk+YjKbwRKJigg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/sizzle": "*" "@types/sizzle": "2.3.2"
} }
}, },
"@types/jqueryui": { "@types/jqueryui": {
@ -19,7 +19,7 @@
"integrity": "sha512-bHE7BiG+5Sviy/eA9Npz5HHF3hv40XjaEbpYtSJPaNwuyxhSJ0qWlE8C5DgNMfobVOZ2aSTrM1iGDCGmvlbxOg==", "integrity": "sha512-bHE7BiG+5Sviy/eA9Npz5HHF3hv40XjaEbpYtSJPaNwuyxhSJ0qWlE8C5DgNMfobVOZ2aSTrM1iGDCGmvlbxOg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/jquery": "*" "@types/jquery": "3.3.31"
} }
}, },
"@types/mathjax": { "@types/mathjax": {