Path: blob/develop/tests/unit/customizations/history/test_db.py
1569 views
# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.1#2# Licensed under the Apache License, Version 2.0 (the "License"). You3# may not use this file except in compliance with the License. A copy of4# the License is located at5#6# http://aws.amazon.com/apache2.0/7#8# or in the "license" file accompanying this file. This file is9# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF10# ANY KIND, either express or implied. See the License for the specific11# language governing permissions and limitations under the License.12import os13import re14import json15import threading16import datetime17import numbers1819from awscli.compat import queue20from awscli.customizations.history.db import DatabaseConnection21from awscli.customizations.history.db import DatabaseHistoryHandler22from awscli.customizations.history.db import DatabaseRecordWriter23from awscli.customizations.history.db import DatabaseRecordReader24from awscli.customizations.history.db import PayloadSerializer25from awscli.customizations.history.db import RecordBuilder26from awscli.testutils import mock, unittest, FileCreator27from tests import CaseInsensitiveDict282930class FakeDatabaseConnection(object):31def __init__(self):32self.execute = mock.MagicMock()33self.closed = False3435def close(self):36self.closed = True373839class TestGetHistoryDBFilename(unittest.TestCase):40def setUp(self):41self.files = FileCreator()4243def tearDown(self):44self.files.remove_all()454647class TestDatabaseConnection(unittest.TestCase):48@mock.patch('awscli.compat.sqlite3.connect')49def test_can_connect_to_argument_file(self, mock_connect):50expected_location = os.path.expanduser(os.path.join(51'~', 'foo', 'bar', 'baz.db'))52DatabaseConnection(expected_location)53mock_connect.assert_called_with(54expected_location, check_same_thread=False, isolation_level=None)5556@mock.patch('awscli.compat.sqlite3.connect')57def test_does_try_to_enable_wal(self, mock_connect):58conn = DatabaseConnection(':memory:')59conn._connection.execute.assert_any_call('PRAGMA journal_mode=WAL')6061def test_does_ensure_table_created_first(self):62db = DatabaseConnection(":memory:")63cursor = db.execute('PRAGMA table_info(records)')64schema = [col[:3] for col in cursor.fetchall()]65expected_schema = [66(0, 'id', 'TEXT'),67(1, 'request_id', 'TEXT'),68(2, 'source', 'TEXT'),69(3, 'event_type', 'TEXT'),70(4, 'timestamp', 'INTEGER'),71(5, 'payload', 'TEXT'),72]73self.assertEqual(expected_schema, schema)7475@mock.patch('awscli.compat.sqlite3.connect')76def test_can_close(self, mock_connect):77connection = mock.Mock()78mock_connect.return_value = connection79conn = DatabaseConnection(':memory:')80conn.close()81self.assertTrue(connection.close.called)828384class TestDatabaseHistoryHandler(unittest.TestCase):85UUID_PATTERN = re.compile(86'^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$',87re.I88)8990def test_emit_does_write_cli_rc_record(self):91writer = mock.Mock(DatabaseRecordWriter)92record_builder = RecordBuilder()93handler = DatabaseHistoryHandler(writer, record_builder)94handler.emit('CLI_RC', 0, 'CLI')95call = writer.write_record.call_args[0][0]96self.assertEqual(call, {97'command_id': mock.ANY,98'event_type': 'CLI_RC',99'payload': 0,100'source': 'CLI',101'timestamp': mock.ANY102})103self.assertTrue(self.UUID_PATTERN.match(call['command_id']))104self.assertIsInstance(call['timestamp'], numbers.Number)105106def test_emit_does_write_cli_version_record(self):107writer = mock.Mock(DatabaseRecordWriter)108record_builder = RecordBuilder()109handler = DatabaseHistoryHandler(writer, record_builder)110handler.emit('CLI_VERSION', 'Version Info', 'CLI')111call = writer.write_record.call_args[0][0]112self.assertEqual(call, {113'command_id': mock.ANY,114'event_type': 'CLI_VERSION',115'payload': 'Version Info',116'source': 'CLI',117'timestamp': mock.ANY118})119self.assertTrue(self.UUID_PATTERN.match(call['command_id']))120self.assertIsInstance(call['timestamp'], numbers.Number)121122def test_emit_does_write_api_call_record(self):123writer = mock.Mock(DatabaseRecordWriter)124record_builder = RecordBuilder()125handler = DatabaseHistoryHandler(writer, record_builder)126payload = {'foo': 'bar'}127handler.emit('API_CALL', payload, 'BOTOCORE')128call = writer.write_record.call_args[0][0]129self.assertEqual(call, {130'command_id': mock.ANY,131'request_id': mock.ANY,132'event_type': 'API_CALL',133'payload': payload,134'source': 'BOTOCORE',135'timestamp': mock.ANY136})137self.assertTrue(self.UUID_PATTERN.match(call['command_id']))138self.assertTrue(self.UUID_PATTERN.match(call['request_id']))139140def test_emit_does_write_http_request_record(self):141writer = mock.Mock(DatabaseRecordWriter)142record_builder = RecordBuilder()143handler = DatabaseHistoryHandler(writer, record_builder)144payload = {'body': b'data'}145# In order for an http_request to have a request_id it must have been146# preceeded by an api_call record.147handler.emit('API_CALL', '', 'BOTOCORE')148handler.emit('HTTP_REQUEST', payload, 'BOTOCORE')149call = writer.write_record.call_args[0][0]150self.assertEqual(call, {151'command_id': mock.ANY,152'request_id': mock.ANY,153'event_type': 'HTTP_REQUEST',154'payload': payload,155'source': 'BOTOCORE',156'timestamp': mock.ANY157})158self.assertTrue(self.UUID_PATTERN.match(call['command_id']))159self.assertTrue(self.UUID_PATTERN.match(call['request_id']))160161def test_emit_does_write_http_response_record(self):162writer = mock.Mock(DatabaseRecordWriter)163record_builder = RecordBuilder()164handler = DatabaseHistoryHandler(writer, record_builder)165payload = {'body': b'data'}166# In order for an http_response to have a request_id it must have been167# preceeded by an api_call record.168handler.emit('API_CALL', '', 'BOTOCORE')169handler.emit('HTTP_RESPONSE', payload, 'BOTOCORE')170call = writer.write_record.call_args[0][0]171self.assertEqual(call, {172'command_id': mock.ANY,173'request_id': mock.ANY,174'event_type': 'HTTP_RESPONSE',175'payload': payload,176'source': 'BOTOCORE',177'timestamp': mock.ANY178})179self.assertTrue(self.UUID_PATTERN.match(call['command_id']))180self.assertTrue(self.UUID_PATTERN.match(call['request_id']))181182def test_emit_does_write_parsed_response_record(self):183writer = mock.Mock(DatabaseRecordWriter)184record_builder = RecordBuilder()185handler = DatabaseHistoryHandler(writer, record_builder)186payload = {'metadata': {'data': 'foobar'}}187# In order for an http_response to have a request_id it must have been188# preceeded by an api_call record.189handler.emit('API_CALL', '', 'BOTOCORE')190handler.emit('PARSED_RESPONSE', payload, 'BOTOCORE')191call = writer.write_record.call_args[0][0]192self.assertEqual(call, {193'command_id': mock.ANY,194'request_id': mock.ANY,195'event_type': 'PARSED_RESPONSE',196'payload': payload,197'source': 'BOTOCORE',198'timestamp': mock.ANY199})200self.assertTrue(self.UUID_PATTERN.match(call['command_id']))201self.assertTrue(self.UUID_PATTERN.match(call['request_id']))202203204class BaseDatabaseRecordTester(unittest.TestCase):205def assert_contains_lines_in_order(self, lines, contents):206for line in lines:207self.assertIn(line, contents)208beginning = contents.find(line)209contents = contents[(beginning + len(line)):]210211212class BaseDatabaseRecordWriterTester(BaseDatabaseRecordTester):213UUID_PATTERN = re.compile(214'^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$',215re.I216)217218def setUp(self):219self.db = DatabaseConnection(':memory:')220self.writer = DatabaseRecordWriter(self.db)221222223class TestDatabaseRecordWriter(BaseDatabaseRecordWriterTester):224def setUp(self):225super(TestDatabaseRecordWriter, self).setUp()226227def _read_last_record(self):228cursor = self.db.execute('SELECT * FROM records')229written_record = cursor.fetchone()230return written_record231232def test_can_close(self):233connection = mock.Mock()234writer = DatabaseRecordWriter(connection)235writer.close()236self.assertTrue(connection.close.called)237238def test_can_write_record(self):239self.writer.write_record({240'command_id': 'command',241'event_type': 'FOO',242'payload': 'bar',243'source': 'TEST',244'timestamp': 1234245})246247# Now that we have verified the order of the fields in the insert248# statement we can verify that the record values are in the correct249# order in the tuple.250# (command_id, request_id, source, event_type, timestamp, payload)251written_record = self._read_last_record()252self.assertEqual(written_record,253('command', None, 'TEST', 'FOO', 1234, '"bar"'))254255def test_commit_count_matches_write_count(self):256records_to_write = 10257for _ in range(records_to_write):258self.writer.write_record({259'command_id': 'command',260'event_type': 'foo',261'payload': '',262'source': 'TEST',263'timestamp': 1234264})265cursor = self.db.execute('SELECT COUNT(*) FROM records')266record_count = cursor.fetchone()[0]267268self.assertEqual(record_count, records_to_write)269270def test_can_write_cli_version_record(self):271self.writer.write_record({272'command_id': 'command',273'event_type': 'CLI_VERSION',274'payload': ('aws-cli/1.11.184 Python/3.6.2 Darwin/15.6.0 '275'botocore/1.7.42'),276'source': 'TEST',277'timestamp': 1234278})279written_record = self._read_last_record()280281self.assertEqual(282written_record,283('command', None, 'TEST', 'CLI_VERSION', 1234,284'"aws-cli/1.11.184 Python/3.6.2 Darwin/15.6.0 botocore/1.7.42"')285)286287def test_can_write_cli_arguments_record(self):288self.writer.write_record({289'command_id': 'command',290'event_type': 'CLI_ARGUMENTS',291'payload': ['s3', 'ls'],292'source': 'TEST',293'timestamp': 1234294})295296written_record = self._read_last_record()297self.assertEqual(298written_record,299('command', None, 'TEST', 'CLI_ARGUMENTS', 1234, '["s3", "ls"]')300)301302def test_can_write_api_call_record(self):303self.writer.write_record({304'command_id': 'command',305'event_type': 'API_CALL',306'payload': {307'service': 's3',308'operation': 'ListBuckets',309'params': {},310},311'source': 'TEST',312'timestamp': 1234313})314315written_record = self._read_last_record()316self.assertEqual(317written_record,318('command', None, 'TEST', 'API_CALL', 1234, json.dumps({319'service': 's3',320'operation': 'ListBuckets',321'params': {},322}))323)324325def test_can_write_http_request_record(self):326self.writer.write_record({327'command_id': 'command',328'event_type': 'HTTP_REQUEST',329'payload': {330'method': 'GET',331'headers': CaseInsensitiveDict({}),332'body': '...',333},334'source': 'TEST',335'timestamp': 1234336})337338written_record = self._read_last_record()339self.assertEqual(340written_record,341('command', None, 'TEST', 'HTTP_REQUEST', 1234, json.dumps({342'method': 'GET',343'headers': {},344'body': '...',345}))346)347348def test_can_write_http_response_record(self):349self.writer.write_record({350'command_id': 'command',351'event_type': 'HTTP_RESPONSE',352'payload': {353'streaming': False,354'headers': {},355'body': '...',356'status_code': 200,357'request_id': '1234abcd'358},359'source': 'TEST',360'timestamp': 1234361})362363written_record = self._read_last_record()364self.assertEqual(365written_record,366('command', None, 'TEST', 'HTTP_RESPONSE', 1234, json.dumps({367'streaming': False,368'headers': {},369'body': '...',370'status_code': 200,371'request_id': '1234abcd'372}))373)374375def test_can_write_parsed_response_record(self):376self.writer.write_record({377'command_id': 'command',378'event_type': 'PARSED_RESPONSE',379'payload': {},380'source': 'TEST',381'timestamp': 1234382})383384written_record = self._read_last_record()385self.assertEqual(386written_record,387('command', None, 'TEST', 'PARSED_RESPONSE', 1234, '{}')388)389390def test_can_write_cli_rc_record(self):391self.writer.write_record({392'command_id': 'command',393'event_type': 'CLI_RC',394'payload': 0,395'source': 'TEST',396'timestamp': 1234397})398399written_record = self._read_last_record()400self.assertEqual(401written_record,402('command', None, 'TEST', 'CLI_RC', 1234, '0')403)404405406class ThreadedRecordBuilder(object):407def __init__(self, tracker):408self._read_q = queue.Queue()409self._write_q = queue.Queue()410self._thread = threading.Thread(411target=self._threaded_request_tracker,412args=(tracker,))413414def _threaded_request_tracker(self, builder):415while True:416event_type = self._read_q.get()417if event_type is False:418return419payload = {'body': b''}420request_id = builder.build_record(event_type, payload, '')421self._write_q.put_nowait(request_id)422423def read_n_results(self, n):424records = [self._write_q.get() for _ in range(n)]425return records426427def request_id_for_event(self, event_type):428self._read_q.put_nowait(event_type)429430def start(self):431self._thread.start()432433def close(self):434self._read_q.put_nowait(False)435self._thread.join()436437438class TestMultithreadRequestId(unittest.TestCase):439def setUp(self):440self.builder = RecordBuilder()441self.threads = []442443def tearDown(self):444for t in self.threads:445t.close()446447def start_n_threads(self, n):448for _ in range(n):449t = ThreadedRecordBuilder(self.builder)450t.start()451self.threads.append(t)452453def test_each_thread_has_separate_request_id(self):454self.start_n_threads(2)455self.threads[0].request_id_for_event('API_CALL')456self.threads[0].request_id_for_event('HTTP_REQUEST')457self.threads[1].request_id_for_event('API_CALL')458self.threads[1].request_id_for_event('HTTP_REQUEST')459460a_records = self.threads[0].read_n_results(2)461b_records = self.threads[1].read_n_results(2)462463# Each thread should have its own set of request_ids so the request464# ids in each set of records should match, but should not match the465# request_ids from the other thread.466a_request_ids = [record['request_id'] for record in a_records]467self.assertEqual(len(a_request_ids), 2)468self.assertEqual(a_request_ids[0], a_request_ids[1])469thread_a_request_id = a_request_ids[0]470471b_request_ids = [record['request_id'] for record in b_records]472self.assertEqual(len(b_request_ids), 2)473self.assertEqual(b_request_ids[0], b_request_ids[1])474thread_b_request_id = b_request_ids[0]475476# Since the request_id is reset by the API_CALL record being written477# and thread b has now written an API_CALL record the request id has478# been reset once. To ensure this doesnt bleed over to other threads479# we will write another record in thread a and ensure that it's480# request_id matches the previous ones from thread a rather than then481# thread b request_id482self.threads[0].request_id_for_event('HTTP_RESPONSE')483self.threads[1].request_id_for_event('HTTP_RESPONSE')484a_record = self.threads[0].read_n_results(1)[0]485b_record = self.threads[1].read_n_results(1)[0]486self.assertEqual(a_record['request_id'], thread_a_request_id)487self.assertEqual(b_record['request_id'], thread_b_request_id)488489490class TestDatabaseRecordReader(BaseDatabaseRecordTester):491def setUp(self):492self.fake_connection = FakeDatabaseConnection()493self.reader = DatabaseRecordReader(self.fake_connection)494495def test_can_close(self):496self.reader.close()497self.assertTrue(self.fake_connection.closed)498499def test_row_factory_set(self):500self.assertEqual(self.fake_connection.row_factory,501self.reader._row_factory)502503def test_iter_latest_records_performs_correct_query(self):504expected_query = (505' SELECT * FROM records\n'506' WHERE id =\n'507' (SELECT id FROM records WHERE timestamp =\n'508' (SELECT max(timestamp) FROM records)) ORDER BY timestamp;'509)510[_ for _ in self.reader.iter_latest_records()]511self.assertEqual(512self.fake_connection.execute.call_args[0][0].strip(),513expected_query.strip())514515def test_iter_latest_records_does_iter_records(self):516records_to_get = [1, 2, 3]517self.fake_connection.execute.return_value.__iter__.return_value = iter(518records_to_get)519records = [r for r in self.reader.iter_latest_records()]520self.assertEqual(records, records_to_get)521522def test_iter_records_performs_correct_query(self):523expected_query = ('SELECT * from records where id = ? '524'ORDER BY timestamp')525[_ for _ in self.reader.iter_records('fake_id')]526self.assertEqual(527self.fake_connection.execute.call_args[0][0].strip(),528expected_query.strip())529530def test_iter_records_does_iter_records(self):531records_to_get = [1, 2, 3]532self.fake_connection.execute.return_value.__iter__.return_value = iter(533records_to_get)534records = [r for r in self.reader.iter_records('fake_id')]535self.assertEqual(records, records_to_get)536537538class TestPayloadSerialzier(unittest.TestCase):539def test_can_serialize_basic_types(self):540original = {541'string': 'foo',542'int': 4,543'list': [1, 2, 'bar'],544'dict': {545'sun': 'moon'546},547'float': 1.2548}549string_value = json.dumps(original, cls=PayloadSerializer)550reloaded = json.loads(string_value)551self.assertEqual(original, reloaded)552553def test_can_serialize_datetime(self):554now = datetime.datetime.now()555iso_now = now.isoformat()556string_value = json.dumps(now, cls=PayloadSerializer)557reloaded = json.loads(string_value)558self.assertEqual(iso_now, reloaded)559560def test_can_serialize_case_insensitive_dict(self):561original = CaseInsensitiveDict({562'fOo': 'bar'563})564string_value = json.dumps(original, cls=PayloadSerializer)565reloaded = json.loads(string_value)566self.assertEqual(original, reloaded)567568def test_can_serialize_unknown_type(self):569original = queue.Queue()570encoded = repr(original)571string_value = json.dumps(original, cls=PayloadSerializer)572reloaded = json.loads(string_value)573self.assertEqual(encoded, reloaded)574575def test_can_serialize_non_utf_8_bytes_type(self):576original = b'\xfe\xed' # Non utf-8 byte squence577encoded = '<Byte sequence>'578string_value = json.dumps(original, cls=PayloadSerializer)579reloaded = json.loads(string_value)580self.assertEqual(encoded, reloaded)581582def test_does_preserve_utf_8_bytes_type(self):583original = b'foobar' # utf-8 byte squence584encoded = 'foobar'585string_value = json.dumps(original, cls=PayloadSerializer)586reloaded = json.loads(string_value)587self.assertEqual(encoded, reloaded)588589def test_can_serialize_non_utf_8_bytes_type_in_dict(self):590original = {'foo': 'bar', 'bytes': b'\xfe\xed'}591encoded = {'foo': 'bar', 'bytes': '<Byte sequence>'}592string_value = json.dumps(original, cls=PayloadSerializer)593reloaded = json.loads(string_value)594self.assertEqual(encoded, reloaded)595596def test_can_serialize_non_utf_8_bytes_type_in_list(self):597original = ['foo', b'\xfe\xed']598encoded = ['foo', '<Byte sequence>']599string_value = json.dumps(original, cls=PayloadSerializer)600reloaded = json.loads(string_value)601self.assertEqual(encoded, reloaded)602603def test_can_serialize_non_utf_8_bytes_type_in_tuple(self):604original = ('foo', b'\xfe\xed')605encoded = ['foo', '<Byte sequence>']606string_value = json.dumps(original, cls=PayloadSerializer)607reloaded = json.loads(string_value)608self.assertEqual(encoded, reloaded)609610def test_can_serialize_non_utf_8_bytes_nested(self):611original = {612'foo': 'bar',613'bytes': b'\xfe\xed',614'list': ['foo', b'\xfe\xed'],615'more_nesting': {616'bytes': b'\xfe\xed',617'tuple': ('bar', 'baz', b'\xfe\ed')618}619}620encoded = {621'foo': 'bar',622'bytes': '<Byte sequence>',623'list': ['foo', '<Byte sequence>'],624'more_nesting': {625'bytes': '<Byte sequence>',626'tuple': ['bar', 'baz', '<Byte sequence>']627}628}629string_value = json.dumps(original, cls=PayloadSerializer)630reloaded = json.loads(string_value)631self.assertEqual(encoded, reloaded)632633634class TestRecordBuilder(unittest.TestCase):635UUID_PATTERN = re.compile(636'^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$',637re.I638)639640def setUp(self):641self.builder = RecordBuilder()642643def _get_request_id_for_event_type(self, event_type):644record = self.builder.build_record(event_type, {'body': b''}, '')645return record.get('request_id')646647def test_does_inject_timestamp(self):648record = self.builder.build_record('TEST', '', '')649self.assertTrue('timestamp' in record)650self.assertTrue(isinstance(record['timestamp'], numbers.Number))651652def test_does_inject_command_id(self):653record = self.builder.build_record('TEST', '', '')654self.assertTrue('timestamp' in record)655self.assertTrue(isinstance(record['timestamp'], numbers.Number))656self.assertTrue('command_id' in record)657self.assertTrue(self.UUID_PATTERN.match(record['command_id']))658659def test_does_create_record_with_correct_fields(self):660record = self.builder.build_record('type', 'payload', 'source')661self.assertEqual(record['event_type'], 'type')662self.assertEqual(record['payload'], 'payload')663self.assertEqual(record['source'], 'source')664self.assertTrue('command_id' in record)665self.assertTrue('timestamp' in record)666667def test_can_process_http_request_with_none_body(self):668try:669self.builder.build_record('HTTP_REQUEST', {'body': None}, '')670except ValueError:671self.fail("Should not raise value error")672673def test_can_process_http_response_with_nono_body(self):674try:675self.builder.build_record('HTTP_RESPONSE', {'body': None}, '')676except ValueError:677self.fail("Should not raise value error")678679def test_can_get_request_id_from_api_call(self):680identifier = self._get_request_id_for_event_type('API_CALL')681self.assertTrue(self.UUID_PATTERN.match(identifier))682683def test_does_get_id_for_http_request_with_api_call(self):684call_identifier = self._get_request_id_for_event_type('API_CALL')685request_identifier = self._get_request_id_for_event_type(686'HTTP_REQUEST')687688self.assertEqual(call_identifier, request_identifier)689self.assertTrue(self.UUID_PATTERN.match(call_identifier))690691def test_does_get_id_for_http_response_with_api_call(self):692call_identifier = self._get_request_id_for_event_type('API_CALL')693response_identifier = self._get_request_id_for_event_type(694'HTTP_RESPONSE')695696self.assertEqual(call_identifier, response_identifier)697self.assertTrue(self.UUID_PATTERN.match(call_identifier))698699def test_does_get_id_for_parsed_response_with_api_call(self):700call_identifier = self._get_request_id_for_event_type('API_CALL')701response_identifier = self._get_request_id_for_event_type(702'PARSED_RESPONSE')703704self.assertEqual(call_identifier, response_identifier)705self.assertTrue(self.UUID_PATTERN.match(call_identifier))706707def test_does_not_get_id_for_http_request_without_api_call(self):708identifier = self._get_request_id_for_event_type('HTTP_REQUEST')709self.assertIsNone(identifier)710711def test_does_not_get_id_for_http_response_without_api_call(self):712identifier = self._get_request_id_for_event_type('HTTP_RESPONSE')713self.assertIsNone(identifier)714715def test_does_not_get_id_for_parsed_response_without_api_call(self):716identifier = self._get_request_id_for_event_type('PARSED_RESPONSE')717self.assertIsNone(identifier)718719720class TestIdentifierLifecycles(unittest.TestCase):721def setUp(self):722self.builder = RecordBuilder()723724def _get_multiple_request_ids(self, events):725fake_payload = {'body': b''}726request_ids = [727self.builder.build_record(728event,729fake_payload.copy(),730''731)['request_id']732for event in events733]734return request_ids735736def test_multiple_http_lifecycle_writes_have_same_request_id(self):737request_ids = self._get_multiple_request_ids(738['API_CALL', 'HTTP_REQUEST', 'HTTP_RESPONSE', 'PARSED_RESPONSE']739)740# All request_ids should match since this is one request lifecycle741unique_request_ids = set(request_ids)742self.assertEqual(len(unique_request_ids), 1)743744def test_request_id_reset_on_api_call(self):745request_ids = self._get_multiple_request_ids(746['API_CALL', 'HTTP_REQUEST', 'HTTP_RESPONSE', 'PARSED_RESPONSE',747'API_CALL', 'HTTP_REQUEST', 'HTTP_RESPONSE', 'PARSED_RESPONSE',748'API_CALL', 'HTTP_REQUEST', 'HTTP_RESPONSE', 'PARSED_RESPONSE']749)750751# There should be three distinct requet_ids since there are three752# distinct calls that end with a parsed response.753unique_request_ids = set(request_ids)754self.assertEqual(len(unique_request_ids), 3)755756# Check that the request ids match the correct events.757first_request_ids = request_ids[:4]758unique_first_request_ids = set(first_request_ids)759self.assertEqual(len(unique_first_request_ids), 1)760761second_request_ids = request_ids[4:8]762unique_second_request_ids = set(second_request_ids)763self.assertEqual(len(unique_second_request_ids), 1)764765third_request_ids = request_ids[8:]766unique_third_request_ids = set(third_request_ids)767self.assertEqual(len(unique_third_request_ids), 1)768769770