test_simpleperf.py (29908B)
1 #!/usr/bin/env python 2 import itertools 3 import os 4 import subprocess 5 import tarfile 6 import zipfile 7 from pathlib import Path 8 from unittest import mock 9 from unittest.mock import call 10 11 import mozunit 12 import pytest 13 14 from mozperftest.system.simpleperf import ( 15 DEFAULT_SIMPLEPERF_OPTS, 16 SimpleperfAlreadyRunningError, 17 SimpleperfBinaryNotFoundError, 18 SimpleperfController, 19 SimpleperfExecutionError, 20 SimpleperfNotRunningError, 21 SimpleperfProfiler, 22 SimpleperfSymbolicationError, 23 ) 24 from mozperftest.tests.support import EXAMPLE_SHELL_TEST, get_running_env 25 26 27 def running_env(**kw): 28 return get_running_env(flavor="custom-script", **kw) 29 30 31 def make_mock_process(return_code=0, context=True): 32 33 process = mock.MagicMock() 34 process.returncode = return_code 35 process.stdout = mock.MagicMock() 36 37 if context: 38 process.__enter__.return_value = process 39 process.__exit__.return_value = None 40 41 return process 42 43 44 def create_mock_symbolication_directories(base, CI=True): 45 46 (mock_work_dir_path := base / "mock_work_dir").mkdir(parents=True, exist_ok=True) 47 48 if CI: 49 (mock_fetch_path := base / "fetch").mkdir(parents=True, exist_ok=True) 50 (symbol_dir := mock_fetch_path / "target.crashreporter-symbols").mkdir( 51 parents=True, exist_ok=True 52 ) 53 (output_dir := base / "output").mkdir(parents=True, exist_ok=True) 54 55 return mock_work_dir_path, mock_fetch_path, symbol_dir, output_dir 56 else: 57 (symbolicator_dir := base / "symbolicator-cli").mkdir( 58 parents=True, exist_ok=True 59 ) 60 (symbol_dir := base / "target.crashreporter-symbols").mkdir( 61 parents=True, exist_ok=True 62 ) 63 (output_dir := base / "unit_test").mkdir(parents=True, exist_ok=True) 64 65 return mock_work_dir_path, symbolicator_dir, symbol_dir, output_dir 66 67 68 class FakeDevice: 69 def __init__(self): 70 self.pushed_files = {} 71 self.commands = [] 72 self.pulled_files = {} 73 74 def push(self, source, destination): 75 self.pushed_files[destination] = source 76 77 def shell(self, command): 78 self.commands.append(command) 79 return "" 80 81 def pull(self, source, destination): 82 self.pulled_files[destination] = source 83 84 85 @mock.patch("mozperftest.system.simpleperf.ADBDevice", new=FakeDevice) 86 def test_simpleperf_setup(): 87 mach_cmd, metadata, env = running_env( 88 app="fenix", tests=[str(EXAMPLE_SHELL_TEST)], output=None 89 ) 90 91 profiler = SimpleperfProfiler(env, mach_cmd) 92 93 # Pass a mock path to the simpleperf NDK. 94 mock_path = Path("mock") / "simpleperf" / "path" 95 profiler.set_arg("path", str(mock_path)) 96 97 # Make sure binary exists 98 with mock.patch("os.path.exists", return_value=True): 99 # Test setup method. 100 profiler.setup() 101 102 # Verify binary was pushed to device properly. 103 expected_source = mock_path / "bin" / "android" / "arm64" / "simpleperf" 104 assert profiler.device.pushed_files["/data/local/tmp"] == expected_source 105 assert "chmod a+x /data/local/tmp/simpleperf" in profiler.device.commands 106 107 # Verify environment variable was set to activate layer. 108 assert os.environ.get("MOZPERFTEST_SIMPLEPERF") == "1" 109 110 # Test run step which should be a no-op. 111 result = profiler.run(metadata) 112 assert result == metadata 113 114 assert metadata.get_extra_options() == ["simpleperf"] 115 116 # Test teardown method. 117 with mock.patch.object(SimpleperfProfiler, "_symbolicate", return_value=None): 118 profiler.teardown() 119 120 # Verify that profile and binary files were removed. 121 cleanup_command = "rm -f /data/local/tmp/perf.data /data/local/tmp/simpleperf" 122 assert cleanup_command in profiler.device.commands 123 124 # Make sure $MOZPERFTEST_SIMPLEPERF is undefined. 125 assert "MOZPERFTEST_SIMPLEPERF" not in os.environ 126 127 128 @mock.patch("mozperftest.system.simpleperf.ADBDevice", new=FakeDevice) 129 @mock.patch("os.path.exists", return_value=True) 130 def test_simpleperf_setup_with_path(mock_exists): 131 """Test setup_simpleperf_path when path is provided.""" 132 mach_cmd, metadata, env = running_env( 133 app="fenix", tests=[str(EXAMPLE_SHELL_TEST)], output=None 134 ) 135 136 profiler = SimpleperfProfiler(env, mach_cmd) 137 custom_path = Path("custom") / "simpleperf" / "path" 138 profiler.set_arg("path", str(custom_path)) 139 140 profiler.setup_simpleperf_path() 141 142 # Verify binary was pushed to device properly. 143 mock_exists.assert_called_once_with( 144 custom_path / "bin" / "android" / "arm64" / "simpleperf" 145 ) 146 147 148 @mock.patch("mozperftest.system.simpleperf.ADBDevice", new=FakeDevice) 149 @mock.patch("os.path.exists", return_value=True) 150 def test_simpleperf_setup_without_path(mock_exists): 151 """Test setup_simpleperf_path when no path is provided and NDK needs to be installed.""" 152 mach_cmd, metadata, env = running_env( 153 app="fenix", tests=[str(EXAMPLE_SHELL_TEST)], output=None 154 ) 155 156 profiler = SimpleperfProfiler(env, mach_cmd) 157 158 # Setup mocks for the imports inside the method 159 mock_platform = mock.MagicMock() 160 mock_platform.system.return_value = "Linux" 161 mock_platform.machine.return_value = "x86_64" 162 163 # Create platform-agnostic paths 164 mock_ndk = Path("mock") / "ndk" 165 166 mock_android = mock.MagicMock() 167 mock_android.NDK_PATH = mock_ndk 168 169 # Mock the imports that happen 170 with mock.patch.dict( 171 "sys.modules", {"platform": mock_platform, "mozboot.android": mock_android} 172 ): 173 # Call the method directly 174 profiler.setup_simpleperf_path() 175 176 # Verify Android NDK was installed 177 mock_android.ensure_android_ndk.assert_called_once_with("linux") 178 179 # Verify simpleperf path was set correctly. 180 expected_path = mock_ndk / "simpleperf" 181 assert profiler.get_arg("path") == expected_path 182 183 # Verify binary was installed. 184 mock_exists.assert_called_once_with( 185 expected_path / "bin" / "android" / "arm64" / "simpleperf" 186 ) 187 188 189 @mock.patch("mozperftest.system.simpleperf.ADBDevice", new=FakeDevice) 190 @mock.patch("os.path.exists", return_value=False) 191 def test_simpleperf_setup_missing_binary(mock_exists): 192 """Test setup_simpleperf_path when the binary doesn't exist.""" 193 mach_cmd, metadata, env = running_env( 194 app="fenix", tests=[str(EXAMPLE_SHELL_TEST)], output=None 195 ) 196 197 profiler = SimpleperfProfiler(env, mach_cmd) 198 missing_path = Path("missing") / "binary" / "path" 199 profiler.set_arg("path", str(missing_path)) 200 201 # This should raise an exception 202 with pytest.raises(SimpleperfBinaryNotFoundError) as excinfo: 203 profiler.setup_simpleperf_path() 204 205 # Verify the error message contains the path 206 assert "Cannot find simpleperf binary" in str(excinfo.value) 207 assert str(missing_path) in str(excinfo.value) 208 209 210 # Tests for SimpleperfController 211 212 213 class MockProcess: 214 def __init__(self, returncode=0): 215 self.returncode = returncode 216 self.stdout = None 217 self.stderr = None 218 219 def communicate(self): 220 return b"stdout data", b"stderr data" 221 222 223 @mock.patch("mozperftest.system.simpleperf.ADBDevice", new=FakeDevice) 224 @mock.patch("mozperftest.system.simpleperf.subprocess.Popen") 225 @mock.patch( 226 "mozperftest.system.simpleperf.SimpleperfProfiler.is_enabled", return_value=True 227 ) 228 def test_simpleperf_controller_start_default_options(mock_is_enabled, mock_popen): 229 """Test for SimpleperfController.start().""" 230 mock_process = MockProcess() 231 mock_popen.return_value = mock_process 232 233 # Create controller 234 controller = SimpleperfController() 235 236 # Test start with default options 237 controller.start(None) 238 239 # Verify subprocess.Popen was called with su, proper paths and the default args. 240 mock_popen.assert_called_once_with( 241 [ 242 "adb", 243 "shell", 244 "su", 245 "-c", 246 f"/data/local/tmp/simpleperf record {DEFAULT_SIMPLEPERF_OPTS} -o /data/local/tmp/perf.data", 247 ], 248 stdout=subprocess.PIPE, 249 stderr=subprocess.PIPE, 250 ) 251 252 # Verify profiler_process was set 253 assert controller.profiler_process == mock_process 254 255 256 @mock.patch("mozperftest.system.simpleperf.ADBDevice", new=FakeDevice) 257 @mock.patch("mozperftest.system.simpleperf.subprocess.Popen") 258 @mock.patch( 259 "mozperftest.system.simpleperf.SimpleperfProfiler.is_enabled", return_value=True 260 ) 261 def test_simpleperf_controller_start_custom_options(mock_is_enabled, mock_popen): 262 """Test that SimpleperfController.start() works with custom options.""" 263 mock_process = MockProcess() 264 mock_popen.return_value = mock_process 265 266 controller = SimpleperfController() 267 custom_opts = "some random options here." 268 269 # Test start() 270 controller.start(custom_opts) 271 272 # Verify the correct arguments are used 273 mock_popen.assert_called_once_with( 274 [ 275 "adb", 276 "shell", 277 "su", 278 "-c", 279 f"/data/local/tmp/simpleperf record {custom_opts} -o /data/local/tmp/perf.data", 280 ], 281 stdout=subprocess.PIPE, 282 stderr=subprocess.PIPE, 283 ) 284 assert controller.profiler_process == mock_process 285 286 287 @mock.patch("mozperftest.system.simpleperf.ADBDevice", new=FakeDevice) 288 @mock.patch("mozperftest.system.simpleperf.Path") 289 @mock.patch( 290 "mozperftest.system.simpleperf.SimpleperfProfiler.is_enabled", return_value=True 291 ) 292 def test_simpleperf_controller_stop(mock_is_enabled, mock_path): 293 """Test that the SimpleperfController.stop() method works correctly.""" 294 mock_process = MockProcess() 295 296 output_dir = Path("mock") / "output" 297 index = 5 298 expected_output = output_dir / f"perf-{index}.data" 299 mock_path.return_value = expected_output 300 301 controller = SimpleperfController() 302 controller.profiler_process = mock_process 303 304 # Test Stop() 305 with mock.patch.object( 306 mock_process, "communicate", return_value=(b"stdout data", b"stderr data") 307 ): 308 controller.stop(str(output_dir), index) 309 310 assert "kill $(pgrep simpleperf)" in controller.device.commands 311 assert ( 312 controller.device.pulled_files[str(expected_output)] 313 == "/data/local/tmp/perf.data" 314 ) 315 assert "rm -f /data/local/tmp/perf.data" in controller.device.commands 316 assert controller.profiler_process is None 317 318 319 @mock.patch("mozperftest.system.simpleperf.ADBDevice", new=FakeDevice) 320 @mock.patch( 321 "mozperftest.system.simpleperf.SimpleperfProfiler.is_enabled", return_value=True 322 ) 323 def test_simpleperf_controller_start_already_running(mock_is_enabled): 324 """Test that SimpleperfController.start() raises an exception if already running.""" 325 controller = SimpleperfController() 326 controller.profiler_process = MockProcess() 327 328 with pytest.raises(SimpleperfAlreadyRunningError) as excinfo: 329 controller.start(None) 330 331 assert "simpleperf already running" in str(excinfo.value) 332 333 334 @mock.patch("mozperftest.system.simpleperf.ADBDevice", new=FakeDevice) 335 @mock.patch( 336 "mozperftest.system.simpleperf.SimpleperfProfiler.is_enabled", return_value=True 337 ) 338 def test_simpleperf_controller_stop_not_running(mock_is_enabled): 339 """Test that SimpleperfController.stop() raises an exception if not running.""" 340 controller = SimpleperfController() 341 controller.profiler_process = None 342 343 output_dir = Path("mock") / "output" 344 345 with pytest.raises(SimpleperfNotRunningError) as excinfo: 346 controller.stop(str(output_dir), 1) 347 348 assert "no profiler process found" in str(excinfo.value) 349 350 351 @mock.patch("mozperftest.system.simpleperf.ADBDevice", new=FakeDevice) 352 @mock.patch("mozperftest.system.simpleperf.subprocess.Popen") 353 @mock.patch( 354 "mozperftest.system.simpleperf.SimpleperfProfiler.is_enabled", return_value=True 355 ) 356 def test_simpleperf_controller_stop_error(mock_is_enabled, mock_popen): 357 """Test that SimpleperfController.stop() handles process errors.""" 358 359 mock_process = MockProcess(returncode=1) 360 mock_popen.return_value = mock_process 361 362 controller = SimpleperfController() 363 controller.start(None) 364 365 output_dir = Path("mock") / "output" 366 367 with pytest.raises(SimpleperfExecutionError) as excinfo: 368 controller.stop(str(output_dir), 1) 369 370 assert "failed to run simpleperf" in str(excinfo.value) 371 372 373 # Tests for Simpleperf Symbolication 374 375 376 @mock.patch("mozperftest.system.simpleperf.ADBDevice", new=FakeDevice) 377 def test_simpleperf_invalid_symbolicate_arguments(): 378 """Test simpleperf symbolication when empty or invalid arguments are passed""" 379 mach_cmd, metadata, env = running_env( 380 app="fenix", tests=[str(EXAMPLE_SHELL_TEST)], output=None 381 ) 382 383 profiler = SimpleperfProfiler(env, mach_cmd) 384 profiler.metadata = mock.Mock() 385 profiler.metadata.script = {"name": "unit_test"} 386 387 # Invalid inputs should throw SimpleperfSymbolicationError exception 388 with pytest.raises(SimpleperfSymbolicationError): 389 profiler._validate_symbolication_paths( 390 "/fake/symbol/path", "/fake/symbolicator/path" 391 ) 392 393 with pytest.raises(SimpleperfSymbolicationError): 394 profiler._validate_symbolication_paths("", "") 395 396 with pytest.raises(SimpleperfSymbolicationError): 397 profiler._validate_symbolication_paths(None, None) 398 399 # Verify local symbolication is skipped if args not provided 400 with mock.patch.object(profiler, "_cleanup") as mock_cleanup, mock.patch( 401 "mozperftest.system.simpleperf.ON_TRY", False 402 ): 403 profiler.teardown() 404 mock_cleanup.assert_called_once() 405 406 # Check if exception was thrown and handled by verifying that 407 # breakpad_symbol_dir and symbolicator_dir do not exist 408 assert not hasattr(profiler, "breakpad_symbol_dir") 409 assert not hasattr(profiler, "symbolicator_dir") 410 411 profiler.set_arg("symbol-path", "/fake/symbol/path") 412 profiler.set_arg("symbolicator-path", "/fake/symbolicator/path") 413 414 # Verify local symbolication is skipped if args are invalid 415 with mock.patch.object(profiler, "_cleanup") as mock_cleanup, mock.patch( 416 "mozperftest.system.simpleperf.ON_TRY", False 417 ): 418 profiler.teardown() 419 mock_cleanup.assert_called_once() 420 421 assert not hasattr(profiler, "breakpad_symbol_dir") 422 assert not hasattr(profiler, "symbolicator_dir") 423 424 425 @mock.patch("mozperftest.system.simpleperf.ADBDevice", new=FakeDevice) 426 @mock.patch("mozperftest.system.simpleperf.SYMBOL_SERVER_TIMEOUT", 0.1) 427 def test_local_simpleperf_symbolicate(tmp_path): 428 429 # Mock profiler 430 mach_cmd, metadata, env = running_env( 431 app="fenix", tests=[str(EXAMPLE_SHELL_TEST)], output=None 432 ) 433 profiler = SimpleperfProfiler(env, mach_cmd) 434 435 # Mock directories 436 mock_work_dir_path, symbolicator_dir, symbol_dir, output_dir = ( 437 create_mock_symbolication_directories(tmp_path, CI=False) 438 ) 439 440 node_path = tmp_path / "node" 441 442 # Mock files 443 (mock_perf_data_path := output_dir / "mock_perf-0.data").write_text("mock-data") 444 (output_dir / "profile-0-unsymbolicated.json").write_text( 445 "mock-unsymbolicated-profile" 446 ) 447 (output_dir / "profile-0.json").write_text("mock-symbolicated-profile") 448 449 # Mock args 450 profiler.set_arg("symbol-path", symbol_dir) 451 profiler.set_arg("symbolicator-path", symbolicator_dir) 452 profiler.env.set_arg("output", output_dir) 453 profiler.test_name = "unit_test" 454 455 # Test local symbolication 456 with mock.patch("mozperftest.system.simpleperf.ON_TRY", False), mock.patch( 457 "tempfile.mkdtemp", return_value=str(mock_work_dir_path) 458 ), mock.patch("shutil.rmtree") as mock_rmtree, mock.patch( 459 "subprocess.Popen" 460 ) as mock_popen, mock.patch( 461 "mozperftest.system.simpleperf.find_node_executable", 462 return_value=[str(node_path)], 463 ): 464 import_process = make_mock_process(context=True) 465 466 load_process = make_mock_process(context=True) 467 load_process.stdout.__enter__.return_value = load_process.stdout 468 load_process.stdout.readline.side_effect = [ 469 "http://127.0.0.1:3000/?symbolServer=http://127.0.0.1:3000", 470 "", 471 ] 472 473 symbolicator_process = make_mock_process(context=True) 474 475 mock_popen.side_effect = [import_process, load_process, symbolicator_process] 476 477 # Test _symbolicate() via teardown() 478 profiler.teardown() 479 480 # Verify the temporary work directory is deleted 481 mock_rmtree.assert_not_called() 482 483 # Expected process calls 484 expected_import = call( 485 [ 486 "samply", 487 "import", 488 str(mock_perf_data_path), 489 "--save-only", 490 "-o", 491 str(output_dir / "profile-0-unsymbolicated.json"), 492 ], 493 stdout=subprocess.PIPE, 494 stderr=subprocess.STDOUT, 495 text=True, 496 bufsize=1, 497 ) 498 499 expected_load = call( 500 [ 501 "samply", 502 "load", 503 str(output_dir / "profile-0-unsymbolicated.json"), 504 "--no-open", 505 "--breakpad-symbol-dir", 506 str(symbol_dir), 507 "--breakpad-symbol-server", 508 "https://symbols.mozilla.org/", 509 ], 510 stdout=subprocess.PIPE, 511 stderr=subprocess.STDOUT, 512 text=True, 513 ) 514 515 expected_symbolicator = call( 516 [ 517 str(node_path), 518 str(symbolicator_dir / "symbolicator-cli.js"), 519 "--input", 520 str(output_dir / "profile-0-unsymbolicated.json"), 521 "--output", 522 str(output_dir / "profile-0.json"), 523 "--server", 524 "http://127.0.0.1:3000", 525 ], 526 stdout=subprocess.PIPE, 527 stderr=subprocess.STDOUT, 528 text=True, 529 bufsize=1, 530 ) 531 532 calls = mock_popen.call_args_list 533 assert expected_import in calls 534 assert expected_load in calls 535 assert expected_symbolicator in calls 536 537 # Expected call order: samply import -> samply load -> symbolicator-cli 538 assert ( 539 calls.index(expected_import) 540 < calls.index(expected_load) 541 < calls.index(expected_symbolicator) 542 ) 543 544 # Verify exported symbolicated profiles 545 output_zip = output_dir / "profile_unit_test.zip" 546 assert output_zip.exists() 547 548 549 @mock.patch("mozperftest.system.simpleperf.ADBDevice", new=FakeDevice) 550 @mock.patch("mozperftest.system.simpleperf.SYMBOL_SERVER_TIMEOUT", 0.1) 551 def test_local_simpleperf_symbolicate_timeout(tmp_path): 552 553 # Mock profiler 554 mach_cmd, metadata, env = running_env( 555 app="fenix", tests=[str(EXAMPLE_SHELL_TEST)], output=None 556 ) 557 profiler = SimpleperfProfiler(env, mach_cmd) 558 559 # Mock directories 560 mock_work_dir_path, symbolicator_dir, symbol_dir, output_dir = ( 561 create_mock_symbolication_directories(tmp_path, CI=False) 562 ) 563 564 node_path = tmp_path / "node" 565 566 # Mock files 567 (output_dir / "mock_perf-0.data").write_text("mock-data") 568 569 # Mock args 570 profiler.set_arg("symbol-path", symbol_dir) 571 profiler.set_arg("symbolicator-path", symbolicator_dir) 572 profiler.env.set_arg("output", output_dir) 573 profiler.test_name = "unit_test" 574 575 # Test timeout error in local run 576 with mock.patch("mozperftest.system.simpleperf.ON_TRY", False), mock.patch( 577 "tempfile.mkdtemp", return_value=str(mock_work_dir_path) 578 ), mock.patch("shutil.rmtree") as mock_rmtree, mock.patch( 579 "subprocess.Popen" 580 ) as mock_popen, mock.patch( 581 "mozperftest.system.simpleperf.find_node_executable", 582 return_value=[str(node_path)], 583 ), mock.patch.object(profiler, "_cleanup") as mock_cleanup: 584 # Mock processes 585 import_process = make_mock_process(context=True) 586 587 load_process = make_mock_process(context=True) 588 load_process.stdout.__enter__.return_value = load_process.stdout 589 load_process.stdout.readline.side_effect = itertools.repeat("") 590 591 mock_popen.side_effect = [import_process, load_process] 592 593 # Test symbolication timeout 594 profiler.teardown() 595 596 expected_symbolicator = call( 597 [ 598 str(node_path), 599 str(symbolicator_dir / "symbolicator-cli.js"), 600 "--input", 601 str(output_dir / "profile-0-unsymbolicated.json"), 602 "--output", 603 str(output_dir / "profile-0.json"), 604 "--server", 605 "http://127.0.0.1:3000", 606 ], 607 stdout=subprocess.PIPE, 608 stderr=subprocess.STDOUT, 609 text=True, 610 bufsize=1, 611 ) 612 613 # Check if timeout error has been thrown and caught by checking if 614 # subsequent symbolicator call has not occured. 615 assert expected_symbolicator not in mock_popen.call_args_list 616 617 # Check for clean exit 618 mock_rmtree.assert_not_called() 619 mock_cleanup.assert_called_once() 620 621 622 @mock.patch("mozperftest.system.simpleperf.ADBDevice", new=FakeDevice) 623 @mock.patch("mozperftest.system.simpleperf.SYMBOL_SERVER_TIMEOUT", 0.1) 624 def test_ci_simpleperf_symbolicate(tmp_path): 625 626 mach_cmd, metadata, env = running_env( 627 app="fenix", tests=[str(EXAMPLE_SHELL_TEST)], output=None 628 ) 629 profiler = SimpleperfProfiler(env, mach_cmd) 630 631 # Mock directories 632 mock_work_dir_path, mock_fetch_path, symbol_dir, output_dir = ( 633 create_mock_symbolication_directories(tmp_path) 634 ) 635 636 # Mock files 637 (mock_perf_data_path := mock_work_dir_path / "mock_perf-0.data").write_text( 638 "mock-data" 639 ) 640 (mock_work_dir_path / "profile-0-unsymbolicated.json").write_text( 641 "mock-unsymbolicated-profile" 642 ) 643 (mock_work_dir_path / "profile-0.json").write_text("mock-symbolicated-profile") 644 645 # Mock executables 646 (samply_path := mock_fetch_path / "samply" / "samply").parent.mkdir( 647 parents=True, exist_ok=True 648 ) 649 (node_path := mock_fetch_path / "node" / "bin" / "node").parent.mkdir( 650 parents=True, exist_ok=True 651 ) 652 ( 653 symbolicator_path := mock_fetch_path 654 / "symbolicator-cli" 655 / "symbolicator-cli.js" 656 ).parent.mkdir(parents=True, exist_ok=True) 657 658 # Mock .zip file with a symbol file 659 mock_sym_zip_file = mock_fetch_path / "target.crashreporter-symbols.zip" 660 with zipfile.ZipFile(mock_sym_zip_file, "w") as mock_zip: 661 mock_zip.writestr("libxul.so/ABCD1234/libxul.so.sym", "some_data") 662 663 # Mock tar file with a perf data file 664 mock_perf_data = output_dir / "unit_test.tgz" 665 with tarfile.open(mock_perf_data, "w:gz") as tar: 666 perf_path = tmp_path / "mock_perf-0.data" 667 perf_path.write_text("mock-data") 668 tar.add(perf_path, arcname=perf_path.name) 669 670 # Set env and metadata 671 profiler.env.set_arg("output", output_dir) 672 profiler.test_name = "unit_test" 673 674 # Test symbolication in CI 675 with mock.patch.dict( 676 os.environ, 677 { 678 "MOZ_FETCHES_DIR": str(mock_fetch_path), 679 }, 680 clear=False, 681 ), mock.patch("mozperftest.system.simpleperf.ON_TRY", True), mock.patch( 682 "mozperftest.utils.ON_TRY", True 683 ), mock.patch("tempfile.mkdtemp", return_value=str(mock_work_dir_path)), mock.patch( 684 "shutil.rmtree" 685 ) as mock_rmtree, mock.patch("subprocess.Popen") as mock_popen: 686 # Mock processes 687 import_process = make_mock_process(context=True) 688 689 load_process = make_mock_process(context=True) 690 load_process.stdout.__enter__.return_value = load_process.stdout 691 load_process.stdout.readline.side_effect = [ 692 "http://127.0.0.1:3000/?symbolServer=http://127.0.0.1:3000", 693 "", 694 ] 695 696 symbolicator_process = make_mock_process(context=True) 697 698 mock_popen.side_effect = [import_process, load_process, symbolicator_process] 699 700 # Test _symbolicate() via teardown() 701 profiler.teardown() 702 703 # Verify the temporary work directory is deleted 704 mock_rmtree.assert_called_once() 705 706 # Verify proper .zip extraction 707 mock_symbol_path = ( 708 mock_fetch_path 709 / "target.crashreporter-symbols/libxul.so/ABCD1234/libxul.so.sym" 710 ) 711 assert mock_symbol_path.exists() 712 assert mock_symbol_path.parent.exists() 713 714 # Verify proper .tgz extraction 715 assert mock_perf_data_path.exists() 716 717 # Expected process calls 718 expected_import = call( 719 [ 720 str(samply_path), 721 "import", 722 str(mock_perf_data_path), 723 "--save-only", 724 "-o", 725 str(mock_work_dir_path / "profile-0-unsymbolicated.json"), 726 ], 727 stdout=subprocess.PIPE, 728 stderr=subprocess.STDOUT, 729 text=True, 730 bufsize=1, 731 ) 732 733 expected_load = call( 734 [ 735 str(samply_path), 736 "load", 737 str(mock_work_dir_path / "profile-0-unsymbolicated.json"), 738 "--no-open", 739 "--breakpad-symbol-dir", 740 str(symbol_dir), 741 "--breakpad-symbol-server", 742 "https://symbols.mozilla.org/", 743 ], 744 stdout=subprocess.PIPE, 745 stderr=subprocess.STDOUT, 746 text=True, 747 ) 748 749 expected_symbolicator = call( 750 [ 751 str(node_path), 752 str(symbolicator_path), 753 "--input", 754 str(mock_work_dir_path / "profile-0-unsymbolicated.json"), 755 "--output", 756 str(mock_work_dir_path / "profile-0.json"), 757 "--server", 758 "http://127.0.0.1:3000", 759 ], 760 stdout=subprocess.PIPE, 761 stderr=subprocess.STDOUT, 762 text=True, 763 bufsize=1, 764 ) 765 766 calls = mock_popen.call_args_list 767 print(calls) 768 assert expected_import in mock_popen.call_args_list 769 assert expected_load in mock_popen.call_args_list 770 assert expected_symbolicator in mock_popen.call_args_list 771 772 # Expected call order: samply import -> samply load -> symbolicator-cli 773 assert ( 774 calls.index(expected_import) 775 < calls.index(expected_load) 776 < calls.index(expected_symbolicator) 777 ) 778 779 # Verify exported symbolicated profiles 780 output_zip = output_dir / "profile_unit_test.zip" 781 assert output_zip.exists() 782 783 784 @mock.patch("mozperftest.system.simpleperf.ADBDevice", new=FakeDevice) 785 @mock.patch("mozperftest.system.simpleperf.SYMBOL_SERVER_TIMEOUT", 0.1) 786 def test_ci_simpleperf_symbolicate_timeout(tmp_path): 787 788 mach_cmd, metadata, env = running_env( 789 app="fenix", tests=[str(EXAMPLE_SHELL_TEST)], output=None 790 ) 791 profiler = SimpleperfProfiler(env, mach_cmd) 792 793 # Mock directories 794 mock_work_dir_path, mock_fetch_path, _, output_dir = ( 795 create_mock_symbolication_directories(tmp_path) 796 ) 797 798 # Mock executables 799 (node_path := mock_fetch_path / "node" / "bin" / "node").parent.mkdir( 800 parents=True, exist_ok=True 801 ) 802 ( 803 symbolicator_path := mock_fetch_path 804 / "symbolicator-cli" 805 / "symbolicator-cli.js" 806 ).parent.mkdir(parents=True, exist_ok=True) 807 808 # Mock .zip file with a symbol file 809 mock_sym_zip_file = mock_fetch_path / "target.crashreporter-symbols.zip" 810 with zipfile.ZipFile(mock_sym_zip_file, "w") as mock_zip: 811 mock_zip.writestr("libxul.so/ABCD1234/libxul.so.sym", "some_data") 812 813 # Mock tar file with a perf data file 814 mock_perf_data = output_dir / "unit_test.tgz" 815 with tarfile.open(mock_perf_data, "w:gz") as tar: 816 perf_path = tmp_path / "mock_perf-0.data" 817 perf_path.write_text("mock-data") 818 tar.add(perf_path, arcname=perf_path.name) 819 820 # Set env and metadata 821 profiler.env.set_arg("output", output_dir) 822 profiler.test_name = "unit_test" 823 824 # Test timeout error in CI 825 with mock.patch("mozperftest.system.simpleperf.ON_TRY", True), mock.patch( 826 "mozperftest.utils.ON_TRY", True 827 ), mock.patch( 828 "tempfile.mkdtemp", return_value=str(mock_work_dir_path) 829 ), mock.patch.dict( 830 os.environ, 831 { 832 "MOZ_FETCHES_DIR": str(mock_fetch_path), 833 }, 834 clear=False, 835 ), mock.patch("shutil.rmtree") as mock_rmtree, mock.patch( 836 "subprocess.Popen" 837 ) as mock_popen, mock.patch( 838 "mozperftest.system.simpleperf.find_node_executable", 839 return_value=[str(node_path)], 840 ), mock.patch.object(profiler, "_cleanup") as mock_cleanup: 841 # Mock processes 842 import_process = make_mock_process(context=True) 843 844 load_process = make_mock_process(context=True) 845 load_process.stdout.__enter__.return_value = load_process.stdout 846 load_process.stdout.readline.side_effect = itertools.repeat("") 847 848 mock_popen.side_effect = [import_process, load_process] 849 850 # Test symbolication timeout 851 profiler.teardown() 852 853 expected_symbolicator = call( 854 [ 855 str(node_path), 856 str(symbolicator_path), 857 "--input", 858 str(mock_work_dir_path / "profile-0-unsymbolicated.json"), 859 "--output", 860 str(mock_work_dir_path / "profile-0.json"), 861 "--server", 862 "http://127.0.0.1:3000", 863 ], 864 stdout=subprocess.PIPE, 865 stderr=subprocess.STDOUT, 866 text=True, 867 bufsize=1, 868 ) 869 870 # Check if timeout error has been thrown and caught by checking if 871 # subsequent symbolicator call has not occured. 872 assert expected_symbolicator not in mock_popen.call_args_list 873 874 # Check for clean exit 875 mock_rmtree.assert_called_once() 876 mock_cleanup.assert_called_once() 877 878 879 if __name__ == "__main__": 880 mozunit.main()