chore(release): prepare public source release

This commit is contained in:
MercuryToolbox Release
2026-07-18 15:41:59 +08:00
commit e365e5df4d
508 changed files with 163373 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
[package]
name = "snip"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
readme.workspace = true
publish.workspace = true
description = "Extract precise code and text snippets for AI-friendly terminal workflows."
keywords.workspace = true
categories.workspace = true
[lints]
workspace = true
[dependencies]
common = { path = "../common", default-features = false }
lexopt.workspace = true
regex-lite.workspace = true
serde.workspace = true
serde_json.workspace = true
[dev-dependencies]
assert_cmd.workspace = true
predicates.workspace = true
tempfile.workspace = true
+867
View File
@@ -0,0 +1,867 @@
//! The `snip` command extracts precise snippets from files or stdin.
use std::ffi::OsString;
use std::fmt::Write as _;
use std::fs;
use std::io::{self, Read};
use std::path::{Path, PathBuf};
use common::{
CliError, CommonArgs, ExitCode, RenderMode, parse_color_choice, parse_format_choice,
parse_input_format, print_json, print_quick_help_error, print_structured, should_read_stdin,
};
use lexopt::prelude::{Long, Short, Value as ArgValue};
use regex_lite::Regex;
use serde::Serialize;
const MAX_SOURCE_BYTES: u64 = 8 * 1024 * 1024;
/// CLI arguments for the `snip` binary.
#[derive(Debug, Clone)]
pub struct Cli {
/// Shared output and stdin policy flags.
pub common: CommonArgs,
/// Extract an exact inclusive line range such as `12:20`.
pub lines: Option<String>,
/// Extract snippets around lines matching this regex.
pub around: Option<String>,
/// Extract the block belonging to this symbol name.
pub symbol: Option<String>,
/// Extra context lines around `--around` and `--symbol` matches.
pub context: usize,
/// Maximum number of regex matches to emit for `--around`.
pub max_matches: usize,
/// Optional files to read when stdin is empty.
pub paths: Vec<PathBuf>,
}
const HELP: &str = "\
Extract precise code and text snippets from files or stdin.
Usage:
snip [OPTIONS] [PATH...]
Options:
--format <FORMAT> Structured output format: text, json, toon
--json Shortcut for --format json
--toon Shortcut for --format toon
--input-format <FORMAT> Override stdin parsing mode: auto, lines, jsonl
--color <WHEN> Control ANSI color output: auto, never
--quiet Suppress non-essential status output
--lines <RANGE> Extract an exact inclusive line range such as 12:20
--around <REGEX> Extract snippets around lines matching this regex
--symbol <NAME> Extract the block belonging to this symbol name
--context <COUNT> Extra context lines around --around and --symbol
--max-matches <COUNT> Maximum number of matches to emit for --around
-h, --help Show this help text
-V, --version Show the command version
Examples:
snip --lines 16:27 .\\fixtures\\reading\\sample.rs
snip --symbol run .\\fixtures\\reading\\sample.rs --json | ConvertFrom-Json
bat --style=plain --paging=never .\\fixtures\\reading\\sample.rs | snip --around helper --context 0
Notes:
for AST-backed full definitions across files or repos, prefer `defsnip`
";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ParseOutcome {
Help,
Version,
Run,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct LineRange {
start: usize,
end: usize,
}
#[derive(Debug)]
enum Selector {
Lines(LineRange),
Around {
pattern: String,
regex: Regex,
context: usize,
max_matches: usize,
},
Symbol {
name: String,
context: usize,
},
}
#[derive(Debug)]
struct Source {
label: String,
extension: Option<String>,
lines: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
struct SnippetLine {
number: usize,
text: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
struct Snippet {
path: String,
start_line: usize,
end_line: usize,
reason: String,
lines: Vec<SnippetLine>,
}
/// Parses CLI arguments and returns a process exit code.
#[must_use]
pub fn main_entry() -> i32 {
match parse_cli_from(std::env::args_os()) {
Ok((ParseOutcome::Help, _)) => {
print!("{HELP}");
ExitCode::Success.as_i32()
}
Ok((ParseOutcome::Version, _)) => {
println!("snip {}", env!("CARGO_PKG_VERSION"));
ExitCode::Success.as_i32()
}
Ok((ParseOutcome::Run, cli)) => match run(&cli) {
Ok(code) => code.as_i32(),
Err(error) => {
print_quick_help_error(&error, HELP);
error.exit_code().as_i32()
}
},
Err(error) => {
print_quick_help_error(&error, HELP);
error.exit_code().as_i32()
}
}
}
fn parse_cli_from<I, T>(args: I) -> Result<(ParseOutcome, Cli), CliError>
where
I: IntoIterator<Item = T>,
T: Into<OsString>,
{
let mut parser = lexopt::Parser::from_iter(args);
let mut cli = Cli {
common: CommonArgs::default(),
lines: None,
around: None,
symbol: None,
context: 2,
max_matches: 1,
paths: Vec::new(),
};
while let Some(argument) = parser
.next()
.map_err(|error| CliError::usage(error.to_string()))?
{
match argument {
Long("help") | Short('h') => return Ok((ParseOutcome::Help, cli)),
Long("version") | Short('V') => return Ok((ParseOutcome::Version, cli)),
Long("json") => cli.common.set_render_mode(RenderMode::Json),
Long("toon") => cli.common.set_render_mode(RenderMode::Toon),
Long("format") => {
let value = parser_value_string(&mut parser, "--format")?;
cli.common.set_render_mode(parse_format_choice(&value)?);
}
Long("input-format") => {
let value = parser_value_string(&mut parser, "--input-format")?;
cli.common.input_format = parse_input_format(&value)?;
}
Long("color") => {
let value = parser_value_string(&mut parser, "--color")?;
cli.common.color = parse_color_choice(&value)?;
}
Long("quiet") => cli.common.quiet = true,
Long("lines") => {
cli.lines = Some(parser_value_string(&mut parser, "--lines")?);
}
Long("around") => {
cli.around = Some(parser_value_string(&mut parser, "--around")?);
}
Long("symbol") => {
cli.symbol = Some(parser_value_string(&mut parser, "--symbol")?);
}
Long("context") => {
cli.context =
parse_usize_flag("--context", &parser_value_string(&mut parser, "--context")?)?;
}
Long("max-matches") => {
cli.max_matches = parse_usize_flag(
"--max-matches",
&parser_value_string(&mut parser, "--max-matches")?,
)?;
}
ArgValue(path) => cli.paths.push(PathBuf::from(path)),
_ => {
return Err(CliError::usage(
"unsupported argument; use --help to see available options",
));
}
}
}
Ok((ParseOutcome::Run, cli))
}
fn parser_value_string(parser: &mut lexopt::Parser, flag: &str) -> Result<String, CliError> {
let value = parser
.value()
.map_err(|error| CliError::usage(error.to_string()))?;
value.into_string().map_err(|invalid| {
CliError::usage(format!(
"{flag} expects UTF-8 text, got '{}'",
invalid.to_string_lossy()
))
})
}
fn parse_usize_flag(flag: &str, value: &str) -> Result<usize, CliError> {
value
.parse::<usize>()
.map_err(|error| CliError::usage(format!("invalid {flag} value '{value}': {error}")))
}
fn run(cli: &Cli) -> Result<ExitCode, CliError> {
let selector = build_selector(cli)?;
let sources = load_sources(cli)?;
let mut snippets = Vec::new();
for source in &sources {
snippets.extend(select_snippets(source, &selector));
}
match cli.common.render_mode() {
RenderMode::Json => print_json(&snippets)?,
RenderMode::Toon => print_structured(&snippets, RenderMode::Toon)?,
RenderMode::Text => print!("{}", render_text_snippets(&snippets)),
}
Ok(if snippets.is_empty() {
ExitCode::NoResults
} else {
ExitCode::Success
})
}
fn build_selector(cli: &Cli) -> Result<Selector, CliError> {
let selector_count = usize::from(cli.lines.is_some())
+ usize::from(cli.around.is_some())
+ usize::from(cli.symbol.is_some());
if selector_count != 1 {
return Err(CliError::usage(
"select exactly one of --lines, --around, or --symbol",
));
}
if cli.max_matches == 0 {
return Err(CliError::usage("--max-matches must be greater than 0"));
}
if let Some(lines) = &cli.lines {
return Ok(Selector::Lines(parse_range(lines)?));
}
if let Some(pattern) = &cli.around {
let regex = Regex::new(pattern)
.map_err(|error| CliError::usage(format!("invalid --around regex: {error}")))?;
return Ok(Selector::Around {
pattern: pattern.clone(),
regex,
context: cli.context,
max_matches: cli.max_matches,
});
}
if let Some(name) = &cli.symbol {
return Ok(Selector::Symbol {
name: name.clone(),
context: cli.context,
});
}
Err(CliError::usage(
"select one of --lines, --around, or --symbol",
))
}
fn parse_range(raw: &str) -> Result<LineRange, CliError> {
let Some((start, end)) = raw.split_once(':') else {
let line = parse_positive(raw, "--lines")?;
return Ok(LineRange {
start: line,
end: line,
});
};
let start = parse_positive(start, "--lines")?;
let end = parse_positive(end, "--lines")?;
if start > end {
return Err(CliError::usage(
"--lines start must be less than or equal to end",
));
}
Ok(LineRange { start, end })
}
fn parse_positive(raw: &str, flag: &str) -> Result<usize, CliError> {
let value = raw
.parse::<usize>()
.map_err(|error| CliError::usage(format!("invalid {flag} value '{raw}': {error}")))?;
if value == 0 {
return Err(CliError::usage(format!(
"{flag} values must be greater than 0"
)));
}
Ok(value)
}
fn load_sources(cli: &Cli) -> Result<Vec<Source>, CliError> {
if should_read_stdin(!cli.paths.is_empty(), cli.common.stdin_is_terminal()) {
let mut buffer = String::new();
io::stdin()
.take(MAX_SOURCE_BYTES + 1)
.read_to_string(&mut buffer)
.map_err(|error| CliError::runtime(format!("failed to read stdin: {error}")))?;
if buffer.len() as u64 > MAX_SOURCE_BYTES {
return Err(CliError::runtime(format!(
"refusing to read stdin larger than {MAX_SOURCE_BYTES} bytes"
)));
}
if !buffer.is_empty() {
return Ok(vec![Source {
label: "<stdin>".to_string(),
extension: None,
lines: text_lines(&buffer),
}]);
}
}
if cli.paths.is_empty() {
return Err(CliError::usage(
"provide at least one path or pipe file content into stdin",
));
}
let mut sources = Vec::new();
for path in &cli.paths {
let content = read_bounded_source(path)?;
sources.push(Source {
label: path.display().to_string(),
extension: extension_label(path),
lines: text_lines(&content),
});
}
Ok(sources)
}
fn read_bounded_source(path: &Path) -> Result<String, CliError> {
read_bounded_source_with_limit(path, MAX_SOURCE_BYTES)
}
fn read_bounded_source_with_limit(path: &Path, max_source_bytes: u64) -> Result<String, CliError> {
let metadata = fs::metadata(path).map_err(|error| {
CliError::runtime(format!("failed to inspect {}: {error}", path.display()))
})?;
if metadata.len() > max_source_bytes {
return Err(CliError::runtime(format!(
"refusing to read {} because it is {} bytes; snip source files are capped at {max_source_bytes} bytes",
path.display(),
metadata.len()
)));
}
fs::read_to_string(path)
.map_err(|error| CliError::runtime(format!("failed to read {}: {error}", path.display())))
}
fn text_lines(content: &str) -> Vec<String> {
content
.trim_start_matches('\u{feff}')
.lines()
.map(ToString::to_string)
.collect::<Vec<_>>()
}
fn extension_label(path: &Path) -> Option<String> {
path.extension()
.and_then(|ext| ext.to_str())
.map(str::to_ascii_lowercase)
}
fn select_snippets(source: &Source, selector: &Selector) -> Vec<Snippet> {
match selector {
Selector::Lines(range) => {
select_range_snippet(source, *range).map_or_else(Vec::new, |snippet| vec![snippet])
}
Selector::Around {
pattern,
regex,
context,
max_matches,
} => select_around_snippets(source, pattern, regex, *context, *max_matches),
Selector::Symbol { name, context } => select_symbol_snippet(source, name, *context)
.map_or_else(Vec::new, |snippet| vec![snippet]),
}
}
fn select_range_snippet(source: &Source, range: LineRange) -> Option<Snippet> {
if range.start > source.lines.len() {
return None;
}
Some(make_snippet(
source,
LineRange {
start: range.start,
end: range.end.min(source.lines.len()),
},
"lines".to_string(),
))
}
fn select_around_snippets(
source: &Source,
pattern: &str,
regex: &Regex,
context: usize,
max_matches: usize,
) -> Vec<Snippet> {
source
.lines
.iter()
.enumerate()
.filter(|(_, line)| regex.is_match(line))
.take(max_matches)
.map(|(index, _)| {
let line = index + 1;
make_snippet(
source,
LineRange {
start: line.saturating_sub(context).max(1),
end: (line + context).min(source.lines.len()),
},
format!("around:{pattern}"),
)
})
.collect::<Vec<_>>()
}
fn select_symbol_snippet(source: &Source, name: &str, context: usize) -> Option<Snippet> {
let line = find_symbol_line(source, name)?;
let range = expand_symbol_range(&source.lines, line, context);
Some(make_snippet(source, range, format!("symbol:{name}")))
}
fn find_symbol_line(source: &Source, name: &str) -> Option<usize> {
let patterns = symbol_patterns(source.extension.as_deref(), name);
source.lines.iter().enumerate().find_map(|(index, line)| {
patterns
.iter()
.any(|pattern| pattern.is_match(line))
.then_some(index + 1)
})
}
fn symbol_patterns(extension: Option<&str>, name: &str) -> Vec<Regex> {
let escaped = regex_lite::escape(name);
let raw_patterns = match extension {
Some("rs") => vec![
format!(r"^\s*(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?:unsafe\s+)?fn\s+{escaped}\b"),
format!(r"^\s*(?:pub(?:\([^)]*\))?\s+)?(?:struct|enum|trait|mod|type)\s+{escaped}\b"),
format!(r"^\s*impl\b.*\b{escaped}\b"),
],
Some("cs") => vec![
format!(
r"^\s*(?:public|private|protected|internal|static|sealed|abstract|partial|\s)+(?:class|struct|enum|interface|record)\s+{escaped}\b"
),
format!(
r"^\s*(?:public|private|protected|internal|static|virtual|override|async|\s)+[\w<>\[\],?]+\s+{escaped}\s*\("
),
format!(r"^\s*namespace\s+.*\b{escaped}\b"),
],
_ => vec![format!(r"\b{escaped}\b")],
};
raw_patterns
.into_iter()
.map(|pattern| Regex::new(&pattern).expect("internal symbol regex must compile"))
.collect::<Vec<_>>()
}
fn expand_symbol_range(lines: &[String], line: usize, context: usize) -> LineRange {
let start_index = line - 1;
find_open_brace_line(lines, start_index).map_or_else(
|| LineRange {
start: line.saturating_sub(context).max(1),
end: (line + context).min(lines.len()),
},
|open_index| LineRange {
start: line.saturating_sub(context).max(1),
end: find_block_end(lines, open_index),
},
)
}
fn find_open_brace_line(lines: &[String], start_index: usize) -> Option<usize> {
let search_end = (start_index + 8).min(lines.len().saturating_sub(1));
(start_index..=search_end).find(|index| lines[*index].contains('{'))
}
fn find_block_end(lines: &[String], open_index: usize) -> usize {
let mut depth = 0_usize;
let mut saw_open = false;
for (index, line) in lines.iter().enumerate().skip(open_index) {
for ch in line.chars() {
if ch == '{' {
depth += 1;
saw_open = true;
} else if ch == '}' && saw_open {
depth = depth.saturating_sub(1);
if depth == 0 {
return index + 1;
}
}
}
}
open_index + 1
}
fn make_snippet(source: &Source, range: LineRange, reason: String) -> Snippet {
Snippet {
path: source.label.clone(),
start_line: range.start,
end_line: range.end,
reason,
lines: source.lines[range.start - 1..range.end]
.iter()
.enumerate()
.map(|(offset, text)| SnippetLine {
number: range.start + offset,
text: text.clone(),
})
.collect::<Vec<_>>(),
}
}
fn render_text_snippets(snippets: &[Snippet]) -> String {
let mut rendered = String::new();
for (index, snippet) in snippets.iter().enumerate() {
if index > 0 {
rendered.push('\n');
}
writeln!(
rendered,
"path={} lines={}:{} reason={}",
snippet.path, snippet.start_line, snippet.end_line, snippet.reason
)
.expect("writing to a String cannot fail");
for line in &snippet.lines {
writeln!(rendered, "{}: {}", line.number, line.text)
.expect("writing to a String cannot fail");
}
}
rendered
}
#[cfg(test)]
mod tests {
use std::fs;
use common::ColorChoice;
use tempfile::tempdir;
use super::*;
fn common_args(json: bool) -> CommonArgs {
CommonArgs {
json,
format: None,
input_format: common::InputFormat::Auto,
color: ColorChoice::Never,
quiet: false,
}
}
fn source(lines: &[&str], extension: Option<&str>) -> Source {
Source {
label: "fixture".to_string(),
extension: extension.map(str::to_string),
lines: lines
.iter()
.map(|line| (*line).to_string())
.collect::<Vec<_>>(),
}
}
#[test]
fn parse_range_accepts_single_lines_and_ranges() {
assert_eq!(
parse_range("4").expect("single line"),
LineRange { start: 4, end: 4 }
);
assert_eq!(
parse_range("2:7").expect("range"),
LineRange { start: 2, end: 7 }
);
let error = parse_range("7:2").expect_err("reversed range should fail");
assert!(matches!(
error,
CliError::Usage(message)
if message.contains("start must be less than or equal")
));
}
#[test]
fn symbol_patterns_and_block_expansion_cover_rust_and_csharp() {
let rust = source(
&[
"pub fn run() {",
" println!(\"hi\");",
"}",
"fn helper() {}",
],
Some("rs"),
);
assert_eq!(find_symbol_line(&rust, "run"), Some(1));
assert_eq!(
expand_symbol_range(&rust.lines, 1, 0),
LineRange { start: 1, end: 3 }
);
let csharp = source(
&[
"public class PlayerController {",
" private int ComputeScore(int baseScore) {",
" return baseScore;",
" }",
"}",
],
Some("cs"),
);
assert_eq!(find_symbol_line(&csharp, "PlayerController"), Some(1));
assert_eq!(find_symbol_line(&csharp, "ComputeScore"), Some(2));
}
#[test]
fn range_and_around_selection_render_compact_text() {
let source = source(&["alpha", "beta", "helper", "delta"], Some("txt"));
let range =
select_range_snippet(&source, LineRange { start: 2, end: 3 }).expect("range snippet");
assert_eq!(range.start_line, 2);
assert_eq!(range.end_line, 3);
let regex = Regex::new("hel.+er").expect("regex");
let around = select_around_snippets(&source, "helper", &regex, 1, 1);
assert_eq!(around.len(), 1);
assert_eq!(around[0].start_line, 2);
assert_eq!(around[0].end_line, 4);
let text = render_text_snippets(&around);
assert!(text.contains("reason=around:helper"));
assert!(text.contains("3: helper"));
}
#[test]
fn selector_builder_and_numeric_parsing_report_usage_errors() {
let error = build_selector(&Cli {
common: common_args(false),
lines: Some("1".to_string()),
around: None,
symbol: None,
context: 0,
max_matches: 0,
paths: Vec::new(),
})
.expect_err("zero max matches should fail");
assert!(matches!(
error,
CliError::Usage(message)
if message.contains("--max-matches must be greater than 0")
));
let error = build_selector(&Cli {
common: common_args(false),
lines: None,
around: Some("[".to_string()),
symbol: None,
context: 0,
max_matches: 1,
paths: Vec::new(),
})
.expect_err("invalid regex should fail");
assert!(matches!(
error,
CliError::Usage(message)
if message.contains("invalid --around regex")
));
assert!(matches!(
build_selector(&Cli {
common: common_args(false),
lines: None,
around: None,
symbol: Some("helper".to_string()),
context: 1,
max_matches: 1,
paths: Vec::new(),
})
.expect("symbol selector"),
Selector::Symbol { .. }
));
let error = parse_positive("0", "--lines").expect_err("zero should fail");
assert!(matches!(
error,
CliError::Usage(message)
if message.contains("greater than 0")
));
let error = build_selector(&Cli {
common: common_args(false),
lines: Some("1".to_string()),
around: Some("helper".to_string()),
symbol: None,
context: 1,
max_matches: 1,
paths: Vec::new(),
})
.expect_err("multiple selectors should fail");
assert!(matches!(
error,
CliError::Usage(message)
if message.contains("select exactly one")
));
}
#[test]
fn source_loading_and_range_selection_cover_file_and_error_paths() {
let temp = tempdir().expect("tempdir");
let path = temp.path().join("sample.rs");
fs::write(&path, "pub fn run() {}\n").expect("fixture");
let loaded = load_sources(&Cli {
common: common_args(false),
lines: Some("1".to_string()),
around: None,
symbol: None,
context: 0,
max_matches: 1,
paths: vec![path],
})
.expect("sources");
assert_eq!(loaded.len(), 1);
assert_eq!(loaded[0].extension.as_deref(), Some("rs"));
let missing = load_sources(&Cli {
common: common_args(false),
lines: Some("1".to_string()),
around: None,
symbol: None,
context: 0,
max_matches: 1,
paths: Vec::new(),
})
.expect_err("missing input should fail");
assert!(matches!(
missing,
CliError::Usage(message)
if message.contains("provide at least one path")
));
let source = source(&["alpha"], Some("txt"));
assert!(select_range_snippet(&source, LineRange { start: 9, end: 9 }).is_none());
}
#[test]
fn source_loading_rejects_large_files() {
let temp = tempdir().expect("tempdir");
let path = temp.path().join("large.txt");
fs::write(&path, vec![b'a'; 129]).expect("large file");
let error =
read_bounded_source_with_limit(&path, 128).expect_err("large input should fail");
assert!(matches!(
error,
CliError::Runtime(message) if message.contains("source files are capped")
));
}
#[test]
fn symbol_lookup_handles_generic_matches_and_braceless_fallbacks() {
let generic = source(&["alpha helper beta", "omega"], None);
let snippet = select_symbol_snippet(&generic, "helper", 1).expect("generic symbol");
assert_eq!(snippet.start_line, 1);
assert_eq!(snippet.end_line, 2);
let lines = vec!["fn demo()".to_string(), "value".to_string()];
assert_eq!(
expand_symbol_range(&lines, 1, 1),
LineRange { start: 1, end: 2 }
);
let unmatched = vec!["fn demo() {".to_string(), "value".to_string()];
assert_eq!(find_block_end(&unmatched, 0), 1);
assert!(select_symbol_snippet(&generic, "missing", 0).is_none());
}
#[test]
fn symbol_lookup_handles_multiline_rust_signatures_before_open_brace() {
let source = source(
&[
"fn parse_cli_from<I, T>(",
" args: I,",
") -> Result<(), CliError>",
"where",
" I: IntoIterator<Item = T>,",
" T: Into<OsString>,",
"{",
" Ok(())",
"}",
],
Some("rs"),
);
let snippet = select_symbol_snippet(&source, "parse_cli_from", 0).expect("symbol");
assert_eq!(snippet.start_line, 1);
assert_eq!(snippet.end_line, 9);
}
#[test]
fn run_maps_success_and_no_results_for_text_and_json_modes() {
let temp = tempdir().expect("tempdir");
let path = temp.path().join("sample.txt");
fs::write(&path, "alpha\nbeta\nhelper\n").expect("fixture");
let success = run(&Cli {
common: common_args(true),
lines: None,
around: Some("helper".to_string()),
symbol: None,
context: 0,
max_matches: 1,
paths: vec![path.clone()],
})
.expect("json run");
assert_eq!(success, ExitCode::Success);
let no_results = run(&Cli {
common: common_args(false),
lines: None,
around: Some("missing".to_string()),
symbol: None,
context: 0,
max_matches: 1,
paths: vec![path],
})
.expect("text run");
assert_eq!(no_results, ExitCode::NoResults);
}
}
+5
View File
@@ -0,0 +1,5 @@
//! Binary entry point for `snip`.
fn main() {
std::process::exit(snip::main_entry());
}
+159
View File
@@ -0,0 +1,159 @@
//! Integration tests for the `snip` command.
use std::fs;
use std::path::PathBuf;
use assert_cmd::Command;
use predicates::prelude::*;
use tempfile::{TempDir, tempdir};
const SAMPLE_RS: &str = "reading/sample.rs";
fn cargo_command() -> Command {
Command::cargo_bin("snip").expect("binary")
}
fn fixture(path: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.join("fixtures")
.join(path)
}
fn temp_file(name: &str, contents: impl AsRef<[u8]>) -> (TempDir, PathBuf) {
let dir = tempdir().expect("tempdir");
let path = dir.path().join(name);
fs::write(&path, contents).expect("fixture");
(dir, path)
}
fn pwsh_command(script: impl AsRef<str>) -> Command {
let mut command = Command::new("pwsh");
command
.arg("-NoProfile")
.arg("-Command")
.arg(script.as_ref());
command
}
fn ps_quote(value: impl std::fmt::Display) -> String {
format!("'{}'", value.to_string().replace('\'', "''"))
}
#[test]
fn extracts_requested_line_ranges() {
let mut command = cargo_command();
command
.arg("--lines")
.arg("18:31")
.arg(fixture(SAMPLE_RS))
.assert()
.success()
.stdout(predicate::str::contains("reason=lines"))
.stdout(predicate::str::contains("18: pub fn run"))
.stdout(predicate::str::contains("31: }"));
}
#[test]
fn extracts_symbol_blocks() {
let mut command = cargo_command();
command
.arg("--symbol")
.arg("ComputeScore")
.arg(fixture("reading/sample.cs"))
.assert()
.success()
.stdout(predicate::str::contains("reason=symbol:ComputeScore"))
.stdout(predicate::str::contains(
"9: private int ComputeScore",
))
.stdout(predicate::str::contains("15: }"));
}
#[test]
fn extracts_regex_matches_with_context_as_json() {
let mut command = cargo_command();
command
.arg("--around")
.arg("Mode::Fast")
.arg("--context")
.arg("1")
.arg("--json")
.arg(fixture("reading/sample.rs"))
.assert()
.success()
.stdout(predicate::str::contains("\"reason\":\"around:Mode::Fast\""))
.stdout(predicate::str::contains("\"start_line\":21"))
.stdout(predicate::str::contains("\"end_line\":23"));
}
#[test]
fn supports_stdin_content_snipping() {
let binary = assert_cmd::cargo::cargo_bin("snip");
let input = fixture(SAMPLE_RS);
let script = format!(
"[System.IO.File]::ReadLines({}) | & {} --around 'helper' --context 0",
ps_quote(input.display()),
ps_quote(binary.display())
);
let mut command = pwsh_command(script);
command
.assert()
.success()
.stdout(predicate::str::contains("<stdin>"))
.stdout(predicate::str::contains("fn helper"));
}
#[test]
fn utf8_bom_stdin_does_not_pollute_first_snippet_line() {
let mut command = cargo_command();
command
.args(["--lines", "1:1"])
.write_stdin("\u{feff}fn main() {}\n")
.assert()
.success()
.stdout(predicate::str::contains("1: fn main() {}"));
}
#[test]
fn utf8_bom_file_does_not_pollute_first_snippet_line() {
let (_dir, path) = temp_file("bom.rs", "\u{feff}fn main() {}\n");
let mut command = cargo_command();
command
.arg("--lines")
.arg("1:1")
.arg(&path)
.assert()
.success()
.stdout(predicate::str::contains("1: fn main() {}"));
}
#[test]
fn invalid_utf8_file_reports_read_error() {
let (_dir, path) = temp_file("invalid-utf8.rs", [0x66, 0x6E, 0x80, 0x0A]);
let mut command = cargo_command();
command
.arg("--lines")
.arg("1:1")
.arg(&path)
.assert()
.failure()
.stderr(predicate::str::contains("failed to read"))
.stderr(predicate::str::contains("invalid-utf8.rs"));
}
#[test]
fn help_includes_selector_examples() {
let mut command = cargo_command();
command
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("--lines"))
.stdout(predicate::str::contains("--symbol"))
.stdout(predicate::str::contains("ConvertFrom-Json"));
}