diff --git a/README.md b/README.md index c9c70fd..8703dc3 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,33 @@ changed: Per-side env vars override shared env vars when both set the same key. +### What ran: the Run configuration block + +Every comment the runner posts — running, completed, and failed — states what it +is benchmarking: a `Comparing () to [diff]` line, and a +collapsed **Run configuration** block holding the refs and env vars in the same +YAML the trigger accepts. + +It is re-rendered from the job rather than quoted from the trigger comment, +because the two differ: one `run benchmark tpch clickbench_1` fans out into one +job (and one comment) per name, and a bare `run benchmarks` expands to the repo's +default suite. The block shows what *this* run did, so pasting it back into a PR +comment reproduces that run on its own. + +```yaml +run benchmark tpch +env: + CARGO_BUILD_JOBS: "1" +baseline: + ref: "v45.0.0" +changed: + env: + DATAFUSION_RUNTIME_MEMORY_LIMIT: "2G" +``` + +Sections with nothing to report are omitted, so an unconfigured run renders as +just its trigger line. + ### Peak memory pool reservation DataFusion records the high-water mark of `MemoryPool::reserved()` per query and diff --git a/controller/src/bin/runner.rs b/controller/src/bin/runner.rs index 3c47904..3d07df5 100644 --- a/controller/src/bin/runner.rs +++ b/controller/src/bin/runner.rs @@ -9,7 +9,7 @@ use tracing::{error, info}; use benchmark_controller::github; use benchmark_controller::runner::config::{BenchType, RunnerConfig}; use benchmark_controller::runner::poster::CommentPoster; -use benchmark_controller::runner::{bench_arrow, bench_datafusion, shell}; +use benchmark_controller::runner::{bench_arrow, bench_datafusion, shell, trigger}; #[tokio::main] async fn main() { @@ -68,9 +68,18 @@ async fn run_benchmark(config: &RunnerConfig, poster: &CommentPoster) -> Result< async fn post_error_comment(config: &RunnerConfig, poster: &CommentPoster) { let tail = shell::tail_log(20).await; + // A failure can happen before either checkout resolves, so there is no + // comparison line here — just the requested configuration. + let bench_names = match config.bench_type { + BenchType::ArrowCriterion => config.bench_name.as_str(), + _ => config.benchmarks.as_str(), + }; + let config_block = trigger::config_block(config, bench_names); + let footer = github::issues_footer(config.runner_repo_url.as_deref()); let body = format!( "Benchmark for [this request]({}) failed.\n\n\ + {config_block}\ Last 20 lines of output:\n\
Click to expand\n\n\ ```\n\ diff --git a/controller/src/job_manager.rs b/controller/src/job_manager.rs index 836a668..5a3dc94 100644 --- a/controller/src/job_manager.rs +++ b/controller/src/job_manager.rs @@ -414,6 +414,14 @@ async fn create_k8s_job( env.push(env_var(k, v)); } + // Also pass the shared block as a JSON map. The pod already has these set + // individually (above); the map lets the runner report them in PR comments + // without having to guess which of its env vars came from the trigger. + env.push(env_var( + "SHARED_ENV_VARS", + serde_json::to_string(&shared_env_vars)?, + )); + // Pass per-side env vars as JSON maps for the runner to apply env.push(env_var("BASELINE_ENV_VARS", &job.baseline_env_vars)); env.push(env_var("CHANGED_ENV_VARS", &job.changed_env_vars)); diff --git a/controller/src/runner/bench_arrow.rs b/controller/src/runner/bench_arrow.rs index 0c11a39..1757583 100644 --- a/controller/src/runner/bench_arrow.rs +++ b/controller/src/runner/bench_arrow.rs @@ -11,6 +11,7 @@ use crate::runner::git; use crate::runner::monitor; use crate::runner::poster::CommentPoster; use crate::runner::shell; +use crate::runner::trigger; /// Run an arrow-rs criterion benchmark comparing a PR branch to its merge-base. pub async fn run(config: &RunnerConfig, poster: &CommentPoster) -> Result<()> { @@ -75,6 +76,15 @@ pub async fn run(config: &RunnerConfig, poster: &CommentPoster) -> Result<()> { } else { format!("{} (merge-base)", &base_sha[..7.min(base_sha.len())]) }; + let comparison = trigger::Comparison { + repo: &config.repo, + changed_display, + changed_sha: &changed_sha, + baseline_label: &baseline_label, + base_sha: &base_sha, + }; + let config_block = trigger::config_block(config, bench_name); + let footer = github::issues_footer(config.runner_repo_url.as_deref()); let running_body = format!( "\u{1f916} Arrow criterion benchmark running (GKE) | [trigger]({})\n\ @@ -84,14 +94,12 @@ pub async fn run(config: &RunnerConfig, poster: &CommentPoster) -> Result<()> { {lscpu}\n\ ```\n\n\
\n\n\ - Comparing {changed_display} ({changed_sha}) to {baseline_label} \ - [diff](https://github.com/{repo}/compare/{base_sha}..{changed_sha})\n\ - BENCH_NAME={bench_name}\n\ + {comparison}\n\n\ + {config_block}\ BENCH_COMMAND={bench_command_display}\n\ - BENCH_FILTER={bench_filter}\n\ Results will be posted here when complete{footer}", config.comment_url, - repo = config.repo, + comparison = comparison.line(), ); let pr_number = config.pr_number()?; poster @@ -193,6 +201,8 @@ pub async fn run(config: &RunnerConfig, poster: &CommentPoster) -> Result<()> { ); format_result_comment( &config.comment_url, + &comparison.line(), + &config_block, &report, &resource_section, &instance_type, @@ -209,6 +219,8 @@ pub async fn run(config: &RunnerConfig, poster: &CommentPoster) -> Result<()> { monitor::format_resource_comment("branch", &branch_stats).to_string(); format_branch_only_result_comment( &config.comment_url, + &comparison.line(), + &config_block, &report, &resource_section, &instance_type, @@ -253,8 +265,14 @@ async fn copy_criterion_baselines(base_dir: &Path, branch_dir: &Path) { } /// Format the result comment body. +/// +/// `comparison` and `config_block` restate what was run, so the result reads on +/// its own rather than only linking back to the trigger comment. +#[allow(clippy::too_many_arguments)] fn format_result_comment( comment_url: &str, + comparison: &str, + config_block: &str, report: &str, resource_section: &str, instance_type: &str, @@ -265,6 +283,8 @@ fn format_result_comment( format!( "\u{1f916} Arrow criterion benchmark completed (GKE) | [trigger]({comment_url})\n\n\ **Instance:** `{instance_type}` ({pod_resources})\n\n\ + {comparison}\n\n\ + {config_block}\
CPU Details (lscpu)\n\n\ ```\n\ {lscpu}\n\ @@ -285,8 +305,11 @@ fn format_result_comment( } /// Format the result comment body for branch-only runs (no baseline comparison). +#[allow(clippy::too_many_arguments)] fn format_branch_only_result_comment( comment_url: &str, + comparison: &str, + config_block: &str, report: &str, resource_section: &str, instance_type: &str, @@ -297,6 +320,8 @@ fn format_branch_only_result_comment( format!( "\u{1f916} Arrow criterion benchmark completed (GKE) | [trigger]({comment_url})\n\n\ **Instance:** `{instance_type}` ({pod_resources})\n\n\ + {comparison}\n\n\ + {config_block}\
CPU Details (lscpu)\n\n\ ```\n\ {lscpu}\n\ @@ -340,10 +365,17 @@ mod tests { ); } + /// Stand-ins for the two "what was run" sections the runner passes in. + const COMPARISON: &str = + "Comparing my-branch (aaa) to bbb (merge-base) [diff](https://example.com/diff)"; + const CONFIG_BLOCK: &str = "
Run configuration\n\n```yaml\nrun benchmark concatenate_kernel\n```\n\n
\n\n"; + #[test] fn result_comment_format() { let comment = format_result_comment( "https://example.com/comment", + COMPARISON, + CONFIG_BLOCK, "test report\n", "resources\n", "c4a-standard-48", @@ -358,12 +390,16 @@ mod tests { assert!(comment.contains("c4a-standard-48")); assert!(comment.contains("12 vCPU / 65 GiB")); assert!(comment.contains("lscpu output")); + assert!(comment.contains("Comparing my-branch (aaa)")); + assert!(comment.contains("run benchmark concatenate_kernel")); } #[test] fn branch_only_result_comment_format() { let comment = format_branch_only_result_comment( "https://example.com/comment", + COMPARISON, + CONFIG_BLOCK, "branch report\n", "branch resources\n", "c4a-standard-48", @@ -372,6 +408,8 @@ mod tests { "", ); assert!(comment.contains("Arrow criterion benchmark completed")); + assert!(comment.contains("Comparing my-branch (aaa)")); + assert!(comment.contains("run benchmark concatenate_kernel")); assert!(comment.contains("New benchmark — branch-only results")); assert!(comment.contains("[trigger](https://example.com/comment)")); assert!(comment.contains("branch report")); diff --git a/controller/src/runner/bench_datafusion.rs b/controller/src/runner/bench_datafusion.rs index 2651763..6166677 100644 --- a/controller/src/runner/bench_datafusion.rs +++ b/controller/src/runner/bench_datafusion.rs @@ -33,6 +33,7 @@ use crate::runner::monitor::{self, ResourceStats}; use crate::runner::pool_peak::{self, BenchPeaks}; use crate::runner::poster::CommentPoster; use crate::runner::shell; +use crate::runner::trigger; /// Run DataFusion benchmarks comparing a PR branch to its merge-base. pub async fn run(config: &RunnerConfig, poster: &CommentPoster) -> Result<()> { @@ -121,6 +122,15 @@ pub async fn run(config: &RunnerConfig, poster: &CommentPoster) -> Result<()> { format!("{} (merge-base)", &base_sha[..7.min(base_sha.len())]) }; + let comparison = trigger::Comparison { + repo: &config.repo, + changed_display, + changed_sha: &changed_sha, + baseline_label: &baseline_label, + base_sha: &base_sha, + }; + let config_block = trigger::config_block(config, benchmarks); + let footer = github::issues_footer(config.runner_repo_url.as_deref()); let running_body = format!( "\u{1f916} Benchmark running (GKE) | [trigger]({})\n\ @@ -130,12 +140,11 @@ pub async fn run(config: &RunnerConfig, poster: &CommentPoster) -> Result<()> { {lscpu}\n\ ```\n\n\
\n\n\ - Comparing {changed_display} ({changed_sha}) to {baseline_label} \ - [diff](https://github.com/{repo}/compare/{base_sha}..{changed_sha}) \ - using: {benchmarks}\n\ + {comparison}\n\n\ + {config_block}\ Results will be posted here when complete{footer}", config.comment_url, - repo = config.repo, + comparison = comparison.line(), ); poster .post_comment(&config.repo, pr_number, &running_body) @@ -339,6 +348,8 @@ pub async fn run(config: &RunnerConfig, poster: &CommentPoster) -> Result<()> { ); let result_body = format_result_comment( &config.comment_url, + &comparison.line(), + &config_block, &report, &resource_section, &pool_section, @@ -722,6 +733,10 @@ fn format_resource_section( /// Format the result comment body. /// +/// `comparison` and `config_block` restate what was run — the same two lines +/// the "running" comment opened with — so the result stands on its own instead +/// of only linking back to the trigger. +/// /// `pool_section` is empty whenever no side recorded a `pool_peak_bytes` — the /// default, since runs set no memory limit unless the trigger comment asks for /// one. Its `
` block is then omitted entirely, leaving the comment as @@ -729,6 +744,8 @@ fn format_resource_section( #[allow(clippy::too_many_arguments)] fn format_result_comment( comment_url: &str, + comparison: &str, + config_block: &str, report: &str, resource_section: &str, pool_section: &str, @@ -749,6 +766,8 @@ fn format_result_comment( format!( "\u{1f916} Benchmark completed (GKE) | [trigger]({comment_url})\n\n\ **Instance:** `{instance_type}` ({pod_resources})\n\n\ + {comparison}\n\n\ + {config_block}\
CPU Details (lscpu)\n\n\ ```\n\ {lscpu}\n\ @@ -777,6 +796,8 @@ mod tests { fn result_comment_format() { let comment = format_result_comment( "https://example.com/comment", + "Comparing my-branch (aaa) to bbb (merge-base) [diff](https://example.com/diff)", + "
Run configuration\n\n```yaml\nrun benchmark tpch\n```\n\n
\n\n", "test report\n", "resources\n", "", @@ -798,10 +819,35 @@ mod tests { assert!(!comment.contains("Memory Pool Peaks")); } + #[test] + fn result_comment_restates_what_was_run() { + // A result comment should be readable without opening the trigger. + let comment = format_result_comment( + "https://example.com/comment", + "Comparing my-branch (aaa) to bbb (merge-base) [diff](https://example.com/diff)", + "
Run configuration\n\n```yaml\nrun benchmark tpch\nbaseline:\n ref: \"v45.0.0\"\n```\n\n
\n\n", + "test report\n", + "resources\n", + "", + "c4a-standard-48", + "12 vCPU / 65 GiB", + "lscpu output", + "", + ); + assert!(comment.contains("Comparing my-branch (aaa) to bbb (merge-base)")); + assert!(comment.contains("
Run configuration")); + assert!(comment.contains("run benchmark tpch")); + assert!(comment.contains("ref: \"v45.0.0\"")); + // Stated up front, before the results themselves. + assert!(comment.find("Run configuration").unwrap() < comment.find("test report").unwrap()); + } + #[test] fn result_comment_includes_pool_section_when_recorded() { let comment = format_result_comment( "https://example.com/comment", + "Comparing my-branch (aaa) to bbb (merge-base) [diff](https://example.com/diff)", + "", "test report\n", "resources\n", "pool peaks table\n", diff --git a/controller/src/runner/config.rs b/controller/src/runner/config.rs index 24dbf54..e2d731e 100644 --- a/controller/src/runner/config.rs +++ b/controller/src/runner/config.rs @@ -3,7 +3,8 @@ //! The controller passes these env vars to PR-triggered runner pods (see //! `job_manager.rs`): `PR_URL`, `COMMENT_ID`, `BENCHMARKS`, `BENCH_TYPE`, //! `BENCH_NAME`, `BENCH_FILTER`, `REPO`, `JOB_ID`, `RUNNER_TOKEN`, -//! `CONTROLLER_URL`, `RUNNER_REPO_URL`. The scheduled main-tracking +//! `CONTROLLER_URL`, `RUNNER_REPO_URL`, `SHARED_ENV_VARS`, +//! `BASELINE_ENV_VARS`, `CHANGED_ENV_VARS`. The scheduled main-tracking //! workflow instead supplies `GITHUB_TOKEN` directly (no PR author to //! distrust). @@ -67,6 +68,10 @@ pub struct RunnerConfig { pub poster_mode: PosterMode, pub sccache_gcs_bucket: Option, pub data_cache_bucket: Option, + /// Env vars from the trigger comment's shared `env:` block. These are + /// already set on the pod (so they reach the build too); the map is kept + /// only so comments can report what was requested. + pub shared_env_vars: HashMap, pub baseline_env_vars: HashMap, pub changed_env_vars: HashMap, pub baseline_ref: Option, @@ -83,6 +88,10 @@ impl RunnerConfig { let comment_url = format!("{pr_url}#issuecomment-{comment_id}"); let bench_type_str = env_required("BENCH_TYPE")?; + let shared_env_vars: HashMap = std::env::var("SHARED_ENV_VARS") + .ok() + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_default(); let baseline_env_vars: HashMap = std::env::var("BASELINE_ENV_VARS") .ok() .and_then(|s| serde_json::from_str(&s).ok()) @@ -104,6 +113,7 @@ impl RunnerConfig { poster_mode: parse_poster_mode()?, sccache_gcs_bucket: std::env::var("SCCACHE_GCS_BUCKET").ok(), data_cache_bucket: std::env::var("DATA_CACHE_BUCKET").ok(), + shared_env_vars, baseline_env_vars, changed_env_vars, baseline_ref: std::env::var("BASELINE_REF").ok(), diff --git a/controller/src/runner/mod.rs b/controller/src/runner/mod.rs index e1a01ae..210f0c2 100644 --- a/controller/src/runner/mod.rs +++ b/controller/src/runner/mod.rs @@ -7,3 +7,4 @@ pub mod monitor; pub mod pool_peak; pub mod poster; pub mod shell; +pub mod trigger; diff --git a/controller/src/runner/trigger.rs b/controller/src/runner/trigger.rs new file mode 100644 index 0000000..0e2975d --- /dev/null +++ b/controller/src/runner/trigger.rs @@ -0,0 +1,297 @@ +//! Rendering of the resolved trigger configuration for runner PR comments. +//! +//! Result comments link back to the triggering comment, but the link alone +//! doesn't say what the run actually did: one trigger comment fans out into one +//! job (and one pod) per benchmark name, and a bare `run benchmarks` expands to +//! the repo's default suite. So instead of quoting the trigger comment +//! verbatim, these helpers re-render *this* run's configuration using the same +//! YAML syntax the trigger accepts — a copy-pasteable reproduction of the run. + +use std::collections::HashMap; + +use crate::runner::config::RunnerConfig; + +/// The resolved two-sided comparison for a run. +pub struct Comparison<'a> { + pub repo: &'a str, + /// Ref name (or PR branch) benchmarked as the "changed" side. + pub changed_display: &'a str, + pub changed_sha: &'a str, + /// Human label for the baseline — a ref name, or ` (merge-base)`. + pub baseline_label: &'a str, + pub base_sha: &'a str, +} + +impl Comparison<'_> { + /// One-line summary with a GitHub compare link between the two sides. + pub fn line(&self) -> String { + format!( + "Comparing {changed} ({changed_sha}) to {baseline} \ + [diff](https://github.com/{repo}/compare/{base_sha}..{changed_sha})", + changed = self.changed_display, + changed_sha = self.changed_sha, + baseline = self.baseline_label, + repo = self.repo, + base_sha = self.base_sha, + ) + } +} + +/// A collapsed `
` block reproducing this run's trigger configuration. +/// +/// `bench_names` is the space-separated benchmark list for the run (the arrow +/// runner passes its single `BENCH_NAME`); empty renders as `run benchmarks`, +/// which is how the default suite is requested. +pub fn config_block(config: &RunnerConfig, bench_names: &str) -> String { + format!( + "
Run configuration\n\n\ + ```yaml\n\ + {}\n\ + ```\n\n\ +
\n\n", + config_yaml(config, bench_names) + ) +} + +/// Render the run's configuration in trigger-comment YAML syntax. +fn config_yaml(config: &RunnerConfig, bench_names: &str) -> String { + let mut lines = Vec::new(); + + let names = bench_names.trim(); + if names.is_empty() { + lines.push("run benchmarks".to_string()); + } else { + lines.push(format!("run benchmark {names}")); + } + + if !config.shared_env_vars.is_empty() { + lines.push("env:".to_string()); + lines.extend(env_lines(&config.shared_env_vars, " ")); + } + + push_side( + &mut lines, + "baseline", + config.baseline_ref.as_deref(), + &config.baseline_env_vars, + ); + push_side( + &mut lines, + "changed", + config.changed_ref.as_deref(), + &config.changed_env_vars, + ); + + // A trigger comment sets the filter through the shared `env:` block, which + // is already rendered above. Only the scheduled main-tracking workflow sets + // BENCH_FILTER outside that block — note it there so it isn't lost. + if !config.bench_filter.is_empty() && !config.shared_env_vars.contains_key("BENCH_FILTER") { + lines.push(format!("# BENCH_FILTER: {}", config.bench_filter)); + } + + lines.join("\n") +} + +/// Append a `baseline:`/`changed:` section, omitting it when the side has +/// neither a custom ref nor env vars. +fn push_side( + lines: &mut Vec, + name: &str, + git_ref: Option<&str>, + env: &HashMap, +) { + if git_ref.is_none() && env.is_empty() { + return; + } + lines.push(format!("{name}:")); + if let Some(git_ref) = git_ref { + lines.push(format!(" ref: {}", quote(git_ref))); + } + if !env.is_empty() { + lines.push(" env:".to_string()); + lines.extend(env_lines(env, " ")); + } +} + +/// Render an env map as sorted `KEY: "VALUE"` lines. Sorted because the map +/// iteration order is otherwise arbitrary and would reshuffle between comments. +fn env_lines(env: &HashMap, indent: &str) -> Vec { + let mut keys: Vec<&String> = env.keys().collect(); + keys.sort(); + keys.iter() + .map(|k| format!("{indent}{k}: {}", quote(&env[*k]))) + .collect() +} + +/// Always double-quote scalars: values like `1` or `on` would otherwise round-trip +/// as a number/bool rather than the string the run actually saw. +fn quote(value: &str) -> String { + format!("\"{}\"", value.replace('\\', "\\\\").replace('"', "\\\"")) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::runner::config::{BenchType, PosterMode, RunnerConfig}; + + fn config() -> RunnerConfig { + RunnerConfig { + pr_url: "https://github.com/apache/datafusion/pull/42".into(), + comment_id: "100".into(), + comment_url: "https://github.com/apache/datafusion/pull/42#issuecomment-100".into(), + benchmarks: "tpch".into(), + bench_type: BenchType::Datafusion, + bench_name: "sql_planner".into(), + bench_filter: String::new(), + repo: "apache/datafusion".into(), + poster_mode: PosterMode::Direct { + github_token: "x".into(), + }, + sccache_gcs_bucket: None, + data_cache_bucket: None, + shared_env_vars: HashMap::new(), + baseline_env_vars: HashMap::new(), + changed_env_vars: HashMap::new(), + baseline_ref: None, + changed_ref: None, + runner_repo_url: None, + } + } + + fn map(pairs: &[(&str, &str)]) -> HashMap { + pairs + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect() + } + + #[test] + fn yaml_minimal_is_just_the_trigger_line() { + assert_eq!(config_yaml(&config(), "tpch"), "run benchmark tpch"); + } + + #[test] + fn yaml_no_names_renders_default_suite() { + assert_eq!(config_yaml(&config(), " "), "run benchmarks"); + } + + #[test] + fn yaml_full_config_round_trips_trigger_syntax() { + let mut cfg = config(); + cfg.shared_env_vars = map(&[("CARGO_BUILD_JOBS", "1")]); + cfg.baseline_ref = Some("v45.0.0".into()); + cfg.changed_ref = Some("v46.0.0".into()); + cfg.baseline_env_vars = map(&[("DATAFUSION_RUNTIME_MEMORY_LIMIT", "1G")]); + cfg.changed_env_vars = map(&[("DATAFUSION_RUNTIME_MEMORY_LIMIT", "2G")]); + + assert_eq!( + config_yaml(&cfg, "tpch"), + "run benchmark tpch\n\ + env:\n\ + \x20 CARGO_BUILD_JOBS: \"1\"\n\ + baseline:\n\ + \x20 ref: \"v45.0.0\"\n\ + \x20 env:\n\ + \x20 DATAFUSION_RUNTIME_MEMORY_LIMIT: \"1G\"\n\ + changed:\n\ + \x20 ref: \"v46.0.0\"\n\ + \x20 env:\n\ + \x20 DATAFUSION_RUNTIME_MEMORY_LIMIT: \"2G\"" + ); + } + + #[test] + fn yaml_re_parses_as_the_same_request() { + // The whole point of re-rendering in trigger syntax: pasting the block + // back into a PR comment reproduces the run. + let mut cfg = config(); + cfg.shared_env_vars = map(&[("CARGO_BUILD_JOBS", "1")]); + cfg.baseline_ref = Some("v45.0.0".into()); + cfg.changed_env_vars = map(&[("DATAFUSION_RUNTIME_MEMORY_LIMIT", "2G")]); + + let yaml = config_yaml(&cfg, "tpch clickbench_1"); + let req = match crate::benchmarks::detect_benchmark(&yaml) { + crate::benchmarks::DetectResult::Parsed(req) => req, + _ => panic!("re-rendered config did not parse: {yaml}"), + }; + assert_eq!(req.benchmarks, vec!["tpch", "clickbench_1"]); + assert_eq!(req.env_vars.get("CARGO_BUILD_JOBS").unwrap(), "1"); + assert_eq!(req.baseline_ref.as_deref(), Some("v45.0.0")); + assert_eq!( + req.changed_env_vars + .get("DATAFUSION_RUNTIME_MEMORY_LIMIT") + .unwrap(), + "2G" + ); + } + + #[test] + fn yaml_side_with_only_env_omits_ref() { + let mut cfg = config(); + cfg.baseline_env_vars = map(&[("FOO", "bar")]); + assert_eq!( + config_yaml(&cfg, "tpch"), + "run benchmark tpch\nbaseline:\n env:\n FOO: \"bar\"" + ); + } + + #[test] + fn yaml_env_keys_are_sorted() { + let mut cfg = config(); + cfg.shared_env_vars = map(&[("ZZZ", "1"), ("AAA", "2"), ("MMM", "3")]); + let yaml = config_yaml(&cfg, "tpch"); + let a = yaml.find("AAA").unwrap(); + let m = yaml.find("MMM").unwrap(); + let z = yaml.find("ZZZ").unwrap(); + assert!(a < m && m < z); + } + + #[test] + fn yaml_notes_a_bench_filter_set_outside_the_env_block() { + // The main-tracking workflow sets BENCH_FILTER directly on the pod. + let mut cfg = config(); + cfg.bench_filter = "sort_tpch".into(); + assert!(config_yaml(&cfg, "sql_planner").contains("# BENCH_FILTER: sort_tpch")); + } + + #[test] + fn yaml_does_not_repeat_a_bench_filter_from_the_env_block() { + // `env: BENCH_FILTER: float` in the trigger comment reaches the runner + // as both a shared env var and `bench_filter` — render it once. + let mut cfg = config(); + cfg.bench_filter = "float".into(); + cfg.shared_env_vars = map(&[("BENCH_FILTER", "float")]); + let yaml = config_yaml(&cfg, "arrow_writer"); + assert!(yaml.contains(" BENCH_FILTER: \"float\"")); + assert!(!yaml.contains("# BENCH_FILTER")); + } + + #[test] + fn quote_escapes_quotes_and_backslashes() { + assert_eq!(quote(r#"a"b\c"#), r#""a\"b\\c""#); + } + + #[test] + fn config_block_is_collapsed_details() { + let block = config_block(&config(), "tpch"); + assert!(block.starts_with("
Run configuration")); + assert!(block.contains("```yaml\nrun benchmark tpch\n```")); + assert!(block.ends_with("
\n\n")); + } + + #[test] + fn comparison_line_links_the_diff() { + let cmp = Comparison { + repo: "apache/datafusion", + changed_display: "my-branch", + changed_sha: "aaaa111", + baseline_label: "bbbb222 (merge-base)", + base_sha: "bbbb222", + }; + assert_eq!( + cmp.line(), + "Comparing my-branch (aaaa111) to bbbb222 (merge-base) \ + [diff](https://github.com/apache/datafusion/compare/bbbb222..aaaa111)" + ); + } +}