[pitboss] sweep after phase 20: 2 deferred items resolved

This commit is contained in:
pitboss 2026-05-20 16:26:42 -05:00
parent bd0135e423
commit 00b0fbaea9
2 changed files with 232 additions and 8 deletions

View file

@ -864,17 +864,23 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
/// Phase 19 (Track M.1) — class-method harness for Rust.
///
/// Emits `src/main.rs` that constructs `entry::<class>::default()`
/// and invokes `instance.<method>(&payload)`. The fixture is
/// expected to derive `Default` on the receiver type so the harness
/// has a zero-arg construction path. When `Default` is unavailable
/// the fixture can provide a `new()` associated function; the
/// harness falls back to that via conditional compilation when
/// `Default` lookup fails.
/// Emits `src/main.rs` that constructs `entry::<class>` and invokes
/// `instance.<method>(&payload)`. The constructor pick is driven by
/// scanning the entry source for the receiver's construction shape:
/// when the class derives `Default` (or implements `Default` directly)
/// the harness emits `<class>::default()`; otherwise it falls back to
/// `<class>::new()`. This keeps the harness compilable against
/// non-Default fixtures without a separate emit path.
fn emit_class_method_harness(spec: &HarnessSpec, class: &str, method: &str) -> HarnessSource {
let shim = probe_shim();
let cargo_toml = generate_cargo_toml(spec.expected_cap);
let entry_label = format!("{class}::{method}");
let entry_src = read_entry_source(&spec.entry_file);
let ctor = if class_derives_default(&entry_src, class) {
"default"
} else {
"new"
};
let body = format!(
r#"//! Nyx dynamic harness — class method (Phase 19 / Track M.1).
mod entry;
@ -883,7 +889,7 @@ fn main() {{
let payload = nyx_payload();
let _ = &payload;
__nyx_install_crash_guard("{entry_label}");
let instance = entry::{class}::default();
let instance = entry::{class}::{ctor}();
let _ = instance.{method}(&payload);
}}
@ -942,6 +948,122 @@ fn b64_decode(input: &[u8]) -> Option<Vec<u8>> {{
}
}
/// True when the entry source declares `class` as a type that derives
/// or implements `Default`. Two byte-level patterns are recognised:
///
/// - `#[derive(...Default...)]` immediately preceding a `struct`/`enum`
/// declaration whose name matches `class`.
/// - An explicit `impl Default for <class>` block anywhere in the file.
///
/// When neither is present the caller falls back to a `<class>::new()`
/// ctor. The scan is conservative: unrecognised entry sources produce
/// `false` (so the harness emits `new()`), which keeps non-Default
/// fixtures compilable.
fn class_derives_default(entry_src: &str, class: &str) -> bool {
let impl_marker = format!("impl Default for {class}");
if entry_src.contains(&impl_marker) {
return true;
}
let struct_marker = format!("struct {class}");
let enum_marker = format!("enum {class}");
let mut search_from = 0usize;
let bytes = entry_src.as_bytes();
loop {
let struct_at = entry_src[search_from..].find(&struct_marker);
let enum_at = entry_src[search_from..].find(&enum_marker);
let (rel, marker_len) = match (struct_at, enum_at) {
(Some(s), Some(e)) if s <= e => (s, struct_marker.len()),
(Some(_), Some(e)) => (e, enum_marker.len()),
(Some(s), None) => (s, struct_marker.len()),
(None, Some(e)) => (e, enum_marker.len()),
(None, None) => return false,
};
let decl_pos = search_from + rel;
let next_byte = bytes.get(decl_pos + marker_len).copied();
let boundary_ok = matches!(next_byte, Some(b) if !b.is_ascii_alphanumeric() && b != b'_');
if boundary_ok {
let window_start = decl_pos.saturating_sub(256);
let window = &entry_src[window_start..decl_pos];
if let Some(derive_pos) = window.rfind("#[derive(") {
if let Some(end_rel) = window[derive_pos..].find(")]") {
let end = derive_pos + end_rel;
let derive_list = &window[derive_pos + "#[derive(".len()..end];
let between = &window[end + ")]".len()..];
// The derive attribute must directly precede the
// declaration — no other item / statement may sit
// between `#[derive(...)]` and the `struct` /
// `enum` token. Forbidden tokens (`;`, `{`, `}`,
// `=`, or another item keyword) signal the derive
// belongs to an earlier declaration.
let between_clean = strip_attrs_and_comments(between);
let forbidden = ['{', '}', ';', '='];
let item_keyword = ["struct", "enum", "fn", "impl", "trait", "type", "mod"]
.iter()
.any(|kw| word_in_text(&between_clean, kw));
let attaches_to_decl = !between_clean.chars().any(|c| forbidden.contains(&c))
&& !item_keyword;
if attaches_to_decl
&& derive_list.split(',').any(|t| t.trim() == "Default")
{
return true;
}
}
}
}
search_from = decl_pos + 1;
}
}
/// Drop `//` line comments and `#[...]` attribute blocks from `text`,
/// returning the remaining bytes joined by spaces. Used by
/// [`class_derives_default`] to decide whether the text between a
/// derive attribute and a declaration is empty (modulo visibility
/// modifiers and other attributes).
fn strip_attrs_and_comments(text: &str) -> String {
let mut out = String::with_capacity(text.len());
for line in text.lines() {
let mut s = line.trim();
while s.starts_with("#[") {
if let Some(end) = s.find(']') {
s = s[end + 1..].trim_start();
} else {
break;
}
}
if let Some(idx) = s.find("//") {
s = &s[..idx];
}
out.push_str(s.trim());
out.push(' ');
}
out
}
/// True when `kw` appears in `text` as a whole word (ASCII word
/// boundaries on both sides).
fn word_in_text(text: &str, kw: &str) -> bool {
let bytes = text.as_bytes();
let kw_bytes = kw.as_bytes();
if kw_bytes.is_empty() {
return false;
}
let mut i = 0usize;
while i + kw_bytes.len() <= bytes.len() {
if &bytes[i..i + kw_bytes.len()] == kw_bytes {
let before_ok = i == 0
|| !bytes[i - 1].is_ascii_alphanumeric() && bytes[i - 1] != b'_';
let after_idx = i + kw_bytes.len();
let after_ok = after_idx >= bytes.len()
|| (!bytes[after_idx].is_ascii_alphanumeric() && bytes[after_idx] != b'_');
if before_ok && after_ok {
return true;
}
}
i += 1;
}
false
}
/// Generate `Cargo.toml` for the harness crate.
///
/// Dependencies are driven by `expected_cap`:
@ -1204,6 +1326,49 @@ mod tests {
assert_eq!(harness.entry_subpath, Some("src/entry.rs".to_string()));
}
#[test]
fn class_derives_default_matches_derive_attribute() {
let src = "#[derive(Default)]\npub struct UserService;";
assert!(class_derives_default(src, "UserService"));
}
#[test]
fn class_derives_default_matches_derive_among_other_traits() {
let src = "#[derive(Clone, Debug, Default, PartialEq)]\nstruct UserService { id: u32 }";
assert!(class_derives_default(src, "UserService"));
}
#[test]
fn class_derives_default_matches_explicit_impl() {
let src = "struct UserService;\nimpl Default for UserService { fn default() -> Self { Self } }";
assert!(class_derives_default(src, "UserService"));
}
#[test]
fn class_derives_default_matches_enum() {
let src = "#[derive(Default)]\nenum Mode { #[default] Off, On }";
assert!(class_derives_default(src, "Mode"));
}
#[test]
fn class_derives_default_false_when_absent() {
let src = "pub struct UserService { id: u32 }\nimpl UserService { pub fn new() -> Self { Self { id: 0 } } }";
assert!(!class_derives_default(src, "UserService"));
}
#[test]
fn class_derives_default_false_when_derive_on_different_type() {
let src = "#[derive(Default)]\nstruct OtherType;\npub struct UserService;";
assert!(!class_derives_default(src, "UserService"));
}
#[test]
fn class_derives_default_respects_word_boundary() {
// `struct UserServiceImpl` must not be treated as `UserService`.
let src = "#[derive(Default)]\nstruct UserServiceImpl;";
assert!(!class_derives_default(src, "UserService"));
}
#[test]
fn emit_env_var_slot() {
let spec = make_spec(PayloadSlot::EnvVar("NYX_INPUT".into()));

View file

@ -2216,6 +2216,65 @@ mod tests {
);
}
/// Phase 20 (Track M.2) deferred-fix companion: when a real
/// `MessageHandler` adapter binds, the spec carries both the
/// `MessageHandler` variant on `entry_kind` and the broker
/// adapter id on `framework.adapter`. The Python emitter's
/// `python_broker_for_adapter` reads `framework.adapter` to
/// route the broker pick, and the `MessageHandler` short-circuit
/// reads `entry_kind` to dispatch — both fields must be
/// populated by `stamp_framework_binding` so real spec-derivation
/// matches the manual fixture path in `tests/message_handler_corpus.rs`.
#[test]
fn spec_attach_framework_binding_stamps_message_handler_and_sets_broker_adapter() {
let mut spec = HarnessSpec {
finding_id: "phase20stamp0001".into(),
entry_file: "src/consumer.py".into(),
entry_name: "on_message".into(),
entry_kind: EntryKind::Function,
lang: Lang::Python,
toolchain_id: "phase20".into(),
payload_slot: PayloadSlot::Param(0),
expected_cap: crate::labels::Cap::CODE_EXEC,
constraint_hints: vec![],
sink_file: "src/consumer.py".into(),
sink_line: 1,
spec_hash: "phase20stamp0001".into(),
derivation: SpecDerivationStrategy::FromFlowSteps,
stubs_required: vec![],
framework: None,
java_toolchain: JavaToolchain::default(),
};
let pre_hash = spec.spec_hash.clone();
let binding = FrameworkBinding {
adapter: "kafka-python".to_owned(),
kind: EntryKind::MessageHandler {
queue: "orders".to_owned(),
message_schema: None,
},
route: None,
request_params: vec![],
response_writer: None,
middleware: vec![],
};
stamp_framework_binding(&mut spec, binding);
assert_eq!(
spec.entry_kind.tag(),
crate::evidence::EntryKindTag::MessageHandler,
"MessageHandler variant must propagate from binding onto spec.entry_kind",
);
if let EntryKind::MessageHandler { queue, .. } = &spec.entry_kind {
assert_eq!(queue, "orders");
} else {
panic!("expected MessageHandler variant");
}
let fw = spec.framework.as_ref().expect("framework must be set");
assert_eq!(fw.adapter, "kafka-python");
assert_ne!(pre_hash, spec.spec_hash);
}
/// Companion guard: when the binding carries a legacy unit
/// variant (`Function` / `HttpRoute`), the stamping branch keeps
/// `spec.entry_kind` and `spec.spec_hash` unchanged.