use std::fs;
use std::net::SocketAddr;
use std::path::Path;
use std::path::PathBuf;
use std::str::FromStr;
use async_compression::tokio::bufread::GzipDecoder;
use bytes::Bytes;
use futures::StreamExt;
use futures::TryStreamExt;
use tempfile::tempdir;
use tokio::io::BufReader;
use tokio_tar::Archive;
use super::*;
async fn collect_streaming_archive(
config: &Config,
supergraph_schema: &str,
router_config: &serde_json::Value,
) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
let stream = Exporter::create_streaming_archive(
config.clone(),
Arc::new(supergraph_schema.to_string()),
Arc::from(router_config.to_string()),
)
.await;
let chunks: Vec<Bytes> = stream
.map(|chunk| chunk.expect("Stream should not fail"))
.collect()
.await;
Ok(chunks.into_iter().fold(Vec::new(), |mut acc, chunk| {
acc.extend_from_slice(&chunk);
acc
}))
}
#[tokio::test]
async fn test_export_service_creation() {
let config = Config {
enabled: true,
listen: SocketAddr::from_str("127.0.0.1:8089").unwrap().into(),
output_directory: PathBuf::from("/tmp/router-diagnostics"),
};
let test_config_json = Arc::new(serde_json::json!({
"test": "configuration"
}));
let export_service = Exporter::new(
config.clone(),
Arc::new("supergraph_schema".to_string()),
Arc::from(test_config_json.to_string()),
);
assert_eq!(
export_service.config().output_directory,
config.output_directory
);
}
#[tokio::test]
async fn test_create_archive() {
let temp_dir = tempdir().expect("Failed to create temp dir");
let output_path = temp_dir.path();
#[cfg(target_family = "unix")]
{
let memory_path = format!("{}/memory", output_path.to_string_lossy());
fs::create_dir_all(&memory_path).expect("Failed to create memory directory");
fs::write(
format!("{}/test_heap.prof", memory_path),
b"test heap dump data",
)
.expect("Failed to write test file");
fs::write(
format!("{}/test_profile.prof", memory_path),
b"test profile data",
)
.expect("Failed to write another test file");
}
let config = Config {
enabled: true,
listen: SocketAddr::from_str("127.0.0.1:8089").unwrap().into(),
output_directory: output_path.to_path_buf(),
};
let test_full_config = serde_json::json!({
"experimental_diagnostics": {
"enabled": true,
"shared_secret": "test-secret"
}
});
let archive_data =
collect_streaming_archive(&config, "test supergraph schema content", &test_full_config)
.await
.expect("Archive creation should succeed");
assert!(!archive_data.is_empty(), "Archive should not be empty");
let reader = BufReader::new(archive_data.as_slice());
let decoder = GzipDecoder::new(reader);
let mut archive = Archive::new(decoder);
let mut entries = archive
.entries()
.expect("Should be able to read archive entries");
let mut found_manifest = false;
let mut found_router_yaml = false;
let mut found_supergraph = false;
let mut found_memory_content = false;
let mut found_system_info = false;
let mut found_html_report = false;
while let Some(entry) = entries.try_next().await.expect("Should read entry") {
let path = entry
.path()
.expect("Should have path")
.to_string_lossy()
.to_string();
match path.as_str() {
"manifest.txt" => found_manifest = true,
"router.yaml" => found_router_yaml = true,
"supergraph.graphql" => found_supergraph = true,
"system_info.txt" => found_system_info = true,
"diagnostics_report.html" => found_html_report = true,
path if path.starts_with("memory/") => found_memory_content = true,
_ => {} }
}
assert!(found_manifest, "Archive should contain manifest.txt");
assert!(found_router_yaml, "Archive should contain router.yaml");
assert!(
found_supergraph,
"Archive should contain supergraph.graphql"
);
assert!(found_system_info, "Archive should contain system_info.txt");
assert!(
found_memory_content,
"Archive should contain memory/ content (files on Linux, README on other platforms)"
);
assert!(
found_html_report,
"Archive should contain diagnostics_report.html"
);
}
#[tokio::test]
async fn test_create_main_manifest() {
let config = Config {
enabled: true,
listen: SocketAddr::from_str("127.0.0.1:8089").unwrap().into(),
output_directory: PathBuf::from("/tmp/test-diagnostics"),
};
let result = Exporter::create_main_manifest(&config);
let manifest_str = result.expect("Manifest creation should succeed");
assert!(
manifest_str.contains("APOLLO ROUTER DIAGNOSTIC ARCHIVE"),
"Should contain title"
);
assert!(
manifest_str.contains("Router Version:"),
"Should contain version info"
);
assert!(
manifest_str.contains(&format!("Platform: {}", std::env::consts::OS)),
"Should contain current platform info"
);
assert!(
manifest_str.contains("Memory Output Directory: /tmp/test-diagnostics"),
"Should contain output directory"
);
assert!(
manifest_str.contains("supergraph.graphql"),
"Should mention supergraph.graphql file"
);
assert!(
manifest_str.contains("system_info.txt"),
"Should mention system_info.txt file"
);
assert!(
manifest_str.contains("memory/"),
"Should mention memory directory or info"
);
assert!(
manifest_str.contains("diagnostics_report.html"),
"Should mention diagnostics_report.html"
);
#[cfg(target_family = "unix")]
{
assert!(
manifest_str.contains("Memory Profiling: Enabled"),
"Should mention memory profiling enabled on Linux"
);
assert!(
manifest_str.contains("jemalloc profiling"),
"Should mention jemalloc on Linux"
);
}
#[cfg(not(target_family = "unix"))]
{
assert!(
manifest_str.contains("Memory Profiling: Not available"),
"Should mention memory profiling not available on non-Linux"
);
assert!(
manifest_str.contains("Heap dumps require Linux platform"),
"Should mention Linux requirement on non-Linux platforms"
);
}
}
#[cfg(target_family = "unix")]
#[tokio::test]
async fn test_archive_with_empty_output_directory() {
let config = Config {
enabled: true,
listen: SocketAddr::from_str("127.0.0.1:8089").unwrap().into(),
output_directory: PathBuf::from("/tmp/nonexistent-diagnostics-dir"),
};
if Path::new(&config.output_directory).exists() {
fs::remove_dir_all(&config.output_directory).ok();
}
let test_full_config = serde_json::json!({"test": "config"});
let archive_data = collect_streaming_archive(&config, "supergraph_schema", &test_full_config)
.await
.expect("Archive creation should succeed even with empty output directory");
assert!(!archive_data.is_empty(), "Archive should not be empty");
let reader = BufReader::new(archive_data.as_slice());
let decoder = GzipDecoder::new(reader);
let mut archive = Archive::new(decoder);
let mut entries = archive
.entries()
.expect("Should be able to read archive entries");
let mut found_manifest = false;
let mut found_memory_dir = false;
while let Some(entry) = entries
.try_next()
.await
.expect("Should be able to read entry")
{
let path = entry
.path()
.expect("Should have path")
.to_string_lossy()
.to_string();
match path.as_str() {
"manifest.txt" => found_manifest = true,
"memory/" => found_memory_dir = true, _ => {} }
}
assert!(found_manifest, "Archive should contain manifest.txt");
assert!(
found_memory_dir,
"Archive should contain empty memory/ directory"
);
}
#[tokio::test]
async fn test_create_archive_sync() {
let temp_dir = tempdir().expect("Failed to create temp dir");
let output_path = temp_dir.path();
let config = Config {
enabled: true,
listen: SocketAddr::from_str("127.0.0.1:8089").unwrap().into(),
output_directory: output_path.to_path_buf(),
};
let test_full_config = serde_json::json!({"test": "config"});
let archive_data = collect_streaming_archive(&config, "supergraph_schema", &test_full_config)
.await
.expect("Archive creation should succeed");
assert!(!archive_data.is_empty(), "Archive should not be empty");
assert!(
archive_data.len() > 100,
"Archive should have substantial content"
);
}
#[cfg(target_family = "unix")]
#[tokio::test]
async fn test_tar_gz_format_compatibility() {
let temp_dir = tempdir().expect("Failed to create temp dir");
let output_path = temp_dir.path();
let memory_path = format!("{}/memory", output_path.to_string_lossy());
fs::create_dir_all(&memory_path).expect("Failed to create memory directory");
fs::write(
format!("{}/heap_dump_1.prof", memory_path),
b"heap dump content 1",
)
.expect("Failed to write test file");
fs::write(
format!("{}/heap_dump_2.prof", memory_path),
b"heap dump content 2",
)
.expect("Failed to write test file");
let config = Config {
enabled: true,
listen: SocketAddr::from_str("127.0.0.1:8089").unwrap().into(),
output_directory: output_path.to_path_buf(),
};
let test_full_config = serde_json::json!({
"server": {
"listen": "127.0.0.1:4000"
},
"experimental_diagnostics": {
"enabled": true,
"shared_secret": "test-secret"
}
});
let archive_data = collect_streaming_archive(&config, "supergraph_schema", &test_full_config)
.await
.expect("Archive creation should succeed");
let extract_dir = tempdir().expect("Failed to create extraction dir");
let extract_path = extract_dir.path();
let archive_file = extract_path.join("test-archive.tar.gz");
fs::write(&archive_file, &archive_data).expect("Failed to write archive file");
let file = tokio::fs::File::open(&archive_file)
.await
.expect("Failed to open archive file");
let reader = BufReader::new(file);
let decoder = GzipDecoder::new(reader);
let mut archive = Archive::new(decoder);
archive
.unpack(extract_path)
.await
.expect("Failed to extract archive");
let manifest_path = extract_path.join("manifest.txt");
assert!(manifest_path.exists(), "Extracted manifest should exist");
let manifest_content = fs::read_to_string(&manifest_path).expect("Failed to read manifest");
assert!(
manifest_content.contains("APOLLO ROUTER DIAGNOSTIC ARCHIVE"),
"Manifest should have correct content"
);
let router_yaml_path = extract_path.join("router.yaml");
assert!(
router_yaml_path.exists(),
"Extracted router.yaml should exist"
);
let router_yaml_content =
fs::read_to_string(&router_yaml_path).expect("Failed to read router.yaml");
assert!(
router_yaml_content.contains("server"),
"router.yaml should contain configuration data"
);
assert!(
router_yaml_content.contains("experimental_diagnostics"),
"router.yaml should contain diagnostics config"
);
let supergraph_path = extract_path.join("supergraph.graphql");
assert!(
supergraph_path.exists(),
"Extracted supergraph.graphql should exist"
);
let supergraph_content =
fs::read_to_string(&supergraph_path).expect("Failed to read supergraph.graphql");
assert_eq!(
supergraph_content, "supergraph_schema",
"supergraph.graphql should contain the schema content"
);
let memory_dir = extract_path.join("memory");
assert!(
memory_dir.exists(),
"Extracted memory directory should exist"
);
let heap_dump_1 = memory_dir.join("heap_dump_1.prof");
assert!(heap_dump_1.exists(), "First heap dump should exist");
let content_1 = fs::read(&heap_dump_1).expect("Failed to read heap dump 1");
assert_eq!(
content_1, b"heap dump content 1",
"Heap dump content should match"
);
let heap_dump_2 = memory_dir.join("heap_dump_2.prof");
assert!(heap_dump_2.exists(), "Second heap dump should exist");
let content_2 = fs::read(&heap_dump_2).expect("Failed to read heap dump 2");
assert_eq!(
content_2, b"heap dump content 2",
"Heap dump content should match"
);
assert!(
archive_data.len() > 2,
"Archive should be large enough for headers"
);
assert_eq!(
&archive_data[0..2],
&[0x1f, 0x8b],
"Should have gzip magic bytes"
);
}
#[cfg(target_family = "unix")]
#[tokio::test]
async fn test_manual_archive_inspection() {
let temp_dir = tempdir().expect("Failed to create temp dir");
let output_path = temp_dir.path();
let memory_path = format!("{}/memory", output_path.to_string_lossy());
fs::create_dir_all(&memory_path).expect("Failed to create memory directory");
fs::write(format!("{}/test.prof", memory_path), b"test profile data")
.expect("Failed to write test file");
let config = Config {
enabled: true,
listen: SocketAddr::from_str("127.0.0.1:8089").unwrap().into(),
output_directory: output_path.to_path_buf(),
};
let test_full_config = serde_json::json!({
"server": {"listen": "127.0.0.1:4000"},
"experimental_diagnostics": {"enabled": true, "shared_secret": "test-secret"}
});
let archive_data = collect_streaming_archive(&config, "supergraph_schema", &test_full_config)
.await
.expect("Archive creation should succeed");
let debug_archive = "/tmp/debug_router_diagnostics.tar.gz";
fs::write(debug_archive, &archive_data).expect("Failed to write debug archive");
tracing::debug!("Debug archive written to: {}", debug_archive);
let tar_list = std::process::Command::new("tar")
.args(["-tzf", debug_archive])
.output()
.expect("Failed to run tar");
tracing::debug!(
"Tar list output: {}",
String::from_utf8_lossy(&tar_list.stdout)
);
tracing::debug!(
"Tar list stderr: {}",
String::from_utf8_lossy(&tar_list.stderr)
);
assert!(tar_list.status.success(), "tar -t should work");
let extract_dir = "/tmp/debug_extract";
let _ = fs::remove_dir_all(extract_dir); fs::create_dir_all(extract_dir).expect("Failed to create extract dir");
let tar_extract = std::process::Command::new("tar")
.args(["-xzf", debug_archive, "-C", extract_dir])
.output()
.expect("Failed to run tar extract");
tracing::debug!(
"Tar extract stderr: {}",
String::from_utf8_lossy(&tar_extract.stderr)
);
assert!(tar_extract.status.success(), "tar -x should work");
assert!(Path::new(&format!("{}/manifest.txt", extract_dir)).exists());
assert!(Path::new(&format!("{}/router.yaml", extract_dir)).exists());
assert!(Path::new(&format!("{}/memory", extract_dir)).exists());
let router_yaml_content = fs::read_to_string(format!("{extract_dir}/router.yaml"))
.expect("Failed to read router.yaml");
tracing::debug!("Router.yaml content:\n{}", router_yaml_content);
}
#[cfg(target_family = "unix")]
#[tokio::test]
async fn test_archive_format_with_system_tar() {
let temp_dir = tempdir().expect("Failed to create temp dir");
let output_path = temp_dir.path();
let config = Config {
enabled: true,
listen: SocketAddr::from_str("127.0.0.1:8089").unwrap().into(),
output_directory: output_path.to_path_buf(),
};
let test_full_config = serde_json::json!({
"server": {"listen": "127.0.0.1:4000"},
"experimental_diagnostics": {"enabled": true}
});
let archive_data = collect_streaming_archive(&config, "supergraph_schema", &test_full_config)
.await
.expect("Archive creation should succeed");
let archive_file = temp_dir.path().join("test.tar.gz");
fs::write(&archive_file, &archive_data).expect("Failed to write archive");
let output = std::process::Command::new("tar")
.args(["-tzf", archive_file.to_str().unwrap()])
.output()
.expect("Failed to run tar command");
assert!(
output.status.success(),
"tar command should succeed: stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let contents = String::from_utf8(output.stdout).expect("tar output should be valid UTF-8");
assert!(
contents.contains("manifest.txt"),
"Archive should contain manifest.txt"
);
assert!(
contents.contains("router.yaml"),
"Archive should contain router.yaml"
);
assert!(
contents.contains("supergraph.graphql"),
"Archive should contain supergraph.graphql"
);
assert!(
contents.contains("memory/"),
"Archive should contain memory directory"
);
}
#[cfg(target_family = "unix")]
#[tokio::test]
async fn test_tar_gz_structure_validation() {
let temp_dir = tempdir().expect("Failed to create temp dir");
let output_path = temp_dir.path();
let config = Config {
enabled: true,
listen: SocketAddr::from_str("127.0.0.1:8089").unwrap().into(),
output_directory: output_path.to_path_buf(),
};
let test_full_config = serde_json::json!({"test": "configuration"});
let archive_data = collect_streaming_archive(&config, "supergraph_schema", &test_full_config)
.await
.expect("Archive creation should succeed");
let reader = BufReader::new(archive_data.as_slice());
let decoder = GzipDecoder::new(reader);
let mut archive = Archive::new(decoder);
let mut entries_stream = archive.entries().expect("Should be able to read entries");
let mut entries = Vec::new();
while let Some(entry) = entries_stream
.try_next()
.await
.expect("Should be able to read entry")
{
entries.push(entry);
}
assert!(!entries.is_empty(), "Archive should contain entries");
let paths: Vec<String> = entries
.iter()
.map(|entry: &tokio_tar::Entry<_>| entry.path().unwrap().to_string_lossy().to_string())
.collect();
assert!(
paths.contains(&"manifest.txt".to_string()),
"Should contain manifest.txt"
);
assert!(
paths.contains(&"router.yaml".to_string()),
"Should contain router.yaml"
);
assert!(
paths.contains(&"supergraph.graphql".to_string()),
"Should contain supergraph.graphql"
);
assert!(
paths
.iter()
.any(|p| p == "memory/" || p.starts_with("memory/")),
"Should contain memory directory or files"
);
for entry in entries {
let path = entry.path().unwrap().to_string_lossy().to_string();
let header = entry.header();
if path == "manifest.txt" || path.ends_with(".prof") || path.contains("router") {
assert_ne!(
header.mode().unwrap(),
0,
"Files should have proper permissions"
);
}
if path.ends_with("/") {
assert!(
header.entry_type().is_dir(),
"Directories should be marked as directories"
);
} else if !header.entry_type().is_dir() {
assert!(
header.entry_type().is_file(),
"Non-directories should be files"
);
}
}
}
#[tokio::test]
async fn test_non_unix_memory_handling() {
let temp_dir = tempdir().expect("Failed to create temp dir");
let output_path = temp_dir.path();
let config = Config {
enabled: true,
listen: SocketAddr::from_str("127.0.0.1:8089").unwrap().into(),
output_directory: output_path.to_path_buf(),
};
let test_full_config = serde_json::json!({"test": "config"});
let archive_data = collect_streaming_archive(&config, "supergraph_schema", &test_full_config)
.await
.expect("Archive creation should succeed");
let reader = BufReader::new(archive_data.as_slice());
let decoder = GzipDecoder::new(reader);
let mut archive = Archive::new(decoder);
let mut entries = archive
.entries()
.expect("Should be able to read archive entries");
let mut found_memory_content = false;
while let Some(entry) = entries
.try_next()
.await
.expect("Should be able to read entry")
{
let path = entry
.path()
.expect("Should have path")
.to_string_lossy()
.to_string();
if path.starts_with("memory/") {
found_memory_content = true;
#[cfg(target_family = "unix")]
{
assert!(
path.ends_with(".prof") || path == "memory/" || path == "memory/README.txt",
"Memory files should be profile files or directories on Linux"
);
}
#[cfg(not(target_family = "unix"))]
{
assert_eq!(
path, "memory/README.txt",
"Non-Linux should only have README in memory directory"
);
}
}
}
assert!(
found_memory_content,
"Should have some memory-related content"
);
}
#[tokio::test]
async fn test_system_info_collection() {
let result = super::super::system_info::collect().await;
assert!(result.is_ok(), "System info collection should succeed");
let system_info = result.unwrap();
assert!(!system_info.is_empty(), "System info should not be empty");
assert!(
system_info.contains("SYSTEM INFORMATION"),
"Should contain system information header"
);
assert!(
system_info.contains("Operating System:"),
"Should contain OS information"
);
assert!(
system_info.contains("Architecture:"),
"Should contain architecture information"
);
assert!(
system_info.contains("Router Version:"),
"Should contain router version"
);
assert!(
system_info.contains("MEMORY INFORMATION"),
"Should contain memory information section"
);
assert!(
system_info.contains("CPU INFORMATION"),
"Should contain CPU information section"
);
assert!(
system_info.contains("Physical CPU cores:"),
"Should contain CPU core count"
);
assert!(
system_info.contains("RELEVANT ENVIRONMENT VARIABLES"),
"Should contain environment variables section"
);
#[cfg(target_family = "unix")]
{
assert!(
system_info.contains("------------------"),
"Should have section separators"
);
}
#[cfg(not(target_family = "unix"))]
{
assert!(
system_info.contains("not available") || system_info.contains("Unknown"),
"Should have appropriate fallback messages for unavailable info"
);
}
}
#[tokio::test]
async fn test_system_info_in_archive_extraction() {
let temp_dir = tempdir().expect("Failed to create temp dir");
let output_path = temp_dir.path();
let config = Config {
enabled: true,
listen: SocketAddr::from_str("127.0.0.1:8089").unwrap().into(),
output_directory: output_path.to_path_buf(),
};
let test_full_config = serde_json::json!({"test": "config"});
let archive_data = collect_streaming_archive(&config, "supergraph_schema", &test_full_config)
.await
.expect("Archive creation should succeed");
let extract_dir = tempdir().expect("Failed to create extraction dir");
let extract_path = extract_dir.path();
let reader = BufReader::new(archive_data.as_slice());
let decoder = GzipDecoder::new(reader);
let mut archive = Archive::new(decoder);
archive
.unpack(extract_path)
.await
.expect("Failed to extract archive");
let system_info_path = extract_path.join("system_info.txt");
assert!(
system_info_path.exists(),
"Extracted system_info.txt should exist"
);
let system_info_content =
fs::read_to_string(&system_info_path).expect("Failed to read system_info.txt");
assert!(
system_info_content.contains("SYSTEM INFORMATION"),
"system_info.txt should contain system information header"
);
assert!(
system_info_content.contains(&format!("Operating System: {}", std::env::consts::OS)),
"system_info.txt should contain correct OS information"
);
assert!(
system_info_content.contains(&format!("Architecture: {}", std::env::consts::ARCH)),
"system_info.txt should contain correct architecture information"
);
assert!(
system_info_content.len() > 500,
"system_info.txt should contain substantial information"
);
}
#[tokio::test]
async fn test_system_info_environment_variables() {
if std::env::var("APOLLO_GRAPH_REF").is_ok() {
let system_info = super::super::system_info::collect()
.await
.expect("Should collect system info");
assert!(
system_info.contains("RELEVANT ENVIRONMENT VARIABLES"),
"Should have environment variables section"
);
assert!(
system_info.contains("APOLLO_GRAPH_REF:"),
"Should contain APOLLO_GRAPH_REF when it's set"
);
assert!(
!system_info.contains("No relevant Apollo environment variables set"),
"Should not show 'no variables' message when APOLLO_GRAPH_REF is set"
);
} else {
let system_info = super::super::system_info::collect()
.await
.expect("Should collect system info");
assert!(
system_info.contains("RELEVANT ENVIRONMENT VARIABLES"),
"Should have environment variables section"
);
assert!(
system_info.contains("No relevant Apollo environment variables set"),
"Should indicate no relevant variables when APOLLO_GRAPH_REF is not set"
);
}
}
#[tokio::test]
async fn test_system_info_cpu_count() {
let system_info = super::super::system_info::collect()
.await
.expect("Should collect system info");
assert!(
system_info.contains("Physical CPU cores:"),
"Should contain CPU core count information"
);
let cpu_count = std::thread::available_parallelism()
.map(|p| p.get())
.unwrap_or(1);
assert!(cpu_count >= 1, "Should detect at least 1 CPU core");
assert!(
system_info.contains(&cpu_count.to_string()),
"System info should contain the detected CPU count"
);
}
#[tokio::test]
async fn test_html_report_in_archive() {
let temp_dir = tempdir().expect("Failed to create temp dir");
let output_path = temp_dir.path();
let config = Config {
enabled: true,
listen: SocketAddr::from_str("127.0.0.1:8089").unwrap().into(),
output_directory: output_path.to_path_buf(),
};
let test_full_config = serde_json::json!({"test": "config"});
let archive_data = collect_streaming_archive(&config, "supergraph_schema", &test_full_config)
.await
.expect("Archive creation should succeed");
let reader = BufReader::new(archive_data.as_slice());
let decoder = GzipDecoder::new(reader);
let mut archive = Archive::new(decoder);
let mut entries_stream = archive.entries().expect("Should be able to read entries");
let mut found_html_report = false;
while let Some(entry) = entries_stream
.try_next()
.await
.expect("Should be able to read entry")
{
let path = entry.path().unwrap().to_string_lossy().to_string();
if path == "diagnostics_report.html" {
found_html_report = true;
let header = entry.header();
assert!(
header.entry_type().is_file(),
"diagnostics_report.html should be a regular file"
);
assert!(
header.size().unwrap() > 1000,
"diagnostics_report.html should have substantial content"
);
}
}
assert!(
found_html_report,
"Archive should contain diagnostics_report.html"
);
}