use std::collections::BTreeMap; use std::fmt::Write; use regex::Regex; use chrono::Duration; extern crate cgi; extern crate querystring; use ci_cgi::{CiConfig, Userrc, ciconfig_read, TestResultsMap, TestStatus, commitdir_get_results, git_get_commit, workers_get, update_lcov}; const COMMIT_FILTER: &str = include_str!("../../commit-filter"); const STYLESHEET: &str = "bootstrap.min.css"; fn filter_results(r: TestResultsMap, tests_matching: &Regex) -> TestResultsMap { r.iter() .filter(|i| tests_matching.is_match(&i.0) ) .map(|(k, v)| (k.clone(), *v)) .collect() } struct Ci { rc: CiConfig, repo: git2::Repository, stylesheet: String, script_name: String, user: Option, branch: Option, commit: Option, tests_matching: Regex, } fn commitdir_get_results_filtered(ci: &Ci, commit_id: &String) -> TestResultsMap { let results = commitdir_get_results(&ci.rc.ktest, commit_id).unwrap_or(BTreeMap::new()); filter_results(results, &ci.tests_matching) } struct CommitResults { id: String, message: String, tests: TestResultsMap, } fn commit_get_results(ci: &Ci, commit: &git2::Commit) -> CommitResults { let id = commit.id().to_string(); let tests = commitdir_get_results_filtered(ci, &id); CommitResults { id: id, message: commit.message().unwrap().to_string(), tests: tests, } } fn branch_get_results(ci: &Ci) -> Result, String> { let mut nr_empty = 0; let mut nr_commits = 0; let mut ret: Vec = Vec::new(); let branch = ci.branch.as_ref().unwrap(); 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 Err(format!("commit not found")); } let reference = reference.unwrap(); if let Err(e) = walk.push(reference.id()) { return Err(format!("Error walking {}: {}", branch, e)); } for commit in walk .filter_map(|i| i.ok()) .filter_map(|i| ci.repo.find_commit(i).ok()) { let r = commit_get_results(ci, &commit); if !r.tests.is_empty() { nr_empty = 0; } else { nr_empty += 1; if nr_empty > 100 { break; } } ret.push(r); nr_commits += 1; if nr_commits > 50 { break; } } while !ret.is_empty() && ret[ret.len() - 1].tests.is_empty() { ret.pop(); } Ok(ret) } fn ci_log(ci: &Ci) -> cgi::Response { let mut out = String::new(); let branch = ci.branch.as_ref().unwrap(); let commits = branch_get_results(ci); if let Err(e) = commits { return error_response(e); } let commits = commits.unwrap(); let mut multiple_test_view = false; for r in &commits { if r.tests.len() > 1 { multiple_test_view = true; } } 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(); if multiple_test_view { 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 r in &commits { if !r.tests.is_empty() { if nr_empty != 0 { writeln!(&mut out, "", nr_empty).unwrap(); nr_empty = 0; } fn count(r: &TestResultsMap, t: TestStatus) -> usize { r.iter().filter(|x| x.1.status == t).count() } let subject_len = r.message.find('\n').unwrap_or(r.message.len()); let duration: u64 = r.tests.iter().map(|x| x.1.duration).sum(); writeln!(&mut out, "").unwrap(); writeln!(&mut out, "", ci.script_name, branch, r.id, &r.id.as_str()[..14]).unwrap(); writeln!(&mut out, "", &r.message[..subject_len]).unwrap(); writeln!(&mut out, "", count(&r.tests, TestStatus::Passed)).unwrap(); writeln!(&mut out, "", count(&r.tests, TestStatus::Failed)).unwrap(); writeln!(&mut out, "", count(&r.tests, TestStatus::Notstarted)).unwrap(); writeln!(&mut out, "", count(&r.tests, TestStatus::Notrun)).unwrap(); writeln!(&mut out, "", count(&r.tests, TestStatus::Inprogress)).unwrap(); writeln!(&mut out, "", count(&r.tests, TestStatus::Unknown)).unwrap(); writeln!(&mut out, "", r.tests.len()).unwrap(); writeln!(&mut out, "", duration).unwrap(); writeln!(&mut out, "").unwrap(); } else { nr_empty += 1; } } } else { 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 r in &commits { if let Some(t) = r.tests.first_key_value() { if nr_empty != 0 { writeln!(&mut out, "", nr_empty).unwrap(); nr_empty = 0; } let subject_len = r.message.find('\n').unwrap_or(r.message.len()); writeln!(&mut out, "", t.1.status.table_class()).unwrap(); writeln!(&mut out, "", ci.script_name, branch, r.id, &r.id.as_str()[..14]).unwrap(); writeln!(&mut out, "", &r.message[..subject_len]).unwrap(); writeln!(&mut out, "", t.1.status.to_str()).unwrap(); writeln!(&mut out, "", t.1.duration).unwrap(); writeln!(&mut out, "", &r.id, t.0).unwrap(); writeln!(&mut out, "", &r.id, t.0).unwrap(); writeln!(&mut out, "", &r.id, t.0).unwrap(); writeln!(&mut out, "").unwrap(); } else { nr_empty += 1; } } } writeln!(&mut out, "
Commit Description Passed Failed Not started Not run In progress Unknown Total Duration
({} untested commits)
{} {} {} {} {} {} {} {} {} {}s
Commit Description Status Duration
({} untested commits)
{} {} {} {}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_commit(ci: &Ci) -> cgi::Response { let commit_id = ci.commit.as_ref().unwrap(); 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 commit_id = commit.id().to_string(); // normalize 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(); update_lcov(&ci.rc.ktest, &commit_id); if ci.rc.ktest.output_dir.join(&commit_id).join("lcov").exists() { writeln!(&mut out, "

Code coverage

", &commit_id).unwrap(); } out.push_str(COMMIT_FILTER); writeln!(&mut out, "").unwrap(); for (name, result) in commitdir_get_results_filtered(ci, &commit_id) { writeln!(&mut out, "", result.status.table_class()).unwrap(); writeln!(&mut out, "", name).unwrap(); writeln!(&mut out, "", result.status.to_str()).unwrap(); writeln!(&mut out, "", result.duration).unwrap(); writeln!(&mut out, "", &commit_id, name).unwrap(); writeln!(&mut out, "", &commit_id, name).unwrap(); writeln!(&mut out, "", &commit_id, name).unwrap(); if let Some(branch) = &ci.branch { writeln!(&mut out, "", ci.script_name, &branch, name).unwrap(); } writeln!(&mut out, "").unwrap(); } writeln!(&mut out, "
{} {} {}s log full log output directory git log
").unwrap(); writeln!(&mut out, "
").unwrap(); writeln!(&mut out, "").unwrap(); writeln!(&mut out, "").unwrap(); cgi::html_response(200, out) } fn ci_list_branches(ci: &Ci, user: &Userrc, out: &mut String) { writeln!(out, "
").unwrap(); for (b, _) in &user.branch { writeln!(out, "", ci.script_name, b, b).unwrap(); } writeln!(out, "
{}
").unwrap(); } fn ci_user(ci: &Ci) -> cgi::Response { let username = ci.user.as_ref().unwrap(); let u = ci.rc.users.get(username); if u.is_none() { return error_response(format!("User {} not found", &username)); } let u = u.unwrap(); 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(); ci_list_branches(ci, &u, &mut out); writeln!(&mut out, "").unwrap(); writeln!(&mut out, "").unwrap(); cgi::html_response(200, out) } fn ci_list_users(ci: &Ci, out: &mut String) { writeln!(out, "
").unwrap(); for (i, _) in &ci.rc.users { writeln!(out, "", ci.script_name, i, i).unwrap(); } writeln!(out, "
{}
").unwrap(); } fn ci_worker_status(ci: &Ci, out: &mut String) -> Option<()>{ use chrono::prelude::Utc; let workers = workers_get(&ci.rc.ktest).ok()?; writeln!(out, "
").unwrap(); writeln!(out, "").unwrap(); writeln!(out, "").unwrap(); writeln!(out, "").unwrap(); writeln!(out, "").unwrap(); writeln!(out, "").unwrap(); writeln!(out, "").unwrap(); let now = Utc::now(); let tests_dir = ci.rc.ktest.ktest_dir.clone().into_os_string().into_string().unwrap() + "/tests/"; for w in workers { let elapsed = (now - w.starttime).max(Duration::zero()); let tests = w.tests.strip_prefix(&tests_dir).unwrap_or(&w.tests); writeln!(out, "").unwrap(); writeln!(out, "", w.hostname, w.workdir).unwrap(); writeln!(out, "", w.branch, w.age).unwrap(); writeln!(out, "", tests).unwrap(); writeln!(out, "", elapsed.num_hours(), elapsed.num_minutes() % 60, elapsed.num_seconds() % 60).unwrap(); writeln!(out, "").unwrap(); } writeln!(out, "
Host.workdir Commit Tests Elapsed time
{}.{} {}~{} {} {}:{:02}:{:02}
").unwrap(); Some(()) } fn ci_home(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(); ci_list_users(ci, &mut out); ci_worker_status(ci, &mut out); 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 rc = ciconfig_read(); if let Err(e) = rc { return error_response(format!("could not read config; {}", e)); } let rc = rc.unwrap(); if !rc.ktest.output_dir.exists() { return error_response(format!("required file missing: JOBSERVER_OUTPUT_DIR (got {:?})", rc.ktest.output_dir)); } unsafe { git2::opts::set_verify_owner_validation(false) .expect("set_verify_owner_validation should never fail"); } let repo = git2::Repository::open(&rc.ktest.linux_repo); if let Err(e) = repo { return error_response(format!("error opening repository {:?}: {}", rc.ktest.linux_repo, e)); } let repo = repo.unwrap(); let query = cgi_header_get(&request, "x-cgi-query-string"); let query: std::collections::HashMap<_, _> = querystring::querify(&query).into_iter().collect(); let tests_matching = query.get("test").unwrap_or(&""); let ci = Ci { rc: rc, repo: repo, stylesheet: String::from(STYLESHEET), script_name: cgi_header_get(&request, "x-cgi-script-name"), user: query.get("user").map(|x| x.to_string()), branch: query.get("branch").map(|x| x.to_string()), commit: query.get("commit").map(|x| x.to_string()), tests_matching: Regex::new(tests_matching).unwrap_or(Regex::new("").unwrap()), }; if ci.commit.is_some() { ci_commit(&ci) } else if ci.branch.is_some() { ci_log(&ci) } else if ci.user.is_some() { ci_user(&ci) } else { ci_home(&ci) } } }