Skip to content

Add HTML landing page and integration tests for --all-ranks-html #121

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,6 @@ regex = "1.9.2"
serde = { version = "1.0.185", features = ["serde_derive"] }
serde_json = "1.0.100"
tinytemplate = "1.1.0"

[dev-dependencies]
tempfile = "3"
35 changes: 24 additions & 11 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use anyhow::{bail, Context};
use std::fs;
use std::path::PathBuf;

use tlparse::generate_multi_rank_html;
use tlparse::{parse_path, ParseConfig};

#[derive(Parser)]
Expand Down Expand Up @@ -54,6 +55,12 @@ pub struct Cli {

fn main() -> anyhow::Result<()> {
let cli = Cli::parse();

// Early validation of incompatible flags
if cli.all_ranks_html && cli.latest {
bail!("--latest cannot be used with --all-ranks-html");
}

let path = if cli.latest {
let input_path = cli.path;
// Path should be a directory
Expand All @@ -78,15 +85,6 @@ fn main() -> anyhow::Result<()> {
cli.path
};

if cli.all_ranks_html {
if cli.latest {
bail!("--latest cannot be used with --all-ranks-html");
}
if cli.no_browser {
bail!("--no-browser not yet implemented with --all-ranks-html");
}
}

let config = ParseConfig {
strict: cli.strict,
strict_compile_id: cli.strict_compile_id,
Expand All @@ -99,7 +97,7 @@ fn main() -> anyhow::Result<()> {
};

if cli.all_ranks_html {
handle_all_ranks(&config, path, cli.out, cli.overwrite)?;
handle_all_ranks(&config, path, cli.out, cli.overwrite, !cli.no_browser)?;
} else {
handle_one_rank(
&config,
Expand Down Expand Up @@ -186,6 +184,7 @@ fn handle_all_ranks(
path: PathBuf,
out_path: PathBuf,
overwrite: bool,
open_browser: bool,
) -> anyhow::Result<()> {
let input_dir = path;
if !input_dir.is_dir() {
Expand Down Expand Up @@ -224,6 +223,14 @@ fn handle_all_ranks(
);
}

let mut sorted_ranks: Vec<String> =
rank_logs.iter().map(|(_, rank)| rank.to_string()).collect();
sorted_ranks.sort_by(|a, b| {
a.parse::<u32>()
.unwrap_or(0)
.cmp(&b.parse::<u32>().unwrap_or(0))
});

for (log_path, rank_num) in rank_logs {
let subdir = out_path.join(format!("rank_{rank_num}"));
println!("Processing rank {rank_num} → {}", subdir.display());
Expand All @@ -235,6 +242,12 @@ fn handle_all_ranks(
"Multi-rank report generated under {}\nIndividual pages: rank_*/index.html",
out_path.display()
);
// TODO: generate and open a landing page

let (landing_page_path, landing_html) = generate_multi_rank_html(&out_path, sorted_ranks, cfg)?;
fs::write(&landing_page_path, landing_html)?;
if open_browser {
opener::open(&landing_page_path)?;
}

Ok(())
}
26 changes: 26 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ pub mod parsers;
mod templates;
mod types;

pub use crate::templates::{CSS, TEMPLATE_MULTI_RANK_INDEX, TEMPLATE_QUERY_PARAM_SCRIPT};

#[derive(Debug)]
enum ParserResult {
NoPayload,
Expand Down Expand Up @@ -1145,3 +1147,27 @@ pub fn parse_path(path: &PathBuf, config: &ParseConfig) -> anyhow::Result<ParseO

Ok(output)
}

pub fn generate_multi_rank_html(
out_path: &PathBuf,
sorted_ranks: Vec<String>,
cfg: &ParseConfig,
) -> anyhow::Result<(PathBuf, String)> {
// Create the TinyTemplate instance for rendering the landing page.
let mut tt = TinyTemplate::new();
tt.add_formatter("format_unescaped", tinytemplate::format_unescaped);
tt.add_template("multi_rank_index.html", TEMPLATE_MULTI_RANK_INDEX)?;

let ctx = MultiRankContext {
css: CSS,
custom_header_html: &cfg.custom_header_html,
num_ranks: sorted_ranks.len(),
ranks: sorted_ranks,
qps: TEMPLATE_QUERY_PARAM_SCRIPT,
};

let html = tt.render("multi_rank_index.html", &ctx)?;
let landing_page_path = out_path.join("index.html");

Ok((landing_page_path, html))
}
Empty file.
175 changes: 175 additions & 0 deletions tests/integration_test.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use tempfile::tempdir;
use tlparse;

fn prefix_exists(map: &HashMap<PathBuf, String>, prefix: &str) -> bool {
Expand Down Expand Up @@ -419,3 +422,175 @@ fn test_provenance_tracking() {
);
}
}

#[test]
fn test_all_ranks_basic() -> Result<(), Box<dyn std::error::Error>> {
let input_dir = PathBuf::from("tests/inputs/multi_rank_logs");
let temp_dir = tempdir().unwrap();
let out_dir = temp_dir.path().join("out");

let output = Command::new(env!("CARGO_BIN_EXE_tlparse"))
.arg(&input_dir)
.arg("--all-ranks-html")
.arg("--overwrite")
.arg("-o")
.arg(&out_dir)
.arg("--no-browser")
.output()?;

assert!(
output.status.success(),
"tlparse command failed. stderr: {}",
String::from_utf8_lossy(&output.stderr)
);

let rank0_index = out_dir.join("rank_0/index.html");
let rank1_index = out_dir.join("rank_1/index.html");
let landing_page = out_dir.join("index.html");

assert!(rank0_index.exists(), "rank 0 index.html should exist");
assert!(rank1_index.exists(), "rank 1 index.html should exist");
assert!(landing_page.exists(), "toplevel index.html should exist");

let landing_content = fs::read_to_string(landing_page).unwrap();
assert!(
landing_content.contains(r#"<a href="rank_0/index.html">"#),
"Landing page should contain a link to rank 0"
);
assert!(
landing_content.contains(r#"<a href="rank_1/index.html">"#),
"Landing page should contain a link to rank 1"
);
Ok(())
}

#[test]
fn test_all_ranks_messy_input() -> Result<(), Box<dyn std::error::Error>> {
let input_dir = PathBuf::from("tests/inputs/multi_rank_messy_input");
let temp_dir = tempdir().unwrap();
let out_dir = temp_dir.path().join("out");

let output = Command::new(env!("CARGO_BIN_EXE_tlparse"))
.arg(&input_dir)
.arg("--all-ranks-html")
.arg("--overwrite")
.arg("-o")
.arg(&out_dir)
.arg("--no-browser")
.output()?;

assert!(
output.status.success(),
"tlparse command failed on messy input. stderr: {}",
String::from_utf8_lossy(&output.stderr)
);

let rank0_index = out_dir.join("rank_0/index.html");
let rank1_index = out_dir.join("rank_1/index.html");
let landing_page = out_dir.join("index.html");

assert!(
rank0_index.exists(),
"rank 0 index.html should exist in messy input test"
);
assert!(
rank1_index.exists(),
"rank 1 index.html should exist in messy input test"
);
assert!(
landing_page.exists(),
"toplevel index.html should exist in messy input test"
);

let landing_content = fs::read_to_string(landing_page).unwrap();
assert!(
landing_content.contains(r#"<a href="rank_0/index.html">"#),
"Landing page should contain a link to rank 0 in messy input test"
);
assert!(
landing_content.contains(r#"<a href="rank_1/index.html">"#),
"Landing page should contain a link to rank 1 in messy input test"
);
Ok(())
}

#[test]
fn test_all_ranks_no_browser() -> Result<(), Box<dyn std::error::Error>> {
let input_dir = PathBuf::from("tests/inputs/multi_rank_logs");
let temp_dir = tempdir().unwrap();
let out_dir = temp_dir.path().join("out");

let output = Command::new(env!("CARGO_BIN_EXE_tlparse"))
.arg(&input_dir)
.arg("--all-ranks-html")
.arg("--overwrite")
.arg("-o")
.arg(&out_dir)
.arg("--no-browser")
.output()?;

assert!(
output.status.success(),
"tlparse command failed. stderr: {}",
String::from_utf8_lossy(&output.stderr)
);

let rank0_index = out_dir.join("rank_0/index.html");
let rank1_index = out_dir.join("rank_1/index.html");
let landing_page = out_dir.join("index.html");

assert!(rank0_index.exists(), "rank 0 index.html should exist");
assert!(rank1_index.exists(), "rank 1 index.html should exist");
assert!(landing_page.exists(), "toplevel index.html should exist");
Ok(())
}

#[test]
fn test_all_ranks_with_latest_fails() -> Result<(), Box<dyn std::error::Error>> {
let input_dir = PathBuf::from("tests/inputs/multi_rank_logs");
let temp_root = tempdir()?; // only used for output cleanup
let out_dir = temp_root.path().join("out");

let output = Command::new(env!("CARGO_BIN_EXE_tlparse"))
.arg(&input_dir)
.arg("--all-ranks-html")
.arg("--latest")
.arg("-o")
.arg(&out_dir)
.output()?;

assert!(
!output.status.success(),
"tlparse should fail when --all-ranks-html and --latest are used together"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("--latest cannot be used with --all-ranks-html"),
"stderr should complain about using --latest with --all-ranks-html"
);
Ok(())
}

#[test]
fn test_all_ranks_no_logs() -> Result<(), Box<dyn std::error::Error>> {
let temp_root = tempdir()?;
let input_dir = temp_root.path().to_path_buf();
let out_dir = temp_root.path().join("out");

let output = Command::new(env!("CARGO_BIN_EXE_tlparse"))
.arg(&input_dir)
.arg("--all-ranks-html")
.arg("--overwrite")
.arg("-o")
.arg(&out_dir)
.arg("--no-browser")
.output()?;

assert!(!output.status.success(), "tlparse should fail on empty dir");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("No rank log files found"),
"stderr should complain about missing log files"
);
Ok(())
}
Loading