"""Operation-count regressions; timings are recorded separately, never asserted.""" import json import threading import unittest from unittest.mock import patch from urllib.request import Request, urlopen from urllib.error import HTTPError import test_catalog_api as fixtures class CatalogPerformanceTests(unittest.TestCase): setUp = fixtures.CatalogTests.setUp tearDown = fixtures.CatalogTests.tearDown open = fixtures.CatalogTests.open def http_server(self, catalog): server = self.api.make_server(catalog, port=0) thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() def stop(): server.shutdown() server.server_close() thread.join(3) self.addCleanup(stop) return 'http://127.0.0.1:' + str(server.server_port) def test_repeated_snapshot_http_read_does_not_query_or_serialize_again(self): catalog = self.open() origin = self.http_server(catalog) with urlopen(origin + '/api/first/public/', timeout=3) as response: first = response.read() queries = [] catalog.db.set_trace_callback(queries.append) with patch.object(self.api.json, 'dumps', wraps=json.dumps) as encode: with urlopen(origin + '/api/first/public/', timeout=3) as response: self.assertEqual(response.read(), first) self.assertEqual(queries, [], 'Warm snapshot HTTP responses must reuse encoded bytes') self.assertEqual(encode.call_count, 0) def test_cache_bounds_lru_and_large_response_bypass(self): catalog = self.open() catalog.RESPONSE_CACHE_ENTRIES = 2 one = '/api/movie/by/1/public/' two = '/api/movie/by/2/public/' three = '/api/movie/by/3/public/' first = catalog.response(one) catalog.response(two) self.assertIs(catalog.response(one), first) catalog.response(three) self.assertEqual(list(catalog._responses), [one, three]) self.assertLessEqual(catalog._response_bytes, catalog.RESPONSE_CACHE_BYTES) catalog.RESPONSE_CACHE_ITEM_BYTES = 1 catalog.response(two) self.assertNotIn(two, catalog._responses) # Byte budget, not just number of entries, forces eviction. catalog.RESPONSE_CACHE_ITEM_BYTES = 1024 * 1024 catalog.RESPONSE_CACHE_BYTES = len(catalog.response(three)) + len(three) + 1 catalog.response(two) self.assertLessEqual(catalog._response_bytes, catalog.RESPONSE_CACHE_BYTES) self.assertEqual(len(catalog._responses), 1) def test_dynamic_responses_errors_and_queries_are_never_retained(self): catalog = self.open() with patch.object(catalog, 'health', side_effect=[{'age': 1}, {'age': 2}]): self.assertNotEqual(catalog.response('/api/health'), catalog.response('/api/health')) with patch.object(self.api, 'load_banners', side_effect=[{'banners': [1]}, {'banners': [2]}]): self.assertNotEqual(catalog.response('/api/banners'), catalog.response('/api/banners')) catalog.response('/api-user/newapi/like.php?action=search-movie&q=Alpha') for _ in range(2): with self.assertRaises(self.api.ApiError): catalog.response('/api/movie/by/999/public/') self.assertEqual(list(catalog._responses), []) def test_warm_head_reuses_get_bytes_and_body_requests_still_fail(self): catalog = self.open() origin = self.http_server(catalog) with urlopen(origin + '/api/first/public/', timeout=3) as response: first = response.read() with urlopen(Request(origin + '/api/first/public/', method='HEAD'), timeout=3) as response: self.assertEqual(response.read(), b'') self.assertEqual(int(response.headers['Content-Length']), len(first)) with self.assertRaises(HTTPError) as error: urlopen(Request(origin + '/api/first/public/', method='GET', data=b'x'), timeout=3) self.assertEqual(error.exception.code, 400) error.exception.close() def test_normalized_search_matches_fallback_with_unicode_and_pagination(self): catalog = self.open() indexed = catalog.search_text self.assertLessEqual(catalog.search_index_bytes, 64 * 1024 * 1024) for query in ['Actor', 'بازیگران', 'بازيگران', 'ALPHA', 'not-present', 'Film 4']: for page in [0, 1, 2]: catalog.search_text = indexed expected = catalog.titles(query=query, page=page) catalog.search_text = {} self.assertEqual(catalog.titles(query=query, page=page), expected) catalog.search_text = indexed def test_search_normalizes_query_not_every_snapshot_row(self): catalog = self.open() with patch.object(self.api, 'normal', wraps=self.api.normal) as normalize: catalog.db.create_function('normal', 1, normalize, deterministic=True) result = catalog.dispatch('/api-user/newapi/like.php?action=search-movie&q=ACTOR&pageno=1') self.assertEqual([row['id'] for row in result], list(range(62, 32, -1))) self.assertLessEqual(normalize.call_count, 2, 'Immutable title text must not be normalized on every search') if __name__ == '__main__': unittest.main()