#![cfg(feature = "cli")]
use std::ffi::OsString;
use std::fs;
use std::path::{Path, PathBuf};
use clap::Parser;
use qec_code::QecError;
use qec_code::cli::{Cli, run};
use qec_code::css::SparseRowsMatrix;
use qec_code::family_contract::{
CssFamilySpec, GeneralizedBicycleSpec, RequestedFamilyId, construct_css,
parse_css_construction_json, verify_css_orthogonality,
};
use tempfile::tempdir;
fn qec_code_bin() -> &'static str {
env!("CARGO_BIN_EXE_qec-code")
}
fn fixture_spec() -> GeneralizedBicycleSpec {
GeneralizedBicycleSpec {
order: 5,
a_exponents: vec![0, 1],
b_exponents: vec![0, 2],
}
}
fn fixture_hx() -> Vec<Vec<usize>> {
vec![
vec![0, 1, 5, 7],
vec![1, 2, 6, 8],
vec![2, 3, 7, 9],
vec![3, 4, 5, 8],
vec![0, 4, 6, 9],
]
}
fn fixture_hz() -> Vec<Vec<usize>> {
vec![
vec![0, 3, 5, 9],
vec![1, 4, 5, 6],
vec![0, 2, 6, 7],
vec![1, 3, 7, 8],
vec![2, 4, 8, 9],
]
}
fn assert_canonical_sparse_rows(rows: &[Vec<usize>]) {
for row in rows {
assert!(
row.windows(2).all(|window| window[0] < window[1]),
"row must be sorted and duplicate-free: {row:?}"
);
}
}
fn write_spec(path: &Path, contents: &str) -> PathBuf {
let spec = path.join("generalized-bicycle.json");
fs::write(&spec, contents).expect("spec fixture should be writable");
spec
}
fn run_qec_code_in_process_os(args: Vec<OsString>) -> Result<String, QecError> {
let mut argv = vec![OsString::from("qec-code")];
argv.extend(args);
run(Cli::parse_from(argv))
}
#[test]
fn generalized_bicycle_order5_matches_fixture() {
let result = construct_css(CssFamilySpec::GeneralizedBicycle(fixture_spec()).into()).unwrap();
assert_eq!(result.construction_id, "generalized_bicycle");
assert_eq!(
result.requested_family_id,
Some(RequestedFamilyId::GeneralizedBicycle)
);
assert_eq!(result.normalized_parameters["order"], serde_json::json!(5));
assert_eq!(
result.normalized_parameters["a_exponents"],
serde_json::json!([0, 1])
);
assert_eq!(
result.normalized_parameters["b_exponents"],
serde_json::json!([0, 2])
);
assert_eq!(result.provenance.adapter, "generalized_bicycle");
assert_eq!(
result.provenance.source,
"CssFamilySpec::GeneralizedBicycle"
);
assert!(
result
.provenance
.normalized_input_digest
.starts_with("sha256:")
);
assert_eq!(result.stats.n, 10);
assert_eq!(result.stats.m_x, 5);
assert_eq!(result.stats.m_z, 5);
assert_eq!(result.stats.rank_x, 4);
assert_eq!(result.stats.rank_z, 4);
assert_eq!(result.stats.k, 2);
assert_eq!(result.stats.d_x, Some(3));
assert_eq!(result.stats.d_z, Some(3));
assert_eq!(result.checks.h_x, fixture_hx());
assert_eq!(result.checks.h_z, fixture_hz());
assert_canonical_sparse_rows(&result.checks.h_x);
assert_canonical_sparse_rows(&result.checks.h_z);
verify_css_orthogonality(result.stats.n, &result.checks.h_x, &result.checks.h_z).unwrap();
let parsed = parse_css_construction_json(
r#"{"schema_version":1,"construction":"generalized_bicycle","order":5,"a_exponents":[0,1],"b_exponents":[0,2]}"#,
)
.unwrap();
assert_eq!(
parsed,
CssFamilySpec::GeneralizedBicycle(fixture_spec()).into()
);
let parsed_result = construct_css(parsed).unwrap();
assert_eq!(
serde_json::to_string(&result).unwrap(),
serde_json::to_string(&parsed_result).unwrap()
);
let unsorted = construct_css(
CssFamilySpec::GeneralizedBicycle(GeneralizedBicycleSpec {
order: 5,
a_exponents: vec![1, 0],
b_exponents: vec![2, 0],
})
.into(),
)
.unwrap();
assert_eq!(
unsorted.normalized_parameters["a_exponents"],
serde_json::json!([0, 1])
);
assert_eq!(unsorted.checks.h_x, fixture_hx());
let dir = tempdir().unwrap();
let spec = write_spec(
dir.path(),
r#"{"schema_version":1,"construction":"generalized_bicycle","order":5,"a_exponents":[0,1],"b_exponents":[0,2]}"#,
);
let output = std::process::Command::new(qec_code_bin())
.args(["code", "css", "construct", "--spec"])
.arg(&spec)
.arg("hx")
.output()
.expect("qec-code binary should run");
assert!(output.status.success());
assert_eq!(output.stderr, b"");
let stdout = String::from_utf8(output.stdout).expect("stdout should be UTF-8");
assert_eq!(
stdout,
format!(
"{}\n",
SparseRowsMatrix::new(10, fixture_hx())
.unwrap()
.to_json_string()
)
);
let in_process = run_qec_code_in_process_os(vec![
OsString::from("code"),
OsString::from("css"),
OsString::from("construct"),
OsString::from("--spec"),
spec.into_os_string(),
OsString::from("hz"),
])
.unwrap();
assert_eq!(
in_process,
SparseRowsMatrix::new(10, fixture_hz())
.unwrap()
.to_json_string()
);
}
#[test]
fn generalized_bicycle_accepts_non_fixture_cyclic_code() {
let result = construct_css(
CssFamilySpec::GeneralizedBicycle(GeneralizedBicycleSpec {
order: 3,
a_exponents: vec![0],
b_exponents: vec![1],
})
.into(),
)
.unwrap();
assert_eq!(result.stats.n, 6);
assert_eq!(result.stats.m_x, 3);
assert_eq!(result.stats.m_z, 3);
assert_eq!(result.stats.rank_x, 3);
assert_eq!(result.stats.rank_z, 3);
assert_eq!(result.stats.k, 0);
assert_eq!(result.stats.d_x, None);
assert_eq!(result.stats.d_z, None);
assert_eq!(result.normalized_parameters["order"], serde_json::json!(3));
assert_eq!(
result.normalized_parameters["a_exponents"],
serde_json::json!([0])
);
assert_eq!(
result.normalized_parameters["b_exponents"],
serde_json::json!([1])
);
assert_eq!(result.checks.h_x, vec![vec![0, 4], vec![1, 5], vec![2, 3]]);
assert_eq!(result.checks.h_z, vec![vec![2, 3], vec![0, 4], vec![1, 5]]);
verify_css_orthogonality(result.stats.n, &result.checks.h_x, &result.checks.h_z).unwrap();
}
#[test]
fn generalized_bicycle_rejects_invalid_exponents() {
for (case, expected) in [
(
GeneralizedBicycleSpec {
order: 0,
a_exponents: vec![0],
b_exponents: vec![0],
},
"order must be nonzero",
),
(
GeneralizedBicycleSpec {
order: 5,
a_exponents: vec![0, 5],
b_exponents: vec![0],
},
"a_exponents exponent 5 is out of range for order 5",
),
(
GeneralizedBicycleSpec {
order: 5,
a_exponents: vec![0, 1, 1],
b_exponents: vec![0],
},
"a_exponents contains duplicate exponent 1",
),
(
GeneralizedBicycleSpec {
order: 5,
a_exponents: vec![],
b_exponents: vec![0],
},
"a_exponents must not be empty",
),
(
GeneralizedBicycleSpec {
order: 5,
a_exponents: vec![0],
b_exponents: vec![],
},
"b_exponents must not be empty",
),
] {
assert!(matches!(
construct_css(CssFamilySpec::GeneralizedBicycle(case).into()),
Err(QecError::InvalidCssConstruction { construction, reason })
if construction == "generalized_bicycle" && reason == expected
));
}
assert!(matches!(
parse_css_construction_json(
r#"{"schema_version":1,"construction":"generalized_bicycle","order":5,"a_exponents":[0],"b_exponents":[0,"x"]}"#
),
Err(QecError::InvalidCssConstruction { construction, reason })
if construction == "generalized_bicycle"
&& reason == "b_exponents[1] must be a nonnegative integer"
));
for (input, expected) in [
(
r#"{"schema_version":1,"construction":"generalized_bicycle","order":5,"b_exponents":[0]}"#,
"missing or invalid a_exponents",
),
(
r#"{"schema_version":1,"construction":"generalized_bicycle","order":5,"a_exponents":true,"b_exponents":[0]}"#,
"missing or invalid a_exponents",
),
(
r#"{"schema_version":1,"construction":"generalized_bicycle","order":5,"a_exponents":[0],"b_exponents":null}"#,
"missing or invalid b_exponents",
),
] {
assert!(matches!(
parse_css_construction_json(input),
Err(QecError::InvalidCssConstruction { construction, reason })
if construction == "generalized_bicycle" && reason == expected
));
}
}