tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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()