|
121 | 121 | at_exit_block = nil |
122 | 122 | exit_called = false |
123 | 123 | read_count = 0 |
| 124 | + cpu_config = nil |
124 | 125 |
|
125 | 126 | File.stub :exist?, ->(path) { path.include?('intel_pstate') } do |
126 | 127 | cpu_config = IntelCPUConfig.new |
|
140 | 141 | end |
141 | 142 | } do |
142 | 143 | capture_io do |
143 | | - cpu_config.configure_for_benchmarking(turbo: false) |
| 144 | + cpu_config.stub :keep_sudo_alive, -> {} do |
| 145 | + cpu_config.configure_for_benchmarking(turbo: false) |
| 146 | + end |
144 | 147 | end |
145 | 148 | assert_operator call_count, :>, 0, "Should call check_call to configure Intel CPU" |
146 | 149 | assert at_exit_called, "Should register at_exit handler to restore CPU settings" |
|
152 | 155 | end |
153 | 156 | end |
154 | 157 |
|
155 | | - # Verify at_exit block restores Intel turbo settings |
156 | | - cleanup_commands = [] |
157 | | - BenchmarkRunner.stub :check_call, ->(cmd, **opts) { cleanup_commands << { cmd: cmd, opts: opts } } do |
| 158 | + # Verify at_exit block restores Intel turbo settings non-interactively. |
| 159 | + # `sudo -n` is what prevents the silent hang when credentials have lapsed. |
| 160 | + restore_args = nil |
| 161 | + cpu_config.stub :system, ->(*args, **_opts) { restore_args = args; true } do |
158 | 162 | at_exit_block.call |
159 | 163 | end |
160 | 164 |
|
161 | | - assert_equal 1, cleanup_commands.length, "at_exit block should call check_call once" |
162 | | - assert_equal "sudo -S sh -c 'echo 0 > /sys/devices/system/cpu/intel_pstate/no_turbo'", cleanup_commands[0][:cmd] |
163 | | - assert_equal({ quiet: true }, cleanup_commands[0][:opts]) |
| 165 | + assert_equal ["sudo", "-n", "sh", "-c", "echo 0 > /sys/devices/system/cpu/intel_pstate/no_turbo"], restore_args |
164 | 166 | end |
165 | 167 |
|
166 | 168 | it 'exits when Intel turbo is not disabled and turbo flag is false' do |
|
261 | 263 | at_exit_block = nil |
262 | 264 | exit_called = false |
263 | 265 | read_count = 0 |
| 266 | + cpu_config = nil |
264 | 267 |
|
265 | 268 | File.stub :exist?, ->(path) { path.include?('cpufreq/boost') } do |
266 | 269 | cpu_config = AMDCPUConfig.new |
|
278 | 281 | } do |
279 | 282 | Dir.stub :glob, ->(pattern) { ['/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor'] } do |
280 | 283 | capture_io do |
281 | | - cpu_config.configure_for_benchmarking(turbo: false) |
| 284 | + cpu_config.stub :keep_sudo_alive, -> {} do |
| 285 | + cpu_config.configure_for_benchmarking(turbo: false) |
| 286 | + end |
282 | 287 | end |
283 | 288 | assert_operator call_count, :>, 0, "Should call check_call to configure AMD CPU" |
284 | 289 | assert at_exit_called, "Should register at_exit handler to restore CPU settings" |
|
291 | 296 | end |
292 | 297 | end |
293 | 298 |
|
294 | | - # Verify at_exit block restores AMD boost settings |
295 | | - cleanup_commands = [] |
296 | | - BenchmarkRunner.stub :check_call, ->(cmd, **opts) { cleanup_commands << { cmd: cmd, opts: opts } } do |
| 299 | + # Verify at_exit block restores AMD boost settings non-interactively. |
| 300 | + restore_args = nil |
| 301 | + cpu_config.stub :system, ->(*args, **_opts) { restore_args = args; true } do |
297 | 302 | at_exit_block.call |
298 | 303 | end |
299 | 304 |
|
300 | | - assert_equal 1, cleanup_commands.length, "at_exit block should call check_call once" |
301 | | - assert_equal "sudo -S sh -c 'echo 1 > /sys/devices/system/cpu/cpufreq/boost'", cleanup_commands[0][:cmd] |
302 | | - assert_equal({ quiet: true }, cleanup_commands[0][:opts]) |
| 305 | + assert_equal ["sudo", "-n", "sh", "-c", "echo 1 > /sys/devices/system/cpu/cpufreq/boost"], restore_args |
303 | 306 | end |
304 | 307 |
|
305 | 308 | it 'exits when AMD boost is not disabled and turbo flag is false' do |
|
363 | 366 | end |
364 | 367 | end |
365 | 368 | end |
| 369 | + |
| 370 | +describe 'sudo credential handling' do |
| 371 | + # The helpers under test live on the base CPUConfig; any concrete subclass |
| 372 | + # exercises them. These guard against the silent at_exit hang where a lapsed |
| 373 | + # sudo timestamp left the turbo-restore command blocking on an invisible |
| 374 | + # password prompt (stderr was suppressed). |
| 375 | + def config |
| 376 | + IntelCPUConfig.new |
| 377 | + end |
| 378 | + |
| 379 | + describe '#sudo_restore' do |
| 380 | + it 'restores non-interactively with sudo -n and stays silent on success' do |
| 381 | + cfg = config |
| 382 | + captured = nil |
| 383 | + fake_system = ->(*args, **_opts) { captured = args; true } |
| 384 | + |
| 385 | + _out, err = capture_io do |
| 386 | + cfg.stub :system, fake_system do |
| 387 | + cfg.send(:sudo_restore, "sudo sh -c 'echo 0 > /no_turbo'", "echo 0 > /no_turbo") |
| 388 | + end |
| 389 | + end |
| 390 | + |
| 391 | + assert_equal ["sudo", "-n", "sh", "-c", "echo 0 > /no_turbo"], captured |
| 392 | + assert_empty err |
| 393 | + end |
| 394 | + |
| 395 | + it 'prints the manual command instead of hanging when credentials lapsed' do |
| 396 | + cfg = config |
| 397 | + |
| 398 | + _out, err = capture_io do |
| 399 | + cfg.stub :system, false do |
| 400 | + cfg.send(:sudo_restore, "sudo sh -c 'echo 0 > /no_turbo'", "echo 0 > /no_turbo") |
| 401 | + end |
| 402 | + end |
| 403 | + |
| 404 | + assert_match(/credentials expired/, err) |
| 405 | + assert_match(%r{sudo sh -c 'echo 0 > /no_turbo'}, err) |
| 406 | + end |
| 407 | + end |
| 408 | + |
| 409 | + describe '#keep_sudo_alive' do |
| 410 | + it 'starts at most one keepalive thread and memoizes it' do |
| 411 | + cfg = config |
| 412 | + started = 0 |
| 413 | + fake_thread = Object.new |
| 414 | + |
| 415 | + Thread.stub :new, ->(*_args, &_blk) { started += 1; fake_thread } do |
| 416 | + cfg.send(:keep_sudo_alive) |
| 417 | + cfg.send(:keep_sudo_alive) |
| 418 | + end |
| 419 | + |
| 420 | + assert_equal 1, started |
| 421 | + assert_same fake_thread, cfg.instance_variable_get(:@sudo_keepalive) |
| 422 | + end |
| 423 | + end |
| 424 | +end |
0 commit comments