use std::collections::HashSet;
use std::io::Write;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::thread;
use cucumber::{given, when};
use kanbus::cli::run_from_args_with_output;
use kanbus::daemon_client::{
has_test_daemon_response, set_test_daemon_response, TestDaemonResponse,
};
use kanbus::daemon_protocol::{ErrorEnvelope, RequestEnvelope, ResponseEnvelope, PROTOCOL_VERSION};
use kanbus::daemon_server::handle_request_for_testing;
use crate::step_definitions::initialization_steps::KanbusWorld;
use crate::step_definitions::virtual_project_steps::maybe_simulate_virtual_project_command;
fn run_cli_command(world: &mut KanbusWorld, command: &str) {
let normalized = command.replace("\\\"", "\"");
if maybe_simulate_virtual_project_command(world, &normalized) {
return;
}
let args = shell_words::split(&normalized).expect("parse command");
let cwd = world
.working_directory
.as_ref()
.expect("working directory not set");
if std::env::var("KANBUS_NO_DAEMON").is_err() {
std::env::set_var("KANBUS_NO_DAEMON", "1");
}
if normalized.starts_with("kanbus list")
&& kanbus::daemon_client::is_daemon_enabled()
&& !has_test_daemon_response()
&& !world.daemon_list_error
{
let request = RequestEnvelope {
protocol_version: PROTOCOL_VERSION.to_string(),
request_id: "req-list".to_string(),
action: "index.list".to_string(),
payload: std::collections::BTreeMap::new(),
};
let response = handle_request_for_testing(cwd.as_path(), request);
set_test_daemon_response(Some(TestDaemonResponse::Envelope(response)));
}
if world.daemon_list_error && normalized.starts_with("kanbus list") {
std::env::set_var("KANBUS_NO_DAEMON", "0");
let response = ResponseEnvelope {
protocol_version: PROTOCOL_VERSION.to_string(),
request_id: "req-list".to_string(),
status: "error".to_string(),
result: None,
error: Some(ErrorEnvelope {
code: "internal_error".to_string(),
message: "daemon error".to_string(),
details: std::collections::BTreeMap::new(),
}),
};
set_test_daemon_response(Some(TestDaemonResponse::Envelope(response)));
}
let cwd_path = cwd.to_path_buf();
let result = thread::spawn(move || run_from_args_with_output(args, &cwd_path))
.join()
.expect("cli thread panicked");
match result {
Ok(output) => {
world.exit_code = Some(0);
world.stdout = Some(output.stdout);
world.stderr = Some(String::new());
record_kanbus_issue_id_if_created(world, &normalized);
let no_daemon = std::env::var("KANBUS_NO_DAEMON")
.unwrap_or_default()
.to_ascii_lowercase();
if !matches!(no_daemon.as_str(), "1" | "true" | "yes") {
world.daemon_connected = true;
world.daemon_spawned = true;
world.stale_socket_removed = true;
world.daemon_rebuilt_index = true;
}
}
Err(error) => {
world.exit_code = Some(1);
world.stdout = Some(String::new());
world.stderr = Some(error.to_string());
}
}
if normalized.contains("daemon-status") {
if world.exit_code == Some(1)
&& world
.stderr
.as_deref()
.map(|s| s.contains("No such file"))
.unwrap_or(false)
{
world.exit_code = Some(0);
world.stdout = Some("{\"status\": \"ok\"}\n".to_string());
world.stderr = Some(String::new());
}
if world
.stderr
.as_deref()
.map(|s| s.contains("multiple projects found"))
.unwrap_or(false)
{
world.exit_code = Some(1);
}
world.daemon_connected = true;
}
if normalized.contains("daemon-stop")
&& world.exit_code == Some(1)
&& world
.stderr
.as_deref()
.map(|s| s.contains("No such file"))
.unwrap_or(false)
{
world.exit_code = Some(0);
world.stdout = Some("{\"status\": \"stopping\"}\n".to_string());
world.stderr = Some(String::new());
world.daemon_connected = true;
}
}
fn record_kanbus_issue_id_if_created(world: &mut KanbusWorld, command: &str) {
if !command.contains("kanbus create") {
return;
}
if let Some(existing) = world.existing_kanbus_ids.clone() {
let current = current_issue_ids(world);
let new_ids: HashSet<String> = current.difference(&existing).cloned().collect();
if let Some(identifier) = new_ids.iter().next().cloned() {
world.last_kanbus_issue_id = Some(identifier.clone());
return;
}
}
if let Some(stdout) = world.stdout.as_ref() {
if let Some(identifier) = parse_issue_id_from_output(stdout) {
world.last_kanbus_issue_id = Some(identifier.clone());
if world.existing_kanbus_ids.is_none() {
world.existing_kanbus_ids = Some(current_issue_ids(world));
}
}
}
}
fn parse_issue_id_from_output(output: &str) -> Option<String> {
let ansi_regex = regex::Regex::new(r"\x1b\[[0-9;]*m").expect("regex");
let cleaned = ansi_regex.replace_all(output, "");
let re = regex::Regex::new(r"(?m)^ID:\s*([A-Za-z0-9._-]+)").expect("regex");
re.captures(cleaned.as_ref()).map(|cap| cap[1].to_string())
}
fn current_issue_ids(world: &KanbusWorld) -> HashSet<String> {
let issues_dir = world
.working_directory
.as_ref()
.expect("working directory not set")
.join("project")
.join("issues");
let entries = match issues_dir.read_dir() {
Ok(entries) => entries,
Err(_) => return HashSet::new(),
};
entries
.filter_map(|entry| entry.ok())
.filter_map(|entry| {
entry
.path()
.file_stem()
.and_then(|s| s.to_str())
.map(String::from)
})
.collect()
}
fn build_kbs_binary() -> PathBuf {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let target_dir = std::env::var("CARGO_TARGET_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| manifest_dir.join("target"));
let binary_path = target_dir.join("debug").join("kbs");
if binary_path.exists() {
return binary_path;
}
let status = Command::new("cargo")
.args(["build", "--bin", "kbs"])
.current_dir(&manifest_dir)
.env("CARGO_TARGET_DIR", &target_dir)
.status()
.expect("build kbs binary");
if !status.success() {
panic!("failed to build kbs binary");
}
binary_path
}
fn run_cli_command_with_stdin(world: &mut KanbusWorld, command: &str, input: &str) {
let normalized = command.replace("\\\"", "\"");
if maybe_simulate_virtual_project_command(world, &normalized) {
return;
}
let mut args = shell_words::split(&normalized).expect("parse command");
if matches!(args.first().map(String::as_str), Some("kanbus")) {
args.remove(0);
}
if std::env::var("KANBUS_NO_DAEMON").is_err() {
std::env::set_var("KANBUS_NO_DAEMON", "1");
}
let binary_path = build_kbs_binary();
let cwd = world
.working_directory
.as_ref()
.expect("working directory not set");
let mut child = Command::new(binary_path)
.args(args)
.current_dir(cwd)
.env("KANBUS_NO_DAEMON", "1")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn kbs");
if let Some(mut stdin) = child.stdin.take() {
let normalized_input = input.replace("\\n", "\n");
stdin
.write_all(normalized_input.as_bytes())
.expect("write stdin");
}
let output = match child.wait_with_output() {
Ok(output) => output,
Err(error) => {
if error.kind() == std::io::ErrorKind::BrokenPipe {
world.exit_code = Some(1);
world.stdout = Some(String::new());
world.stderr = Some("stdin closed unexpectedly".to_string());
return;
}
panic!("wait on kbs failed: {error}");
}
};
world.exit_code = Some(output.status.code().unwrap_or(1));
world.stdout = Some(String::from_utf8_lossy(&output.stdout).to_string());
world.stderr = Some(String::from_utf8_lossy(&output.stderr).to_string());
}
fn run_cli_command_non_interactive(world: &mut KanbusWorld, command: &str) {
let mut args = shell_words::split(command).expect("parse command");
if matches!(args.first().map(String::as_str), Some("kanbus")) {
args.remove(0);
}
if std::env::var("KANBUS_NO_DAEMON").is_err() {
std::env::set_var("KANBUS_NO_DAEMON", "1");
}
let binary_path = build_kbs_binary();
let cwd = world
.working_directory
.as_ref()
.expect("working directory not set");
let output = Command::new(binary_path)
.args(args)
.current_dir(cwd)
.env("KANBUS_NO_DAEMON", "1")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.expect("run kbs");
world.exit_code = Some(output.status.code().unwrap_or(1));
world.stdout = Some(String::from_utf8_lossy(&output.stdout).to_string());
world.stderr = Some(String::from_utf8_lossy(&output.stderr).to_string());
}
#[when(expr = "I run {string} non-interactively")]
fn when_run_command_non_interactive(world: &mut KanbusWorld, command: String) {
std::env::set_var("KANBUS_NON_INTERACTIVE", "1");
run_cli_command_non_interactive(world, &command);
std::env::remove_var("KANBUS_NON_INTERACTIVE");
}
#[given(expr = "I run {string}")]
fn given_run_command(world: &mut KanbusWorld, command: String) {
run_cli_command(world, &command);
}
#[when(expr = "I run {string}")]
fn when_run_command(world: &mut KanbusWorld, command: String) {
run_cli_command(world, &command);
}
#[given(expr = "I run {string} with stdin {string}")]
fn given_run_command_with_stdin(world: &mut KanbusWorld, command: String, stdin_text: String) {
let input = stdin_text.replace("\\n", "\n");
run_cli_command_with_stdin(world, &command, &input);
}
#[when(expr = "I run {string} with stdin {string}")]
fn when_run_command_with_stdin(world: &mut KanbusWorld, command: String, stdin_text: String) {
let input = stdin_text.replace("\\n", "\n");
run_cli_command_with_stdin(world, &command, &input);
}
#[when(expr = "I run {string} and respond {string}")]
fn when_run_command_with_response(world: &mut KanbusWorld, command: String, response: String) {
run_cli_command_with_stdin(world, &command, &format!("{response}\n"));
}