1# Copyright 2024 The Chromium Authors 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import argparse 6from typing import List 7from unittest import mock 8 9from immutabledict import immutabledict 10 11from crossbench import path as pth 12from crossbench.browsers.browser import Browser 13from crossbench.env import HostEnvironment 14from crossbench.plt.linux import LinuxPlatform 15from crossbench.probes.cpu_frequency_map import CPUFrequencyMap 16from crossbench.probes.frequency import FrequencyProbe, FrequencyProbeContext 17from crossbench.runner.run import Run 18from tests import test_helper 19from tests.crossbench.base import CrossbenchFakeFsTestCase 20 21 22# TODO(crbug.com/372862708): Turn most of these into unit tests for 23# CPUFrequencyMap and leave only 1-2 tests that verify the communication between 24# the 2 classes. 25 26 27class FrequencyProbeTestCase(CrossbenchFakeFsTestCase): 28 __test__ = True 29 30 def setUp(self): 31 super().setUp() 32 self.platform = LinuxPlatform() 33 34 def test_parse_invalid_map_value(self): 35 with self.assertRaisesRegex(argparse.ArgumentTypeError, "Invalid value"): 36 FrequencyProbe.from_config({"cpus": {"cpu0": "invalid"}}) 37 38 def test_parse_conflicting_wildcard(self): 39 with self.assertRaisesRegex(argparse.ArgumentTypeError, 40 "should be the only key"): 41 FrequencyProbe.from_config({"cpus": {"*": "max", "cpu0": "min"}}) 42 43 def test_key(self): 44 key1 = FrequencyProbe.from_config({"cpus": {"cpu0": 1111}}).key 45 key2 = FrequencyProbe.from_config({"cpus": {"*": 2222}}).key 46 47 self.assertIsNotNone(key1) 48 self.assertIsNotNone(key2) 49 self.assertNotEqual(key1, key2) 50 51 def test_validate_fails_due_to_missing_cpus_dir(self): 52 probe = FrequencyProbe.from_config({"cpus": {"cpu0": 42}}) 53 # No call to self._create_cpu_dir(). 54 55 with self.assertRaisesRegex(FileNotFoundError, 56 "/sys/devices/system/cpu not found"): 57 probe.validate_browser( 58 mock.Mock(spec=HostEnvironment), self._create_mock_browser()) 59 60 def test_validate_fails_due_to_missing_cpu_name(self): 61 probe = FrequencyProbe.from_config({"cpus": {"nonexistent-cpu": 1}}) 62 self._create_cpu_dir("cpu0", [1]) 63 64 with self.assertRaisesRegex(ValueError, "nonexistent-cpu"): 65 probe.validate_browser( 66 mock.Mock(spec=HostEnvironment), self._create_mock_browser()) 67 68 def test_validate_fails_due_to_missing_numerical_frequency(self): 69 probe = FrequencyProbe.from_config({"cpus": {"cpu0": 42}}) 70 self._create_cpu_dir("cpu0", [1, 2]) 71 self._create_cpu_dir("cpu1", [42]) 72 73 with self.assertRaisesRegex( 74 ValueError, r"Target frequency 42 for cpu0 not allowed in linux.*. " 75 r"Available frequencies: \[1, 2\]"): 76 probe.validate_browser( 77 mock.Mock(spec=HostEnvironment), self._create_mock_browser()) 78 79 def test_validate_fails_due_to_missing_numerical_frequency_with_wildcard( 80 self): 81 probe = FrequencyProbe.from_config({"cpus": {"*": 42}}) 82 self._create_cpu_dir("cpu0", [1, 2]) 83 84 with self.assertRaisesRegex( 85 ValueError, r"Target frequency 42 for cpu0 not allowed in linux.*. " 86 r"Available frequencies: \[1, 2\]"): 87 probe.validate_browser( 88 mock.Mock(spec=HostEnvironment), self._create_mock_browser()) 89 90 def test_validate_succeeds_with_extremes(self): 91 probe = FrequencyProbe.from_config({"cpus": {"cpu0": "max", "cpu1": "min"}}) 92 self._create_cpu_dir("cpu0", [1, 2]) 93 self._create_cpu_dir("cpu1", [1, 2]) 94 browser = self._create_mock_browser() 95 96 # Implicitly asserts no exception occurs. 97 probe.validate_browser(mock.Mock(spec=HostEnvironment), browser) 98 frequency_map = probe.cpu_frequency_map.get_target_frequencies( 99 browser.platform) 100 101 self.assertDictEqual( 102 dict(frequency_map), { 103 pth.AnyPosixPath("/sys/devices/system/cpu/cpu0/cpufreq"): 2, 104 pth.AnyPosixPath("/sys/devices/system/cpu/cpu1/cpufreq"): 1, 105 }) 106 107 108 def test_validate_succeeds_without_wildcard(self): 109 probe = FrequencyProbe.from_config( 110 {"cpus": { 111 "cpu0": 2, 112 "cpu1": 2, 113 "cpu2": 2 114 }}) 115 # Use different orders to stress the parsing logic. 116 self._create_cpu_dir("cpu0", [2, 1, 3]) 117 self._create_cpu_dir("cpu1", [1, 2, 3]) 118 self._create_cpu_dir("cpu2", [1, 3, 2]) 119 self._create_cpu_dir("cpu3", [42, 42, 42]) 120 self._create_cpu_dir("cpu4", [42, 42, 42]) 121 browser = self._create_mock_browser() 122 123 # Implicitly asserts no exception occurs. 124 probe.validate_browser(mock.Mock(spec=HostEnvironment), browser) 125 frequency_map = probe.cpu_frequency_map.get_target_frequencies( 126 browser.platform) 127 128 self.assertDictEqual( 129 dict(frequency_map), { 130 pth.AnyPosixPath("/sys/devices/system/cpu/cpu0/cpufreq"): 2, 131 pth.AnyPosixPath("/sys/devices/system/cpu/cpu1/cpufreq"): 2, 132 pth.AnyPosixPath("/sys/devices/system/cpu/cpu2/cpufreq"): 2, 133 }) 134 135 def test_validate_succeeds_with_wildcard(self): 136 probe = FrequencyProbe.from_config({"cpus": {"*": 2}}) 137 # Use different orders to stress the parsing logic. 138 self._create_cpu_dir("cpu0", [2, 1, 3]) 139 self._create_cpu_dir("cpu1", [1, 2, 3]) 140 self._create_cpu_dir("cpu2", [1, 3, 2]) 141 browser = self._create_mock_browser() 142 143 # Implicitly asserts no exception occurs. 144 probe.validate_browser(mock.Mock(spec=HostEnvironment), browser) 145 frequency_map = probe.cpu_frequency_map.get_target_frequencies( 146 browser.platform) 147 148 self.assertDictEqual( 149 dict(frequency_map), { 150 pth.AnyPosixPath("/sys/devices/system/cpu/cpu0/cpufreq"): 2, 151 pth.AnyPosixPath("/sys/devices/system/cpu/cpu1/cpufreq"): 2, 152 pth.AnyPosixPath("/sys/devices/system/cpu/cpu2/cpufreq"): 2, 153 }) 154 155 def test_validate_succeeds_for_empty_configs(self): 156 browser = self._create_mock_browser() 157 self._create_cpu_dir("cpu0", [1, 2, 3]) 158 159 FrequencyProbe.from_config({}).validate_browser( 160 mock.Mock(spec=HostEnvironment), browser) 161 FrequencyProbe.from_config({ 162 "cpus": {}, 163 }).validate_browser(mock.Mock(spec=HostEnvironment), browser) 164 165 def test_validate_string_wildcard(self): 166 probe = FrequencyProbe.from_config({"cpus": "max"}) 167 self._create_cpu_dir("cpu0", [1, 2, 3]) 168 self._create_cpu_dir("cpu1", [1, 2, 3]) 169 browser = self._create_mock_browser() 170 171 # Implicitly asserts no exception occurs. 172 probe.validate_browser(mock.Mock(spec=HostEnvironment), browser) 173 frequency_map = probe.cpu_frequency_map.get_target_frequencies( 174 browser.platform) 175 176 self.assertDictEqual( 177 dict(frequency_map), { 178 pth.AnyPosixPath("/sys/devices/system/cpu/cpu0/cpufreq"): 3, 179 pth.AnyPosixPath("/sys/devices/system/cpu/cpu1/cpufreq"): 3, 180 }) 181 182 def test_start_and_stop(self): 183 self._create_cpu_dir("cpu0", [1, 2, 3]) 184 mock_map = mock.Mock(spec=CPUFrequencyMap) 185 mock_map.get_target_frequencies = mock.Mock( 186 return_value=immutabledict( 187 {pth.AnyPosixPath("/sys/devices/system/cpu/cpu0/cpufreq"): 2})) 188 mock_probe = mock.Mock(spec=FrequencyProbe) 189 type(mock_probe).cpu_frequency_map = mock.PropertyMock( 190 return_value=mock_map) 191 mock_run = mock.Mock(spec=Run) 192 mock_run.browser = self._create_mock_browser() 193 context = FrequencyProbeContext(mock_probe, mock_run) 194 195 min_file = pth.AnyPosixPath( 196 "/sys/devices/system/cpu/cpu0/cpufreq/scaling_min_freq") 197 max_file = pth.AnyPosixPath( 198 "/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq") 199 self.assertEqual(self.platform.cat(min_file), "1\n") 200 self.assertEqual(self.platform.cat(max_file), "3\n") 201 202 context.start() 203 204 self.assertEqual(self.platform.cat(min_file), "2\n") 205 self.assertEqual(self.platform.cat(max_file), "2\n") 206 mock_map.get_target_frequencies.assert_called_with(self.platform) 207 208 context.stop() 209 210 self.assertEqual(self.platform.cat(min_file), "1\n") 211 self.assertEqual(self.platform.cat(max_file), "3\n") 212 213 def _create_mock_browser(self): 214 mock_browser = mock.Mock(spec=Browser) 215 mock_browser.platform = self.platform 216 return mock_browser 217 218 def _create_cpu_dir(self, cpu_name: str, available_frequencies: List[int]): 219 cpu_dir = pth.AnyPosixPath(f"/sys/devices/system/cpu/{cpu_name}/cpufreq") 220 self.platform.mkdir(cpu_dir, parents=True, exist_ok=True) 221 self.platform.set_file_contents( 222 cpu_dir / "scaling_available_frequencies", 223 " ".join(map(str, available_frequencies)) + "\n") 224 self.platform.set_file_contents(cpu_dir / "scaling_min_freq", 225 str(min(available_frequencies)) + "\n") 226 self.platform.set_file_contents(cpu_dir / "scaling_max_freq", 227 str(max(available_frequencies)) + "\n") 228 229if __name__ == "__main__": 230 test_helper.run_pytest(__file__) 231