luff 0.2.1

Print files with formatting
Documentation
//! Shared `proptest` strategies for the WASM module.
//!
//! Centralizes generation of [`VirtualFile`], [`ProcessorOptions`],
//! and related types so that property tests across `virtual_fs`,
//! `processor`, and `options` all share the same input distribution.

#![allow(dead_code)]

use proptest::prelude::*;

use super::options::ProcessorOptions;
use super::virtual_fs::VirtualFile;
use crate::format::OutputFormat;

/// Strategy for valid path components (no control chars, no `/`, non-empty).
fn path_component() -> impl Strategy<Value = String> {
    // Printable ASCII excluding `/` and leading `.` (dotfiles tested separately).
    prop::string::string_regex("[a-zA-Z_][a-zA-Z0-9_.\\-]{0,30}").unwrap()
}

/// Strategy for a valid relative path with 1–5 components.
pub fn valid_path() -> impl Strategy<Value = String> {
    prop::collection::vec(path_component(), 1..=5).prop_map(|parts| parts.join("/"))
}

/// Strategy for a dotfile path (at least one component starts with `.`).
pub fn dotfile_path() -> impl Strategy<Value = String> {
    (
        prop::collection::vec(path_component(), 0..=3),
        prop::string::string_regex("\\.[a-zA-Z][a-zA-Z0-9_]{0,15}").unwrap(),
        prop::collection::vec(path_component(), 0..=2),
    )
        .prop_map(|(prefix, dot_component, suffix)| {
            let mut parts = prefix;
            parts.push(dot_component);
            parts.extend(suffix);
            parts.join("/")
        })
}

/// Strategy for file content (arbitrary UTF-8, bounded length).
pub fn file_content() -> impl Strategy<Value = String> {
    prop::string::string_regex("[\\s\\S]{0,500}").unwrap()
}

/// Strategy for a valid `VirtualFile`.
pub fn virtual_file() -> impl Strategy<Value = VirtualFile> {
    (valid_path(), file_content())
        .prop_map(|(path, content)| VirtualFile::new_unchecked(path, content))
}

/// Strategy for a `Vec<VirtualFile>` of bounded length.
pub fn virtual_files(max: usize) -> impl Strategy<Value = Vec<VirtualFile>> {
    prop::collection::vec(virtual_file(), 0..=max)
}

/// Strategy for a file extension (no leading dot).
pub fn extension() -> impl Strategy<Value = String> {
    prop::string::string_regex("[a-z]{1,8}").unwrap()
}

/// Strategy for a valid glob pattern.
///
/// Only produces patterns that `globset` will accept, so builder
/// `build()` calls remain infallible in property tests. Mixes
/// static real-world patterns with patterns derived from random
/// path components and extensions to exercise varied matching.
fn glob_pattern() -> impl Strategy<Value = String> {
    prop_oneof![
        // Real-world patterns
        Just("**/target/**".to_string()),
        Just("*.lock".to_string()),
        Just("dist/**".to_string()),
        Just("**/node_modules/**".to_string()),
        Just("**/*.min.js".to_string()),
        // Derived from random components — exercises glob matching
        // against paths generated by `valid_path()`
        path_component().prop_map(|c| format!("**/{c}/**")),
        extension().prop_map(|e| format!("*.{e}")),
    ]
}

/// Strategy for `ProcessorOptions` with random but valid settings.
///
/// Covers all option axes including `max_output_bytes` and glob
/// patterns to exercise the `OutputTooLarge` error path and glob
/// filtering in property tests.
pub fn processor_options() -> impl Strategy<Value = ProcessorOptions> {
    (
        prop::bool::ANY,                              // include_dotfiles
        prop::option::of(1..500usize),                // max_files
        prop::option::of(64..100_000usize),           // max_output_bytes
        prop::collection::vec(extension(), 0..=5),    // ignore_extensions
        prop::collection::vec(glob_pattern(), 0..=3), // ignore_globs
        prop_oneof![Just(OutputFormat::Markdown), Just(OutputFormat::Tree)],
    )
        .prop_map(
            |(
                include_dotfiles,
                max_files,
                max_output_bytes,
                ignore_extensions,
                ignore_globs,
                output_format,
            )| {
                let mut builder = ProcessorOptions::builder()
                    .output_format(output_format)
                    .ignore_extensions(ignore_extensions)
                    .ignore_globs(ignore_globs)
                    .include_dotfiles(include_dotfiles);
                if let Some(max) = max_files {
                    builder = builder.max_files(max);
                }
                if let Some(max) = max_output_bytes {
                    builder = builder.max_output_bytes(max);
                }
                builder.build().expect("generated globs are always valid")
            },
        )
}