veekun_pokedex/pokedex/tests/test_schema.py
Andrew Ekstedt 0094e9584c Modernize our use of py.test
This commit updates the tests to take advantage of some of py.test's
newer features.  Requires py.test 2.3 or newer.  Tested with 2.3.0 and
2.5.2.

Tests which were parametrized now use py.test's built-in
parametrization[1].

The session and lookup objects are now implemented as fixtures[2].
The media root is a fixture as well.  Fixtures are automatically passed
to any function that expects them.

Since the session is now created in one place, it is now possible to
provide an engine URI on the command line when running py.test.  Ditto
for the index directory.  (But the environment variables still work of
course.)

Slow tests are now marked as such and not run unless the --all option is
given.

A couple media tests are marked as xfail (expected to fail) because they
are broken.

[1]: http://pytest.org/latest/parametrize.html
[2]: http://pytest.org/latest/fixture.html
2014-07-06 21:45:05 -07:00

213 lines
7.2 KiB
Python

# encoding: utf8
import pytest
from sqlalchemy import Column, Integer, String, create_engine
from sqlalchemy.orm import class_mapper, joinedload, sessionmaker
from sqlalchemy.orm.session import Session
from sqlalchemy.ext.declarative import declarative_base
from pokedex.db import tables, markdown
from pokedex.db.multilang import MultilangScopedSession, MultilangSession, \
create_translation_table
parametrize = pytest.mark.parametrize
@parametrize('varname', [attr for attr in dir(tables) if not attr.startswith('_')])
def test_variable_names(varname):
"""We want pokedex.db.tables to export tables using the class name"""
table = getattr(tables, varname)
try:
if not issubclass(table, tables.TableBase) or table is tables.TableBase:
return
except TypeError:
return
classname = table.__name__
if classname and varname[0].isupper():
assert varname == classname, '%s refers to %s' % (varname, classname)
@parametrize('table', tables.mapped_classes)
def test_variable_names_2(table):
"""We also want all of the tables exported"""
assert getattr(tables, table.__name__) is table
def test_class_order():
"""The declarative classes should be defined in alphabetical order.
Except for Language which should be first.
"""
class_names = [table.__name__ for table in tables.mapped_classes]
def key(name):
return name != 'Language', name
print [(a,b) for (a,b) in zip(class_names, sorted(class_names, key=key)) if a!=b]
assert class_names == sorted(class_names, key=key)
def test_i18n_table_creation():
"""Creates and manipulates a magical i18n table, completely independent of
the existing schema and data. Makes sure that the expected behavior of the
various proxies and columns works.
"""
Base = declarative_base()
engine = create_engine("sqlite:///:memory:", echo=True)
Base.metadata.bind = engine
# Need this for the foreign keys to work!
class Language(Base):
__tablename__ = 'languages'
id = Column(Integer, primary_key=True, nullable=False)
identifier = Column(String(2), nullable=False, unique=True)
class Foo(Base):
__tablename__ = 'foos'
__singlename__ = 'foo'
id = Column(Integer, primary_key=True, nullable=False)
translation_classes = []
FooText = create_translation_table('foo_text', Foo, 'texts',
language_class=Language,
name = Column(String(100)),
)
# OK, create all the tables and gimme a session
Base.metadata.create_all()
sm = sessionmaker(class_=MultilangSession)
sess = MultilangScopedSession(sm)
# Create some languages and foos to bind together
lang_en = Language(identifier='en')
sess.add(lang_en)
lang_jp = Language(identifier='jp')
sess.add(lang_jp)
lang_ru = Language(identifier='ru')
sess.add(lang_ru)
foo = Foo()
sess.add(foo)
# Commit so the above get primary keys filled in, then give the
# session the language id
sess.commit()
# Note that this won't apply to sessions created in other threads, but that
# ought not be a problem!
sess.default_language_id = lang_en.id
# Give our foo some names, as directly as possible
foo_text = FooText()
foo_text.foreign_id = foo.id
foo_text.local_language_id = lang_en.id
foo_text.name = 'english'
sess.add(foo_text)
foo_text = FooText()
foo_text.foo_id = foo.id
foo_text.local_language_id = lang_jp.id
foo_text.name = 'nihongo'
sess.add(foo_text)
# Commit! This will expire all of the above.
sess.commit()
### Test 1: re-fetch foo and check its attributes
foo = sess.query(Foo).params(_default_language='en').one()
# Dictionary of language identifiers => names
assert foo.name_map[lang_en] == 'english'
assert foo.name_map[lang_jp] == 'nihongo'
# Default language, currently English
assert foo.name == 'english'
sess.expire_all()
### Test 2: querying by default language name should work
foo = sess.query(Foo).filter_by(name='english').one()
assert foo.name == 'english'
sess.expire_all()
### Test 3: joinedload on the default name should appear to work
# THIS SHOULD WORK SOMEDAY
# .options(joinedload(Foo.name)) \
foo = sess.query(Foo) \
.options(joinedload(Foo.texts_local)) \
.one()
assert foo.name == 'english'
sess.expire_all()
### Test 4: joinedload on all the names should appear to work
# THIS SHOULD ALSO WORK SOMEDAY
# .options(joinedload(Foo.name_map)) \
foo = sess.query(Foo) \
.options(joinedload(Foo.texts)) \
.one()
assert foo.name_map[lang_en] == 'english'
assert foo.name_map[lang_jp] == 'nihongo'
sess.expire_all()
### Test 5: Mutating the dict collection should work
foo = sess.query(Foo).one()
foo.name_map[lang_en] = 'different english'
foo.name_map[lang_ru] = 'new russian'
sess.commit()
assert foo.name_map[lang_en] == 'different english'
assert foo.name_map[lang_ru] == 'new russian'
classes = []
for cls in tables.mapped_classes:
classes.append(cls)
classes += cls.translation_classes
@parametrize('cls', classes)
def test_texts(cls):
"""Check DB schema for integrity of text columns & translations.
Mostly protects against copy/paste oversights and rebase hiccups.
If there's a reason to relax the tests, do it
"""
if hasattr(cls, 'local_language') or hasattr(cls, 'language'):
good_formats = 'markdown plaintext gametext'.split()
assert_text = '%s is language-specific'
else:
good_formats = 'identifier latex'.split()
assert_text = '%s is not language-specific'
columns = sorted(cls.__table__.c, key=lambda c: c.name)
text_columns = []
for column in columns:
format = column.info.get('format', None)
if format is not None:
if format not in good_formats:
pytest.fail(assert_text % column)
if (format != 'identifier') and (column.name == 'identifier'):
pytest.fail('%s: identifier column name/type mismatch' % column)
if column.info.get('official', None) and format not in 'gametext plaintext':
pytest.fail('%s: official text with bad format' % column)
text_columns.append(column)
else:
if isinstance(column.type, tables.Unicode):
pytest.fail('%s: text column without format' % column)
if column.name == 'name' and format != 'plaintext':
pytest.fail('%s: non-plaintext name' % column)
# No mention of English in the description
if column.doc and u'English' in column.doc:
pytest.fail("%s: description mentions English" % column)
# If there's more than one text column in a translation table,
# they have to be nullable, to support missing translations
if hasattr(cls, 'local_language') and len(text_columns) > 1:
for column in text_columns:
assert column.nullable
@parametrize('table', tables.mapped_classes)
def test_identifiers_with_names(table):
"""Test that named tables have identifiers
"""
for translation_class in table.translation_classes:
if hasattr(translation_class, 'name'):
assert hasattr(table, 'identifier'), table