@@ -199,6 +199,108 @@ def setUp(self):
199199 self .addCleanup (observation .stop )
200200
201201
202+ class InstalledInspectionFailureTest (unittest .TestCase ):
203+ """Real lane/compile/inspector subprocesses with controlled producer tools.
204+
205+ Inspector text is deliberately unrecognized, not a FreeBSD format claim.
206+ """
207+ def setUp (self ):
208+ self .temp = tempfile .TemporaryDirectory ()
209+ self .addCleanup (self .temp .cleanup )
210+ self .root = Path (self .temp .name ).resolve ()
211+ self .output = self .root / 'matrix'
212+ self .output .mkdir ()
213+ self .source = self .root / 'source'
214+ (self .source / 'test/installed' ).mkdir (parents = True )
215+ (self .source / 'test/installed/package_consumer.cpp' ).write_text ('int main() { return 0; }\n ' )
216+ (self .source / 'examples' ).mkdir ()
217+ (self .source / 'examples/host_poll.hpp' ).write_text ('// fixture\n ' )
218+ spec = importlib .util .spec_from_file_location ('installed_inspection_runner' ,
219+ Path (__file__ ).with_name ('check-v3-installed-consumer.py' ))
220+ self .runner = importlib .util .module_from_spec (spec )
221+ spec .loader .exec_module (self .runner )
222+ self .args = SimpleNamespace (compiler = os .environ .get ('CXX' , 'c++' ), cc = 'cc' , make = 'fixture-make' ,
223+ jobs = 1 , prerequisite_prefix = [], pkg_config_dir = [], provider_prefix = self .root / 'provider' ,
224+ toolchain_prefix = None , optimized = False )
225+ self .dynamic = 'Dynamic section with unrecognized dependency syntax\n ' + 'raw dynamic entry\n ' * 400
226+ self .diagnostic = 'inspector diagnostic\n '
227+ self .inspector_exit = 0
228+ self .popen = subprocess .Popen
229+ environment = patch .dict (os .environ )
230+ environment .start ()
231+ self .addCleanup (environment .stop )
232+
233+ def create_process (self , argv , ** options ):
234+ if argv [0 ] == 'bash' :
235+ prefix = self .output / 'tls-no/prefix/lib/pkgconfig'
236+ code = f'from pathlib import Path; p=Path({ str (prefix )!r} ); p.mkdir(parents=True); (p/"libhttpserver.pc").write_text("fixture metadata")'
237+ elif argv [0 ] == 'fixture-make' :
238+ code = ''
239+ elif argv [0 ] == 'pkg-config' :
240+ value = str (self .output / 'tls-no/prefix/lib/pkgconfig' ) if '--variable=pcfiledir' in argv else ''
241+ code = f'print({ value !r} )'
242+ elif argv [0 ] == 'readelf' :
243+ code = f'import sys; sys.stdout.write({ self .dynamic !r} ); sys.stderr.write({ self .diagnostic !r} ); sys.exit({ self .inspector_exit } )'
244+ else :
245+ return self .popen (argv , ** options )
246+ return self .popen ([sys .executable , '-c' , code ], ** options )
247+
248+ def fail_lane (self ):
249+ with patch .object (self .runner .audit .platform , 'system' , return_value = 'FreeBSD' ), \
250+ patch .object (subprocess , 'Popen' , side_effect = self .create_process ):
251+ with self .assertRaises (self .runner .audit .AuditError ) as caught :
252+ self .runner .package_lane (self .args , self .source , self .output , 'no' , matrix .load ())
253+ return caught .exception
254+
255+ def test_early_graph_failure_persists_full_output_and_failed_consumer (self ):
256+ self .runner .audit .COMMANDS .append ({'argv' : ['stale-previous-lane-command' ]})
257+ error = self .fail_lane ()
258+ self .assertEqual (str (error ), 'empty ELF dependency inspection' )
259+ receipt_path = self .output / 'tls-no/audit-failure.json'
260+ self .assertTrue (receipt_path .is_file (), 'early graph failure must persist inspection evidence' )
261+ receipt = json .loads (receipt_path .read_text ())
262+ consumer = self .output / ('tls-no/consumer/package_consumer' + ('.exe' if os .name == 'nt' else '' ))
263+ self .assertEqual (receipt ['violations' ], [str (error )])
264+ self .assertEqual (receipt ['failed_artifact' ], dict (id = str (consumer ),
265+ sha256 = hashlib .sha256 (consumer .read_bytes ()).hexdigest ()))
266+ command = receipt ['commands' ][- 1 ]
267+ self .assertEqual (len (receipt ['commands' ]), 1 )
268+ self .assertEqual (command ['argv' ], ['readelf' , '-d' , str (consumer )])
269+ self .assertEqual (command ['returncode' ], 0 )
270+ self .assertEqual (command ['stdout' ], self .dynamic )
271+ self .assertEqual (command ['stderr' ], self .diagnostic )
272+ selected_tool = shutil .which ('readelf' )
273+ if selected_tool :
274+ self .assertEqual (command ['resolved_tool' ], str (Path (selected_tool ).resolve ()))
275+ else :
276+ self .assertNotIn ('resolved_tool' , command )
277+ self .assertFalse ((self .output / 'tls-no/audit.json' ).exists ())
278+
279+ def test_nonzero_inspector_remains_failed_with_actual_exit_evidence (self ):
280+ self .inspector_exit = 7
281+ error = self .fail_lane ()
282+ self .assertIn ('command failed: readelf' , str (error ))
283+ receipt_path = self .output / 'tls-no/audit-failure.json'
284+ self .assertTrue (receipt_path .is_file (), 'nonzero inspector must persist its actual result' )
285+ receipt = json .loads (receipt_path .read_text ())
286+ self .assertEqual (receipt ['violations' ], [str (error )])
287+ self .assertEqual (receipt ['commands' ][- 1 ]['returncode' ], 7 )
288+ self .assertEqual (receipt ['commands' ][- 1 ]['stdout' ], self .dynamic )
289+ self .assertEqual (receipt ['commands' ][- 1 ]['stderr' ], self .diagnostic )
290+
291+ def test_failure_receipt_write_error_keeps_original_graph_failure (self ):
292+ real_write = Path .write_text
293+ def write (path , * args , ** kwargs ):
294+ if path .name == 'audit-failure.json' :
295+ raise OSError ('evidence disk unavailable' )
296+ return real_write (path , * args , ** kwargs )
297+ with patch .object (Path , 'write_text' , write ):
298+ error = self .fail_lane ()
299+ self .assertEqual (str (error ), 'empty ELF dependency inspection' )
300+ self .assertEqual (error .__notes__ , ['could not persist audit failure evidence: evidence disk unavailable' ])
301+ self .assertEqual (self .runner .audit .COMMANDS [- 1 ]['stdout' ], self .dynamic )
302+
303+
202304class MatrixTest (unittest .TestCase ):
203305 def setUp (self ):
204306 self .inventory = matrix .load ()
0 commit comments