Skip to content

Commit b802f1d

Browse files
authored
feat: generic command-oracle verifier with pytest output parsing (#260)
* refactor: extract run_command method from ESBMC into BaseSourceVerifier * refactor: decouple fix-code command from ESBMC-specific types * feat: add command-oracle verifier with regex-based output parsing * test: add tests for CommandOracle pytest output parser * chore: remove unused Config import from test_config * fix: improve ESBMC error handling and source file defaults
1 parent 3aaf15c commit b802f1d

15 files changed

Lines changed: 832 additions & 40 deletions

esbmc_ai/__main__.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616

1717
from esbmc_ai import Config, ChatCommand, __author__, __version__
1818
from esbmc_ai.addon_loader import AddonLoader
19+
from esbmc_ai.base_component import BaseComponent
1920
from esbmc_ai.command_result import CommandResult
2021
from esbmc_ai.log_utils import LogCategories, get_log_level, init_logging
21-
from esbmc_ai.verifiers.base_source_verifier import BaseSourceVerifier
22-
from esbmc_ai.verifiers.esbmc import ESBMC
22+
from esbmc_ai.verifiers import BaseSourceVerifier, ESBMC, CommandOracle
2323
from esbmc_ai.component_manager import ComponentManager
2424
import esbmc_ai.commands
2525

@@ -110,15 +110,19 @@ def _load_config(
110110

111111
def _init_builtin_components() -> None:
112112
"""Initializes the builtin verifiers and commands."""
113-
component_manager = ComponentManager()
113+
component_manager: ComponentManager = ComponentManager()
114114

115115
# Built-in verifiers
116-
esbmc = ESBMC.create()
116+
esbmc: BaseComponent = ESBMC.create()
117117
assert isinstance(esbmc, BaseSourceVerifier)
118118
component_manager.add_verifier(esbmc)
119-
# Load component-specific configuration
120119
component_manager.load_component_config(esbmc, builtin=True)
121120

121+
generic_cmd: BaseComponent = CommandOracle.create()
122+
assert isinstance(generic_cmd, BaseSourceVerifier)
123+
component_manager.add_verifier(generic_cmd)
124+
component_manager.load_component_config(generic_cmd, builtin=True)
125+
122126
# Init built-in commands - Loads everything in the esbmc_ai.commands module.
123127
commands: list[ChatCommand] = []
124128
for cmd_classname in getattr(esbmc_ai.commands, "__all__"):

esbmc_ai/chats/solution_generator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
OracleTemplateKeyProvider,
1212
TemplateKeyProvider,
1313
)
14-
from esbmc_ai.verifiers.esbmc import ESBMCOutput
14+
from esbmc_ai.verifier_output import VerifierOutput
1515
from esbmc_ai.chats import KeyTemplateRenderer
1616

1717

@@ -68,7 +68,7 @@ def generate_solution(
6868
self,
6969
initial_message_prompt: PromptTemplate,
7070
solution: Solution,
71-
verifier_output: ESBMCOutput,
71+
verifier_output: VerifierOutput,
7272
) -> str:
7373
"""Prompts the LLM to repair the source code using the verifier output.
7474
Returns the extracted code from the LLM's response."""

esbmc_ai/commands/fix_code_command.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# Author: Yiannis Charalambous
22

33
from enum import Enum
4-
import os
54
from pathlib import Path
65
from typing import Any
76
from pydantic import Field, field_validator
@@ -18,7 +17,8 @@
1817
from esbmc_ai.verifier_output import VerifierOutput
1918
from esbmc_ai.chat_command import ChatCommand
2019
from esbmc_ai.loading_widget import BaseLoadingWidget, LoadingWidget
21-
from esbmc_ai.verifiers.esbmc import ESBMC, ESBMCOutput
20+
from esbmc_ai.verifiers.base_source_verifier import BaseSourceVerifier
21+
from esbmc_ai.verifiers.esbmc import ESBMCOutput
2222

2323

2424
class FixCodeCommandResult(CommandResult):
@@ -137,10 +137,8 @@ def execute(self) -> FixCodeCommandResult:
137137
solution.add_source_file(source_file)
138138

139139

140-
verifier: Any = ComponentManager().get_verifier("esbmc")
141-
assert isinstance(verifier, ESBMC)
140+
verifier: Any = ComponentManager().verifier
142141
verifier_output: VerifierOutput = verifier.verify_source(solution=solution)
143-
assert isinstance(verifier_output, ESBMCOutput)
144142

145143
if verifier_output.successful:
146144
self.logger.info("File verified successfully")
@@ -227,8 +225,8 @@ def _attempt_repair(
227225
solution_generator: SolutionGenerator,
228226
prompt: PromptTemplate,
229227
solution: Solution,
230-
verifier: ESBMC,
231-
verifier_output: ESBMCOutput,
228+
verifier: BaseSourceVerifier,
229+
verifier_output: VerifierOutput,
232230
) -> tuple[FixCodeCommandResult | None, ESBMCOutput]:
233231
source_file: SourceFile = solution.files[0]
234232

esbmc_ai/config.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33

44
from collections import defaultdict
5+
from enum import Enum
56
from importlib.machinery import ModuleSpec
67
from importlib.util import find_spec
78
import logging
@@ -231,6 +232,49 @@ def on_after_set_output_dir(cls, value: Path | None) -> Path | None:
231232
return value
232233

233234

235+
class _CommandOracleOutputTypes(Enum):
236+
PYTEST = "PYTEST"
237+
238+
239+
class CommandOracleConfig(BaseModel):
240+
"""Configures the command-based verifier."""
241+
242+
cmd: str = Field(
243+
default="",
244+
description=(
245+
"The command to run to verify the files. The following"
246+
"variables are supported:\n"
247+
"1. {files} - Represents the source files."
248+
"2. {includes} - Represents the include directories."
249+
),
250+
)
251+
252+
exit_success: int = Field(
253+
default=0,
254+
description=("The exit code expected when the command runs successfully."),
255+
)
256+
257+
parser: _CommandOracleOutputTypes | None = Field(
258+
default=None,
259+
description=(
260+
"The type of verifier output to expect. Current options: "
261+
f"{", ".join(t.value for t in _CommandOracleOutputTypes)}"
262+
),
263+
)
264+
265+
@field_validator("parser", mode="before")
266+
@classmethod
267+
def on_set_parser(cls, value: str | None) -> _CommandOracleOutputTypes | None:
268+
if not value:
269+
return None
270+
271+
value_upper: str = value.upper()
272+
if value_upper in _CommandOracleOutputTypes:
273+
return _CommandOracleOutputTypes(value_upper)
274+
275+
raise ValueError(f"Invalid parser set: {value}")
276+
277+
234278
class ESBMCConfig(BaseModel):
235279
"""ESBMC-specific configuration.
236280
@@ -287,6 +331,11 @@ class VerifierConfig(BaseModel):
287331
"This is not supported by all verifiers.",
288332
)
289333

334+
command_oracle: CommandOracleConfig = Field(
335+
default_factory=CommandOracleConfig,
336+
description='Command oracle "command-oracle" specific configuration.',
337+
)
338+
290339
esbmc: ESBMCConfig = Field(
291340
default_factory=ESBMCConfig,
292341
description="ESBMC-specific configuration.",

esbmc_ai/solution.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ def formatted(self) -> str:
239239
def format_as(
240240
self,
241241
style: _SourceFileFormatStyles = "markdown",
242-
include_line_numbers: bool = False,
242+
include_line_numbers: bool = True,
243243
max_lines: int | None = None,
244244
working_dir: Path | None = None,
245245
) -> str:

esbmc_ai/verifiers/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from .base_source_verifier import BaseSourceVerifier
22
from .esbmc import ESBMC
3+
from .cmd_oracle import CommandOracle
34

4-
__all__ = ["BaseSourceVerifier", "ESBMC"]
5+
__all__ = ["BaseSourceVerifier", "ESBMC", "CommandOracle"]

esbmc_ai/verifiers/base_source_verifier.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
from abc import abstractmethod
66
from pathlib import Path
7+
from time import perf_counter
8+
from subprocess import PIPE, STDOUT, run, CompletedProcess
79
from typing import Any, override
810
from hashlib import sha256
911
import pickle
@@ -140,3 +142,30 @@ def verify_source(
140142
"""Verifies source_file."""
141143
_ = solution
142144
raise NotImplementedError()
145+
146+
def run_command(
147+
self,
148+
cmd: list[str],
149+
cwd: Path,
150+
process_timeout: float | None,
151+
) -> tuple[CompletedProcess, float]:
152+
"""Runs the verifier."""
153+
154+
# Add slack time to process to allow verifier to timeout and end gracefully.
155+
process_timeout = process_timeout + 5 if process_timeout else None
156+
# Measure execution time
157+
start_time = perf_counter()
158+
159+
# Run ESBMC from solution working_dir and get output
160+
process: CompletedProcess = run(
161+
cmd,
162+
cwd=cwd,
163+
timeout=process_timeout,
164+
stdout=PIPE,
165+
stderr=STDOUT,
166+
check=False,
167+
)
168+
169+
duration: float = perf_counter() - start_time
170+
171+
return process, duration

0 commit comments

Comments
 (0)