Repository navigation
feat: Add test-suite validation to Qiskit IVR example #1716
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0cd414f
0f8070e
d3cf27e
36c82c9
2f5a50f
87f59dc
56a20a0
58cf1d6
46a68fb
c4d02d3
01d56f1
da2b6cd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,11 +2,19 @@ | |
| """Helper functions for Qiskit code validation. | ||
|
|
||
| This module provides utilities for extracting code from markdown and validating | ||
| Qiskit code against migration rules using the flake8-qiskit-migration plugin. | ||
| Qiskit code against migration rules using the flake8-qiskit-migration plugin, | ||
| and against a benchmark problem's own test suite. | ||
| """ | ||
|
|
||
| import ast | ||
| import builtins | ||
| import ctypes | ||
| import os | ||
| import re | ||
| import tempfile | ||
| import threading | ||
| import traceback | ||
| from typing import Any | ||
|
|
||
| try: | ||
| from flake8_qiskit_migration.plugin import Plugin | ||
|
|
@@ -108,3 +116,121 @@ def validate_input_code(prompt: str) -> tuple[bool, str]: | |
| return False, error_msg | ||
|
|
||
| return True, "" | ||
|
|
||
|
|
||
| # Time limit for running a candidate against its tests. | ||
| TIMEOUT_SECONDS = 30 | ||
|
|
||
|
|
||
| class _Timeout(BaseException): | ||
| """Raised into the validating thread when the code runs too long. | ||
|
|
||
| A `BaseException`, unlike `TimeoutError`, so generated code that catches | ||
| `Exception` cannot swallow it. | ||
| """ | ||
|
|
||
|
|
||
| # Builtins the executed code does not get: they could escape or stall the check. | ||
| # This is defense in depth, not a sandbox: `__import__` is kept for `import` | ||
| # statements, so the executed code can still use any module. | ||
| _SAFE_BUILTINS = { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Optional:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Solved in gretadolcetti@01d56f1 |
||
| k: v | ||
| for k, v in vars(builtins).items() | ||
| if k not in {"exec", "eval", "compile", "breakpoint", "input"} | ||
| } | ||
|
|
||
|
|
||
| def _run_tests(program: str, entry: str, test_name: str) -> tuple[bool, str]: | ||
| """Execute `program` and call its test function, stopping it after the time limit.""" | ||
| # Mellea validates on a worker thread, so the timeout is raised via the C API. | ||
| tid = ctypes.c_ulong(threading.get_ident()) | ||
| timer = threading.Timer( | ||
| TIMEOUT_SECONDS, | ||
| ctypes.pythonapi.PyThreadState_SetAsyncExc, | ||
| (tid, ctypes.py_object(_Timeout)), | ||
| ) | ||
| try: | ||
| try: | ||
| timer.start() | ||
| ns: dict[str, Any] = {"__builtins__": _SAFE_BUILTINS} | ||
| exec(program, ns) | ||
| if entry not in ns: | ||
| return False, f"Entry point `{entry}` not defined" | ||
| if test_name == "check": | ||
| ns["check"](ns[entry]) | ||
| else: | ||
| ns[test_name]() | ||
| return True, "" | ||
| finally: | ||
| timer.cancel() | ||
| timer.join() | ||
| # cancel() cannot recall a _Timeout the timer already raised into this | ||
| # thread: clear it, so it cannot surface later inside Mellea. If it is | ||
| # delivered first, the except below still catches it. | ||
| ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None) | ||
| except _Timeout: | ||
| print(f"Validation failed: Execution timed out after {TIMEOUT_SECONDS}s") | ||
| return False, f"Execution timed out after {TIMEOUT_SECONDS}s" | ||
| except SystemExit as e: | ||
| # Not an Exception: left uncaught, it would stop the repair loop. | ||
| print(f"Validation failed: the code called sys.exit({e.code!r})") | ||
| return False, f"The code called sys.exit({e.code!r}) instead of returning" | ||
| except AssertionError as e: | ||
| print(f"Validation failed: Test assertion failed: {e}") | ||
| return False, f"Test assertion failed: {e}" | ||
| except Exception as e: | ||
| print(f"{type(e).__name__}: {e}\n{traceback.format_exc()}") | ||
| return False, f"{type(e).__name__}: {e}\n{traceback.format_exc()}" | ||
|
|
||
|
|
||
| def validate_correctness(problem: dict[str, Any], code: str) -> tuple[bool, str]: | ||
| """Run `code` against a benchmark problem's own test suite. | ||
|
|
||
| Adapted from the Qiskit HumanEval test harness (Copyright IBM, Apache-2.0). | ||
| Supports Qiskit HumanEval (`check(candidate)`) and QuantumKatas | ||
| (`test_<entry_point>()`). | ||
|
|
||
| Warning: this `exec()`s untrusted code in the current process; it is not a | ||
| sandbox. The time limit is best effort: it is only checked between Python | ||
| bytecodes, so code blocked inside a C call (a simulator run, `time.sleep`) is | ||
| stopped only once that call returns, and a bare `except:` in the code can | ||
| swallow it. | ||
|
|
||
| Args: | ||
| problem: Problem dict with `prompt`, `test` and `entry_point` keys | ||
| code: Candidate solution | ||
|
|
||
| Returns: | ||
| Tuple of (is_valid, error_message) | ||
| """ | ||
| # Code-stub prompts are prepended; natural-language prompts (QHE hard) are not. | ||
| prompt = problem["prompt"] | ||
| is_stub = prompt.lstrip().startswith(("from ", "import ", "def ", "#")) | ||
| entry = problem["entry_point"] | ||
| program = f"{prompt if is_stub else ''}\n{code}\n{problem['test']}" | ||
|
|
||
| # Call the test function the test code defines, so a candidate that defines its | ||
| # own `check()` cannot stand in for the real test. | ||
| test_fns = { | ||
| node.name | ||
| for node in ast.parse(problem["test"]).body | ||
| if isinstance(node, ast.FunctionDef) | ||
| } | ||
| test_name = "check" if "check" in test_fns else f"test_{entry}" | ||
| if test_name not in test_fns: | ||
| return False, f"No check() or test_{entry}() in the test code" | ||
|
|
||
| # Run in a scratch directory (some tasks write files) with a non-interactive | ||
| # matplotlib backend, and restore both afterwards. | ||
| cwd, mpl_backend = os.getcwd(), os.environ.get("MPLBACKEND") | ||
| os.environ["MPLBACKEND"] = "Agg" | ||
|
ajbozarth marked this conversation as resolved.
Resolved
|
||
| with tempfile.TemporaryDirectory() as tmp: | ||
| os.chdir(tmp) | ||
| try: | ||
| return _run_tests(program, entry, test_name) | ||
| finally: | ||
| os.chdir(cwd) | ||
| if mpl_backend is None: | ||
| os.environ.pop("MPLBACKEND", None) | ||
| else: | ||
| os.environ["MPLBACKEND"] = mpl_backend | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since
validate_correctnessis deliberately not wired into the example, this snippet is the only thing justifying the helper's inclusion — so it has to get a reader from the downloaded dataset to a call, and it doesn't yet. Setup downloads a.jsonl, but nothing turns it into theproblemdict, andm/strategyare undefined. One-line fix, plus a pointer:And state that the snippet is a drop-in into the example's existing setup — as written it reads as a description of the helper, not instructions for using it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Solved in gretadolcetti@01d56f1