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
with:
node-version: 12
- name: Install dependencies
run: |
sudo apt install portaudio19-dev
pip install -r requirements.qt
- name: Set up Protoc
uses: Arduino/actions/setup-protoc@master
- name: Run checks
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
.DS_Store
.build
.coverage
.DS_Store
.mypy_cache
.pytype
__pycache__
anki/buildhash.py
anki/backend_pb2.*
aqt/forms
locale
rs/ankirs/src/backend_proto.rs
rs/target
tools/runanki.system
ts/node_modules
web/deckbrowser.js

View file

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

165
Makefile
View file

@ -1,13 +1,14 @@
PREFIX := /usr
SHELL := bash
.SHELLFLAGS := -eu -o pipefail -c
.ONESHELL:
.DELETE_ON_ERROR:
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
RUNARGS :=
.SUFFIXES:
BLACKARGS := -t py36 anki aqt
BLACKARGS := -t py36 anki aqt tests
RUSTARGS := --release --strip
ISORTARGS := anki aqt tests
$(shell mkdir -p .build)
@ -40,6 +41,8 @@ install:
-xdg-mime default anki.desktop application/x-apkg
@echo
@echo "Install complete."
# fixme: _ankirs.so needs to be copied into system python env or
# 'maturin build' used
uninstall:
rm -rf ${DESTDIR}${PREFIX}/share/anki
@ -55,20 +58,48 @@ uninstall:
# 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 $<
touch $@
@touch $@
.build/pycheckreqs: requirements.check .build/pyrunreqs
.build/py-check-reqs: requirements.check .build/py-run-deps
pip install -r $<
./tools/typecheck-setup.sh
touch $@
@touch $@
.build/jsreqs: ts/package.json
# TS prerequisites
######################
.build/ts-deps: ts/package.json
(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
######################
@ -76,18 +107,31 @@ RUNREQS := .build/pyrunreqs .build/jsreqs
TSDEPS := $(wildcard ts/src/*.ts)
JSDEPS := $(patsubst ts/src/%.ts, web/%.js, $(TSDEPS))
# Rust source
######################
RSDEPS := $(shell find rs -type f | egrep -v 'target|/\.|proto.rs')
# Building
######################
BUILDDEPS := .build/ui .build/js
BUILDDEPS := .build/ui .build/js .build/rs .build/py-proto
.build/ui: $(RUNREQS) $(shell find designer -type f)
./tools/build_ui.sh
touch $@
@touch $@
.build/js: .build/jsreqs $(TSDEPS)
.build/js: .build/ts-deps $(TSDEPS)
(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
@ -97,6 +141,7 @@ build: $(BUILDDEPS)
clean:
rm -rf .build
rm -rf $(JSDEPS)
rm -rf rs/target
# Running
######################
@ -109,61 +154,89 @@ run: build
######################
.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
######################
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
touch $@
@touch $@
.build/pytest: $(PYCHECKDEPS) $(wildcard tests/*.py)
.build/py-test: $(PYCHECKDEPS) $(PYTESTDEPS)
./tools/tests.sh
touch $@
@touch $@
.build/pylint: $(PYCHECKDEPS)
pylint -j 0 --rcfile=.pylintrc -f colorized --extension-pkg-whitelist=PyQt5 anki aqt
touch $@
.build/py-lint: $(PYCHECKDEPS)
pylint -j 0 --rcfile=.pylintrc -f colorized --extension-pkg-whitelist=PyQt5,_ankirs anki aqt
@touch $@
.build/pyimports: $(PYCHECKDEPS)
isort anki aqt --check # if this fails, run 'make fixpyimports'
touch $@
.build/py-imports: $(PYCHECKDEPS) $(PYTESTDEPS)
isort $(ISORTARGS) --check # if this fails, run 'make fix-py-imports'
@touch $@
.build/pyfmt: $(PYCHECKDEPS)
black --check $(BLACKARGS) # if this fails, run 'make fixpyfmt'
touch $@
.build/py-fmt: $(PYCHECKDEPS) $(PYTESTDEPS)
black --check $(BLACKARGS) # if this fails, run 'make fix-py-fmt'
@touch $@
.PHONY: mypy pytest pylint pyimports pyfmt
mypy: .build/mypy
pytest: .build/pytest
pylint: .build/pylint
pyimports: .build/pyimports
pyfmt: .build/pyfmt
.PHONY: py-mypy py-test py-lint py-imports py-fmt
py-mypy: .build/py-mypy
py-test: .build/py-test
py-lint: .build/py-lint
py-imports: .build/py-imports
py-fmt: .build/py-fmt
.PHONY: fixpyimports fixpyfmt
.PHONY: fix-py-imports fix-py-fmt
fixpyimports:
isort anki aqt
fix-py-imports:
isort $(ISORTARGS)
fixpyfmt:
fix-py-fmt:
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
######################
TSCHECKDEPS := $(BUILDDEPS) $(TSDEPS)
.build/checkpretty: $(TSCHECKDEPS)
(cd ts && npm run check-pretty) # if this fails, run 'make pretty'
touch $@
.build/ts-fmt: $(TSCHECKDEPS)
(cd ts && npm run check-pretty) # if this fails, run 'make fix-ts-fmt'
@touch $@
.build/pretty: $(TSCHECKDEPS)
.PHONY: fix-ts-fmt ts-fmt
ts-fmt: .build/ts-fmt
fix-ts-fmt:
(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
- lame
- 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
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:
$ brew install python mpv lame portaudio
$ brew install python mpv lame portaudio protobuf npm rustup-init
Windows users
--------------
The build scripts have not been tested on Windows, and you'll find things
easiest if you build Anki using WSL.
https://docs.microsoft.com/en-us/windows/wsl/install-win10
The build process uses a GNU makefile, so you'll either need to run
GNU make via WSL (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.notes
import anki.template
from anki.backend import Backend
from anki.cards import Card
from anki.consts import *
from anki.db import DB
@ -84,8 +85,12 @@ class _Collection:
ls: int
conf: Dict[str, 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.db = db
self.path = db._path

View file

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

View file

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

View file

@ -556,6 +556,9 @@ select id from notes where mid = ?)"""
##########################################################################
def _updateRequired(self, m: NoteType) -> None:
self._updateRequiredNew(m)
def _updateRequiredLegacy(self, m: NoteType) -> None:
if m["type"] == MODEL_CLOZE:
# nothing to do
return
@ -566,6 +569,14 @@ select id from notes where mid = ?)"""
req.append([t["ord"], ret[0], ret[1]])
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(
self, m: NoteType, flds: List[str], t: Template
) -> Tuple[Union[str, List[int]], ...]:

View file

@ -4,6 +4,7 @@
from typing import Any, List, Optional, Tuple
import anki # pylint: disable=unused-import
from anki.utils import (
fieldChecksum,
guid64,
@ -19,7 +20,10 @@ class Note:
tags: List[str]
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:
assert not (model and id)
self.col = col

View file

@ -8,6 +8,7 @@ import os
import re
from typing import Any, Dict, Tuple
from anki.backend import Backend
from anki.collection import _Collection
from anki.consts import *
from anki.db import DB
@ -26,6 +27,10 @@ def Collection(
path: str, lock: bool = True, server: bool = False, log: bool = False
) -> _Collection:
"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")
path = os.path.abspath(path)
create = not os.path.exists(path)
@ -46,7 +51,7 @@ def Collection(
db.execute("pragma journal_mode = wal")
db.setAutocommit(False)
# 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:
_upgrade(col, ver)
elif ver > SCHEMA_VERSION:
@ -60,7 +65,11 @@ def Collection(
addBasicModel(col)
col.save()
if lock:
col.lock()
try:
col.lock()
except:
col.db.close()
raise
return col

View file

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

View file

@ -210,14 +210,15 @@ class Template:
return "{unknown field %s}" % tag_name
return txt
@classmethod
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
currentRegex = clozeReg % ord
if not re.search(currentRegex, txt):
# No Cloze deletion was found in txt.
return ""
txt = self._removeFormattingFromMathjax(txt, ord)
txt = cls._removeFormattingFromMathjax(txt, ord)
def repl(m):
# replace chosen cloze with type
@ -237,7 +238,8 @@ class Template:
# and display other clozes normally
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.
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
# provides more descriptive function signatures than just 'Dict[str, Any]'
@ -31,3 +31,8 @@ QAData = Tuple[
# Corresponds to 'cardFlags' column. TODO: document
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)
# profile manager
pm = ProfileManager(opts.base)
pmLoadResult = pm.setupMeta()
pm = None
try:
pm = ProfileManager(opts.base)
pmLoadResult = pm.setupMeta()
except:
# will handle below
pass
# gl workarounds
setupGL(pm)
if pm:
# gl workarounds
setupGL(pm)
# apply user-provided scale factor
os.environ["QT_SCALE_FACTOR"] = str(pm.uiScale())
# opt in to full hidpi support?
if not os.environ.get("ANKI_NOHIGHDPI"):
@ -348,9 +356,6 @@ def _run(argv=None, exec=True):
if os.environ.get("ANKI_SOFTWAREOPENGL"):
QCoreApplication.setAttribute(Qt.AA_UseSoftwareOpenGL)
# apply user-provided scale factor
os.environ["QT_SCALE_FACTOR"] = str(pm.uiScale())
# create the app
QCoreApplication.setApplicationName("Anki")
QGuiApplication.setDesktopFileName("anki.desktop")
@ -359,6 +364,16 @@ def _run(argv=None, exec=True):
# we've signaled the primary instance, so we should close
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
if isMac:
app.setAttribute(Qt.AA_DontShowIconsInMenus)
@ -392,12 +407,14 @@ environment points to a valid, writable folder.""",
)
return
if pmLoadResult.firstTime:
pm.setDefaultLang()
if pmLoadResult.loadError:
QMessageBox.warning(
None,
"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.""",
)

View file

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

View file

@ -97,22 +97,7 @@ class ProfileManager:
######################################################################
def ensureBaseExists(self):
try:
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
self._ensureExists(self.base)
# Folder migration
######################################################################
@ -385,7 +370,6 @@ create table if not exists profiles
"insert or replace into profiles values ('_global', ?)",
self._pickle(metaConf),
)
self._setDefaultLang()
return result
def _ensureProfile(self):
@ -409,7 +393,7 @@ please see:
######################################################################
# On first run, allow the user to choose the default language
def _setDefaultLang(self):
def setDefaultLang(self):
# create dialog
class NoCloseDiag(QDialog):
def reject(self):
@ -452,7 +436,7 @@ please see:
None, "Anki", en % name, QMessageBox.Yes | QMessageBox.No, QMessageBox.No
)
if r != QMessageBox.Yes:
return self._setDefaultLang()
return self.setDefaultLang()
self.setLang(code)
def setLang(self, code):

View file

@ -416,11 +416,16 @@ body {{ zoom: {}; background: {}; {} }}
self.evalWithCallback("$(document.body).height()", self._onHeight)
def _onHeight(self, qvar):
from aqt import mw
if qvar is None:
from aqt import mw
mw.progress.timer(1000, mw.reset, False)
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)

View file

@ -34,3 +34,5 @@ ignore_missing_imports = True
ignore_missing_imports = True
[mypy-jsonschema.*]
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
mypy==0.750
# fixme: when isort 5.0 is released, switch to pypy
git+https://github.com/dae/isort#egg=isort
# fixme: when pylint supports isort 5.0, switch to pypy

View file

@ -8,3 +8,7 @@ jsonschema
psutil; sys_platform == "win32"
distro; sys_platform != "win32" and sys_platform != "darwin"
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
def assertException(exception, func):
found = False
try:
@ -25,6 +29,7 @@ def getEmptyCol():
col = aopen(nam)
return col
getEmptyCol.master = ""
# Fallback for when the DB needs options passed in.
@ -34,10 +39,12 @@ def getEmptyDeckWith(**kwargs):
os.unlink(nam)
return aopen(nam, **kwargs)
def getUpgradeDeckPath(name="anki12.anki"):
src = os.path.join(testDir, "support", name)
(fd, dst) = tempfile.mkstemp(suffix=".anki2")
shutil.copy(src, dst)
return dst
testDir = os.path.dirname(__file__)

View file

@ -1,73 +1,57 @@
import os.path
from nose.tools import assert_equals
from mock import MagicMock
from tempfile import TemporaryDirectory
from zipfile import ZipFile
from mock import MagicMock
from nose2.tools.such import helper
from aqt.addons import AddonManager
def test_readMinimalManifest():
assertReadManifest(
'{"package": "yes", "name": "no"}',
{"package": "yes", "name": "no"}
'{"package": "yes", "name": "no"}', {"package": "yes", "name": "no"}
)
def test_readExtraKeys():
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"]},
)
def test_invalidManifest():
assertReadManifest(
'{"one": 1}',
{}
)
assertReadManifest('{"one": 1}', {})
def test_mustHaveName():
assertReadManifest(
'{"package": "something"}',
{}
)
assertReadManifest('{"package": "something"}', {})
def test_mustHavePackage():
assertReadManifest(
'{"name": "something"}',
{}
)
assertReadManifest('{"name": "something"}', {})
def test_invalidJson():
assertReadManifest(
'this is not a JSON dictionary',
{}
)
assertReadManifest("this is not a JSON dictionary", {})
def test_missingManifest():
assertReadManifest(
'{"package": "what", "name": "ever"}',
{},
nameInZip="not-manifest.bin"
'{"package": "what", "name": "ever"}', {}, nameInZip="not-manifest.bin"
)
def test_ignoreExtraKeys():
assertReadManifest(
'{"package": "a", "name": "b", "game": "c"}',
{"package": "a", "name": "b"}
'{"package": "a", "name": "b", "game": "c"}', {"package": "a", "name": "b"}
)
def test_conflictsMustBeStrings():
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())
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
def test_previewCards():
deck = getEmptyCol()
f = deck.newNote()
f['Front'] = '1'
f['Back'] = '2'
f["Front"] = "1"
f["Back"] = "2"
# non-empty and active
cards = deck.previewCards(f, 0)
assert len(cards) == 1
@ -22,11 +23,12 @@ def test_previewCards():
# make sure we haven't accidentally added cards to the db
assert deck.cardCount() == 1
def test_delete():
deck = getEmptyCol()
f = deck.newNote()
f['Front'] = '1'
f['Back'] = '2'
f["Front"] = "1"
f["Back"] = "2"
deck.addNote(f)
cid = f.cards()[0].id
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 graves") == 2
def test_misc():
d = getEmptyCol()
f = d.newNote()
f['Front'] = '1'
f['Back'] = '2'
f["Front"] = "1"
f["Back"] = "2"
d.addNote(f)
c = f.cards()[0]
id = d.models.current()['id']
assert c.template()['ord'] == 0
id = d.models.current()["id"]
assert c.template()["ord"] == 0
def test_genrem():
d = getEmptyCol()
f = d.newNote()
f['Front'] = '1'
f['Back'] = ''
f["Front"] = "1"
f["Back"] = ""
d.addNote(f)
assert len(f.cards()) == 1
m = d.models.current()
mm = d.models
# adding a new template should automatically create cards
t = mm.newTemplate("rev")
t['qfmt'] = '{{Front}}'
t['afmt'] = ""
t["qfmt"] = "{{Front}}"
t["afmt"] = ""
mm.addTemplate(m, t)
mm.save(m, templates=True)
assert len(f.cards()) == 2
# if the template is changed to remove cards, they'll be removed
t['qfmt'] = "{{Back}}"
t["qfmt"] = "{{Back}}"
mm.save(m, templates=True)
d.remCards(d.emptyCids())
assert len(f.cards()) == 1
# if we add to the note, a card should be automatically generated
f.load()
f['Back'] = "1"
f["Back"] = "1"
f.flush()
assert len(f.cards()) == 2
def test_gendeck():
d = getEmptyCol()
cloze = d.models.byName("Cloze")
d.models.setCurrent(cloze)
f = d.newNote()
f['Text'] = '{{c1::one}}'
f["Text"] = "{{c1::one}}"
d.addNote(f)
assert d.cardCount() == 1
assert f.cards()[0].did == 1
# set the model to a new default deck
newId = d.decks.id("new")
cloze['did'] = newId
cloze["did"] = newId
d.models.save(cloze, updateReqs=False)
# a newly generated card should share the first card's deck
f['Text'] += '{{c2::two}}'
f["Text"] += "{{c2::two}}"
f.flush()
assert f.cards()[1].did == 1
# and same with multiple cards
f['Text'] += '{{c3::three}}'
f["Text"] += "{{c3::three}}"
f.flush()
assert f.cards()[2].did == 1
# 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.did = newId
c.flush()
f['Text'] += '{{c4::four}}'
f["Text"] += "{{c4::four}}"
f.flush()
assert f.cards()[3].did == newId

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,43 +1,47 @@
# 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
from anki.consts import MODEL_CLOZE
from anki.utils import isWin, joinFields, stripHTML
from tests.shared import getEmptyCol
def test_modelDelete():
deck = getEmptyCol()
f = deck.newNote()
f['Front'] = '1'
f['Back'] = '2'
f["Front"] = "1"
f["Back"] = "2"
deck.addNote(f)
assert deck.cardCount() == 1
deck.models.rem(deck.models.current())
assert deck.cardCount() == 0
def test_modelCopy():
deck = getEmptyCol()
m = deck.models.current()
m2 = deck.models.copy(m)
assert m2['name'] == "Basic copy"
assert m2['id'] != m['id']
assert len(m2['flds']) == 2
assert len(m['flds']) == 2
assert len(m2['flds']) == len(m['flds'])
assert len(m['tmpls']) == 1
assert len(m2['tmpls']) == 1
assert m2["name"] == "Basic copy"
assert m2["id"] != m["id"]
assert len(m2["flds"]) == 2
assert len(m["flds"]) == 2
assert len(m2["flds"]) == len(m["flds"])
assert len(m["tmpls"]) == 1
assert len(m2["tmpls"]) == 1
assert deck.models.scmhash(m) == deck.models.scmhash(m2)
def test_fields():
d = getEmptyCol()
f = d.newNote()
f['Front'] = '1'
f['Back'] = '2'
f["Front"] = "1"
f["Back"] = "2"
d.addNote(f)
m = d.models.current()
# make sure renaming a field updates the templates
d.models.renameField(m, m['flds'][0], "NewFront")
assert "{{NewFront}}" in m['tmpls'][0]['qfmt']
d.models.renameField(m, m["flds"][0], "NewFront")
assert "{{NewFront}}" in m["tmpls"][0]["qfmt"]
h = d.models.scmhash(m)
# add a field
f = d.models.newField("foo")
@ -46,44 +50,46 @@ def test_fields():
assert d.models.scmhash(m) != h
# rename it
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
d.models.remField(m, m['flds'][1])
d.models.remField(m, m["flds"][1])
assert d.getNote(d.models.nids(m)[0]).fields == ["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"]
# 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", ""]
# add another and put in middle
f = d.models.newField("baz")
d.models.addField(m, f)
f = d.getNote(d.models.nids(m)[0])
f['baz'] = "2"
f["baz"] = "2"
f.flush()
assert d.getNote(d.models.nids(m)[0]).fields == ["1", "", "2"]
# 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", ""]
# 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"]
# 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"]
def test_templates():
d = getEmptyCol()
m = d.models.current(); mm = d.models
m = d.models.current()
mm = d.models
t = mm.newTemplate("Reverse")
t['qfmt'] = "{{Back}}"
t['afmt'] = "{{Front}}"
t["qfmt"] = "{{Back}}"
t["afmt"] = "{{Front}}"
mm.addTemplate(m, t)
mm.save(m)
f = d.newNote()
f['Front'] = '1'
f['Back'] = '2'
f["Front"] = "1"
f["Back"] = "2"
d.addNote(f)
assert d.cardCount() == 2
(c, c2) = f.cards()
@ -92,11 +98,12 @@ def test_templates():
assert c2.ord == 1
# switch templates
d.models.moveTemplate(m, c.template(), 1)
c.load(); c2.load()
c.load()
c2.load()
assert c.ord == 1
assert c2.ord == 0
# 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
# and should have updated the other cards' ordinals
c = f.cards()[0]
@ -105,64 +112,67 @@ def test_templates():
# it shouldn't be possible to orphan notes by removing templates
t = mm.newTemplate("template name")
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():
d = getEmptyCol()
d.models.setCurrent(d.models.byName("Cloze"))
m = d.models.current(); mm = d.models
#We replace the default Cloze template
m = d.models.current()
mm = d.models
# We replace the default Cloze template
t = mm.newTemplate("ChainedCloze")
t['qfmt'] = "{{text:cloze:Text}}"
t['afmt'] = "{{text:cloze:Text}}"
t["qfmt"] = "{{text:cloze:Text}}"
t["afmt"] = "{{text:cloze:Text}}"
mm.addTemplate(m, t)
mm.save(m)
d.models.remTemplate(m, m['tmpls'][0])
d.models.remTemplate(m, m["tmpls"][0])
f = d.newNote()
f['Text'] = '{{c1::firstQ::firstA}}{{c2::secondQ::secondA}}'
f["Text"] = "{{c1::firstQ::firstA}}{{c2::secondQ::secondA}}"
d.addNote(f)
assert d.cardCount() == 2
(c, c2) = f.cards()
# first card should have first ord
assert c.ord == 0
assert c2.ord == 1
def test_text():
d = getEmptyCol()
m = d.models.current()
m['tmpls'][0]['qfmt'] = "{{text:Front}}"
m["tmpls"][0]["qfmt"] = "{{text:Front}}"
d.models.save(m)
f = d.newNote()
f['Front'] = 'hello<b>world'
f["Front"] = "hello<b>world"
d.addNote(f)
assert "helloworld" in f.cards()[0].q()
def test_cloze():
d = getEmptyCol()
d.models.setCurrent(d.models.byName("Cloze"))
f = d.newNote()
assert f.model()['name'] == "Cloze"
assert f.model()["name"] == "Cloze"
# a cloze model with no clozes is not empty
f['Text'] = 'nothing'
f["Text"] = "nothing"
assert d.addNote(f)
# try with one cloze
f = d.newNote()
f['Text'] = "hello {{c1::world}}"
f["Text"] = "hello {{c1::world}}"
assert d.addNote(f) == 1
assert "hello <span class=cloze>[...]</span>" in f.cards()[0].q()
assert "hello <span class=cloze>world</span>" in f.cards()[0].a()
# and with a comment
f = d.newNote()
f['Text'] = "hello {{c1::world::typical}}"
f["Text"] = "hello {{c1::world::typical}}"
assert d.addNote(f) == 1
assert "<span class=cloze>[typical]</span>" in f.cards()[0].q()
assert "<span class=cloze>world</span>" in f.cards()[0].a()
# and with 2 clozes
f = d.newNote()
f['Text'] = "hello {{c1::world}} {{c2::bar}}"
f["Text"] = "hello {{c1::world}} {{c2::bar}}"
assert d.addNote(f) == 2
(c1, c2) = f.cards()
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
# list
f = d.newNote()
f['Text'] = "a {{c1::b}} {{c1::c}}"
f["Text"] = "a {{c1::b}} {{c1::c}}"
assert d.addNote(f) == 1
assert "<span class=cloze>b</span> <span class=cloze>c</span>" in (
f.cards()[0].a())
assert "<span class=cloze>b</span> <span class=cloze>c</span>" in (f.cards()[0].a())
# if we add another cloze, a card should be generated
cnt = d.cardCount()
f['Text'] = "{{c2::hello}} {{c1::foo}}"
f["Text"] = "{{c2::hello}} {{c1::foo}}"
f.flush()
assert d.cardCount() == cnt + 1
# 0 or negative indices are not supported
f['Text'] += "{{c0::zero}} {{c-1:foo}}"
f["Text"] += "{{c0::zero}} {{c-1:foo}}"
f.flush()
assert len(f.cards()) == 2
def test_cloze_mathjax():
d = getEmptyCol()
d.models.setCurrent(d.models.byName("Cloze"))
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 len(f.cards()) == 5
assert "class=cloze" in f.cards()[0].q()
@ -200,56 +212,70 @@ def test_cloze_mathjax():
assert "class=cloze" in f.cards()[4].q()
f = d.newNote()
f['Text'] = r'\(a\) {{c1::b}} \[ {{c1::c}} \]'
f["Text"] = r"\(a\) {{c1::b}} \[ {{c1::c}} \]"
assert d.addNote(f)
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():
d = getEmptyCol()
d.models.setCurrent(d.models.byName("Cloze"))
m = d.models.current(); mm = d.models
#We replace the default Cloze template
m = d.models.current()
mm = d.models
# We replace the default Cloze template
t = mm.newTemplate("ChainedCloze")
t['qfmt'] = "{{cloze:text:Text}}"
t['afmt'] = "{{cloze:text:Text}}"
t["qfmt"] = "{{cloze:text:Text}}"
t["afmt"] = "{{cloze:text:Text}}"
mm.addTemplate(m, t)
mm.save(m)
d.models.remTemplate(m, m['tmpls'][0])
d.models.remTemplate(m, m["tmpls"][0])
f = d.newNote()
q1 = '<span style=\"color:red\">phrase</span>'
a1 = '<b>sentence</b>'
q2 = '<span style=\"color:red\">en chaine</span>'
a2 = '<i>chained</i>'
f['Text'] = "This {{c1::%s::%s}} demonstrates {{c1::%s::%s}} clozes." % (q1,a1,q2,a2)
q1 = '<span style="color:red">phrase</span>'
a1 = "<b>sentence</b>"
q2 = '<span style="color:red">en chaine</span>'
a2 = "<i>chained</i>"
f["Text"] = "This {{c1::%s::%s}} demonstrates {{c1::%s::%s}} clozes." % (
q1,
a1,
q2,
a2,
)
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 "This <span class=cloze>phrase</span> demonstrates <span class=cloze>en chaine</span> clozes." in f.cards()[0].a()
assert (
"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():
deck = getEmptyCol()
basic = deck.models.byName("Basic")
cloze = deck.models.byName("Cloze")
# 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['qfmt'] = "{{Back}}"
t['afmt'] = "{{Front}}"
t["qfmt"] = "{{Back}}"
t["afmt"] = "{{Front}}"
mm.addTemplate(m, t)
mm.save(m)
f = deck.newNote()
f['Front'] = 'f'
f['Back'] = 'b123'
f["Front"] = "f"
f["Back"] = "b123"
deck.addNote(f)
# switch fields
map = {0: 1, 1: 0}
deck.models.change(basic, [f.id], basic, map, None)
f.load()
assert f['Front'] == 'b123'
assert f['Back'] == 'f'
assert f["Front"] == "b123"
assert f["Back"] == "f"
# switch cards
c0 = f.cards()[0]
c1 = f.cards()[1]
@ -258,7 +284,9 @@ def test_modelChange():
assert c0.ord == 0
assert c1.ord == 1
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 "b123" in c1.q()
assert c0.ord == 1
@ -267,6 +295,9 @@ def test_modelChange():
assert f.cards()[0].id == c1.id
# delete first card
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)
f.load()
c0.load()
@ -279,30 +310,31 @@ def test_modelChange():
# but we have two cards, as a new one was generated
assert len(f.cards()) == 2
# an unmapped field becomes blank
assert f['Front'] == 'b123'
assert f['Back'] == 'f'
assert f["Front"] == "b123"
assert f["Back"] == "f"
deck.models.change(basic, [f.id], basic, map, None)
f.load()
assert f['Front'] == ''
assert f['Back'] == 'f'
assert f["Front"] == ""
assert f["Back"] == "f"
# another note to try model conversion
f = deck.newNote()
f['Front'] = 'f2'
f['Back'] = 'b2'
f["Front"] = "f2"
f["Back"] = "b2"
deck.addNote(f)
assert deck.models.useCount(basic) == 2
assert deck.models.useCount(cloze) == 0
map = {0: 0, 1: 1}
deck.models.change(basic, [f.id], cloze, map, map)
f.load()
assert f['Text'] == "f2"
assert f["Text"] == "f2"
assert len(f.cards()) == 2
# 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
deck.models.change(cloze, [f.id], basic, map, map)
assert deck.db.scalar("select count() from cards where nid = ?", f.id) == 1
def test_templates():
d = dict(Foo="x", Bar="y")
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("{{#Baz}}{{#Foo}}{{Foo}}{{/Foo}}{{/Baz}}", d) == ""
def test_availOrds():
d = getEmptyCol()
m = d.models.current(); mm = d.models
t = m['tmpls'][0]
m = d.models.current()
mm = d.models
t = m["tmpls"][0]
f = d.newNote()
f['Front'] = "1"
f["Front"] = "1"
# simple templates
assert mm.availOrds(m, joinFields(f.fields)) == [0]
t['qfmt'] = "{{Back}}"
t["qfmt"] = "{{Back}}"
mm.save(m, templates=True)
assert not mm.availOrds(m, joinFields(f.fields))
# AND
t['qfmt'] = "{{#Front}}{{#Back}}{{Front}}{{/Back}}{{/Front}}"
t["qfmt"] = "{{#Front}}{{#Back}}{{Front}}{{/Back}}{{/Front}}"
mm.save(m, templates=True)
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)
assert not mm.availOrds(m, joinFields(f.fields))
# OR
t['qfmt'] = "{{Front}}\n{{Back}}"
t["qfmt"] = "{{Front}}\n{{Back}}"
mm.save(m, templates=True)
assert mm.availOrds(m, joinFields(f.fields)) == [0]
t['Front'] = ""
t['Back'] = "1"
t["Front"] = ""
t["Back"] = "1"
assert mm.availOrds(m, joinFields(f.fields)) == [0]
def test_req():
def reqSize(model):
if model['type'] == MODEL_CLOZE:
if model["type"] == MODEL_CLOZE:
return
assert (len(model['tmpls']) == len(model['req']))
assert len(model["tmpls"]) == len(model["req"])
d = getEmptyCol()
mm = d.models
basic = mm.byName("Basic")
assert 'req' in basic
assert "req" in 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)")
reqSize(opt)
assert opt['req'][0] == [0, 'all', [0]]
assert opt['req'][1] == [1, 'all', [1, 2]]
#testing any
opt['tmpls'][1]['qfmt'] = "{{Back}}{{Add Reverse}}"
r = opt["req"][0]
assert r[1] in ("any", "all")
assert r[2] == [0]
assert opt["req"][1] == [1, "all", [1, 2]]
# testing any
opt["tmpls"][1]["qfmt"] = "{{Back}}{{Add Reverse}}"
mm.save(opt, templates=True)
assert opt['req'][1] == [1, 'any', [1, 2]]
#testing None
opt['tmpls'][1]['qfmt'] = "{{^Add Reverse}}{{Back}}{{/Add Reverse}}"
assert opt["req"][1] == [1, "any", [1, 2]]
# testing None
opt["tmpls"][1]["qfmt"] = "{{^Add Reverse}}{{Back}}{{/Add Reverse}}"
mm.save(opt, templates=True)
assert opt['req'][1] == [1, 'none', []]
assert opt["req"][1] == [1, "none", []]
def test_updatereqs_performance():
import time
d = getEmptyCol()
mm = d.models
m = mm.byName("Basic")
for i in range(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)
opt = mm.byName("Basic (type in the answer)")
reqSize(opt)
r = opt["req"][0]
assert r[1] in ("any", "all")
assert r[2] == [0]
# def test_updatereqs_performance():
# import time
# d = getEmptyCol()
# mm = d.models
# m = mm.byName("Basic")
# for i in range(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
import time
import copy
import time
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.utils import intTime
from tests.shared import getEmptyCol as getEmptyColOrig
def getEmptyCol():
col = getEmptyColOrig()
col.changeSchedulerVer(1)
return col
def test_clock():
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.")
def checkRevIvl(d, c, targetIvl):
min, max = d.sched._fuzzIvlRange(targetIvl)
return min <= c.ivl <= max
def test_basics():
d = getEmptyCol()
d.reset()
assert not d.sched.getCard()
def test_new():
d = getEmptyCol()
d.reset()
assert d.sched.newCount == 0
# add a note
f = d.newNote()
f['Front'] = "one"; f['Back'] = "two"
f["Front"] = "one"
f["Back"] = "two"
d.addNote(f)
d.reset()
assert d.sched.newCount == 1
@ -71,15 +77,16 @@ def test_new():
# assert qs[n] in c.q()
# d.sched.answerCard(c, 2)
def test_newLimits():
d = getEmptyCol()
# add some notes
g2 = d.decks.id("Default::foo")
for i in range(30):
f = d.newNote()
f['Front'] = str(i)
f["Front"] = str(i)
if i > 4:
f.model()['did'] = g2
f.model()["did"] = g2
d.addNote(f)
# give the child deck a different configuration
c2 = d.decks.confId("new conf")
@ -92,33 +99,36 @@ def test_newLimits():
assert c.did == 1
# limit the parent to 10 cards, meaning we get 10 in total
conf1 = d.decks.confForDid(1)
conf1['new']['perDay'] = 10
conf1["new"]["perDay"] = 10
d.reset()
assert d.sched.newCount == 10
# if we limit child to 4, we should get 9
conf2 = d.decks.confForDid(g2)
conf2['new']['perDay'] = 4
conf2["new"]["perDay"] = 4
d.reset()
assert d.sched.newCount == 9
def test_newBoxes():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
d.reset()
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)
# should handle gracefully
d.sched._cardConf(c)['new']['delays'] = [1]
d.sched._cardConf(c)["new"]["delays"] = [1]
d.sched.answerCard(c, 2)
def test_learn():
d = getEmptyCol()
# add a note
f = d.newNote()
f['Front'] = "one"; f['Back'] = "two"
f["Front"] = "one"
f["Back"] = "two"
f = d.addNote(f)
# set as a learn card and rebuild queues
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
c = d.sched.getCard()
assert c
d.sched._cardConf(c)['new']['delays'] = [0.5, 3, 10]
d.sched._cardConf(c)["new"]["delays"] = [0.5, 3, 10]
# fail it
d.sched.answerCard(c, 1)
# 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
t = round(c.due - time.time())
assert t >= 25 and t <= 40
@ -139,8 +149,8 @@ def test_learn():
d.sched.answerCard(c, 2)
# it should by due in 3 minutes
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
log = d.db.first("select * from revlog order by id desc")
assert log[3] == 2
@ -150,8 +160,8 @@ def test_learn():
d.sched.answerCard(c, 2)
# it should by due in 10 minutes
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
assert c.queue == 1
assert c.type == 1
@ -159,7 +169,7 @@ def test_learn():
assert c.queue == 2
assert c.type == 2
# 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
# or normal removal
c.type = 0
@ -188,14 +198,15 @@ def test_learn():
assert c.queue == 2
assert c.due == 321
def test_learn_collapsed():
d = getEmptyCol()
# add 2 notes
f = d.newNote()
f['Front'] = "1"
f["Front"] = "1"
f = d.addNote(f)
f = d.newNote()
f['Front'] = "2"
f["Front"] = "2"
f = d.addNote(f)
# set as a learn card and rebuild queues
d.db.execute("update cards set queue=0, type=0")
@ -214,27 +225,28 @@ def test_learn_collapsed():
c = d.sched.getCard()
assert not c.q().endswith("2")
def test_learn_day():
d = getEmptyCol()
# add a note
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
f = d.addNote(f)
d.sched.reset()
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
d.sched.answerCard(c, 2)
# two reps to graduate, 1 more today
assert c.left%1000 == 3
assert c.left//1000 == 1
assert c.left % 1000 == 3
assert c.left // 1000 == 1
assert d.sched.counts() == (0, 1, 0)
c = d.sched.getCard()
ni = d.sched.nextIvl
assert ni(c, 2) == 86400
# answering it will place it in queue 3
d.sched.answerCard(c, 2)
assert c.due == d.sched.today+1
assert c.due == d.sched.today + 1
assert c.queue == 3
assert not d.sched.getCard()
# for testing, move it back a day
@ -244,7 +256,7 @@ def test_learn_day():
assert d.sched.counts() == (0, 1, 0)
c = d.sched.getCard()
# 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
d.sched.answerCard(c, 1)
assert c.queue == 1
@ -266,17 +278,19 @@ def test_learn_day():
c.flush()
d.reset()
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()
d.sched.answerCard(c, 1)
assert c.queue == 3
assert d.sched.counts() == (0, 0, 0)
def test_reviews():
d = getEmptyCol()
# add a note
f = d.newNote()
f['Front'] = "one"; f['Back'] = "two"
f["Front"] = "one"
f["Back"] = "two"
d.addNote(f)
# set the card up as a review card, due 8 days ago
c = f.cards()[0]
@ -295,7 +309,7 @@ def test_reviews():
##################################################
# different delay to new
d.reset()
d.sched._cardConf(c)['lapse']['delays'] = [2, 20]
d.sched._cardConf(c)["lapse"]["delays"] = [2, 20]
d.sched.answerCard(c, 1)
assert c.queue == 1
# it should be due tomorrow, with an interval of 1
@ -313,7 +327,7 @@ def test_reviews():
# check ests.
ni = d.sched.nextIvl
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
##################################################
c = copy.copy(cardcopy)
@ -355,8 +369,10 @@ def test_reviews():
c.flush()
# steup hook
hooked = []
def onLeech(card):
hooked.append(1)
addHook("leech", onLeech)
d.sched.answerCard(c, 1)
assert hooked
@ -364,10 +380,11 @@ def test_reviews():
c.load()
assert c.queue == -1
def test_button_spacing():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
# 1 day ivl review card due now
c = f.cards()[0]
@ -384,13 +401,14 @@ def test_button_spacing():
assert ni(c, 3) == "3 days"
assert ni(c, 4) == "4 days"
def test_overdue_lapse():
# disabled in commit 3069729776990980f34c25be66410e947e9d51a2
return
d = getEmptyCol()
# add a note
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
# simulate a review that was lapsed and is now due for its normal review
c = f.cards()[0]
@ -419,13 +437,15 @@ def test_overdue_lapse():
d.sched.reset()
assert d.sched.counts() == (0, 0, 1)
def test_finished():
d = getEmptyCol()
# nothing due
assert "Congratulations" in d.sched.finishedMsg()
assert "limit" not in d.sched.finishedMsg()
f = d.newNote()
f['Front'] = "one"; f['Back'] = "two"
f["Front"] = "one"
f["Back"] = "two"
d.addNote(f)
# have a new card
assert "new cards available" in d.sched.finishedMsg()
@ -438,44 +458,46 @@ def test_finished():
assert "Congratulations" in d.sched.finishedMsg()
assert "limit" not in d.sched.finishedMsg()
def test_nextIvl():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"; f['Back'] = "two"
f["Front"] = "one"
f["Back"] = "two"
d.addNote(f)
d.reset()
conf = d.decks.confForDid(1)
conf['new']['delays'] = [0.5, 3, 10]
conf['lapse']['delays'] = [1, 5, 9]
conf["new"]["delays"] = [0.5, 3, 10]
conf["lapse"]["delays"] = [1, 5, 9]
c = d.sched.getCard()
# new cards
##################################################
ni = d.sched.nextIvl
assert ni(c, 1) == 30
assert ni(c, 2) == 180
assert ni(c, 3) == 4*86400
assert ni(c, 3) == 4 * 86400
d.sched.answerCard(c, 1)
# cards in learning
##################################################
assert ni(c, 1) == 30
assert ni(c, 2) == 180
assert ni(c, 3) == 4*86400
assert ni(c, 3) == 4 * 86400
d.sched.answerCard(c, 2)
assert ni(c, 1) == 30
assert ni(c, 2) == 600
assert ni(c, 3) == 4*86400
assert ni(c, 3) == 4 * 86400
d.sched.answerCard(c, 2)
# normal graduation is tomorrow
assert ni(c, 2) == 1*86400
assert ni(c, 3) == 4*86400
assert ni(c, 2) == 1 * 86400
assert ni(c, 3) == 4 * 86400
# lapsed cards
##################################################
c.type = 2
c.ivl = 100
c.factor = STARTING_FACTOR
assert ni(c, 1) == 60
assert ni(c, 2) == 100*86400
assert ni(c, 3) == 100*86400
assert ni(c, 2) == 100 * 86400
assert ni(c, 3) == 100 * 86400
# review cards
##################################################
c.queue = 2
@ -484,8 +506,8 @@ def test_nextIvl():
# failing it should put it at 60s
assert ni(c, 1) == 60
# or 1 day if relearn is false
d.sched._cardConf(c)['lapse']['delays']=[]
assert ni(c, 1) == 1*86400
d.sched._cardConf(c)["lapse"]["delays"] = []
assert ni(c, 1) == 1 * 86400
# (* 100 1.2 86400)10368000.0
assert ni(c, 2) == 10368000
# (* 100 2.5 86400)21600000.0
@ -494,10 +516,11 @@ def test_nextIvl():
assert ni(c, 4) == 28080000
assert d.sched.nextIvlStr(c, 4) == "10.8 months"
def test_misc():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
c = f.cards()[0]
# burying
@ -508,10 +531,11 @@ def test_misc():
d.reset()
assert d.sched.getCard()
def test_suspend():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
c = f.cards()[0]
# suspending
@ -525,7 +549,11 @@ def test_suspend():
d.reset()
assert d.sched.getCard()
# 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()
c = d.sched.getCard()
d.sched.answerCard(c, 1)
@ -551,10 +579,11 @@ def test_suspend():
assert c.due == 1
assert c.did == 1
def test_cram():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
c = f.cards()[0]
c.ivl = 100
@ -566,7 +595,7 @@ def test_cram():
c.startTimer()
c.flush()
d.reset()
assert d.sched.counts() == (0,0,0)
assert d.sched.counts() == (0, 0, 0)
cardcopy = copy.copy(c)
# create a dynamic deck and refresh it
did = d.decks.newDyn("Cram")
@ -575,18 +604,18 @@ def test_cram():
# should appear as new in the deck list
assert sorted(d.sched.deckDueList())[0][4] == 1
# 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
c = d.sched.getCard()
assert d.sched.answerButtons(c) == 2
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['delays'] = [1, 10]
cram["delays"] = [1, 10]
assert d.sched.answerButtons(c) == 3
assert d.sched.nextIvl(c, 1) == 60
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)
# elapsed time was 75 days
# factor = 2.5+1.2/2 = 1.85
@ -595,12 +624,11 @@ def test_cram():
assert c.odue == 138
assert c.queue == 1
# should be logged as a cram rep
assert d.db.scalar(
"select type from revlog order by id desc limit 1") == 3
assert d.db.scalar("select type from revlog order by id desc limit 1") == 3
# check ivls again
assert d.sched.nextIvl(c, 1) == 60
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, 2) == 138 * 60 * 60 * 24
assert d.sched.nextIvl(c, 3) == 138 * 60 * 60 * 24
# when it graduates, due is updated
c = d.sched.getCard()
d.sched.answerCard(c, 2)
@ -616,7 +644,7 @@ def test_cram():
# check ivls again - passing should be idempotent
assert d.sched.nextIvl(c, 1) == 60
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)
assert c.ivl == 138
assert c.odue == 138
@ -630,20 +658,20 @@ def test_cram():
assert len(d.sched.deckDueList()) == 1
c.load()
assert c.ivl == 1
assert c.due == d.sched.today+1
assert c.due == d.sched.today + 1
# make it due
d.reset()
assert d.sched.counts() == (0,0,0)
assert d.sched.counts() == (0, 0, 0)
c.due = -5
c.ivl = 100
c.flush()
d.reset()
assert d.sched.counts() == (0,0,1)
assert d.sched.counts() == (0, 0, 1)
# cram again
did = d.decks.newDyn("Cram")
d.sched.rebuildDyn(did)
d.reset()
assert d.sched.counts() == (0,0,1)
assert d.sched.counts() == (0, 0, 1)
c.load()
assert d.sched.answerButtons(c) == 4
# 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
assert c.did == 1
def test_cram_rem():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
oldDue = f.cards()[0].due
did = d.decks.newDyn("Cram")
@ -681,16 +710,17 @@ def test_cram_rem():
assert c.type == c.queue == 0
assert c.due == oldDue
def test_cram_resched():
# add card
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
# cram deck
did = d.decks.newDyn("Cram")
cram = d.decks.get(did)
cram['resched'] = False
cram["resched"] = False
d.sched.rebuildDyn(did)
d.reset()
# graduate should return it to new
@ -786,22 +816,25 @@ def test_cram_resched():
# d.sched.answerCard(c, 2)
# print c.__dict__
def test_ordcycle():
d = getEmptyCol()
# 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['qfmt'] = "{{Back}}"
t['afmt'] = "{{Front}}"
t["qfmt"] = "{{Back}}"
t["afmt"] = "{{Front}}"
mm.addTemplate(m, t)
t = mm.newTemplate("f2")
t['qfmt'] = "{{Front}}"
t['afmt'] = "{{Back}}"
t["qfmt"] = "{{Front}}"
t["afmt"] = "{{Back}}"
mm.addTemplate(m, t)
mm.save(m)
# create a new note; it should have 3 cards
f = d.newNote()
f['Front'] = "1"; f['Back'] = "1"
f["Front"] = "1"
f["Back"] = "1"
d.addNote(f)
assert d.cardCount() == 3
d.reset()
@ -810,10 +843,12 @@ def test_ordcycle():
assert d.sched.getCard().ord == 1
assert d.sched.getCard().ord == 2
def test_counts_idx():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"; f['Back'] = "two"
f["Front"] = "one"
f["Back"] = "two"
d.addNote(f)
d.reset()
assert d.sched.counts() == (1, 0, 0)
@ -832,10 +867,11 @@ def test_counts_idx():
d.sched.answerCard(c, 1)
assert d.sched.counts() == (0, 2, 0)
def test_repCounts():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
d.reset()
# lrnReps should be accurate on pass/fail
@ -853,7 +889,7 @@ def test_repCounts():
d.sched.answerCard(d.sched.getCard(), 2)
assert d.sched.counts() == (0, 0, 0)
f = d.newNote()
f['Front'] = "two"
f["Front"] = "two"
d.addNote(f)
d.reset()
# initial pass should be correct too
@ -865,14 +901,14 @@ def test_repCounts():
assert d.sched.counts() == (0, 0, 0)
# immediate graduate should work
f = d.newNote()
f['Front'] = "three"
f["Front"] = "three"
d.addNote(f)
d.reset()
d.sched.answerCard(d.sched.getCard(), 3)
assert d.sched.counts() == (0, 0, 0)
# and failing a review should too
f = d.newNote()
f['Front'] = "three"
f["Front"] = "three"
d.addNote(f)
c = f.cards()[0]
c.type = 2
@ -884,12 +920,13 @@ def test_repCounts():
d.sched.answerCard(d.sched.getCard(), 1)
assert d.sched.counts() == (0, 1, 0)
def test_timing():
d = getEmptyCol()
# add a few review cards, due today
for i in range(5):
f = d.newNote()
f['Front'] = "num"+str(i)
f["Front"] = "num" + str(i)
d.addNote(f)
c = f.cards()[0]
c.type = 2
@ -900,7 +937,7 @@ def test_timing():
d.reset()
c = d.sched.getCard()
# 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)
# the next card should be another review
c = d.sched.getCard()
@ -910,11 +947,12 @@ def test_timing():
c = d.sched.getCard()
assert c.queue == 1
def test_collapse():
d = getEmptyCol()
# add a note
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
d.reset()
# test collapsing
@ -924,16 +962,17 @@ def test_collapse():
d.sched.answerCard(c, 3)
assert not d.sched.getCard()
def test_deckDue():
d = getEmptyCol()
# add a note with default deck
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
# and one that's a child
f = d.newNote()
f['Front'] = "two"
default1 = f.model()['did'] = d.decks.id("Default::1")
f["Front"] = "two"
default1 = f.model()["did"] = d.decks.id("Default::1")
d.addNote(f)
# make it a review card
c = f.cards()[0]
@ -942,13 +981,13 @@ def test_deckDue():
c.flush()
# add one more with a new deck
f = d.newNote()
f['Front'] = "two"
foobar = f.model()['did'] = d.decks.id("foo::bar")
f["Front"] = "two"
foobar = f.model()["did"] = d.decks.id("foo::bar")
d.addNote(f)
# and one that's a sibling
f = d.newNote()
f['Front'] = "three"
foobaz = f.model()['did'] = d.decks.id("foo::baz")
f["Front"] = "three"
foobaz = f.model()["did"] = d.decks.id("foo::baz")
d.addNote(f)
d.reset()
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][4] == 0
# 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.deckDueTree()
def test_deckTree():
d = getEmptyCol()
d.decks.id("new::b::c")
@ -983,75 +1024,80 @@ def test_deckTree():
names.remove("new")
assert "new" not in names
def test_deckFlow():
d = getEmptyCol()
# add a note with default deck
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
# and one that's a child
f = d.newNote()
f['Front'] = "two"
default1 = f.model()['did'] = d.decks.id("Default::2")
f["Front"] = "two"
default1 = f.model()["did"] = d.decks.id("Default::2")
d.addNote(f)
# and another that's higher up
f = d.newNote()
f['Front'] = "three"
default1 = f.model()['did'] = d.decks.id("Default::1")
f["Front"] = "three"
default1 = f.model()["did"] = d.decks.id("Default::1")
d.addNote(f)
# should get top level one first, then ::1, then ::2
d.reset()
assert d.sched.counts() == (3,0,0)
assert d.sched.counts() == (3, 0, 0)
for i in "one", "three", "two":
c = d.sched.getCard()
assert c.note()['Front'] == i
assert c.note()["Front"] == i
d.sched.answerCard(c, 2)
def test_reorder():
d = getEmptyCol()
# add a note with default deck
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
f2 = d.newNote()
f2['Front'] = "two"
f2["Front"] = "two"
d.addNote(f2)
assert f2.cards()[0].due == 2
found=False
found = False
# 50/50 chance of being reordered
for i in range(20):
d.sched.randomizeCards(1)
if f.cards()[0].due != f.id:
found=True
found = True
break
assert found
d.sched.orderCards(1)
assert f.cards()[0].due == 1
# shifting
f3 = d.newNote()
f3['Front'] = "three"
f3["Front"] = "three"
d.addNote(f3)
f4 = d.newNote()
f4['Front'] = "four"
f4["Front"] = "four"
d.addNote(f4)
assert f.cards()[0].due == 1
assert f2.cards()[0].due == 2
assert f3.cards()[0].due == 3
assert f4.cards()[0].due == 4
d.sched.sortCards([
f3.cards()[0].id, f4.cards()[0].id], start=1, shift=True)
d.sched.sortCards([f3.cards()[0].id, f4.cards()[0].id], start=1, shift=True)
assert f.cards()[0].due == 3
assert f2.cards()[0].due == 4
assert f3.cards()[0].due == 1
assert f4.cards()[0].due == 2
def test_forget():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
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()
d.reset()
assert d.sched.counts() == (0, 0, 1)
@ -1059,10 +1105,11 @@ def test_forget():
d.reset()
assert d.sched.counts() == (1, 0, 0)
def test_resched():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
c = f.cards()[0]
d.sched.reschedCards([c.id], 0, 0)
@ -1072,14 +1119,15 @@ def test_resched():
assert c.queue == c.type == 2
d.sched.reschedCards([c.id], 1, 1)
c.load()
assert c.due == d.sched.today+1
assert c.due == d.sched.today + 1
assert c.ivl == +1
def test_norelearn():
d = getEmptyCol()
# add a note
f = d.newNote()
f['Front'] = "one"
f["Front"] = "one"
d.addNote(f)
c = f.cards()[0]
c.type = 2
@ -1093,13 +1141,15 @@ def test_norelearn():
c.flush()
d.reset()
d.sched.answerCard(c, 1)
d.sched._cardConf(c)['lapse']['delays'] = []
d.sched._cardConf(c)["lapse"]["delays"] = []
d.sched.answerCard(c, 1)
def test_failmult():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "one"; f['Back'] = "two"
f["Front"] = "one"
f["Back"] = "two"
d.addNote(f)
c = f.cards()[0]
c.type = 2
@ -1111,7 +1161,7 @@ def test_failmult():
c.lapses = 1
c.startTimer()
c.flush()
d.sched._cardConf(c)['lapse']['mult'] = 0.5
d.sched._cardConf(c)["lapse"]["mult"] = 0.5
c = d.sched.getCard()
d.sched.answerCard(c, 1)
assert c.ivl == 50

View file

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

View file

@ -1,12 +1,15 @@
# coding: utf-8
import os
from tests.shared import getEmptyCol
import os
import tempfile
from tests.shared import getEmptyCol
def test_stats():
d = getEmptyCol()
f = d.newNote()
f['Front'] = "foo"
f["Front"] = "foo"
d.addNote(f)
c = f.cards()[0]
# card stats
@ -17,15 +20,20 @@ def test_stats():
d.sched.answerCard(c, 2)
assert d.cardStats(c)
def test_graphs_empty():
d = getEmptyCol()
assert d.stats().report()
def test_graphs():
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()
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)
return

View file

@ -2,15 +2,18 @@ from anki.template import Template
def test_remove_formatting_from_mathjax():
t = Template('')
assert t._removeFormattingFromMathjax(r'\(2^{{c3::2}}\)', 3) == r'\(2^{{C3::2}}\)'
t = Template("")
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\) '
r'{{c4::blah}} {{c5::text with \(x^2\) jax}}')
txt = (
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
# formatting.
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) == (
r'\(a\) {{c1::b}} \[ {{C1::c}} \]')
r"\(a\) {{c1::b}} \[ {{C1::c}} \]"
)

View file

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

View file

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

View file

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

4
ts/package-lock.json generated
View file

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