github.com/web-platform-tests/wpt.fyi@v0.0.0-20240530210107-70cf978996f1/results-processor/processor_test.py (about) 1 # Copyright 2019 The WPT Dashboard Project. All rights reserved. 2 # Use of this source code is governed by a BSD-style license that can be 3 # found in the LICENSE file. 4 5 import json 6 import unittest 7 from unittest.mock import call, patch 8 9 from werkzeug.datastructures import MultiDict 10 11 import test_util 12 import wptreport 13 from processor import Processor, process_report 14 from test_server import AUTH_CREDENTIALS 15 16 17 class ProcessorTest(unittest.TestCase): 18 def fake_download(self, expected_path, response): 19 def _download(path): 20 if expected_path is None: 21 self.fail('Unexpected download:' + path) 22 self.assertEqual(expected_path, path) 23 return response 24 return _download 25 26 def test_known_extension(self): 27 self.assertEqual( 28 Processor.known_extension('https://wpt.fyi/test.json.gz'), 29 '.json.gz') 30 self.assertEqual( 31 Processor.known_extension('https://wpt.fyi/test.txt.gz'), 32 '.txt.gz') 33 self.assertEqual( 34 Processor.known_extension('https://wpt.fyi/test.json'), '.json') 35 self.assertEqual( 36 Processor.known_extension('https://wpt.fyi/test.txt'), '.txt') 37 self.assertEqual( 38 Processor.known_extension('artifact.zip'), '.zip') 39 40 def test_download(self): 41 with Processor() as p: 42 p._download_gcs = self.fake_download( 43 'gs://wptd/foo/bar.json', '/fake/bar.json') 44 p._download_http = self.fake_download( 45 'https://wpt.fyi/test.txt.gz', '/fake/test.txt.gz') 46 47 p.download( 48 ['gs://wptd/foo/bar.json'], 49 ['https://wpt.fyi/test.txt.gz'], 50 None) 51 self.assertListEqual(p.results, ['/fake/bar.json']) 52 self.assertListEqual(p.screenshots, ['/fake/test.txt.gz']) 53 54 def test_download_azure(self): 55 with Processor() as p: 56 p._download_gcs = self.fake_download(None, None) 57 p._download_http = self.fake_download( 58 'https://wpt.fyi/artifact.zip', 'artifact_test.zip') 59 60 p.download([], [], 'https://wpt.fyi/artifact.zip') 61 self.assertEqual(len(p.results), 2) 62 self.assertTrue(p.results[0].endswith( 63 '/artifact_test/wpt_report_1.json')) 64 self.assertTrue(p.results[1].endswith( 65 '/artifact_test/wpt_report_2.json')) 66 self.assertEqual(len(p.screenshots), 2) 67 self.assertTrue(p.screenshots[0].endswith( 68 '/artifact_test/wpt_screenshot_1.txt')) 69 self.assertTrue(p.screenshots[1].endswith( 70 '/artifact_test/wpt_screenshot_2.txt')) 71 72 def test_download_azure_errors(self): 73 with Processor() as p: 74 p._download_gcs = self.fake_download(None, None) 75 p._download_http = self.fake_download( 76 'https://wpt.fyi/artifact.zip', None) 77 78 # Incorrect param combinations (both results & azure_url): 79 with self.assertRaises(AssertionError): 80 p.download(['https://wpt.fyi/test.json.gz'], 81 [], 82 'https://wpt.fyi/artifact.zip') 83 84 # Download failure: no exceptions should be raised. 85 p.download([], [], 'https://wpt.fyi/artifact.zip') 86 self.assertEqual(len(p.results), 0) 87 88 89 class MockProcessorTest(unittest.TestCase): 90 @patch('processor.Processor') 91 def test_params_plumbing_success(self, MockProcessor): 92 # Set up mock context manager to return self. 93 mock = MockProcessor.return_value 94 mock.__enter__.return_value = mock 95 mock.check_existing_run.return_value = False 96 mock.results = ['/tmp/wpt_report.json.gz'] 97 mock.raw_results_url = 'https://wpt.fyi/test/report.json' 98 mock.results_url = 'https://wpt.fyi/test' 99 mock.test_run_id = 654321 100 101 # NOTE: if you need to change the following params, you probably also 102 # want to change api/receiver/api.go. 103 params = MultiDict({ 104 'uploader': 'blade-runner', 105 'id': '654321', 106 'callback_url': 'https://test.wpt.fyi/api', 107 'labels': 'foo,bar', 108 'results': 'https://wpt.fyi/wpt_report.json.gz', 109 'browser_name': 'Chrome', 110 'browser_version': '70', 111 'os_name': 'Linux', 112 'os_version': '5.0', 113 'revision': '21917b36553562d21c14fe086756a57cbe8a381b', 114 }) 115 process_report('12345', params) 116 mock.assert_has_calls([ 117 call.update_status('654321', 'WPTFYI_PROCESSING', None, 118 'https://test.wpt.fyi/api'), 119 call.download(['https://wpt.fyi/wpt_report.json.gz'], [], None), 120 ]) 121 mock.report.update_metadata.assert_called_once_with( 122 revision='21917b36553562d21c14fe086756a57cbe8a381b', 123 browser_name='Chrome', browser_version='70', 124 os_name='Linux', os_version='5.0') 125 mock.create_run.assert_called_once_with( 126 '654321', 'foo,bar', 'blade-runner', 'https://test.wpt.fyi/api') 127 128 @patch('processor.Processor') 129 def test_params_plumbing_error(self, MockProcessor): 130 # Set up mock context manager to return self. 131 mock = MockProcessor.return_value 132 mock.__enter__.return_value = mock 133 mock.results = ['/tmp/wpt_report.json.gz'] 134 mock.load_report.side_effect = wptreport.InvalidJSONError 135 136 params = MultiDict({ 137 'uploader': 'blade-runner', 138 'id': '654321', 139 'results': 'https://wpt.fyi/wpt_report.json.gz', 140 }) 141 # Suppress print_exception. 142 with patch('traceback.print_exception'): 143 process_report('12345', params) 144 mock.assert_has_calls([ 145 call.update_status('654321', 'WPTFYI_PROCESSING', None, None), 146 call.download(['https://wpt.fyi/wpt_report.json.gz'], [], None), 147 call.load_report(), 148 call.update_status( 149 '654321', 'INVALID', 150 "Invalid JSON (['https://wpt.fyi/wpt_report.json.gz'])", None), 151 ]) 152 mock.create_run.assert_not_called() 153 154 @patch('processor.Processor') 155 def test_params_plumbing_empty(self, MockProcessor): 156 # Set up mock context manager to return self. 157 mock = MockProcessor.return_value 158 mock.__enter__.return_value = mock 159 mock.results = [] 160 161 params = MultiDict({ 162 'uploader': 'blade-runner', 163 'id': '654321', 164 }) 165 with self.assertLogs(): 166 process_report('12345', params) 167 mock.assert_has_calls([ 168 call.update_status('654321', 'WPTFYI_PROCESSING', None, None), 169 call.download([], [], None), 170 call.update_status('654321', 'EMPTY', None, None), 171 ]) 172 mock.create_run.assert_not_called() 173 174 @patch('processor.Processor') 175 def test_params_plumbing_duplicate(self, MockProcessor): 176 # Set up mock context manager to return self. 177 mock = MockProcessor.return_value 178 mock.__enter__.return_value = mock 179 mock.check_existing_run.return_value = True 180 mock.results = ['/tmp/wpt_report.json.gz'] 181 mock.raw_results_url = 'https://wpt.fyi/test/report.json' 182 183 params = MultiDict({ 184 'uploader': 'blade-runner', 185 'id': '654321', 186 'results': 'https://wpt.fyi/wpt_report.json.gz', 187 }) 188 with self.assertLogs(): 189 process_report('12345', params) 190 mock.update_status.assert_has_calls([ 191 call('654321', 'WPTFYI_PROCESSING', None, None), 192 call('654321', 'DUPLICATE', None, None), 193 ]) 194 mock.create_run.assert_not_called() 195 196 197 class ProcessorDownloadServerTest(unittest.TestCase): 198 """This class tests behaviours of Processor related to downloading 199 artifacts (e.g. JSON reports) from an external server. test_server is used 200 to emulate the success and failure modes of an external server. 201 """ 202 def setUp(self): 203 self.server, self.url = test_util.start_server(False) 204 205 def tearDown(self): 206 self.server.terminate() 207 self.server.wait() 208 209 def test_download_single(self): 210 with Processor() as p: 211 # The endpoint returns "Hello, world!". 212 path = p._download_single(self.url + '/download/test.txt') 213 self.assertTrue(path.endswith('.txt')) 214 with open(path, 'rb') as f: 215 self.assertEqual(f.read(), b'Hello, world!') 216 217 def test_download(self): 218 with Processor() as p: 219 p.TIMEOUT_WAIT = 0.1 # to speed up tests 220 url_404 = self.url + '/404' 221 url_timeout = self.url + '/slow' 222 with self.assertLogs() as lm: 223 p.download( 224 [self.url + '/download/test.txt', url_timeout], 225 [url_404], 226 None) 227 self.assertEqual(len(p.results), 1) 228 self.assertTrue(p.results[0].endswith('.txt')) 229 self.assertEqual(len(p.screenshots), 0) 230 self.assertListEqual( 231 lm.output, 232 ['ERROR:processor:Timed out fetching: ' + url_timeout, 233 'ERROR:processor:Failed to fetch (404): ' + url_404]) 234 235 def test_download_content_disposition(self): 236 with Processor() as p: 237 # The response of this endpoint sets Content-Disposition with 238 # artifact_test.zip as the filename. 239 path = p._download_single(self.url + '/download/attachment') 240 self.assertTrue(path.endswith('.zip')) 241 242 243 class ProcessorAPIServerTest(unittest.TestCase): 244 """This class tests API calls from Processor to webapp (e.g. 245 /api/results/create, /api/status). test_server is used to emulate webapp 246 and verify credentials and payloads. 247 """ 248 def setUp(self): 249 self.server, self.url = test_util.start_server(True) 250 251 def tearDown(self): 252 if self.server.poll() is None: 253 self.server.kill() 254 255 def test_update_status(self): 256 with Processor() as p: 257 p._auth = AUTH_CREDENTIALS 258 p.report.update_metadata( 259 browser_name='Chrome', 260 browser_version='70', 261 os_name='Linux', 262 os_version='5.0', 263 revision='21917b36553562d21c14fe086756a57cbe8a381b') 264 p.update_status( 265 run_id='12345', stage='INVALID', 266 error='Sample error', callback_url=self.url) 267 self.server.terminate() 268 _, err = self.server.communicate() 269 response = json.loads(err) 270 self.assertDictEqual(response, { 271 'id': 12345, 'stage': 'INVALID', 'error': 'Sample error', 272 'browser_name': 'Chrome', 'browser_version': '70', 273 'os_name': 'Linux', 'os_version': '5.0', 274 'full_revision_hash': '21917b36553562d21c14fe086756a57cbe8a381b', 275 }) 276 277 def test_create_run(self): 278 api = self.url + '/api/results/create' 279 with Processor() as p: 280 p._auth = AUTH_CREDENTIALS 281 p.report.update_metadata( 282 browser_name='chrome', 283 browser_version='70', 284 os_name='Linux', 285 revision='21917b36553562d21c14fe086756a57cbe8a381b') 286 p.create_run('12345', '', 'blade-runner', callback_url=api) 287 # p.test_run_id is set based on the response from the API, which in 288 # turn is set according to the request. Hence this verifies that we 289 # pass the run ID to the API correctly. 290 self.assertEqual(p.test_run_id, 12345) 291 self.server.terminate() 292 # This is needed to close the stdio pipes. 293 self.server.communicate()