mod support;
use std::env;
use std::fs;
use std::process;
use std::process::Command;
use std::process::Output;
use support::compile_diagnostics::source_error_matches;
fn check_dependent(case: &str, source: &str, features: &[&str]) -> Output {
let directory = env::temp_dir().join(format!("qubit-redact-public-api-{}-{case}", process::id()));
fs::create_dir_all(directory.join("src")).expect("the dependent source directory should be creatable");
fs::write(
directory.join("Cargo.toml"),
format!(
"[package]\nname = \"qubit-redact-public-api-{case}\"\nversion = \"0.0.0\"\nedition = \"2024\"\n\n[workspace]\n\n[dependencies]\nqubit-redact = {{ path = \"{}\", features = {} }}\n",
env!("CARGO_MANIFEST_DIR"),
serde_json::to_string(features).expect("feature list serialization"),
),
)
.expect("the dependent manifest should be writable");
fs::write(directory.join("src/main.rs"), source).expect("the dependent source should be writable");
let output = Command::new(env!("CARGO"))
.args(["check", "--offline", "--message-format=json"])
.current_dir(&directory)
.output()
.expect("cargo check for the dependent crate should run");
fs::remove_dir_all(directory).expect("the dependent crate should be removable");
output
}
fn assert_rejected(case: &str, source: &str, expected_diagnostic: &str) {
let output = check_dependent(case, source, &["json"]);
let diagnostics = format!(
"{}\n{}",
String::from_utf8_lossy(&output.stderr),
String::from_utf8_lossy(&output.stdout)
);
assert!(!output.status.success(), "the removed {case} API must not compile",);
assert!(
source_error_matches(&output.stdout, expected_diagnostic, None),
"the {case} diagnostics must mention {expected_diagnostic}: {diagnostics}",
);
}
#[test]
fn test_raw_batch_publication_types_are_not_public() {
assert_rejected(
"raw-batch-types",
concat!(
"use qubit_",
"redact::{DiagnosticRedactionHandleError, DiagnosticRedactionBatchOutput};\nfn main() {}\n"
),
"DiagnosticRedactionBatchOutput",
);
}
#[test]
fn test_inspection_result_alias_is_not_public() {
assert_rejected(
"inspection-alias",
concat!("use qubit_", "redact::{RedactionInspectionResult};\nfn main() {}\n"),
"RedactionInspectionResult",
);
}
#[test]
fn test_domain_scope_types_are_not_exported_from_the_crate_root() {
assert_rejected(
"root-domain-scopes",
concat!(
"use qubit_",
"redact::{RedactionEntries, RedactionFields, RedactionItems};\nfn main() {}\n"
),
"RedactionFields",
);
}
#[test]
fn test_legacy_batch_finish_method_is_not_public() {
assert_rejected(
"raw-batch-finish",
concat!(
"use qubit_",
"redact::{Redactor};\nfn main() { let batch = Redactor::strict().diagnostic_batch(); let _ = batch.finish_for_diagnostics(\"<incomplete>\"); }\n"
),
"finish_for_diagnostics",
);
}
#[test]
fn test_field_scope_rejects_item_methods() {
assert_rejected(
"field-item-method",
r#"
use qubit_redact::Redact;
use qubit_redact::RedactionWriter;
struct Value;
impl Redact for Value {
fn write_redacted(&self, writer: &mut RedactionWriter<'_>) {
writer.record("Value", |fields| { fields.unredacted_item(|| "value"); });
}
}
fn main() {}
"#,
"unredacted_item",
);
}
#[test]
fn test_field_scope_rejects_entry_methods() {
assert_rejected(
"field-entry-method",
r#"
use qubit_redact::Redact;
use qubit_redact::RedactionWriter;
struct Value;
impl Redact for Value {
fn write_redacted(&self, writer: &mut RedactionWriter<'_>) {
writer.record("Value", |fields| { fields.unredacted_entry("key", || "value"); });
}
}
fn main() {}
"#,
"unredacted_entry",
);
}
#[test]
fn test_item_scope_rejects_field_methods() {
assert_rejected(
"item-field-method",
r#"
use qubit_redact::Redact;
use qubit_redact::RedactionWriter;
struct Value;
impl Redact for Value {
fn write_redacted(&self, writer: &mut RedactionWriter<'_>) {
writer.sequence(|items| { items.unredacted("value", || "value"); });
}
}
fn main() {}
"#,
"unredacted",
);
}
#[test]
fn test_item_scope_rejects_entry_methods() {
assert_rejected(
"item-entry-method",
r#"
use qubit_redact::Redact;
use qubit_redact::RedactionWriter;
struct Value;
impl Redact for Value {
fn write_redacted(&self, writer: &mut RedactionWriter<'_>) {
writer.sequence(|items| { items.unredacted_entry("key", || "value"); });
}
}
fn main() {}
"#,
"unredacted_entry",
);
}
#[test]
fn test_entry_scope_rejects_item_methods() {
assert_rejected(
"entry-item-method",
r#"
use qubit_redact::Redact;
use qubit_redact::RedactionWriter;
use qubit_redact::Sensitivity;
struct Value;
impl Redact for Value {
fn write_redacted(&self, writer: &mut RedactionWriter<'_>) {
writer.map(|entries| { entries.sensitive_item(Sensitivity::Secret, || "value"); });
}
}
fn main() {}
"#,
"sensitive_item",
);
}
#[test]
fn test_entry_scope_rejects_field_methods() {
assert_rejected(
"entry-field-method",
r#"
use qubit_redact::Redact;
use qubit_redact::RedactionWriter;
struct Value;
impl Redact for Value {
fn write_redacted(&self, writer: &mut RedactionWriter<'_>) {
writer.map(|entries| { entries.unredacted("value", || "value"); });
}
}
fn main() {}
"#,
"unredacted",
);
}
#[test]
fn test_scalar_derive_rejects_collection_fields() {
for features in [&["derive"][..], &["derive", "bigdecimal"][..]] {
let accepted = check_dependent(
"scalar-positive",
concat!(
"use qubit_",
"redact::RedactScalar;\n#[derive(RedactScalar)] struct Scalar(u64); fn main() {}"
),
features,
);
assert!(
accepted.status.success(),
"scalar positive control: {}",
String::from_utf8_lossy(&accepted.stderr)
);
let rejected = check_dependent(
"scalar-collection",
concat!(
"use qubit_",
"redact::RedactScalar;\n#[derive(RedactScalar)] struct Collection(Vec<u64>); fn main() {}"
),
features,
);
assert!(!rejected.status.success(), "collections are not scalar leaves");
assert!(
source_error_matches(&rejected.stdout, "Vec<u64>: RedactScalar", Some("E0277")),
"expected scalar capability rejection: {}\n{}",
String::from_utf8_lossy(&rejected.stderr),
String::from_utf8_lossy(&rejected.stdout),
);
}
}