Initial import
This commit is contained in:
@@ -0,0 +1,802 @@
|
||||
# Sub-plan 5 — Quality & CI (Tasks 10–14)
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans.
|
||||
|
||||
**Goal:** Integration tests (JSON-RPC roundtrip), property-based tests, Criterion benchmarks, Qiskit cross-validation golden files, and GitHub Actions CI + release workflows.
|
||||
|
||||
**Starting state (after sub-plan 4):**
|
||||
- Full working MCP server binary
|
||||
- All tool handlers implemented and unit-tested
|
||||
- `cargo test` passes, `cargo build` succeeds
|
||||
|
||||
**Deliverable:** `cargo test` passes all test suites. `cargo bench` compiles. CI YAML is valid. Five commits added.
|
||||
|
||||
**Important — lib target:** Tasks 11–12 need `use quantum_bridge_mcp::...` from `tests/`. This requires adding `src/lib.rs` and a `[lib]` entry in `Cargo.toml`. Task 11 includes these steps.
|
||||
|
||||
**Main plan reference:** `docs/superpowers/plans/2026-04-28-quantum-bridge-mcp-v1.md` Tasks 10–14.
|
||||
|
||||
---
|
||||
|
||||
## Task 10: Integration Tests — MCP protocol roundtrip
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/integration/mod.rs`
|
||||
- Create: `tests/integration/mcp_protocol.rs`
|
||||
|
||||
- [ ] **Step 1: Create test directory**
|
||||
|
||||
```bash
|
||||
mkdir -p tests/integration
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create tests/integration/mod.rs**
|
||||
|
||||
```rust
|
||||
// Integration test module — empty file for cargo test discovery.
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Create tests/integration/mcp_protocol.rs**
|
||||
|
||||
```rust
|
||||
//! Roundtrip: spawn binary, send JSON-RPC on stdin, assert stdout.
|
||||
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::process::{Child, Command, Stdio};
|
||||
|
||||
struct McpProcess { child: Child }
|
||||
|
||||
impl McpProcess {
|
||||
fn spawn() -> Self {
|
||||
let child = Command::new(env!("CARGO_BIN_EXE_quantum-bridge-mcp"))
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.expect("failed to spawn binary");
|
||||
Self { child }
|
||||
}
|
||||
|
||||
fn send(&mut self, msg: &str) {
|
||||
let stdin = self.child.stdin.as_mut().unwrap();
|
||||
writeln!(stdin, "{}", msg).unwrap();
|
||||
stdin.flush().unwrap();
|
||||
}
|
||||
|
||||
fn recv_line(&mut self) -> String {
|
||||
let stdout = self.child.stdout.as_mut().unwrap();
|
||||
let mut reader = BufReader::new(stdout);
|
||||
let mut line = String::new();
|
||||
reader.read_line(&mut line).unwrap();
|
||||
line.trim().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for McpProcess {
|
||||
fn drop(&mut self) { let _ = self.child.kill(); }
|
||||
}
|
||||
|
||||
fn initialize(proc: &mut McpProcess) -> serde_json::Value {
|
||||
proc.send(r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.0.1"}}}"#);
|
||||
serde_json::from_str(&proc.recv_line()).expect("valid JSON")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initialize_returns_server_info() {
|
||||
let mut proc = McpProcess::spawn();
|
||||
let resp = initialize(&mut proc);
|
||||
assert!(resp["result"]["serverInfo"]["name"].as_str().is_some());
|
||||
assert_eq!(resp["result"]["protocolVersion"], "2024-11-05");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tools_list_contains_three_tools() {
|
||||
let mut proc = McpProcess::spawn();
|
||||
initialize(&mut proc);
|
||||
proc.send(r#"{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}"#);
|
||||
let resp: serde_json::Value = serde_json::from_str(&proc.recv_line()).unwrap();
|
||||
let tools = resp["result"]["tools"].as_array().unwrap();
|
||||
assert_eq!(tools.len(), 3);
|
||||
let names: Vec<&str> = tools.iter().map(|t| t["name"].as_str().unwrap()).collect();
|
||||
assert!(names.contains(&"list_backends"));
|
||||
assert!(names.contains(&"validate_circuit"));
|
||||
assert!(names.contains(&"run_circuit"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_list_backends_returns_local_simulator() {
|
||||
let mut proc = McpProcess::spawn();
|
||||
initialize(&mut proc);
|
||||
proc.send(r#"{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"list_backends","arguments":{}}}"#);
|
||||
let resp: serde_json::Value = serde_json::from_str(&proc.recv_line()).unwrap();
|
||||
let text = resp["result"]["content"][0]["text"].as_str().unwrap();
|
||||
let payload: serde_json::Value = serde_json::from_str(text).unwrap();
|
||||
assert_eq!(payload["backends"][0]["name"], "local_simulator");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_validate_circuit_with_valid_bell_returns_is_valid_true() {
|
||||
let bell = "OPENQASM 3.0;\\ninclude \\\"stdgates.inc\\\";\\nqubit[2] q;\\nbit[2] c;\\nh q[0];\\ncx q[0], q[1];\\nc = measure q;";
|
||||
let call = format!(
|
||||
r#"{{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{{"name":"validate_circuit","arguments":{{"circuit":"{}"}}}}}}"#,
|
||||
bell
|
||||
);
|
||||
let mut proc = McpProcess::spawn();
|
||||
initialize(&mut proc);
|
||||
proc.send(&call);
|
||||
let resp: serde_json::Value = serde_json::from_str(&proc.recv_line()).unwrap();
|
||||
let text = resp["result"]["content"][0]["text"].as_str().unwrap();
|
||||
let payload: serde_json::Value = serde_json::from_str(text).unwrap();
|
||||
assert_eq!(payload["is_valid"], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_run_circuit_x_gate_returns_100_ones() {
|
||||
let x_circ = "OPENQASM 3.0;\\ninclude \\\"stdgates.inc\\\";\\nqubit[1] q;\\nbit[1] c;\\nx q[0];\\nc = measure q;";
|
||||
let call = format!(
|
||||
r#"{{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{{"name":"run_circuit","arguments":{{"circuit":"{}","shots":100}}}}}}"#,
|
||||
x_circ
|
||||
);
|
||||
let mut proc = McpProcess::spawn();
|
||||
initialize(&mut proc);
|
||||
proc.send(&call);
|
||||
let resp: serde_json::Value = serde_json::from_str(&proc.recv_line()).unwrap();
|
||||
let text = resp["result"]["content"][0]["text"].as_str().unwrap();
|
||||
let payload: serde_json::Value = serde_json::from_str(text).unwrap();
|
||||
assert_eq!(payload["counts"]["1"].as_u64().unwrap(), 100);
|
||||
}
|
||||
```
|
||||
|
||||
> **Note:** Uses `env!("CARGO_BIN_EXE_quantum-bridge-mcp")` — cargo sets this automatically. Run with `cargo test --test integration`. If rmcp uses length-prefixed framing instead of newlines, adjust `recv_line` to skip the header bytes.
|
||||
|
||||
- [ ] **Step 4: Run integration tests**
|
||||
|
||||
```bash
|
||||
cargo test --test integration 2>&1
|
||||
```
|
||||
|
||||
Expected: all 5 pass.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add tests/integration/
|
||||
git commit -m "test: add MCP protocol integration tests (initialize, tools/list, tools/call)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 11: Property-Based Tests + lib target
|
||||
|
||||
**Files:**
|
||||
- Create: `src/lib.rs`
|
||||
- Modify: `Cargo.toml` (add `[lib]` section)
|
||||
- Modify: `src/main.rs` (remove mod declarations now in lib.rs)
|
||||
- Create: `tests/proptest/mod.rs`
|
||||
- Create: `tests/proptest/invariants.rs`
|
||||
|
||||
- [ ] **Step 1: Create src/lib.rs**
|
||||
|
||||
```rust
|
||||
pub mod error;
|
||||
pub mod executor;
|
||||
pub mod types;
|
||||
pub mod validator;
|
||||
pub(crate) mod tools;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add [lib] to Cargo.toml**
|
||||
|
||||
```toml
|
||||
[lib]
|
||||
name = "quantum_bridge_mcp"
|
||||
path = "src/lib.rs"
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update src/main.rs — remove mod declarations, use crate name**
|
||||
|
||||
```rust
|
||||
use anyhow::Result;
|
||||
use quantum_bridge_mcp::tools::QuantumBridgeServer;
|
||||
use rmcp::{ServiceExt, transport::stdio};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_writer(std::io::stderr)
|
||||
.with_ansi(false)
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::from_default_env()
|
||||
.add_directive(tracing::Level::INFO.into()),
|
||||
)
|
||||
.init();
|
||||
|
||||
tracing::info!("quantum-bridge-mcp starting");
|
||||
|
||||
let service = QuantumBridgeServer::new().serve(stdio()).await?;
|
||||
service.waiting().await?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Verify cargo test still passes**
|
||||
|
||||
```bash
|
||||
cargo test 2>&1
|
||||
```
|
||||
|
||||
Expected: all existing tests still pass.
|
||||
|
||||
- [ ] **Step 5: Create proptest files**
|
||||
|
||||
```bash
|
||||
mkdir -p tests/proptest
|
||||
```
|
||||
|
||||
`tests/proptest/mod.rs`:
|
||||
```rust
|
||||
// Property-based test module.
|
||||
```
|
||||
|
||||
`tests/proptest/invariants.rs`:
|
||||
|
||||
```rust
|
||||
//! Quantum mechanics invariants that must hold for any valid circuit.
|
||||
|
||||
use proptest::prelude::*;
|
||||
use quantum_bridge_mcp::executor::{CanExecute, LocalSimulator};
|
||||
use quantum_bridge_mcp::types::{CircuitSource, ShotCount};
|
||||
|
||||
proptest! {
|
||||
/// Shot counts always sum to exactly the requested number of shots.
|
||||
#[test]
|
||||
fn shot_counts_always_sum_to_requested_shots(shots in 1u32..=1_000u32) {
|
||||
let circuit = r#"OPENQASM 3.0;
|
||||
include "stdgates.inc";
|
||||
qubit[2] q;
|
||||
bit[2] c;
|
||||
h q[0];
|
||||
cx q[0], q[1];
|
||||
c = measure q;"#;
|
||||
let sim = LocalSimulator::new();
|
||||
let result = sim.run(&CircuitSource(circuit.to_string()), ShotCount(shots), false).unwrap();
|
||||
let total: u64 = result.counts.values().sum();
|
||||
prop_assert_eq!(total, shots as u64);
|
||||
}
|
||||
|
||||
/// Statevector norm is always 1.0 (±1e-9).
|
||||
#[test]
|
||||
fn statevector_norm_is_one(angle in -std::f64::consts::PI..=std::f64::consts::PI) {
|
||||
let circuit = format!(
|
||||
"OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nrx({angle}) q[0];\nc = measure q;"
|
||||
);
|
||||
let sim = LocalSimulator::new();
|
||||
let result = sim.run(&CircuitSource(circuit), ShotCount(1), true).unwrap();
|
||||
let sv = result.statevector.unwrap();
|
||||
let norm: f64 = sv.iter().map(|(r, i)| r * r + i * i).sum();
|
||||
prop_assert!((norm - 1.0).abs() < 1e-9, "norm={norm}");
|
||||
}
|
||||
|
||||
/// Bitstrings in counts have length equal to the number of qubits.
|
||||
#[test]
|
||||
fn count_bitstrings_have_correct_length(
|
||||
n_qubits in 1usize..=5usize,
|
||||
shots in 10u32..=100u32,
|
||||
) {
|
||||
let gates: String = (0..n_qubits).map(|i| format!("h q[{i}];\n")).collect();
|
||||
let circuit = format!(
|
||||
"OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[{n_qubits}] q;\nbit[{n_qubits}] c;\n{gates}c = measure q;\n"
|
||||
);
|
||||
let sim = LocalSimulator::new();
|
||||
let result = sim.run(&CircuitSource(circuit), ShotCount(shots), false).unwrap();
|
||||
for key in result.counts.keys() {
|
||||
prop_assert_eq!(key.len(), n_qubits, "wrong length for key '{key}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Run proptest**
|
||||
|
||||
```bash
|
||||
cargo test --test proptest 2>&1
|
||||
```
|
||||
|
||||
Expected: 3 property tests pass (each runs 256 cases).
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add src/lib.rs src/main.rs Cargo.toml tests/proptest/
|
||||
git commit -m "test: add property-based tests for shot normalisation, statevector norm, and bitstring length"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 12: Benchmarks
|
||||
|
||||
**Files:**
|
||||
- Create: `benches/simulation.rs`
|
||||
|
||||
- [ ] **Step 1: Create bench file**
|
||||
|
||||
```bash
|
||||
mkdir -p benches
|
||||
```
|
||||
|
||||
`benches/simulation.rs`:
|
||||
|
||||
```rust
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use quantum_bridge_mcp::executor::{CanExecute, LocalSimulator};
|
||||
use quantum_bridge_mcp::types::{CircuitSource, ShotCount};
|
||||
|
||||
const BELL: &str = r#"OPENQASM 3.0;
|
||||
include "stdgates.inc";
|
||||
qubit[2] q;
|
||||
bit[2] c;
|
||||
h q[0];
|
||||
cx q[0], q[1];
|
||||
c = measure q;"#;
|
||||
|
||||
fn make_h_circuit(n: usize, shots: u32) -> (CircuitSource, ShotCount) {
|
||||
let gates: String = (0..n).map(|i| format!("h q[{i}];\n")).collect();
|
||||
(
|
||||
CircuitSource(format!(
|
||||
"OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[{n}] q;\nbit[{n}] c;\n{gates}c = measure q;\n"
|
||||
)),
|
||||
ShotCount(shots),
|
||||
)
|
||||
}
|
||||
|
||||
fn bench_bell_1024_shots(c: &mut Criterion) {
|
||||
let sim = LocalSimulator::new();
|
||||
c.bench_function("bell_1024_shots", |b| {
|
||||
b.iter(|| sim.run(&CircuitSource(BELL.to_string()), ShotCount(1024), false).unwrap())
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_10_qubits_10k_shots(c: &mut Criterion) {
|
||||
let sim = LocalSimulator::new();
|
||||
let (circ, shots) = make_h_circuit(10, 10_000);
|
||||
c.bench_function("10_qubits_10k_shots", |b| {
|
||||
b.iter(|| sim.run(&circ, shots, false).unwrap())
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_20_qubits_10k_shots(c: &mut Criterion) {
|
||||
let sim = LocalSimulator::new();
|
||||
let (circ, shots) = make_h_circuit(20, 10_000);
|
||||
c.bench_function("20_qubits_10k_shots", |b| {
|
||||
b.iter(|| sim.run(&circ, shots, false).unwrap())
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_bell_1024_shots, bench_10_qubits_10k_shots, bench_20_qubits_10k_shots);
|
||||
criterion_main!(benches);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run benchmarks (smoke pass — verifies they compile and run)**
|
||||
|
||||
```bash
|
||||
cargo bench -- --test 2>&1
|
||||
```
|
||||
|
||||
Expected: all three benchmark functions report a timing without panicking.
|
||||
|
||||
- [ ] **Step 3: Run full benchmarks to check SLAs**
|
||||
|
||||
```bash
|
||||
cargo bench 2>&1 | grep -E "bell|qubits"
|
||||
```
|
||||
|
||||
SLA targets (spec §6):
|
||||
- `bell_1024_shots` < **5 ms**
|
||||
- `10_qubits_10k_shots` < **50 ms**
|
||||
- `20_qubits_10k_shots` < **500 ms**
|
||||
|
||||
If any target is missed, the bottleneck is likely `extract_gate_ops` allocating on every call. Check if the AST is being re-parsed unnecessarily — consider caching or pre-converting on first call.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add benches/simulation.rs
|
||||
git commit -m "bench: add Criterion benchmarks for Bell, 10-qubit, and 20-qubit circuits"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 13: Reference Golden Files + Cross-Validation
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/reference/bell.qasm`
|
||||
- Create: `tests/reference/ghz3.qasm`
|
||||
- Create: `tests/reference/invalid/` (4 × .qasm + .error.txt pairs)
|
||||
- Create: `scripts/gen_reference.py`
|
||||
- Create: `tests/reference/cross_validate.rs`
|
||||
|
||||
- [ ] **Step 1: Create directory structure and circuit files**
|
||||
|
||||
```bash
|
||||
mkdir -p tests/reference/invalid scripts
|
||||
```
|
||||
|
||||
`tests/reference/bell.qasm`:
|
||||
```
|
||||
OPENQASM 3.0;
|
||||
include "stdgates.inc";
|
||||
qubit[2] q;
|
||||
bit[2] c;
|
||||
h q[0];
|
||||
cx q[0], q[1];
|
||||
c = measure q;
|
||||
```
|
||||
|
||||
`tests/reference/ghz3.qasm`:
|
||||
```
|
||||
OPENQASM 3.0;
|
||||
include "stdgates.inc";
|
||||
qubit[3] q;
|
||||
bit[3] c;
|
||||
h q[0];
|
||||
cx q[0], q[1];
|
||||
cx q[1], q[2];
|
||||
c = measure q;
|
||||
```
|
||||
|
||||
`tests/reference/invalid/unsupported_gate.qasm`:
|
||||
```
|
||||
OPENQASM 3.0;
|
||||
include "stdgates.inc";
|
||||
qubit[1] q;
|
||||
u3(0.1, 0.2, 0.3) q[0];
|
||||
```
|
||||
|
||||
`tests/reference/invalid/unsupported_gate.error.txt`:
|
||||
```
|
||||
gate 'u3' is not supported
|
||||
```
|
||||
|
||||
`tests/reference/invalid/undeclared_qubit.qasm`:
|
||||
```
|
||||
OPENQASM 3.0;
|
||||
include "stdgates.inc";
|
||||
qubit[1] q;
|
||||
h q[5];
|
||||
```
|
||||
|
||||
`tests/reference/invalid/undeclared_qubit.error.txt`:
|
||||
```
|
||||
out of range
|
||||
```
|
||||
|
||||
`tests/reference/invalid/syntax_error.qasm`:
|
||||
```
|
||||
OPENQASM 3.0;
|
||||
this is not valid;
|
||||
```
|
||||
|
||||
`tests/reference/invalid/syntax_error.error.txt`:
|
||||
```
|
||||
error
|
||||
```
|
||||
|
||||
`tests/reference/invalid/qubit_limit.qasm`:
|
||||
```
|
||||
OPENQASM 3.0;
|
||||
qubit[29] q;
|
||||
```
|
||||
|
||||
`tests/reference/invalid/qubit_limit.error.txt`:
|
||||
```
|
||||
exceeds
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Write scripts/gen_reference.py**
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""Generate reference counts/statevectors via Qiskit Aer. Requires: pip install qiskit qiskit-aer"""
|
||||
import json, sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from qiskit import QuantumCircuit
|
||||
from qiskit_aer import AerSimulator
|
||||
except ImportError:
|
||||
print("ERROR: pip install qiskit qiskit-aer", file=sys.stderr); sys.exit(1)
|
||||
|
||||
SHOTS = 10_000
|
||||
SEED = 42
|
||||
REF_DIR = Path(__file__).parent.parent / "tests" / "reference"
|
||||
|
||||
def run_circuit(qasm_path: Path) -> dict:
|
||||
qc = QuantumCircuit.from_qasm_file(str(qasm_path))
|
||||
sim = AerSimulator(method="statevector")
|
||||
counts = sim.run(qc, shots=SHOTS, seed_simulator=SEED).result().get_counts()
|
||||
qc_sv = QuantumCircuit.from_qasm_file(str(qasm_path))
|
||||
qc_sv.save_statevector()
|
||||
sv = list(sim.run(qc_sv, shots=0).result().get_statevector())
|
||||
return {
|
||||
"shots": SHOTS, "seed": SEED,
|
||||
"counts": {k: int(v) for k, v in counts.items()},
|
||||
"statevector": [[float(a.real), float(a.imag)] for a in sv],
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
for qasm in REF_DIR.glob("*.qasm"):
|
||||
out = REF_DIR / qasm.with_suffix(".json").name
|
||||
data = run_circuit(qasm)
|
||||
out.write_text(json.dumps(data, indent=2))
|
||||
print(f"wrote {out}")
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Write tests/reference/cross_validate.rs**
|
||||
|
||||
```rust
|
||||
//! Cross-validates LocalSimulator counts against Qiskit golden files (chi-squared, α=0.01).
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
use quantum_bridge_mcp::executor::{CanExecute, LocalSimulator, MAX_LOCAL_QUBITS};
|
||||
use quantum_bridge_mcp::types::{CircuitSource, ShotCount};
|
||||
use quantum_bridge_mcp::validator::CircuitValidator;
|
||||
|
||||
fn load_circuit(name: &str) -> CircuitSource {
|
||||
CircuitSource(std::fs::read_to_string(Path::new("tests/reference").join(name)).unwrap())
|
||||
}
|
||||
|
||||
fn load_expected_counts(name: &str) -> HashMap<String, u64> {
|
||||
let path = Path::new("tests/reference").join(name);
|
||||
if !path.exists() { return HashMap::new(); }
|
||||
let data: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap();
|
||||
data["counts"].as_object().unwrap().iter()
|
||||
.map(|(k, v)| (k.clone(), v.as_u64().unwrap()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn chi_squared_passes(observed: &HashMap<String, u64>, expected: &HashMap<String, u64>) -> bool {
|
||||
if expected.is_empty() { return true; }
|
||||
let chi_sq: f64 = expected.iter().map(|(k, &exp)| {
|
||||
let obs = *observed.get(k).unwrap_or(&0) as f64;
|
||||
let e = exp as f64;
|
||||
if e == 0.0 { 0.0 } else { (obs - e).powi(2) / e }
|
||||
}).sum();
|
||||
let critical = (expected.len() - 1) as f64 * 3.0 + 6.63; // conservative
|
||||
chi_sq < critical
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bell_counts_match_qiskit_golden() {
|
||||
let expected = load_expected_counts("bell_counts.json");
|
||||
if expected.is_empty() { return; }
|
||||
let result = LocalSimulator::new().run(&load_circuit("bell.qasm"), ShotCount(10_000), false).unwrap();
|
||||
assert!(chi_squared_passes(&result.counts, &expected),
|
||||
"Bell diverges.\nSpinoza: {:?}\nQiskit: {:?}", result.counts, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ghz3_counts_match_qiskit_golden() {
|
||||
let expected = load_expected_counts("ghz3_counts.json");
|
||||
if expected.is_empty() { return; }
|
||||
let result = LocalSimulator::new().run(&load_circuit("ghz3.qasm"), ShotCount(10_000), false).unwrap();
|
||||
assert!(chi_squared_passes(&result.counts, &expected),
|
||||
"GHZ-3 diverges.\nSpinoza: {:?}\nQiskit: {:?}", result.counts, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_circuits_produce_validation_errors() {
|
||||
use std::fs;
|
||||
let validator = CircuitValidator::new(MAX_LOCAL_QUBITS);
|
||||
for entry in fs::read_dir("tests/reference/invalid").unwrap() {
|
||||
let path = entry.unwrap().path();
|
||||
if path.extension().map(|e| e == "qasm").unwrap_or(false) {
|
||||
let expected_frag = fs::read_to_string(path.with_extension("error.txt"))
|
||||
.unwrap().trim().to_lowercase();
|
||||
let source = fs::read_to_string(&path).unwrap();
|
||||
let result = validator.validate(&quantum_bridge_mcp::types::CircuitSource(source)).unwrap();
|
||||
assert!(!result.is_valid, "{} should be invalid", path.display());
|
||||
let msgs: String = result.diagnostics.iter().map(|d| d.message.to_lowercase()).collect::<Vec<_>>().join(" ");
|
||||
assert!(msgs.contains(&expected_frag),
|
||||
"expected '{}' in messages for {}\nGot: {}", expected_frag, path.display(), msgs);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run cross-validation tests**
|
||||
|
||||
```bash
|
||||
cargo test --test cross_validate 2>&1
|
||||
```
|
||||
|
||||
Expected: `bell_counts_match_qiskit_golden` and `ghz3_counts_match_qiskit_golden` are **skipped** (no JSON yet); `invalid_circuits_produce_validation_errors` **passes**.
|
||||
|
||||
Optional — generate golden files (needs Python + Qiskit):
|
||||
```bash
|
||||
python3 scripts/gen_reference.py && cargo test --test cross_validate
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add tests/reference/ scripts/gen_reference.py
|
||||
git commit -m "test: add cross-validation golden files and gen_reference.py script"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 14: GitHub Actions CI + Release
|
||||
|
||||
**Files:**
|
||||
- Create: `.github/workflows/ci.yml`
|
||||
- Create: `.github/workflows/release.yml`
|
||||
|
||||
- [ ] **Step 1: Create .github/workflows/ci.yml**
|
||||
|
||||
```bash
|
||||
mkdir -p .github/workflows
|
||||
```
|
||||
|
||||
```yaml
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, master]
|
||||
pull_request:
|
||||
branches: [main, master]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUST_BACKTRACE: 1
|
||||
|
||||
jobs:
|
||||
rust:
|
||||
name: Rust checks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: ${{ runner.os }}-cargo-
|
||||
- run: cargo fmt --check
|
||||
- run: cargo clippy -- -D warnings
|
||||
- run: cargo test
|
||||
- run: cargo bench -- --test
|
||||
|
||||
cross-validate:
|
||||
name: Cross-validate with Qiskit
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-qiskit
|
||||
- run: pip install qiskit qiskit-aer
|
||||
- run: python3 scripts/gen_reference.py
|
||||
- run: cargo test --test cross_validate
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create .github/workflows/release.yml**
|
||||
|
||||
```yaml
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
dist:
|
||||
name: Build ${{ matrix.target }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- { os: ubuntu-latest, target: x86_64-unknown-linux-gnu }
|
||||
- { os: ubuntu-latest, target: aarch64-unknown-linux-gnu }
|
||||
- { os: macos-latest, target: x86_64-apple-darwin }
|
||||
- { os: macos-latest, target: aarch64-apple-darwin }
|
||||
- { os: windows-latest, target: x86_64-pc-windows-msvc }
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
- name: Install cross (aarch64 Linux)
|
||||
if: matrix.target == 'aarch64-unknown-linux-gnu'
|
||||
run: cargo install cross
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "${{ matrix.target }}" = "aarch64-unknown-linux-gnu" ]; then
|
||||
cross build --release --target ${{ matrix.target }}
|
||||
else
|
||||
cargo build --release --target ${{ matrix.target }}
|
||||
fi
|
||||
- name: Package (Unix)
|
||||
if: matrix.os != 'windows-latest'
|
||||
run: |
|
||||
cd target/${{ matrix.target }}/release
|
||||
tar czf quantum-bridge-mcp-${{ matrix.target }}.tar.gz quantum-bridge-mcp
|
||||
mv quantum-bridge-mcp-${{ matrix.target }}.tar.gz ../../../
|
||||
- name: Package (Windows)
|
||||
if: matrix.os == 'windows-latest'
|
||||
shell: pwsh
|
||||
run: |
|
||||
cd target/${{ matrix.target }}/release
|
||||
Compress-Archive quantum-bridge-mcp.exe quantum-bridge-mcp-${{ matrix.target }}.zip
|
||||
mv quantum-bridge-mcp-${{ matrix.target }}.zip ../../../
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: quantum-bridge-mcp-${{ matrix.target }}
|
||||
path: quantum-bridge-mcp-${{ matrix.target }}.*
|
||||
|
||||
github-release:
|
||||
needs: dist
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
merge-multiple: true
|
||||
- uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: "quantum-bridge-mcp-*"
|
||||
generate_release_notes: true
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Validate YAML**
|
||||
|
||||
```bash
|
||||
python3 -c "import yaml; yaml.safe_load(open('.github/workflows/ci.yml'))" && echo "ci.yml OK"
|
||||
python3 -c "import yaml; yaml.safe_load(open('.github/workflows/release.yml'))" && echo "release.yml OK"
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Full local verification pass**
|
||||
|
||||
```bash
|
||||
cargo fmt --check && cargo clippy -- -D warnings && cargo test 2>&1 | tail -10
|
||||
```
|
||||
|
||||
Expected: all green.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add .github/
|
||||
git commit -m "ci: add GitHub Actions for Rust checks, Qiskit cross-validation, and multi-platform release"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Final verification — all sub-plans complete
|
||||
|
||||
```bash
|
||||
cargo fmt --check \
|
||||
&& cargo clippy -- -D warnings \
|
||||
&& cargo test \
|
||||
&& cargo test --test integration \
|
||||
&& cargo test --test proptest \
|
||||
&& cargo test --test cross_validate \
|
||||
&& cargo bench -- --test \
|
||||
&& echo "ALL CHECKS PASSED"
|
||||
```
|
||||
|
||||
Expected: `ALL CHECKS PASSED`. V1 implementation complete.
|
||||
Reference in New Issue
Block a user