#![allow(
clippy::arithmetic_side_effects,
clippy::indexing_slicing,
clippy::unwrap_used,
clippy::expect_used,
clippy::panic
)]
#![allow(dead_code)]
use std::fs;
use std::net::TcpListener;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use tempfile::TempDir;
pub mod mock_upstream;
pub fn read_config(dir: &Path) -> String {
fs::read_to_string(dir.join(".conproxy/conproxy.toml")).expect("Failed to read config")
}
pub fn write_project_config(dir: &Path) {
let conproxy_dir = dir.join(".conproxy");
fs::create_dir_all(&conproxy_dir).expect("create .conproxy");
fs::create_dir_all(conproxy_dir.join("cache")).expect("create cache");
fs::create_dir_all(conproxy_dir.join("index")).expect("create index");
fs::create_dir_all(conproxy_dir.join("packages")).expect("create packages");
fs::create_dir_all(conproxy_dir.join("web")).expect("create web");
let toml = r#"[upstreams.dummy]
url = "http://127.0.0.1:1"
type = "elasticsearch"
index = "test"
[contexts.default]
default = true
[[contexts.default.upstreams]]
ref = "dummy"
priority = 0
"#;
fs::write(conproxy_dir.join("conproxy.toml"), toml).expect("write toml");
fs::write(conproxy_dir.join(".gitignore"), "cache/\n*.pid\n").expect("write gitignore");
}
pub fn parse_json_output(output: &Output) -> serde_json::Value {
let out = stdout(output);
serde_json::from_str(&out).unwrap_or_else(|e| {
panic!(
"Failed to parse JSON output: {}\nstdout: {}\nstderr: {}",
e,
out,
stderr(output)
)
})
}
pub struct FreePorts {
pub grpc_port: u16,
pub http_port: u16,
}
pub fn find_free_ports() -> FreePorts {
let grpc = TcpListener::bind("127.0.0.1:0").expect("bind grpc port 0");
let grpc_port = grpc.local_addr().expect("local_addr grpc").port();
drop(grpc);
let http = TcpListener::bind("127.0.0.1:0").expect("bind http port 0");
let http_port = http.local_addr().expect("local_addr http").port();
drop(http);
FreePorts {
grpc_port,
http_port,
}
}
pub fn find_free_port() -> u16 {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind to port 0 should succeed");
let port = listener.local_addr().expect("local_addr").port();
drop(listener);
port
}
pub fn start_proxy_daemon(dir: &Path, ports: &FreePorts) -> bool {
let listen_addr = format!("127.0.0.1:{}", ports.grpc_port);
let http_listen_addr = format!("127.0.0.1:{}", ports.http_port);
let config_path = dir.join(".conproxy/conproxy.toml");
if let Ok(config_content) = fs::read_to_string(&config_path) {
let updated = if config_content.contains("[server]") {
let section = format!(
"[server]\nlisten = \"{listen_addr}\"\nhttp_listen = \"{http_listen_addr}\""
);
config_content.replace("[server]", §ion)
} else {
format!(
"[server]\nlisten = \"{listen_addr}\"\nhttp_listen = \"{http_listen_addr}\"\n\n{config_content}"
)
};
let _ = fs::write(&config_path, updated);
}
let output = run_conproxy_in(dir, &["start", "--daemon", "--listen", &listen_addr]);
if !output.status.success() {
return false;
}
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while std::time::Instant::now() < deadline {
std::thread::sleep(std::time::Duration::from_millis(200));
let status_output = run_conproxy_in(dir, &["status", "--json"]);
if status_output.status.success() {
let out = stdout(&status_output);
if out.contains("\"running\": true") || out.contains("\"running\":true") {
return true;
}
}
}
let daemon_log = dir.join(".conproxy/daemon.log");
if let Ok(log) = fs::read_to_string(&daemon_log) {
let mut tail: Vec<&str> = log.lines().rev().take(40).collect::<Vec<_>>();
tail.reverse();
eprintln!(
"--- daemon.log (last 40 lines) ---\n{}\n---",
tail.join("\n")
);
} else {
eprintln!("--- daemon.log not found at {} ---", daemon_log.display());
}
false
}
pub fn start_proxy_daemon_ephemeral(dir: &Path) -> FreePorts {
let ports = find_free_ports();
let started = start_proxy_daemon(dir, &ports);
assert!(
started,
"proxy daemon failed to start on ephemeral gRPC:{} HTTP:{}; tests must not silently skip",
ports.grpc_port, ports.http_port,
);
ports
}
pub fn stop_proxy(dir: &Path) {
let _ = run_conproxy_in(dir, &["stop"]);
}
pub struct ProxyGuard<'a> {
pub dir: &'a Path,
}
impl<'a> ProxyGuard<'a> {
pub fn new(dir: &'a Path) -> Self {
ProxyGuard { dir }
}
}
impl Drop for ProxyGuard<'_> {
fn drop(&mut self) {
stop_proxy(self.dir);
}
}
pub fn conproxy_bin() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_conproxy"))
}
pub fn temp_dir() -> TempDir {
tempfile::tempdir().expect("Failed to create temp dir")
}
pub const CONPROXY_PID_ENV: &str = "CONPROXY_PID_FILE";
pub fn test_pid_file_path(dir: &Path) -> PathBuf {
dir.join(".conproxy").join("test-proxy.pid")
}
pub fn run_conproxy_in(dir: &Path, args: &[&str]) -> Output {
let pid_file = test_pid_file_path(dir);
let mut cmd = Command::new(conproxy_bin());
cmd.current_dir(dir).args(args);
cmd.env(CONPROXY_PID_ENV, pid_file);
cmd.env("PROXY_URL", "http://127.0.0.1:8081");
cmd.output()
.unwrap_or_else(|e| panic!("Failed to run conproxy binary: {e}"))
}
pub fn run_conproxy_with_env(dir: &Path, args: &[&str], env: &[(&str, &str)]) -> Output {
let mut cmd = Command::new(conproxy_bin());
cmd.current_dir(dir).args(args);
for (key, value) in env {
cmd.env(key, value);
}
cmd.output().expect("Failed to run conproxy binary")
}
pub fn assert_success(output: &Output) {
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
panic!("Command failed!\nstdout: {}\nstderr: {}", stdout, stderr);
}
}
pub fn assert_failure(output: &Output) {
assert!(
!output.status.success(),
"Expected command to fail but it succeeded"
);
}
pub fn stdout(output: &Output) -> String {
String::from_utf8_lossy(&output.stdout).to_string()
}
pub fn stderr(output: &Output) -> String {
String::from_utf8_lossy(&output.stderr).to_string()
}
pub fn stdout_contains(output: &Output, needle: &str) -> bool {
stdout(output).contains(needle)
}
pub fn stderr_contains(output: &Output, needle: &str) -> bool {
stderr(output).contains(needle)
}
pub fn create_git_repo(parent: &Path, repo_name: &str, files: &[(&str, &str)]) -> PathBuf {
let repo_path = parent.join(repo_name);
fs::create_dir_all(&repo_path).unwrap();
let output = Command::new("git")
.current_dir(&repo_path)
.args(["init"])
.output()
.expect("Failed to init git repo");
assert!(output.status.success());
Command::new("git")
.current_dir(&repo_path)
.args(["config", "user.email", "test@test.com"])
.output()
.unwrap();
Command::new("git")
.current_dir(&repo_path)
.args(["config", "user.name", "Test"])
.output()
.unwrap();
for (name, content) in files {
let path = repo_path.join(name);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(path, content).unwrap();
}
Command::new("git")
.current_dir(&repo_path)
.args(["add", "."])
.output()
.unwrap();
Command::new("git")
.current_dir(&repo_path)
.args(["commit", "-m", "Initial commit"])
.output()
.unwrap();
repo_path
}
pub fn create_git_repo_with_tag(
parent: &Path,
repo_name: &str,
files: &[(&str, &str)],
tag: &str,
) -> PathBuf {
let repo_path = create_git_repo(parent, repo_name, files);
Command::new("git")
.current_dir(&repo_path)
.args(["tag", tag])
.output()
.unwrap();
repo_path
}
pub fn init_and_install(work_dir: &Path, repo_path: &Path) -> Output {
write_project_config(work_dir);
let output = run_conproxy_in(work_dir, &["install", "--git", repo_path.to_str().unwrap()]);
assert_success(&output);
output
}