use std::{
io::Write,
os::unix::fs::PermissionsExt,
path::Path,
process::{Command, Stdio},
sync::Arc,
};
mod integration_tests;
use anyhow::Context;
use insta_cmd::assert_cmd_snapshot;
use rand::{SeedableRng, prelude::Distribution};
use tempfile::{NamedTempFile, TempDir, TempPath};
pub struct CommandOutput {
pub status: std::process::ExitStatus,
pub stdout: String,
pub stderr: String,
}
#[derive(Default)]
pub struct GpuTracePerfRun {
args: Vec<String>,
envs: Vec<(String, String)>,
pub tempfiles: Vec<TempPath>,
pub tempdirs: Vec<Arc<TempDir>>,
}
impl GpuTracePerfRun {
pub fn new() -> Self {
GpuTracePerfRun {
args: vec![],
envs: vec![],
tempfiles: vec![],
tempdirs: vec![],
}
}
pub fn arg(&mut self, arg: &str) {
self.args.push(arg.to_string());
}
pub fn path_arg(&mut self, path: &Path) {
self.arg(path.to_str().unwrap())
}
pub fn setup_run(&mut self, samples: usize) {
self.arg("run");
self.arg("--samples");
self.arg(&format! {"{samples}"})
}
pub fn tempdir(&mut self) -> Arc<TempDir> {
let tmpdir = env!("CARGO_TARGET_TMPDIR");
let tempdir = Arc::new(
TempDir::new_in(tmpdir)
.context("creating temp dir for output")
.unwrap(),
);
self.tempdirs.push(tempdir.clone());
tempdir
}
pub fn setup_snapshot(&mut self) -> Arc<TempDir> {
let output = self.tempdir();
self.arg("snapshot");
self.arg("--output");
self.path_arg(output.path());
output
}
pub fn setup_ci_replay(
&mut self,
config: &Path,
device: &str,
cache_dir: Option<Arc<TempDir>>,
) -> Arc<TempDir> {
let output = self.tempdir();
self.arg("replay");
self.arg("--config");
self.path_arg(config);
self.arg("--device");
self.arg(device);
self.arg("--output");
self.path_arg(output.path());
self.arg("--cache-dir");
let cache_dir = cache_dir.unwrap_or_else(|| self.tempdir());
self.path_arg(cache_dir.path());
output
}
pub fn add_trace(&mut self, trace: &str) {
self.arg("-t");
self.arg(&format!("src/test_data/{trace}"));
}
pub fn env(&mut self, env: &str, value: &str) {
self.envs.push((env.to_string(), value.to_string()));
}
pub fn command(&self) -> Command {
let gtp = env!("CARGO_BIN_EXE_gpu-trace-perf");
let mut command = Command::new(gtp);
command.stdout(Stdio::piped()).stderr(Stdio::piped());
let manifest_path = env!("CARGO_MANIFEST_PATH");
let basedir = Path::new(manifest_path).parent().unwrap();
command.current_dir(basedir);
command.arg("--no-log-time");
for arg in &self.args {
command.arg(arg);
}
for (env, value) in &self.envs {
command.env(env, value);
}
command
}
pub fn run(self) -> CommandOutput {
let mut child = self.command();
let output = child
.spawn()
.with_context(|| format!("Spawning {:?}", &child))
.unwrap()
.wait_with_output()
.context("waiting for gpu-trace-perf")
.unwrap();
let mut output = CommandOutput {
status: output.status,
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
};
for (i, tempfile) in self.tempfiles.iter().enumerate() {
let path = tempfile.display().to_string();
let replacement = format!("TEMPFILE{i}");
output.stdout = output.stdout.replace(&path, &replacement);
output.stderr = output.stderr.replace(&path, &replacement);
}
output
}
pub fn snapshot_output(&self, snapshot_name: &str) {
let filters = self.insta_filters();
let filters_ref: Vec<(&str, &str)> = filters
.iter()
.map(|(a, b)| (a.as_str(), b.as_str()))
.collect();
insta::with_settings!({
filters => filters_ref,
}, {
assert_cmd_snapshot!(snapshot_name, self.command());
});
}
pub fn env_passthrough(&mut self, name: &str) {
self.env_script(name, "\"$@\"");
}
pub fn env_reproducible_outputs(&mut self, name: &str, mean: f64, delta: f64) {
let mut rng = rand_chacha::ChaCha12Rng::seed_from_u64(1);
let dist = rand::distr::Uniform::new(mean - delta, mean + delta).unwrap();
let mut tempfile1 = NamedTempFile::new().unwrap();
let tempfile2 = NamedTempFile::new().unwrap();
for _ in 0..100 {
let sample = dist.sample(&mut rng);
tempfile1
.write_all(format!("{sample}\n").as_bytes())
.unwrap();
}
let samples = tempfile1.path().display().to_string();
let samples_tmp = tempfile2.path().display().to_string();
let env_script = format!(
r#"
set -e
set -o pipefail
sample=`tail -n 1 {samples}`
invsample=`echo "scale=10;1/$sample" | bc`
lines=`cat {samples} | wc -l`
echo "sample $sample lines $lines" 1>&2
head -n $((lines - 1)) {samples} > {samples_tmp}
mv {samples_tmp} {samples}
"$@" \
| sed "s|Measured FPS:.*fps|Measured FPS: $sample fps|g" \
| sed "s|Replay FPS:.*fps|Replay FPS: $sample fps|g" \
| sed 's|EID [0-9]*: .*||2g' \
| sed "s|EID [0-9]*: .*|EID 1: $invsample|" \
"#
);
self.env_script(name, &env_script);
self.tempfiles.push(tempfile1.into_temp_path());
self.tempfiles.push(tempfile2.into_temp_path());
}
pub fn env_script(&mut self, name: &str, contents: &str) {
let mut tempfile = NamedTempFile::new().unwrap();
tempfile
.write_all(format!("#!/bin/bash\n{contents}\n").as_bytes())
.unwrap();
let path = tempfile.path();
self.arg("--env");
self.arg(name);
self.arg(&format!("{}", path.display()));
let exec = 0o755;
let mut permissions = tempfile.as_file().metadata().unwrap().permissions();
permissions.set_mode(exec);
std::fs::set_permissions(path, permissions).unwrap();
self.tempfiles.push(tempfile.into_temp_path());
}
pub fn insta_filters(&self) -> Vec<(String, String)> {
let mut filters: Vec<_> = self
.tempfiles
.iter()
.enumerate()
.map(|(i, file)| (file.display().to_string(), format!("TEMPFILE{i}")))
.collect();
filters.append(
&mut self
.tempdirs
.iter()
.enumerate()
.map(|(i, dir)| (dir.path().display().to_string(), format!("TEMPDIR{i}")))
.collect(),
);
filters.push(("line [0-9]+".to_string(), "line LINENO".to_string()));
filters.push((
": .*: Assertion `.*' failed".to_string(),
": ASSERTION_FAILURE".to_string(),
));
filters.push((
": .*: Assertion `.*' failed".to_string(),
": ASSERTION_FAILURE".to_string(),
));
filters.push((
r"peak [^:]*: (.*)".to_string(),
r"peak TYPE: AMOUNT".to_string(),
));
filters.push(("https?://[^ /]+".to_string(), "http://HOST/".to_string()));
filters
}
}
fn extract_links(html: &str) -> Vec<String> {
let document = scraper::Html::parse_document(html);
let selector = scraper::Selector::parse("[href],[src]").unwrap();
let mut links = std::collections::HashSet::new();
for elem in document.select(&selector) {
for attr in ["href", "src"] {
if let Some(url) = elem.attr(attr) {
if !url.starts_with('#') && !url.is_empty() {
links.insert(url.to_string());
}
}
}
}
let onclick_selector = scraper::Selector::parse("[onclick]").unwrap();
for elem in document.select(&onclick_selector) {
if let Some(onclick) = elem.attr("onclick") {
if let Some(pos) = onclick.find("showLogModal('") {
let after = &onclick[pos + "showLogModal('".len()..];
if let Some(end) = after.find('\'') {
let url = &after[..end];
if !url.starts_with('#') && !url.is_empty() {
links.insert(url.to_string());
}
}
}
}
}
links.into_iter().collect()
}
pub fn check_html_links(output_dir: &std::path::Path) {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async {
let base_url = http_serve_dir(output_dir).await;
let client = reqwest::Client::new();
let mut visited = std::collections::HashSet::new();
let mut broken = Vec::new();
let index_html = client
.get(format!("{base_url}/index.html"))
.send()
.await
.expect("fetching index.html")
.text()
.await
.unwrap();
visited.insert("index.html".to_string());
let initial_links = extract_links(&index_html);
assert!(!initial_links.is_empty(), "No links found in index.html");
let mut worklist: Vec<String> = initial_links;
while let Some(link) = worklist.pop() {
if !visited.insert(link.clone()) {
continue;
}
let url = if link.starts_with("http") {
link.clone()
} else {
format!("{base_url}/{link}")
};
let resp = client
.get(&url)
.send()
.await
.unwrap_or_else(|e| panic!("requesting {url}: {e}"));
if !resp.status().is_success() {
broken.push(format!("{link}: {}", resp.status()));
continue;
}
if link.ends_with(".html") {
let html = resp.text().await.unwrap();
for l in extract_links(&html) {
if !visited.contains(&l) {
worklist.push(l);
}
}
}
}
assert!(broken.is_empty(), "Broken links:\n{}", broken.join("\n"));
});
}
pub async fn http_serve_dir(root: &std::path::Path) -> String {
use hyper::service::service_fn;
use hyper_staticfile::Static;
use hyper_util::rt::{TokioExecutor, TokioIo};
use hyper_util::server::conn::auto::Builder;
use tokio::net::TcpListener;
let static_ = Static::new(root);
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
tokio::spawn(async move {
loop {
let (stream, _) = listener.accept().await.unwrap();
let static_ = static_.clone();
tokio::spawn(async move {
Builder::new(TokioExecutor::new())
.serve_connection(
TokioIo::new(stream),
service_fn(move |req| {
let static_ = static_.clone();
async move { static_.serve(req).await }
}),
)
.await
.ok();
});
}
});
format!("http://127.0.0.1:{port}")
}
#[should_panic(expected = "busted.html")]
#[test]
fn check_html_links_test() {
let dir = TempDir::new().unwrap();
std::fs::write(
dir.path().join("index.html"),
r#"
<html>
<body>
<a href="file2.html">text</a>
</body>
</html>
"#
.as_bytes(),
)
.unwrap();
std::fs::write(
dir.path().join("file2.html"),
r#"
<html>
<body>
<a href="busted.html">text2</a>
</body>
</html>
"#
.as_bytes(),
)
.unwrap();
check_html_links(dir.path());
}