@@ -84,19 +84,55 @@ def _split_pipeline_name(pipeline_name):
8484 return proof_uid , solver
8585
8686
87+ # Marker emitted by cbmc when the SMT backend returned `unknown` on the
88+ # verification query. cbmc still exits non-zero (cprover-status: ERROR)
89+ # in this case, so the pipeline shows up as `fail` in litani; but no
90+ # property was actually refuted -- the solver simply could not decide.
91+ # We surface this as a distinct "Inconclusive" outcome.
92+ _SOLVER_UNKNOWN_MARKER = 'SMT2 solver returned "unknown"'
93+
94+
95+ def _is_solver_inconclusive (stdout_file ):
96+ """Return True iff the cbmc safety-check job's stdout-file (result.xml)
97+ contains the cbmc message indicating the SMT backend returned `unknown`.
98+ """
99+ if not stdout_file :
100+ return False
101+ try :
102+ with open (stdout_file , encoding = "utf-8" , errors = "replace" ) as f :
103+ return _SOLVER_UNKNOWN_MARKER in f .read ()
104+ except OSError :
105+ return False
106+
107+
87108def _parse_proof_pipeline (proof_pipeline ):
88109 """Parse a single proof pipeline, returning
89110 (name, solver, status, duration, has_timeout)."""
90111 duration = 0
91112 has_timeout = False
113+ inconclusive = False
92114 for stage in proof_pipeline ["ci_stages" ]:
93115 for job in stage ["jobs" ]:
94116 if job .get ("timeout_reached" , False ):
95117 has_timeout = True
96118 if "duration" in job :
97119 duration += int (job ["duration" ])
98-
99- status = "Timeout" if has_timeout else proof_pipeline ["status" ].title ()
120+ # Identify the safety-check job by its description suffix.
121+ # Litani stores both description and stdout_file under
122+ # wrapper_arguments (the args passed to `litani add-job`).
123+ wa = job .get ("wrapper_arguments" ) or {}
124+ desc = wa .get ("description" ) or ""
125+ if desc .endswith (": checking safety properties" ) and _is_solver_inconclusive (
126+ wa .get ("stdout_file" )
127+ ):
128+ inconclusive = True
129+
130+ if has_timeout :
131+ status = "Timeout"
132+ elif inconclusive :
133+ status = "Inconclusive"
134+ else :
135+ status = proof_pipeline ["status" ].title ()
100136 name , solver = _split_pipeline_name (proof_pipeline ["name" ])
101137 return name , solver , status , duration , has_timeout
102138
@@ -172,11 +208,23 @@ def export_result_json(output_path, run_file, omitted_pairs=None):
172208 for name , solver , status , duration_str in proof_table [1 :]: # skip header
173209 is_success = status == "Success"
174210 is_omitted = status == "-"
211+ is_inconclusive = status == "Inconclusive"
175212
176213 if is_omitted :
177214 runtimes .append ({"name" : name , "solver" : solver , "status" : "omitted" })
178215 continue
179216
217+ if is_inconclusive :
218+ runtimes .append (
219+ {
220+ "name" : name ,
221+ "solver" : solver ,
222+ "status" : "inconclusive" ,
223+ "duration" : duration_str ,
224+ }
225+ )
226+ continue
227+
180228 if not is_success :
181229 failures .append (
182230 {
@@ -198,15 +246,17 @@ def export_result_json(output_path, run_file, omitted_pairs=None):
198246 failed = sum (1 for f in failures if f ["status" ] != "Timeout" )
199247 timeout = sum (1 for f in failures if f ["status" ] == "Timeout" )
200248 omitted = sum (1 for r in runtimes if r .get ("status" ) == "omitted" )
249+ inconclusive = sum (1 for r in runtimes if r .get ("status" ) == "inconclusive" )
201250
202251 result = {
203252 "mldsa_parameter_set" : os .getenv ("MLD_CONFIG_PARAMETER_SET" , "unknown" ),
204253 "summary" : {
205254 "total" : total ,
206- "success" : total - failed - timeout - omitted ,
255+ "success" : total - failed - timeout - omitted - inconclusive ,
207256 "failed" : failed ,
208257 "timeout" : timeout ,
209258 "omitted" : omitted ,
259+ "inconclusive" : inconclusive ,
210260 },
211261 "failures" : failures ,
212262 "runtimes" : runtimes ,
@@ -244,16 +294,23 @@ def print_proof_results(out_file, omitted_pairs=None):
244294 "summarizing all proof results"
245295 )
246296
247- # Check for timeouts by examining status table
248- has_timeout = any (row [0 ] == "Timeout" for row in status_table [1 :])
249- has_failure = run_dict ["status" ] != "success"
297+ # Check for timeouts and real failures. "Inconclusive" rows count as
298+ # neither: the solver could not decide, but no property was refuted.
299+ proof_statuses = [row [2 ] for row in proof_table [1 :]] # status column
300+ has_timeout = any (s == "Timeout" for s in proof_statuses )
301+ has_real_failure = any (s == "Fail" for s in proof_statuses )
302+ has_inconclusive = any (s == "Inconclusive" for s in proof_statuses )
250303
251- if has_timeout or has_failure :
304+ if has_timeout or has_real_failure :
252305 logging .error ("Not all proofs passed." )
253306 if has_timeout :
254307 logging .error ("Some proofs timed out." )
255308 logging .error (msg )
256309 sys .exit (1 )
310+ if has_inconclusive :
311+ logging .warning (
312+ "Some (proof, solver) pairs were inconclusive (solver returned 'unknown')."
313+ )
257314 logging .info (msg )
258315
259316
0 commit comments