119 lines
6.6 KiB
Python
119 lines
6.6 KiB
Python
"""Compiler launch recovery must not hide deterministic compiler diagnostics."""
|
|
from __future__ import annotations
|
|
|
|
from contextlib import ExitStack
|
|
import os
|
|
from pathlib import Path
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
from app.simulation.native_codegen import build, processes
|
|
|
|
|
|
class NativeCompilerProcessTests(unittest.TestCase):
|
|
def mocks(self, responses, *, windows=True):
|
|
stack=ExitStack();self.addCleanup(stack.close)
|
|
stack.enter_context(patch.object(build,'WINDOWS',windows))
|
|
stack.enter_context(patch.object(processes,'IS_WINDOWS',windows))
|
|
stack.enter_context(patch.object(subprocess,'CREATE_NO_WINDOW',0x08000000,create=True))
|
|
stack.enter_context(patch.object(build.LOGGER,'warning'))
|
|
run=stack.enter_context(patch.object(subprocess,'run',side_effect=responses))
|
|
sleep=stack.enter_context(patch.object(build.time,'sleep'))
|
|
return run,sleep
|
|
|
|
@staticmethod
|
|
def result(code=0,stdout=b'ok',stderr=b''):
|
|
return subprocess.CompletedProcess(['gcc'],code,stdout,stderr)
|
|
|
|
def test_transient_child_launch_failure_recovers_and_keeps_history(self):
|
|
failure=self.result(1,b'',b'gcc.exe: error: CreateProcess: No such file or directory\n')
|
|
run,sleep=self.mocks([failure,failure,self.result(stdout=b'preprocessed')])
|
|
log=[]
|
|
self.assertEqual(build._command(['gcc','-E','model.c'],log=log),b'preprocessed')
|
|
self.assertEqual(run.call_count,3);self.assertEqual(sleep.call_count,2)
|
|
self.assertEqual(sum(line.startswith('Compiler launch retry ') for line in log),2)
|
|
self.assertEqual(run.call_args.kwargs['stdin'],subprocess.DEVNULL)
|
|
self.assertTrue(run.call_args.kwargs['close_fds'])
|
|
self.assertEqual(run.call_args.kwargs['creationflags'],0x08000000)
|
|
|
|
def test_c_and_link_errors_are_not_retried(self):
|
|
for diagnostic in (b'model.c:12: error: syntax error',b'ld: undefined reference to missing',
|
|
b'model.c:1: error: #error gcc.exe: error: CreateProcess: fake'):
|
|
with self.subTest(diagnostic=diagnostic):
|
|
run,sleep=self.mocks([self.result(1,b'',diagnostic)])
|
|
with self.assertRaises(build.NativeCommandError) as caught:
|
|
build._command(['gcc','-c','model.c'],log=[])
|
|
self.assertEqual(caught.exception.details['stderr'],diagnostic.decode())
|
|
self.assertEqual(run.call_count,1);sleep.assert_not_called()
|
|
|
|
def test_retry_exhaustion_retains_final_command_and_error(self):
|
|
failure=self.result(1,b'',b'gcc.exe: error: CreateProcess: No such file or directory')
|
|
run,sleep=self.mocks([failure]*6)
|
|
with self.assertRaises(build.NativeCommandError) as caught:
|
|
build._command(['gcc','-E','model.c'],log=[])
|
|
self.assertEqual(run.call_count,6);self.assertEqual(sleep.call_count,5)
|
|
self.assertEqual(caught.exception.details['attempts'],6)
|
|
self.assertEqual(caught.exception.details['command'],['gcc','-E','model.c'])
|
|
self.assertEqual(caught.exception.details['exitCode'],1)
|
|
|
|
def test_empty_version_output_is_validated_and_can_recover(self):
|
|
run,_=self.mocks([self.result(stdout=b''),self.result(stdout=b'gcc 8.1\n')])
|
|
result=build._command(['gcc','--version'],log=[],require_stdout=True)
|
|
self.assertEqual(result,b'gcc 8.1\n');self.assertEqual(run.call_count,2)
|
|
|
|
def test_empty_linux_version_has_a_clear_error_without_windows_retries(self):
|
|
run,sleep=self.mocks([self.result(stdout=b'')],windows=False)
|
|
with self.assertRaises(build.NativeCommandError) as caught:
|
|
build._command(['/usr/bin/gcc','--version'],log=[],require_stdout=True)
|
|
self.assertEqual(caught.exception.details['errorType'],'EmptyCompilerOutput')
|
|
self.assertEqual(run.call_count,1);sleep.assert_not_called()
|
|
self.assertIsNone(run.call_args.kwargs['env'])
|
|
self.assertEqual(run.call_args.kwargs['creationflags'],0)
|
|
|
|
def test_timeout_and_missing_executable_are_not_retried(self):
|
|
for exc in (subprocess.TimeoutExpired(['gcc'],1),FileNotFoundError('gcc absent')):
|
|
run,sleep=self.mocks([exc])
|
|
with self.assertRaises(type(exc)):
|
|
build._command(['gcc'],log=[])
|
|
self.assertEqual(run.call_count,1);sleep.assert_not_called()
|
|
|
|
def test_windows_environment_is_per_process_and_uses_selected_toolchain(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
compiler=Path(directory)/'gcc.exe';compiler.touch()
|
|
before=dict(os.environ)
|
|
with patch.object(processes,'IS_WINDOWS',True):
|
|
env=processes.command_environment(str(compiler))
|
|
key=next(k for k in env if k.upper()=='PATH')
|
|
self.assertTrue(env[key].startswith(str(compiler.parent.resolve())+os.pathsep))
|
|
self.assertEqual(dict(os.environ),before)
|
|
with patch.object(processes,'IS_WINDOWS',False):
|
|
self.assertIsNone(processes.command_environment(str(compiler)))
|
|
|
|
def test_driver_error_classifier_does_not_assume_missing_file(self):
|
|
with patch.object(processes,'IS_WINDOWS',True):
|
|
self.assertTrue(processes.is_driver_launch_failure('gcc.EXE: error: CreateProcess: No such file or directory'))
|
|
self.assertTrue(processes.is_driver_launch_failure("x86_64-w64-mingw32-gcc.exe: fatal error: cannot execute 'cc1': CreateProcess: Permission denied"))
|
|
self.assertFalse(processes.is_driver_launch_failure('source.c:1: error: #error gcc.exe: error: CreateProcess: fake'))
|
|
with patch.object(processes,'IS_WINDOWS',False):
|
|
self.assertFalse(processes.is_driver_launch_failure('gcc.exe: error: CreateProcess: No such file or directory'))
|
|
|
|
def test_startup_check_keeps_exhausted_identity_query_details(self):
|
|
from threading import Event
|
|
from app.simulation.native_codegen import self_test
|
|
error=build.NativeCommandError('empty identity', ['gcc','--version'], exit_code=0,
|
|
error_type='EmptyCompilerOutput',attempts=6)
|
|
state={}
|
|
with patch.object(self_test,'toolchain',side_effect=error):
|
|
with self.assertRaises(self_test.CheckFailure) as caught:
|
|
self_test.check_native_runtime(lambda **values: state.update(values),Event())
|
|
self.assertEqual(state['stage'],'toolchain')
|
|
self.assertEqual(caught.exception.details['command'],['gcc','--version'])
|
|
self.assertEqual(caught.exception.details['attempts'],6)
|
|
self.assertEqual(caught.exception.details['errorType'],'EmptyCompilerOutput')
|
|
|
|
|
|
if __name__=='__main__':
|
|
unittest.main()
|