Browse / github.com/storytold/craft-libs / patch
craft-testkit: platform regression source guards (closes #139)
craft-testkit: source guards for platform rules that broke before (from FilmCraft's web-clock test and PhotoCraft's shader test) This issue proposes source guards for craft-testkit, the dev-only crate of #115's craft-testkit entry: tests that fail when an app's code repeats a platform fault that already reached users of another app. It follows candidate 11 of #2 (shared tooling and tests). The rewritten #24, whose documents #38 also carries, asks each shared API to define its errors and post-failure state, and it lists five test environments, all unverified (`docs/shared-crate-contract.md`, `docs/platform-test-inventory.json` at `9548258`). Each rule below has been broken in at least one app and fixed there, and most a
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- zachshallbetter (imported from pull request #163, open)
- Base
82a24b006eefa036b43c09a67fb9a3fbd17ffe63- Size
- +924 −0 in 7 files
- Review
- pending
- Shared
- 2026-10-28
- Id
0043275396723c05fdb98ec3
How it went
No other operator has reported using it yet.
Diff
raw · Cargo.lock Cargo.toml crates/testkit/Cargo.toml crates/testkit/README.md crates/testkit/src/guards.rs crates/testkit/src/lib.rs crates/testkit/tests/guards.rs
@@ -659,6 +659,10 @@ dependencies = [ "toml_edit", ] +[[package]]+name = "craft-testkit"+version = "0.1.0"+ [[package]] name = "craft-ui" version = "0.1.0"@@ -26,6 +26,7 @@ craft-docpath = { path = "crates/docpath" } craft-print = { path = "crates/print" } craft-quit = { path = "crates/quit" } craft-settings = { path = "crates/settings" }+craft-testkit = { path = "crates/testkit" } craft-ui = { path = "crates/ui" } egui = { version = "0.36", default-features = false } rayon = "1"new file mode 100644@@ -0,0 +1,13 @@+[package]+name = "craft-testkit"+version.workspace = true+edition.workspace = true+license.workspace = true+rust-version.workspace = true+repository.workspace = true+description = "Source-level platform regression guards and verification utilities for the Craft application suite"++[dependencies]++[lints]+workspace = truenew file mode 100644@@ -0,0 +1,79 @@+# craft-testkit++Source-level platform regression guards and verification utilities for the Craft application suite.++## Overview++`craft-testkit` provides lightweight, non-panicking source guards that prevent applications in the Craft ecosystem from repeating platform defects that were previously diagnosed and resolved in peer applications:++1. **`predefined_quit`**: Flags direct calls to `muda`'s `PredefinedMenuItem::quit` outside comments. On macOS, this predefined menu item routes directly to `NSApplication terminate:`, completely bypassing application dirty document checks and unsaved changes save prompts. Applications should instead wire Quit through `craft-quit` lifecycle interceptors.+2. **`wgsl_float_literals` / `wgsl_float_literals_in`**: Scans WGSL shader sources using PhotoCraft's decimal float scanner and flags literals whose absolute value exceeds `f32::MAX` (such as `3.40282347e38`). While native compilers (`naga`) round these down, browser WebGPU compilers (`Tint` in Chromium) reject them with fatal pipeline compilation errors, forcing applications into CPU fallback rendering. Exact values equal to `f32::MAX` must be written as exact hex float literals (`0x1.fffffep+127f`).+3. **`std_instant`**: Flags lines naming `std::time::Instant` outside comments without `web_time`. On `wasm32-unknown-unknown`, `std::time::Instant::now()` panics at runtime with `"time not implemented on this platform"`. Code targeting the web must use `web_time::Instant`.+4. **`rust_sources`**: Recursively discovers a crate's `.rs` source files while excluding integration tests (`tests/`), test modules (`tests.rs`, `tests_*.rs`, `*_tests.rs`, `*_test.rs`), and build artifacts (`target/`, `.git/`).++## Epistemic Scope: Syntactic Guards vs Runtime Validation++Source-level guards provide deterministic syntactic evidence about codebase structure:++### What They Establish+- Checked source files do not contain forbidden symbol invocations, known out-of-range literal representations, or platform-hazardous imports.+- Reusable, deterministic checks that run in milliseconds as regular Cargo tests without external runners.++### What They Cannot Establish+- **Runtime execution safety**: Syntactic checks do not verify whether code branches actually execute at runtime.+- **Semantic type resolution**: The scanners do not resolve arbitrary type aliases (e.g. `use std::time as t; t::Instant::now()`) or expand procedural macros.+- **Complex target predicates**: While `std_instant` recognizes direct item-level non-wasm target conditions (e.g. `#[cfg(not(target_arch = "wasm32"))]`), it does not evaluate arbitrary multi-crate boolean expressions. Applications should scope `std_instant` tests to the specific crates that compile into their web distribution.+- **Driver and host OS behavior**: Syntactic checks do not verify WebGPU driver compatibility or host window manager event dispatch.++Source guards serve as fast, zero-dependency regression gates in application test suites, complementing platform integration tests and CI workflows.++## Consumer Adoption Examples++### 1. PhotoCraft: Adopt `wgsl_float_literals` and `predefined_quit`++In `photocraft/crates/gpu/src/lib.rs`, replace the in-tree `decimal_float_literals` helper with `craft_testkit::wgsl_float_literals`:++```rust+#[test]+fn shader_float_literals_fit_f32() {+ let bad = craft_testkit::wgsl_float_literals(SHADER);+ assert!(+ bad.is_empty(),+ "float literals outside f32 finite range reject on WebGPU (Tint):\n{}",+ bad.iter().map(|f| f.to_string()).collect::<Vec<_>>().join("\n")+ );+}+```++In `photocraft/apps/photocraft/tests/native_menu_tests.rs`:++```rust+#[test]+fn no_unintercepted_predefined_quit_in_menu() {+ let sources = craft_testkit::rust_sources("src").expect("scan sources");+ let findings = craft_testkit::predefined_quit(&sources);+ assert!(+ findings.is_empty(),+ "predefined muda Quit bypasses dirty document save prompts:\n{}",+ findings.iter().map(|f| f.to_string()).collect::<Vec<_>>().join("\n")+ );+}+```++### 2. FilmCraft: Adopt `std_instant` and `wgsl_float_literals`++In `filmcraft/crates/engine/src/wasm_clock_tests.rs`, replace in-tree textual search with `craft_testkit::std_instant`:++```rust+#[test]+fn no_std_instant_in_code_the_web_build_runs() {+ let sources = craft_testkit::rust_sources(Path::new(env!("CARGO_MANIFEST_DIR")).join("src"))+ .expect("scan engine sources");+ let findings = craft_testkit::std_instant(&sources);+ assert!(+ findings.is_empty(),+ "std::time::Instant panics on wasm32; use web_time::Instant:\n{}",+ findings.iter().map(|f| f.to_string()).collect::<Vec<_>>().join("\n")+ );+}+```new file mode 100644@@ -0,0 +1,465 @@+//! Platform regression guards checking source code for known defects and lifecycle hazards.++use std::fmt;+#[cfg(not(target_arch = "wasm32"))]+use std::path::Path;++/// A diagnostic finding reported by a source guard.+#[derive(Debug, Clone, PartialEq, Eq)]+pub struct Finding {+ /// File path where the violation was detected.+ pub path: String,+ /// 1-based line number of the violation.+ pub line: usize,+ /// Diagnostic context (the offending code snippet or float literal).+ pub text: String,+}++impl Finding {+ /// Create a new diagnostic finding.+ pub fn new(path: impl Into<String>, line: usize, text: impl Into<String>) -> Self {+ Self { path: path.into(), line, text: text.into() }+ }+}++impl fmt::Display for Finding {+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {+ if self.path.is_empty() { write!(f, "{}: {}", self.line, self.text) } else { write!(f, "{}:{}: {}", self.path, self.line, self.text) }+ }+}++/// Abstraction over a source file path and its text content.+pub trait AsFileSource {+ /// Path of the source file.+ fn path(&self) -> &str;+ /// Text content of the source file.+ fn content(&self) -> &str;+}++impl AsFileSource for (String, String) {+ fn path(&self) -> &str {+ &self.0+ }+ fn content(&self) -> &str {+ &self.1+ }+}++impl AsFileSource for &(String, String) {+ fn path(&self) -> &str {+ &self.0+ }+ fn content(&self) -> &str {+ &self.1+ }+}++impl AsFileSource for (&str, &str) {+ fn path(&self) -> &str {+ self.0+ }+ fn content(&self) -> &str {+ self.1+ }+}++impl AsFileSource for &(&str, &str) {+ fn path(&self) -> &str {+ self.0+ }+ fn content(&self) -> &str {+ self.1+ }+}++impl AsFileSource for (String, &str) {+ fn path(&self) -> &str {+ &self.0+ }+ fn content(&self) -> &str {+ self.1+ }+}++impl AsFileSource for &(String, &str) {+ fn path(&self) -> &str {+ &self.0+ }+ fn content(&self) -> &str {+ self.1+ }+}++impl AsFileSource for (&str, String) {+ fn path(&self) -> &str {+ self.0+ }+ fn content(&self) -> &str {+ &self.1+ }+}++impl AsFileSource for &(&str, String) {+ fn path(&self) -> &str {+ self.0+ }+ fn content(&self) -> &str {+ &self.1+ }+}++/// Replace code comments and string literals with whitespace while preserving newlines and line counts.+///+/// Handles single-line comments (`//`), block comments (`/* ... */`, including nested comments),+/// normal string literals (`"..."`), and raw string literals (`r#"..."#`).+fn mask_comments_and_strings(code: &str) -> String {+ let mut out = String::with_capacity(code.len());+ let chars: Vec<char> = code.chars().collect();+ let len = chars.len();+ let mut i = 0;+ let mut in_line_comment = false;+ let mut block_comment_depth: usize = 0;+ let mut in_normal_string = false;+ let mut raw_string_hashes: Option<usize> = None;+ let mut escape = false;++ while i < len {+ let c = chars[i];+ if in_line_comment {+ if c == '\n' {+ in_line_comment = false;+ out.push('\n');+ } else {+ out.push(' ');+ }+ i += 1;+ } else if block_comment_depth > 0 {+ if c == '*' && i + 1 < len && chars[i + 1] == '/' {+ block_comment_depth = block_comment_depth.saturating_sub(1);+ out.push(' ');+ out.push(' ');+ i += 2;+ } else if c == '/' && i + 1 < len && chars[i + 1] == '*' {+ block_comment_depth = block_comment_depth.saturating_add(1);+ out.push(' ');+ out.push(' ');+ i += 2;+ } else if c == '\n' {+ out.push('\n');+ i += 1;+ } else {+ out.push(' ');+ i += 1;+ }+ } else if let Some(hashes) = raw_string_hashes {+ if c == '"' {+ let mut match_hashes = true;+ if i + hashes < len {+ for h in 0..hashes {+ if chars[i + 1 + h] != '#' {+ match_hashes = false;+ break;+ }+ }+ } else {+ match_hashes = false;+ }+ if match_hashes {+ raw_string_hashes = None;+ for _ in 0..=hashes {+ out.push(' ');+ }+ i += 1 + hashes;+ continue;+ }+ }+ if c == '\n' {+ out.push('\n');+ } else {+ out.push(' ');+ }+ i += 1;+ } else if in_normal_string {+ if escape {+ escape = false;+ out.push(' ');+ } else if c == '\\' {+ escape = true;+ out.push(' ');+ } else if c == '"' {+ in_normal_string = false;+ out.push(' ');+ } else if c == '\n' {+ out.push('\n');+ } else {+ out.push(' ');+ }+ i += 1;+ } else {+ if c == '/' && i + 1 < len && chars[i + 1] == '/' {+ in_line_comment = true;+ out.push(' ');+ out.push(' ');+ i += 2;+ } else if c == '/' && i + 1 < len && chars[i + 1] == '*' {+ block_comment_depth = 1;+ out.push(' ');+ out.push(' ');+ i += 2;+ } else if c == '"' {+ in_normal_string = true;+ out.push(' ');+ i += 1;+ } else if c == 'r' && i + 1 < len && (chars[i + 1] == '"' || chars[i + 1] == '#') {+ let mut j = i + 1;+ let mut hashes = 0;+ while j < len && chars[j] == '#' {+ hashes += 1;+ j += 1;+ }+ if j < len && chars[j] == '"' {+ raw_string_hashes = Some(hashes);+ for _ in i..=j {+ out.push(' ');+ }+ i = j + 1;+ } else {+ out.push(c);+ i += 1;+ }+ } else {+ out.push(c);+ i += 1;+ }+ }+ }+ out+}++/// Guard: detects direct calls to `muda`'s predefined Quit menu item.+///+/// Using `PredefinedMenuItem::quit` connects the macOS menu directly to `NSApplication terminate:`,+/// which bypasses dirty document checks, unsaved changes save prompts, and lifecycle cleanup.+/// Applications should route Quit through custom menu items or interceptors connected to `craft-quit`.+pub fn predefined_quit<I>(files: I) -> Vec<Finding>+where+ I: IntoIterator,+ I::Item: AsFileSource,+{+ let mut findings = Vec::new();+ for file in files {+ let path = file.path();+ let content = file.content();+ let masked = mask_comments_and_strings(content);++ for (orig_line, (idx, masked_line)) in content.lines().zip(masked.lines().enumerate()) {+ let normalized = masked_line.replace(' ', "");+ if normalized.contains("PredefinedMenuItem::quit") || normalized.contains("PredefinedMenuItem::Quit") {+ findings.push(Finding::new(path, idx + 1, orig_line.trim()));+ }+ }+ }+ findings+}++/// Guard: detects uses of `std::time::Instant` outside comments in code paths running on the web.+///+/// On `wasm32-unknown-unknown`, `std::time::Instant::now()` panics with "time not implemented+/// on this platform". Crates running in the browser should use `web_time::Instant`, which maps to+/// `std::time::Instant` on native platforms and `performance.now()` on the web.+///+/// ### Syntactic Scope and Limitations+///+/// * **Targeted files**: Callers should pass files from crates that compile into the application's web build.+/// * **Allowed patterns**: Comments, string literals, lines containing `web_time`, and items decorated with+/// explicit non-wasm target conditions (e.g. `#[cfg(not(target_arch = "wasm32"))]` or `#[cfg(target_os = ...)]`)+/// are skipped.+/// * **Limitations**: This is a lexical source guard. It does not perform semantic type resolution, macro+/// expansion, or track arbitrary module aliases (e.g. `use std::time as t; t::Instant`).+pub fn std_instant<I>(files: I) -> Vec<Finding>+where+ I: IntoIterator,+ I::Item: AsFileSource,+{+ let mut findings = Vec::new();+ for file in files {+ let path = file.path();+ let content = file.content();+ let masked = mask_comments_and_strings(content);++ let mut in_cfg_non_wasm = false;+ let mut cfg_brace_depth: usize = 0;++ for (orig_line, (idx, masked_line)) in content.lines().zip(masked.lines().enumerate()) {+ let is_comment_only = masked_line.trim().is_empty();+ let line_has_cfg_non_wasm = !is_comment_only+ && (orig_line.contains("cfg(not(target_arch = \"wasm32\"))")+ || orig_line.contains("cfg(target_arch = \"x86_64\")")+ || orig_line.contains("cfg(target_arch = \"aarch64\")")+ || orig_line.contains("cfg(target_os ="));++ if line_has_cfg_non_wasm {+ in_cfg_non_wasm = true;+ }++ let names_std_instant =+ masked_line.contains("time::Instant") || (masked_line.contains("use std::time::") && masked_line.contains("Instant"));++ if names_std_instant && !masked_line.contains("web_time") && !in_cfg_non_wasm {+ findings.push(Finding::new(path, idx + 1, orig_line.trim()));+ }++ if in_cfg_non_wasm && !is_comment_only {+ let opens = masked_line.chars().filter(|&c| c == '{').count();+ let closes = masked_line.chars().filter(|&c| c == '}').count();+ if cfg_brace_depth == 0 {+ if opens > 0 {+ cfg_brace_depth = cfg_brace_depth.saturating_add(opens).saturating_sub(closes);+ if cfg_brace_depth == 0 {+ in_cfg_non_wasm = false;+ }+ } else if masked_line.contains(';') {+ in_cfg_non_wasm = false;+ }+ } else {+ cfg_brace_depth = cfg_brace_depth.saturating_add(opens).saturating_sub(closes);+ if cfg_brace_depth == 0 {+ in_cfg_non_wasm = false;+ }+ }+ }+ }+ }+ findings+}++/// Guard: scans a single WGSL shader string and detects decimal float literals outside `f32`'s finite range.+///+/// Browser WebGPU compilers (such as Chrome's Tint) reject float literals exceeding `f32::MAX`+/// (e.g. `3.40282347e38`), triggering fatal pipeline build errors and fallback to CPU rendering.+/// Exact values equal to `f32::MAX` should be written as hex float literals (e.g. `0x1.fffffep+127f`).+///+/// ### Policy and Limitations+///+/// * **Conservative mathematical bound**: The guard evaluates the parsed mathematical magnitude against+/// `f64::from(f32::MAX)`. Any decimal literal exceeding this threshold is flagged, even if native+/// compilers like `naga` would round it down to `f32::MAX`.+/// * **Hexadecimal floats**: Exact hex float literals (`0x...p...`) are skipped as they represent exact bit patterns.+/// * **Comments and strings**: Comments (`//` and `/* ... */`) and string contents are masked out prior to scanning.+pub fn wgsl_float_literals(wgsl: &str) -> Vec<Finding> {+ wgsl_float_literals_in([("", wgsl)])+}++/// Guard: scans multiple WGSL shader files and detects decimal float literals outside `f32`'s finite range.+pub fn wgsl_float_literals_in<I>(files: I) -> Vec<Finding>+where+ I: IntoIterator,+ I::Item: AsFileSource,+{+ let mut findings = Vec::new();+ for file in files {+ let path = file.path();+ let content = file.content();+ let masked = mask_comments_and_strings(content);++ for (n, line) in masked.lines().enumerate() {+ let code = line.as_bytes();+ let mut i = 0;+ while i < code.len() {+ let digit_or_dot = code[i].is_ascii_digit() || (code[i] == b'.' && code.get(i + 1).is_some_and(u8::is_ascii_digit));+ let word_before = i > 0 && (code[i - 1].is_ascii_alphanumeric() || code[i - 1] == b'_' || code[i - 1] == b'.');+ if !digit_or_dot || word_before {+ i += 1;+ continue;+ }+ // Skip hex float literals (e.g. 0x1.fffffep+127f, 0x1.0p0h)+ if code[i] == b'0' && matches!(code.get(i + 1), Some(b'x' | b'X')) {+ i += 2;+ while i < code.len() && (code[i].is_ascii_hexdigit() || matches!(code[i], b'.' | b'p' | b'P' | b'+' | b'-' | b'f' | b'h')) {+ i += 1;+ }+ continue;+ }+ let start = i;+ while i < code.len() && (code[i].is_ascii_digit() || code[i] == b'.') {+ i += 1;+ }+ if i < code.len() && matches!(code[i], b'e' | b'E') {+ let mut j = i + 1;+ if j < code.len() && matches!(code[j], b'+' | b'-') {+ j += 1;+ }+ if j < code.len() && code[j].is_ascii_digit() {+ i = j;+ while i < code.len() && code[i].is_ascii_digit() {+ i += 1;+ }+ }+ }+ let literal = String::from_utf8_lossy(&code[start..i]);+ if i < code.len() && matches!(code[i], b'f' | b'h') {+ i += 1;+ }+ let is_overflow = match literal.parse::<f64>() {+ Ok(v) => !v.is_finite() || v.abs() > f64::from(f32::MAX),+ Err(_) => true,+ };+ if is_overflow {+ findings.push(Finding::new(path, n + 1, literal.into_owned()));+ }+ }+ }+ }+ findings+}++/// Recursively collect all Rust source files under `dir`, excluding test files and test directories.+///+/// Excluded files:+/// * `tests.rs`+/// * `tests_*.rs`+/// * `*_tests.rs`+/// * `*_test.rs`+/// * `test.rs`+/// * Any file within a directory component named `tests`, `benches`, `examples`, `.git`, or `target`.+///+/// Results are sorted lexicographically by file path for deterministic behavior across platforms.+#[cfg(not(target_arch = "wasm32"))]+pub fn rust_sources(dir: impl AsRef<Path>) -> std::io::Result<Vec<(String, String)>> {+ let root = dir.as_ref();+ let mut files = Vec::new();+ let mut dirs = vec![root.to_path_buf()];++ while let Some(d) = dirs.pop() {+ let entries = std::fs::read_dir(&d)?;+ for entry in entries {+ let entry = entry?;+ let p = entry.path();+ let metadata = std::fs::metadata(&p)?;+ if metadata.is_dir() {+ let dirname = p.file_name().and_then(|n| n.to_str()).unwrap_or_default();+ if dirname.starts_with('.') || dirname == "target" || dirname == "tests" || dirname == "benches" || dirname == "examples" {+ continue;+ }+ dirs.push(p);+ } else if metadata.is_file() {+ let name = p.file_name().and_then(|n| n.to_str()).unwrap_or_default();+ if !name.ends_with(".rs") {+ continue;+ }+ if name == "tests.rs" || name.starts_with("tests_") || name.ends_with("_tests.rs") || name.ends_with("_test.rs") || name == "test.rs"+ {+ continue;+ }+ if p.components().any(|c| c.as_os_str() == "tests") {+ continue;+ }+ let content = std::fs::read_to_string(&p)?;+ files.push((p.display().to_string(), content));+ }+ }+ }++ files.sort_by(|a, b| a.0.cmp(&b.0));+ Ok(files)+}new file mode 100644@@ -0,0 +1,32 @@+#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]+#![forbid(unsafe_code)]++//! Source-level platform regression guards and verification utilities for the Craft application suite.+//!+//! This crate provides non-panicking source analysis guards that detect known platform hazards+//! and lifecycle regressions across Craft applications:+//!+//! * [`guards::predefined_quit`]: flags direct calls to `muda`'s predefined Quit item that bypass+//! dirty document save prompts and window teardown on macOS.+//! * [`guards::wgsl_float_literals`]: flags decimal float literals exceeding `f32`'s finite range,+//! which break browser WebGPU compilers (Tint) and trigger CPU fallbacks.+//! * [`guards::std_instant`]: flags uses of `std::time::Instant` in code paths targeting the web,+//! where `std::time::Instant::now()` panics on `wasm32`.+//! * [`guards::rust_sources`]: directory crawler collecting `.rs` sources while excluding tests.+//!+//! ### Epistemic Scope: What Source Guards Establish vs What They Cannot+//!+//! Source-level guards provide deterministic syntactic evidence about codebase structure.+//! They establish that checked source files do not contain forbidden symbol names, known invalid+//! literal patterns, or unsafe platform invocations.+//!+//! They do **not** establish runtime correctness, protocol concurrency safety, or platform behavior+//! under foreign host environments. They are lightweight syntactic regression gates designed to complement,+//! rather than replace, platform integration tests and CI execution.++pub mod guards;++pub use guards::{Finding, predefined_quit, std_instant, wgsl_float_literals, wgsl_float_literals_in};++#[cfg(not(target_arch = "wasm32"))]+pub use guards::rust_sources;new file mode 100644@@ -0,0 +1,330 @@+use craft_testkit::{Finding, predefined_quit, std_instant, wgsl_float_literals, wgsl_float_literals_in};++#[test]+fn test_finding_display() {+ let f1 = Finding::new("src/menu.rs", 42, "PredefinedMenuItem::quit()");+ assert_eq!(f1.to_string(), "src/menu.rs:42: PredefinedMenuItem::quit()");++ let f2 = Finding::new("", 156, "3.40282347e38");+ assert_eq!(f2.to_string(), "156: 3.40282347e38");+}++#[test]+fn test_predefined_quit_positive() {+ let source = r#"+fn setup_menu() {+ let quit = PredefinedMenuItem::quit(Some("Quit"));+ bar.add(quit);+}+"#;+ let findings = predefined_quit([("src/menu.rs", source)]);+ assert_eq!(findings.len(), 1);+ assert_eq!(findings[0].path, "src/menu.rs");+ assert_eq!(findings[0].line, 3);+ assert_eq!(findings[0].text, "let quit = PredefinedMenuItem::quit(Some(\"Quit\"));");+ assert_eq!(findings[0].to_string(), "src/menu.rs:3: let quit = PredefinedMenuItem::quit(Some(\"Quit\"));");+}++#[test]+fn test_predefined_quit_negative_and_comments() {+ let source = r#"+// PredefinedMenuItem::quit(None) - do not use this!+/*+ * PredefinedMenuItem::quit is hazardous+ */+fn setup_menu() {+ let sep = PredefinedMenuItem::separator();+ let hide = PredefinedMenuItem::hide(Some("Hide"));+ let note = "PredefinedMenuItem::quit in string should not trigger";+}+"#;+ let findings = predefined_quit([("src/safe_menu.rs", source)]);+ assert!(findings.is_empty(), "expected zero findings for comments, separators, and strings: {findings:?}");+}++/// Historical failure: FilmCraft `apps/filmcraft/src/native_menu.rs:56`+/// used `PredefinedMenuItem::quit`, routing directly to `NSApplication terminate:` and bypassing save prompts.+#[test]+fn test_historical_filmcraft_native_menu_quit_regression() {+ let problematic_source = r#"+ let predefined = [+ (hide_label, PredefinedMenuItem::hide(Some(app.ui.language.tr(hide_label)))),+ (hide_others_label, PredefinedMenuItem::hide_others(Some(app.ui.language.tr(hide_others_label)))),+ (quit_label, PredefinedMenuItem::quit(Some(app.ui.language.tr(quit_label)))),+ ];+"#;+ let findings = predefined_quit([("apps/filmcraft/src/native_menu.rs", problematic_source)]);+ assert_eq!(findings.len(), 1);+ assert_eq!(findings[0].line, 5);+ assert_eq!(findings[0].text, "(quit_label, PredefinedMenuItem::quit(Some(app.ui.language.tr(quit_label)))),");++ // Corrected source wires Quit via MenuItem::with_id and craft-quit lifecycle+ let corrected_source = r#"+ let quit = MenuItem::with_id("app.quit", label("app.quit", "Quit FilmCraft"), true, None);+ let predefined = [+ (hide_label, PredefinedMenuItem::hide(Some(app.ui.language.tr(hide_label)))),+ (hide_others_label, PredefinedMenuItem::hide_others(Some(app.ui.language.tr(hide_others_label)))),+ ];+"#;+ let corrected_findings = predefined_quit([("apps/filmcraft/src/native_menu.rs", corrected_source)]);+ assert!(corrected_findings.is_empty());+}++#[test]+fn test_std_instant_positive() {+ let source = r#"+use std::time::Instant;+use std::time::{Duration, Instant as StdInstant};++fn measure() {+ let t0 = std::time::Instant::now();+}+"#;+ let findings = std_instant([("src/clock.rs", source)]);+ assert_eq!(findings.len(), 3);+ assert_eq!(findings[0].line, 2);+ assert_eq!(findings[1].line, 3);+ assert_eq!(findings[2].line, 6);+}++#[test]+fn test_std_instant_negative_and_web_time() {+ let source = r#"+// std::time::Instant::now() causes wasm panic+/*+ use std::time::Instant;+*/+use web_time::Instant;+use std::time::Duration;++fn measure() {+ let t0 = web_time::Instant::now();+ let d = Duration::from_millis(100);+}+"#;+ let findings = std_instant([("src/safe_clock.rs", source)]);+ assert!(findings.is_empty(), "expected zero findings for web_time, duration, and comments: {findings:?}");+}++/// Historical failure: FilmCraft `crates/engine/src/wasm_clock_tests.rs`+/// jobs running synchronously on wasm32 called `std::time::Instant::now()`, panicking at runtime.+#[test]+fn test_historical_filmcraft_wasm_clock_instant_regression() {+ let problematic_source = r#"+ use std::time::Instant;++ pub fn run_job() {+ let t0 = std::time::Instant::now();+ }+"#;+ let findings = std_instant([("crates/engine/src/job.rs", problematic_source)]);+ assert_eq!(findings.len(), 2);++ let corrected_source = r#"+ use web_time::Instant;++ pub fn run_job() {+ let t0 = web_time::Instant::now();+ }+"#;+ let corrected_findings = std_instant([("crates/engine/src/job.rs", corrected_source)]);+ assert!(corrected_findings.is_empty());++ // User's specific conditional compilation test case:+ let native_guarded_inline = r#"+#[cfg(not(target_arch = "wasm32"))] fn timestamp() -> std::time::Instant { std::time::Instant::now() }+"#;+ assert!(std_instant([("src/native.rs", native_guarded_inline)]).is_empty());++ let native_guarded_multiline = r#"+#[cfg(not(target_arch = "wasm32"))]+fn timestamp() -> std::time::Instant {+ std::time::Instant::now()+}+"#;+ assert!(std_instant([("src/native.rs", native_guarded_multiline)]).is_empty());+}++/// Historical failure: PhotoCraft PR #1722 / #1982 `crates/gpu/src/compose.wgsl:156`+/// `const F32_MAX: f32 = 3.40282347e38;` is slightly above f32::MAX. Naga rounds it natively,+/// but Chromium WebGPU compiler (Tint) rejects it, disabling the GPU compositor.+#[test]+fn test_historical_photocraft_compose_wgsl_regression() {+ let problematic_shader = r#"+// line 156 of compose.wgsl+const F32_MAX: f32 = 3.40282347e38;+"#;+ let findings = wgsl_float_literals(problematic_shader);+ assert_eq!(findings.len(), 1);+ assert_eq!(findings[0].line, 3);+ assert_eq!(findings[0].text, "3.40282347e38");++ // Corrected shader uses exact hexadecimal float representation+ let corrected_shader = r#"+// exact f32::MAX bit for bit+const F32_MAX: f32 = 0x1.fffffep+127f;+"#;+ let corrected_findings = wgsl_float_literals(corrected_shader);+ assert!(corrected_findings.is_empty());+}++#[test]+fn test_wgsl_float_literals_controls() {+ // 1. Scientific notation and signed values+ let signed_and_sci = r#"+const OVER_POS: f32 = 1e39;+const OVER_NEG: f32 = -1e39;+const VALID_POS: f32 = 1.5e10;+const VALID_NEG: f32 = -1.5e10;+const VALID_TINY: f32 = 1e-40;+"#;+ let findings = wgsl_float_literals(signed_and_sci);+ assert_eq!(findings.len(), 2);+ assert_eq!(findings[0].text, "1e39");+ assert_eq!(findings[1].text, "1e39"); // Scanned magnitude of -1e39++ // 2. Hexadecimal floating-point representations+ let hex_floats = r#"+const EXACT_MAX: f32 = 0x1.fffffep+127f;+const ONE_P0: f32 = 0x1.0p0;+const HALF_HEX: f32 = 0x1.0p-1h;+"#;+ assert!(wgsl_float_literals(hex_floats).is_empty());++ // 3. Literals appearing inside comments+ let in_comments = r#"+// const F32_MAX = 3.40282347e38;+/*+ const OVERFLOW = 1e40;+*/+"#;+ assert!(wgsl_float_literals(in_comments).is_empty());++ // 4. Numeric suffixes (f, h)+ let suffixes = r#"+const BAD_F: f32 = 3.40282347e38f;+const GOOD_F: f32 = 1.0f;+const GOOD_H: f16 = 0.5h;+"#;+ let suffix_findings = wgsl_float_literals(suffixes);+ assert_eq!(suffix_findings.len(), 1);+ assert_eq!(suffix_findings[0].text, "3.40282347e38");++ // 5. Embedded in identifiers or other tokens+ let idents = r#"+let var_1 = 1.0;+let layer2 = 2.0;+let coord_0 = 0.0;+"#;+ assert!(wgsl_float_literals(idents).is_empty());+}++#[test]+fn test_wgsl_float_literals_in_multiple_files() {+ let s1 = "const A: f32 = 1.0;\nconst B: f32 = 3.40282347e38;\n";+ let s2 = "const OK: f32 = 0x1.fffffep+127f;\n";+ let s3 = "const BAD: f32 = 9.99e38;\n";++ let files = [("shaders/compose.wgsl", s1), ("shaders/clean.wgsl", s2), ("shaders/fx.wgsl", s3)];+ let findings = wgsl_float_literals_in(files);+ assert_eq!(findings.len(), 2);+ assert_eq!(findings[0].path, "shaders/compose.wgsl");+ assert_eq!(findings[0].line, 2);+ assert_eq!(findings[0].text, "3.40282347e38");+ assert_eq!(findings[1].path, "shaders/fx.wgsl");+ assert_eq!(findings[1].line, 1);+ assert_eq!(findings[1].text, "9.99e38");+}++#[cfg(not(target_arch = "wasm32"))]+#[test]+fn test_predefined_quit_qualified_and_whitespace() {+ let source = r#"+fn setup() {+ let q1 = muda::PredefinedMenuItem::quit(None);+ let q2 = PredefinedMenuItem :: quit(Some("Quit"));+ let q3 = crate::menu::PredefinedMenuItem::Quit;+}+"#;+ let findings = predefined_quit([("src/menu.rs", source)]);+ assert_eq!(findings.len(), 3);+ assert_eq!(findings[0].line, 3);+ assert_eq!(findings[1].line, 4);+ assert_eq!(findings[2].line, 5);+}++#[test]+fn test_std_instant_module_alias() {+ let source = r#"+use std::time;++fn run() {+ let t0 = time::Instant::now();+}+"#;+ let findings = std_instant([("src/worker.rs", source)]);+ assert_eq!(findings.len(), 1);+ assert_eq!(findings[0].line, 5);+}++#[test]+fn test_wgsl_exact_f32_max_boundary() {+ // 3.4028234663852886e38 is mathematical f32::MAX bit for bit+ let exact_valid = r#"+const EXACT_MAX: f32 = 3.4028234663852886e38;+const NEG_EXACT_MAX: f32 = -3.4028234663852886e38;+"#;+ assert!(wgsl_float_literals(exact_valid).is_empty());++ // 3.40282347e38 is slightly above f32::MAX+ let just_over = r#"+const OVER_POS: f32 = 3.40282347e38;+const OVER_NEG: f32 = -3.40282347e38;+"#;+ let findings = wgsl_float_literals(just_over);+ assert_eq!(findings.len(), 2);+ assert_eq!(findings[0].text, "3.40282347e38");+ assert_eq!(findings[1].text, "3.40282347e38");+}++#[cfg(not(target_arch = "wasm32"))]+#[test]+fn test_rust_sources_filtering() {+ use craft_testkit::rust_sources;+ use std::fs::{self, File};+ use std::io::Write;++ let temp_dir = std::env::temp_dir().join(format!("craft_testkit_test_{}", std::process::id()));+ let _ = fs::remove_dir_all(&temp_dir);+ fs::create_dir_all(temp_dir.join("src").join("sub")).expect("create temp sub");+ fs::create_dir_all(temp_dir.join("tests")).expect("create temp tests");+ fs::create_dir_all(temp_dir.join("target")).expect("create temp target");+ fs::create_dir_all(temp_dir.join(".github")).expect("create temp dot-dir");++ let create_file = |path: std::path::PathBuf, content: &str| {+ let mut f = File::create(path).expect("create file");+ f.write_all(content.as_bytes()).expect("write content");+ };++ create_file(temp_dir.join("src").join("lib.rs"), "pub fn lib() {}");+ create_file(temp_dir.join("src").join("sub").join("mod.rs"), "pub fn sub() {}");+ create_file(temp_dir.join("src").join("tests.rs"), "// should exclude");+ create_file(temp_dir.join("src").join("tests_foo.rs"), "// should exclude");+ create_file(temp_dir.join("src").join("foo_tests.rs"), "// should exclude");+ create_file(temp_dir.join("src").join("foo_test.rs"), "// should exclude");+ create_file(temp_dir.join("tests").join("integ.rs"), "// should exclude");+ create_file(temp_dir.join("target").join("gen.rs"), "// should exclude");+ create_file(temp_dir.join(".github").join("action.rs"), "// should exclude dot-dir");+ create_file(temp_dir.join("src").join("README.md"), "# Not a rust source");++ let sources = rust_sources(&temp_dir).expect("rust_sources");+ let names: Vec<String> = sources.iter().map(|(p, _)| std::path::Path::new(p).file_name().unwrap().to_string_lossy().into_owned()).collect();++ assert_eq!(names, vec!["lib.rs", "mod.rs"]);++ // Test passing &[(&str, &str)] to guards+ let findings = predefined_quit(&sources);+ assert!(findings.is_empty());++ let _ = fs::remove_dir_all(&temp_dir);+}