use std::fmt::Write; use std::fs::File; use std::io::Read; use std::path::Path; extern crate cgi; extern crate querystring; mod lib; use lib::*; const COMMIT_FILTER: &str = include_str!("../commit-filter"); const STYLESHEET: &str = "/bootstrap.min.css"; fn read_file(f: &Path) -> Option { let mut ret = String::new(); let mut file = File::open(f).ok()?; file.read_to_string(&mut ret).ok()?; Some(ret) } #[derive(PartialEq)] enum TestStatus { InProgress, Passed, Failed, NotRun, NotStarted, Unknown, } impl TestStatus { fn from_str(status: &str) -> TestStatus { if status.is_empty() { TestStatus::InProgress } else if status.contains("PASSED") { TestStatus::Passed } else if status.contains("FAILED") { TestStatus::Failed } else if status.contains("NOTRUN") { TestStatus::NotRun } else if status.contains("NOT STARTED") { TestStatus::NotStarted } else { TestStatus::Unknown } } fn to_str(&self) -> &'static str { match self { TestStatus::InProgress => "In progress", TestStatus::Passed => "Passed", TestStatus::Failed => "Failed", TestStatus::NotRun => "Not run", TestStatus::NotStarted => "Not started", TestStatus::Unknown => "Unknown", } } fn table_class(&self) -> &'static str { match self { TestStatus::InProgress => "table-secondary", TestStatus::Passed => "table-success", TestStatus::Failed => "table-danger", TestStatus::NotRun => "table-secondary", TestStatus::NotStarted => "table-secondary", TestStatus::Unknown => "table-secondary", } } } struct TestResult { name: String, status: TestStatus, duration: usize, } fn read_test_result(testdir: &std::fs::DirEntry) -> Option { Some(TestResult { name: testdir.file_name().into_string().unwrap(), status: TestStatus::from_str(&read_file(&testdir.path().join("status"))?), duration: read_file(&testdir.path().join("duration"))?.parse().ok()? }) } struct Ci { ktestrc: Ktestrc, repo: git2::Repository, stylesheet: String, script_name: String, } fn commit_get_results(ci: &Ci, commit_id: &String) -> Vec { let mut dirents: Vec<_> = ci.ktestrc.ci_output_dir.join(commit_id) .read_dir() .expect("read_dir call failed") .filter_map(|i| i.ok()) .collect(); dirents.sort_by_key(|x| x.file_name()); dirents.iter().map(|x| read_test_result(x)).filter_map(|i| i).collect() } fn ci_log(ci: &Ci, branch: String) -> cgi::Response { let mut out = String::new(); let mut walk = ci.repo.revwalk().unwrap(); let reference = git_get_commit(&ci.repo, branch.clone()); if reference.is_err() { /* XXX: return a 404 */ return error_response(format!("commit not found")); } let reference = reference.unwrap(); if let Err(e) = walk.push(reference.id()) { return error_response(format!("Error walking {}: {}", branch, e)); } writeln!(&mut out, "").unwrap(); writeln!(&mut out, "{}", branch).unwrap(); writeln!(&mut out, "", ci.stylesheet).unwrap(); writeln!(&mut out, "").unwrap(); writeln!(&mut out, "
").unwrap(); writeln!(&mut out, "").unwrap(); writeln!(&mut out, "").unwrap(); writeln!(&mut out, "").unwrap(); writeln!(&mut out, "").unwrap(); writeln!(&mut out, "").unwrap(); writeln!(&mut out, "").unwrap(); writeln!(&mut out, "").unwrap(); writeln!(&mut out, "").unwrap(); writeln!(&mut out, "").unwrap(); writeln!(&mut out, "").unwrap(); writeln!(&mut out, "").unwrap(); writeln!(&mut out, "").unwrap(); writeln!(&mut out, "").unwrap(); let mut nr_empty = 0; for commit in walk .filter_map(|i| i.ok()) .filter_map(|i| ci.repo.find_commit(i).ok()) { let id = commit.id().to_string(); let r = commit_get_results(ci, &id); if !r.is_empty() { if nr_empty != 0 { writeln!(&mut out, " ... ").unwrap(); nr_empty = 0; } fn count(r: &Vec, t: TestStatus) -> usize { r.iter().filter(|x| x.status == t).count() } let message = commit.message().unwrap(); let subject_len = message.find('\n').unwrap_or(message.len()); let duration: usize = r.iter().map(|x| x.duration).sum(); writeln!(&mut out, "").unwrap(); writeln!(&mut out, "", ci.script_name, id, &id.as_str()[..14]).unwrap(); writeln!(&mut out, "", &message[..subject_len]).unwrap(); writeln!(&mut out, "", count(&r, TestStatus::Passed)).unwrap(); writeln!(&mut out, "", count(&r, TestStatus::Failed)).unwrap(); writeln!(&mut out, "", count(&r, TestStatus::NotStarted)).unwrap(); writeln!(&mut out, "", count(&r, TestStatus::NotRun)).unwrap(); writeln!(&mut out, "", count(&r, TestStatus::InProgress)).unwrap(); writeln!(&mut out, "", count(&r, TestStatus::Unknown)).unwrap(); writeln!(&mut out, "", r.len()).unwrap(); writeln!(&mut out, "", duration).unwrap(); writeln!(&mut out, "").unwrap(); } else { nr_empty += 1; if nr_empty > 100 { break; } } } writeln!(&mut out, "
Commit Description Passed Failed Not started Not run In progress Unknown Total Duration
{} {} {} {} {} {} {} {} {} {}s
").unwrap(); writeln!(&mut out, "
").unwrap(); writeln!(&mut out, "").unwrap(); writeln!(&mut out, "").unwrap(); cgi::html_response(200, out) } fn ci_commit(ci: &Ci, commit_id: String) -> cgi::Response { let mut out = String::new(); let commit = git_get_commit(&ci.repo, commit_id.clone()); if commit.is_err() { /* XXX: return a 404 */ return error_response(format!("commit not found")); } let commit = commit.unwrap(); let message = commit.message().unwrap(); let subject_len = message.find('\n').unwrap_or(message.len()); writeln!(&mut out, "").unwrap(); writeln!(&mut out, "{}", &message[..subject_len]).unwrap(); writeln!(&mut out, "", ci.stylesheet).unwrap(); writeln!(&mut out, "").unwrap(); writeln!(&mut out, "
").unwrap(); writeln!(&mut out, "

