blob: b69a9f49d4718599cf49798d236d64cd89ab8b92 [file] [log] [blame]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import pymongo
import mock
from unittest import TestCase
import allura
from allura.tests import decorators as td
from allura.model.session import (
IndexerSessionExtension,
BatchIndexer,
ArtifactSessionExtension,
substitute_extensions,
)
def test_extensions_cm():
session = mock.Mock(_kwargs=dict(extensions=[]))
extension = mock.Mock()
with substitute_extensions(session, [extension]) as sess:
assert session.flush.call_count == 1
assert session.close.call_count == 1
assert sess == session
assert sess._kwargs['extensions'] == [extension]
assert session.flush.call_count == 2
assert session.close.call_count == 2
assert session._kwargs['extensions'] == []
def test_extensions_cm_raises():
session = mock.Mock(_kwargs=dict(extensions=[]))
extension = mock.Mock()
with td.raises(ValueError):
with substitute_extensions(session, [extension]) as sess:
session.flush.side_effect = AttributeError
assert session.flush.call_count == 1
assert session.close.call_count == 1
assert sess == session
assert sess._kwargs['extensions'] == [extension]
raise ValueError('test')
assert session.flush.call_count == 1
assert session.close.call_count == 1
assert session._kwargs['extensions'] == []
def test_extensions_cm_flush_raises():
session = mock.Mock(_kwargs=dict(extensions=[]))
extension = mock.Mock()
with td.raises(AttributeError):
with substitute_extensions(session, [extension]) as sess:
session.flush.side_effect = AttributeError
assert session.flush.call_count == 1
assert session.close.call_count == 1
assert sess == session
assert sess._kwargs['extensions'] == [extension]
assert session.flush.call_count == 2
assert session.close.call_count == 1
assert session._kwargs['extensions'] == []
@with_nose_compatibility
class TestSessionExtension(TestCase):
def _mock_indexable(self, **kw):
m = mock.Mock(**kw)
m.__ming__ = mock.MagicMock()
m.index_id.return_value = id(m)
return m
@with_nose_compatibility
class TestIndexerSessionExtension(TestSessionExtension):
def setUp(self):
session = mock.Mock()
self.ExtensionClass = IndexerSessionExtension
self.extension = self.ExtensionClass(session)
self.tasks = {'add': mock.Mock(), 'del': mock.Mock()}
self.extension.TASKS = mock.Mock()
self.extension.TASKS.get.return_value = self.tasks
def test_flush(self):
added = [self._mock_indexable(_id=i) for i in (1, 2, 3)]
modified = [self._mock_indexable(_id=i) for i in (4, 5)]
deleted = [self._mock_indexable(_id=i) for i in (6, 7)]
self.extension.objects_added = added
self.extension.objects_modified = modified
self.extension.objects_deleted = deleted
self.extension.after_flush()
self.tasks['add'].post.assert_called_once_with([1, 2, 3, 4, 5])
self.tasks['del'].post.assert_called_once_with(list(map(id, deleted)))
def test_flush_skips_update(self):
modified = [self._mock_indexable(_id=i) for i in range(5)]
modified[1].should_update_index.return_value = False
modified[4].should_update_index.return_value = False
self.extension.objects_modified = modified
self.extension.after_flush()
self.tasks['add'].post.assert_called_once_with([0, 2, 3])
def test_flush_skips_task_if_all_objects_filtered_out(self):
modified = [self._mock_indexable(_id=i) for i in range(5)]
for m in modified:
m.should_update_index.return_value = False
self.extension.objects_modified = modified
self.extension.after_flush()
assert self.tasks['add'].post.call_count == 0
@with_nose_compatibility
class TestArtifactSessionExtension(TestSessionExtension):
def setUp(self):
session = mock.Mock(disable_index=False)
self.ExtensionClass = ArtifactSessionExtension
self.extension = self.ExtensionClass(session)
@mock.patch.object(allura.model.index.Shortlink, 'from_artifact')
@mock.patch.object(allura.model.index.ArtifactReference, 'from_artifact')
@mock.patch('allura.model.session.index_tasks')
def test_flush_skips_update(self, index_tasks, ref_fa, shortlink_fa):
modified = [self._mock_indexable(_id=i) for i in range(5)]
modified[1].should_update_index.return_value = False
modified[4].should_update_index.return_value = False
ref_fa.side_effect = lambda obj: mock.Mock(_id=obj._id)
self.extension.objects_modified = modified
self.extension.after_flush()
index_tasks.add_artifacts.post.assert_called_once_with([0, 2, 3])
@mock.patch('allura.model.session.index_tasks')
def test_flush_skips_task_if_all_objects_filtered_out(self, index_tasks):
modified = [self._mock_indexable(_id=i) for i in range(5)]
for m in modified:
m.should_update_index.return_value = False
self.extension.objects_modified = modified
self.extension.after_flush()
assert index_tasks.add_artifacts.post.call_count == 0
@with_nose_compatibility
class TestBatchIndexer(TestCase):
def setUp(self):
session = mock.Mock()
self.extcls = BatchIndexer
self.ext = self.extcls(session)
def _mock_indexable(self, **kw):
m = mock.Mock(**kw)
m.index_id.return_value = id(m)
return m
@mock.patch('allura.model.ArtifactReference.query.find')
def test_update_index(self, find):
m = self._mock_indexable
objs_deleted = [m(_id=i) for i in (1, 2, 3)]
arefs = [m(_id=i) for i in (4, 5, 6)]
find.return_value = [m(_id=i) for i in (7, 8, 9)]
self.ext.update_index(objs_deleted, arefs)
self.assertEqual(self.ext.to_delete,
{o.index_id() for o in objs_deleted})
self.assertEqual(self.ext.to_add, {4, 5, 6})
# test deleting something that was previously added
objs_deleted += [m(_id=4)]
find.return_value = [m(_id=4)]
self.ext.update_index(objs_deleted, [])
self.assertEqual(self.ext.to_delete,
{o.index_id() for o in objs_deleted})
self.assertEqual(self.ext.to_add, {5, 6})
@mock.patch('allura.model.session.index_tasks')
def test_flush(self, index_tasks):
objs_deleted = [self._mock_indexable(_id=i) for i in (1, 2, 3)]
del_index_ids = {o.index_id() for o in objs_deleted}
self.extcls.to_delete = del_index_ids
self.extcls.to_add = {4, 5, 6}
self.ext.flush()
index_tasks.del_artifacts.post.assert_called_once_with(
list(del_index_ids))
index_tasks.add_artifacts.post.assert_called_once_with([4, 5, 6])
self.assertEqual(self.ext.to_delete, set())
self.assertEqual(self.ext.to_add, set())
@mock.patch('allura.model.session.index_tasks')
def test_flush_chunks_huge_lists(self, index_tasks):
self.extcls.to_delete = set(range(100 * 1000 + 1))
self.extcls.to_add = set(range(1000 * 1000 + 1))
self.ext.flush()
self.assertEqual(
len(index_tasks.del_artifacts.post.call_args_list[0][0][0]),
100 * 1000)
self.assertEqual(
len(index_tasks.del_artifacts.post.call_args_list[1][0][0]), 1)
self.assertEqual(
len(index_tasks.add_artifacts.post.call_args_list[0][0][0]),
1000 * 1000)
self.assertEqual(
len(index_tasks.add_artifacts.post.call_args_list[1][0][0]), 1)
self.assertEqual(self.ext.to_delete, set())
self.assertEqual(self.ext.to_add, set())
@mock.patch('allura.tasks.index_tasks')
def test_flush_noop(self, index_tasks):
self.ext.flush()
self.assertEqual(0, index_tasks.del_artifacts.post.call_count)
self.assertEqual(0, index_tasks.add_artifacts.post.call_count)
self.assertEqual(self.ext.to_delete, set())
self.assertEqual(self.ext.to_add, set())
@mock.patch('allura.tasks.index_tasks')
def test__post_too_large(self, index_tasks):
def on_post(chunk):
if len(chunk) > 1:
e = pymongo.errors.InvalidDocument(
"BSON document too large (16906035 bytes) - the connected server supports BSON document sizes up to 16777216 bytes.")
# ming injects a 2nd arg with the document, so we do too
e.args = e.args + ("doc: {'task_name': 'allura.tasks.index_tasks.add_artifacts', ........",)
raise e
index_tasks.add_artifacts.post.side_effect = on_post
self.ext._post(index_tasks.add_artifacts, list(range(5)))
expected = [
mock.call([0, 1, 2, 3, 4]),
mock.call([0, 1]),
mock.call([0]),
mock.call([1]),
mock.call([2, 3, 4]),
mock.call([2]),
mock.call([3, 4]),
mock.call([3]),
mock.call([4])
]
self.assertEqual(
expected, index_tasks.add_artifacts.post.call_args_list)
@mock.patch('allura.tasks.index_tasks')
def test__post_other_error(self, index_tasks):
def on_post(chunk):
raise pymongo.errors.InvalidDocument("Cannot encode object...")
index_tasks.add_artifacts.post.side_effect = on_post
with td.raises(pymongo.errors.InvalidDocument):
self.ext._post(index_tasks.add_artifacts, list(range(5)))