use std::fs;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use tempfile::TempDir;
#[allow(dead_code)]
pub fn llman_bin() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_llman"))
}
#[allow(dead_code)]
pub fn llman_command(config_dir: &Path) -> Command {
let mut cmd = Command::new(llman_bin());
cmd.args(["--config-dir", config_dir.to_str().expect("config dir")]);
cmd
}
#[allow(dead_code)]
pub fn prepare_work_and_config_dirs(root: &Path) -> (PathBuf, PathBuf) {
let work_dir = root.join("work");
fs::create_dir_all(&work_dir).expect("mkdir work");
let work_dir = fs::canonicalize(&work_dir).expect("canonicalize work");
let config_dir = root.join("config");
fs::create_dir_all(&config_dir).expect("mkdir config");
(work_dir, config_dir)
}
#[allow(dead_code)]
pub fn write_claude_code_config(config_dir: &Path, content: &str) -> PathBuf {
fs::create_dir_all(config_dir).expect("create config dir");
let config_path = config_dir.join("claude-code.toml");
fs::write(&config_path, content).expect("write claude-code.toml");
config_path
}
#[allow(dead_code)]
pub fn write_claude_code_sessions_index(project_dir: &Path, entries_json: &str) -> PathBuf {
let index_path = project_dir.join("sessions-index.json");
let index = format!(
r#"{{
"version": 1,
"entries": {entries_json},
"originalPath": "/ignored"
}}"#
);
fs::write(&index_path, index).expect("write sessions-index");
index_path
}
#[allow(dead_code)]
pub fn run_llman(args: &[&str], work_dir: &Path, config_dir: &Path) -> Output {
llman_command(config_dir)
.args(args)
.current_dir(work_dir)
.output()
.expect("run llman")
}
#[cfg(unix)]
#[allow(dead_code)]
pub fn llman_command_with_editor(config_dir: &Path, editor_raw: &str) -> Command {
let mut cmd = llman_command(config_dir);
cmd.env_remove("VISUAL").env("EDITOR", editor_raw);
cmd
}
#[cfg(unix)]
#[allow(dead_code)]
pub fn run_llman_with_editor(
args: &[&str],
work_dir: &Path,
config_dir: &Path,
editor_raw: &str,
) -> Output {
llman_command_with_editor(config_dir, editor_raw)
.args(args)
.current_dir(work_dir)
.output()
.expect("run llman with editor")
}
#[cfg(unix)]
#[allow(dead_code)]
pub fn write_executable_script(path: &Path, content: &str) -> PathBuf {
fs::write(path, content).expect("write script");
let mut perms = fs::metadata(path).expect("metadata").permissions();
perms.set_mode(0o755);
fs::set_permissions(path, perms).expect("chmod");
path.to_path_buf()
}
#[allow(dead_code)]
pub fn assert_success(output: &Output) {
if output.status.success() {
return;
}
panic!(
"Command failed: {}\nstdout:\n{}\nstderr:\n{}",
output.status,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
#[allow(dead_code)]
pub fn git_head(work_dir: &Path) -> Option<String> {
let output = Command::new("git")
.args(["rev-parse", "--verify", "HEAD"])
.current_dir(work_dir)
.output()
.ok()?;
if !output.status.success() {
return None;
}
let head = String::from_utf8(output.stdout).ok()?;
let head = head.trim();
(!head.is_empty()).then(|| head.to_string())
}
#[allow(dead_code)]
pub mod test_constants {
pub const DEFAULT_MIN_COMMENT_LENGTH: u32 = 20;
pub const SHORT_COMMENT_LENGTH: u32 = 5;
pub const PRESERVED_COMMENT_LENGTH: u32 = 25;
pub const DEFAULT_PRESERVE_PATTERNS: &[&str] = &["^\\s*#\\s*(TODO|FIXME):"];
pub const PYTHON_FILE_PATTERN: &str = "**/*.py";
pub const JS_FILE_PATTERN: &str = "**/*.js";
pub const RUST_FILE_PATTERN: &str = "**/*.rs";
pub const TS_FILE_PATTERN: &str = "**/*.ts";
}
#[allow(dead_code)]
pub mod test_content {
pub const PYTHON_CODE_WITH_COMMENTS: &str = r#"#!/usr/bin/env python3
# This is a short comment that should be removed
def hello():
# Another short comment
print("Hello") # Inline comment
# TODO: This should be preserved
# FIXME: This should also be preserved
return "done"
"#;
pub const JAVASCRIPT_CODE_WITH_COMMENTS: &str = r#"// Short comment
function hello() {
console.log("Hello"); // Inline comment
// TODO: This should be preserved
return "done";
}
"#;
pub const RUST_CODE_WITH_COMMENTS: &str = r#"// Short comment
fn main() {
println!("Hello"); // Inline comment
/// This is a doc comment and should be preserved
// TODO: This should be preserved
}
"#;
pub const TYPESCRIPT_CODE_WITH_COMMENTS: &str = r#"// Short comment
interface User {
name: string;
age: number; // Inline comment
// TODO: This should be preserved
}
/**
* This is a JSDoc comment that should be preserved
*/
function greet(user: User): void {
console.log(`Hello, ${user.name}!`);
// FIXME: Implementation needed
}
"#;
pub const PYTHON_CODE_IMPORTANT_COMMENTS: &str = r#"#!/usr/bin/env python3
# x
def important_function():
# TODO: This is a TODO item
# FIXME: This needs to be fixed
# NOTE: This is an important note
# y
print("Hello")
"#;
}
#[allow(dead_code)]
pub fn create_test_file_with_content(
dir: &Path,
filename: &str,
content: &str,
) -> std::path::PathBuf {
let file_path = dir.join(filename);
fs::write(&file_path, content).expect("Failed to write test file");
file_path
}
pub fn create_test_config(dir: &Path, config_content: &str) -> std::path::PathBuf {
let config_dir = dir.join(".llman");
fs::create_dir_all(&config_dir).expect("Failed to create .llman directory");
let config_path = config_dir.join("config.yaml");
fs::write(&config_path, config_content).expect("Failed to write config file");
config_path
}
pub struct TestEnvironment {
#[allow(dead_code)]
pub(crate) temp_dir: TempDir,
pub work_dir: std::path::PathBuf,
}
impl Default for TestEnvironment {
fn default() -> Self {
Self::new()
}
}
impl TestEnvironment {
pub fn new() -> Self {
let temp_dir = TempDir::new().expect("Failed to create temp directory");
let work_dir = temp_dir.path().to_path_buf();
std::process::Command::new("git")
.args(["init", "--quiet"])
.current_dir(&work_dir)
.output()
.expect("Failed to initialize git repo");
Self { temp_dir, work_dir }
}
#[allow(dead_code)]
pub fn path(&self) -> &Path {
&self.work_dir
}
#[allow(dead_code)]
pub fn create_file(&self, filename: &str, content: &str) -> std::path::PathBuf {
create_test_file_with_content(&self.work_dir, filename, content)
}
pub fn create_config(&self, config_content: &str) -> std::path::PathBuf {
create_test_config(&self.work_dir, config_content)
}
#[allow(dead_code)]
pub fn create_python_clean_config(&self, min_comment_length: u32) -> std::path::PathBuf {
let config = format!(
r#"
version: "0.1"
tools:
clean-useless-comments:
scope:
include:
- "{}"
lang-rules:
python:
single-line-comments: true
preserve-patterns:
- "^\\s*#\\s*(TODO|FIXME):"
min-comment-length: {}
"#,
test_constants::PYTHON_FILE_PATTERN,
min_comment_length
);
self.create_config(&config)
}
#[allow(dead_code)]
pub fn create_javascript_clean_config(&self, min_comment_length: u32) -> std::path::PathBuf {
let config = format!(
r#"
version: "0.1"
tools:
clean-useless-comments:
scope:
include:
- "{}"
lang-rules:
javascript:
single-line-comments: true
preserve-patterns:
- "^\\s*//\\s*(TODO|FIXME):"
min-comment-length: {}
"#,
test_constants::JS_FILE_PATTERN,
min_comment_length
);
self.create_config(&config)
}
#[allow(dead_code)]
pub fn create_rust_clean_config(&self, min_comment_length: u32) -> std::path::PathBuf {
let config = format!(
r#"
version: "0.1"
tools:
clean-useless-comments:
scope:
include:
- "{}"
lang-rules:
rust:
single-line-comments: true
doc-comments: false
preserve-patterns:
- "^\\s*//\\s*(TODO|FIXME):"
- "^\\s*///"
min-comment-length: {}
"#,
test_constants::RUST_FILE_PATTERN,
min_comment_length
);
self.create_config(&config)
}
#[allow(dead_code)]
pub fn create_typescript_clean_config(&self, min_comment_length: u32) -> std::path::PathBuf {
let config = format!(
r#"
version: "0.1"
tools:
clean-useless-comments:
scope:
include:
- "{}"
lang-rules:
javascript:
single-line-comments: true
multi-line-comments: true
doc-comments: false
preserve-patterns:
- "^\\s*//\\s*(TODO|FIXME):"
- "^\\s*/\\*\\*\\s*.*?\\*/"
min-comment-length: {}
"#,
test_constants::TS_FILE_PATTERN,
min_comment_length
);
self.create_config(&config)
}
}