{}

", &message[..subject_len]).unwrap(); out.push_str(COMMIT_FILTER); writeln!(&mut out, "").unwrap(); for result in commit_get_results(ci, &commit_id) { writeln!(&mut out, "", result.status.table_class()).unwrap(); writeln!(&mut out, "", result.name).unwrap(); writeln!(&mut out, "", result.status.to_str()).unwrap(); writeln!(&mut out, "", result.duration).unwrap(); writeln!(&mut out, "", &commit_id, result.name).unwrap(); writeln!(&mut out, "", &commit_id, result.name).unwrap(); writeln!(&mut out, "", &commit_id, result.name).unwrap(); writeln!(&mut out, "").unwrap(); } writeln!(&mut out, "
{} {} {}s log full log output directory
").unwrap(); writeln!(&mut out, "
").unwrap(); writeln!(&mut out, "").unwrap(); writeln!(&mut out, "").unwrap(); cgi::html_response(200, out) } fn ci_list_branches(ci: &Ci) -> cgi::Response { let mut out = String::new(); writeln!(&mut out, "").unwrap(); writeln!(&mut out, "CI branch list").unwrap(); writeln!(&mut out, "", ci.stylesheet).unwrap(); writeln!(&mut out, "").unwrap(); writeln!(&mut out, "").unwrap(); let lines = read_lines(&ci.ktestrc.ci_branches_to_test).unwrap(); let branches: std::collections::HashSet<_> = lines .filter_map(|i| i.ok()) .map(|i| if let Some(w) = i.split_whitespace().nth(0) { Some(String::from(w)) } else { None }) .filter_map(|i| i) .collect(); let mut branches: Vec<_> = branches.iter().collect(); branches.sort(); for b in branches { writeln!(&mut out, "", ci.script_name, b, b).unwrap(); } writeln!(&mut out, "
{}
").unwrap(); writeln!(&mut out, "").unwrap(); writeln!(&mut out, "").unwrap(); writeln!(&mut out, "").unwrap(); cgi::html_response(200, out) } fn cgi_header_get(request: &cgi::Request, name: &str) -> String { request.headers().get(name) .map(|x| x.to_str()) .transpose().ok().flatten() .map(|x| x.to_string()) .unwrap_or(String::new()) } fn error_response(msg: String) -> cgi::Response { let mut out = String::new(); writeln!(&mut out, "{}", msg).unwrap(); let env: Vec<_> = std::env::vars().collect(); writeln!(&mut out, "env: {:?}", env).unwrap(); cgi::text_response(200, out) } cgi::cgi_main! {|request: cgi::Request| -> cgi::Response { let ktestrc = ktestrc_read(); if !ktestrc.ci_linux_repo.exists() { return error_response(format!("required file missing: JOBSERVER_LINUX_DIR (got {:?})", ktestrc.ci_linux_repo.as_os_str())); } if !ktestrc.ci_output_dir.exists() { return error_response(format!("required file missing: JOBSERVER_OUTPUT_DIR (got {:?})", ktestrc.ci_output_dir.as_os_str())); } if !ktestrc.ci_branches_to_test.exists() { return error_response(format!("required file missing: JOBSERVER_BRANCHES_TO_TEST (got {:?})", ktestrc.ci_branches_to_test.as_os_str())); } let repo = git2::Repository::open(&ktestrc.ci_linux_repo).unwrap(); let ci = Ci { ktestrc: ktestrc, repo: repo, stylesheet: String::from(STYLESHEET), script_name: cgi_header_get(&request, "x-cgi-script-name"), }; let query = cgi_header_get(&request, "x-cgi-query-string"); let query: std::collections::HashMap<_, _> = querystring::querify(&query).into_iter().collect(); if let Some(commit) = query.get("commit") { ci_commit(&ci, commit.to_string()) } else if let Some(log) = query.get("log") { ci_log(&ci, log.to_string()) } else { ci_list_branches(&ci) } } }