# -*- coding: utf-8 -*- import mock import flask import pytest import webtest import sqlalchemy as sa from flask_sqlalchemy import SQLAlchemy from nplusone.core import exceptions from nplusone.ext.flask_sqlalchemy import NPlusOne from nplusone.ext.flask_sqlalchemy import setup_state from tests import utils @pytest.fixture(scope='module', autouse=True) def setup(): setup_state() @pytest.fixture def db(): return SQLAlchemy() @pytest.fixture def models(db): return utils.make_models(db.Model) @pytest.fixture() def objects(db, app, models): hobby = models.Hobby() address = models.Address() user = models.User(addresses=[address], hobbies=[hobby]) db.session.add(user) db.session.commit() db.session.close() @pytest.fixture def logger(): return mock.Mock() @pytest.fixture def app(db, models, logger): app = flask.Flask(__name__) app.config['TESTING'] = True app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['NPLUSONE_LOGGER'] = logger db.init_app(app) with app.app_context(): db.create_all() yield app @pytest.fixture def wrapper(app): return NPlusOne(app) @pytest.fixture def routes(app, models, wrapper): @app.route('/many_to_one/') def many_to_one(): users = models.User.query.all() return str(users[0].addresses) @app.route('/many_to_one_one/') def many_to_one_one(): user = models.User.query.filter_by(id=1).one() return str(user.addresses) @app.route('/many_to_one_first/') def many_to_one_first(): user = models.User.query.first() return str(user.addresses) @app.route('/many_to_one_ignore/') def many_to_one_ignore(): with wrapper.ignore('lazy_load'): users = models.User.query.all() return str(users[0].addresses) @app.route('/many_to_many/') def many_to_many(): users = models.User.query.all() return str(users[0].hobbies) @app.route('/many_to_many_impossible/') def many_to_many_impossible(): user = models.User.query.first() users = models.User.query.all() # noqa return str(user.hobbies) @app.route('/many_to_many_impossible_one/') def many_to_many_impossible_one(): user = models.User.query.one() users = models.User.query.all() # noqa return str(user.hobbies) @app.route('/eager_join/') def eager_join(): users = models.User.query.options(sa.orm.subqueryload('hobbies')).all() return str(users[0].hobbies if users else None) @app.route('/eager_subquery/') def eager_subquery(): users = models.User.query.options(sa.orm.subqueryload('hobbies')).all() # Touch class-level descriptor to exercise `None` instance checks print(models.User.hobbies) return str(users[0].hobbies if users else None) @app.route('/eager_join_unused/') def eager_join_unused(): users = models.User.query.options(sa.orm.joinedload('hobbies')).all() return str(users[0]) @app.route('/eager_subquery_unused/') def eager_subquery_unused(): users = models.User.query.options(sa.orm.subqueryload('hobbies')).all() return str(users[0]) @app.route('/eager_nested/') def eager_nested(): hobbies = models.Hobby.query.options( sa.orm.joinedload(models.Hobby.users).joinedload( models.User.addresses, ) ).all() return str(hobbies[0].users[0].addresses) @app.route('/eager_nested_unused/') def eager_nested_unused(): hobbies = models.Hobby.query.options( sa.orm.joinedload(models.Hobby.users).joinedload( models.User.addresses, ) ).all() return str(hobbies[0]) @pytest.fixture def client(app, routes, wrapper): return webtest.TestApp(app) class TestNPlusOne: def test_many_to_one(self, objects, client, logger): client.get('/many_to_one/') assert len(logger.log.call_args_list) == 1 args = logger.log.call_args[0] assert 'User.addresses' in args[1] def test_many_to_one_one(self, objects, client, logger): client.get('/many_to_one_one/') assert not logger.log.called def test_many_to_one_first(self, objects, client, logger): client.get('/many_to_one_first/') assert not logger.log.called def test_many_to_one_ignore(self, objects, client, logger): client.get('/many_to_one_ignore/') assert not logger.log.called def test_many_to_many(self, objects, client, logger): client.get('/many_to_many/') assert len(logger.log.call_args_list) == 1 args = logger.log.call_args[0] assert 'User.hobbies' in args[1] def test_many_to_many_impossible(self, objects, client, logger): client.get('/many_to_many_impossible/') assert not logger.log.called def test_many_to_many_impossible_one(self, objects, client, logger): client.get('/many_to_many_impossible_one/') assert not logger.log.called def test_eager_join(self, objects, client, logger): client.get('/eager_join/') assert not logger.log.called def test_eager_subquery(self, objects, client, logger): client.get('/eager_subquery/') assert not logger.log.called def test_eager_join_empty(self, models, objects, client, logger): models.User.query.delete() client.get('/eager_join/') assert not logger.log.called def test_eager_subquery_empty(self, models, objects, client, logger): models.User.query.delete() client.get('/eager_subquery/') assert not logger.log.called def test_eager_join_unused(self, objects, client, logger): client.get('/eager_join_unused/') assert len(logger.log.call_args_list) == 1 args = logger.log.call_args[0] assert 'User.hobbies' in args[1] def test_eager_subquery_unused(self, objects, client, logger): client.get('/eager_subquery_unused/') assert len(logger.log.call_args_list) == 1 args = logger.log.call_args[0] assert 'User.hobbies' in args[1] def test_eager_nested_unused(self, app, wrapper, objects, client, logger): client.get('/eager_nested/') assert not logger.log.called def test_eager_nested(self, app, wrapper, objects, client, logger): client.get('/eager_nested_unused/') assert len(logger.log.call_args_list) == 2 calls = [call[0] for call in logger.log.call_args_list] assert any('Hobby.users' in call[1] for call in calls) assert any('User.addresses' in call[1] for call in calls) def test_many_to_many_raise(self, app, wrapper, objects, client, logger): app.config['NPLUSONE_RAISE'] = True with pytest.raises(exceptions.NPlusOneError): client.get('/many_to_many/') def test_many_to_many_whitelist(self, app, wrapper, objects, client, logger): app.config['NPLUSONE_WHITELIST'] = [{'model': 'User'}] client.get('/many_to_many/') assert not logger.log.called def test_many_to_many_whitelist_wildcard(self, app, wrapper, objects, client, logger): app.config['NPLUSONE_WHITELIST'] = [{'model': 'U*r'}] client.get('/many_to_many/') assert not logger.log.called def test_many_to_many_whitelist_decoy(self, app, wrapper, objects, client, logger): app.config['NPLUSONE_WHITELIST'] = [{'model': 'Hobby'}] client.get('/many_to_many/') assert logger.log.called