diff options
Diffstat (limited to 'ci-web')
-rw-r--r-- | ci-web/Cargo.toml | 12 | ||||
-rw-r--r-- | ci-web/commit-filter | 70 | ||||
-rw-r--r-- | ci-web/src/get-test-job.rs | 218 | ||||
-rw-r--r-- | ci-web/src/lib.rs | 90 | ||||
-rw-r--r-- | ci-web/src/main.rs | 124 |
5 files changed, 405 insertions, 109 deletions
diff --git a/ci-web/Cargo.toml b/ci-web/Cargo.toml index 1b8b1d6..c247c34 100644 --- a/ci-web/Cargo.toml +++ b/ci-web/Cargo.toml @@ -1,12 +1,20 @@ [package] -name = "ci-web" +name = "ci-cgi" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[[bin]] +name = "get-test-job" +path = "src/get-test-job.rs" + +#[workspace] +#members = ["get-test-job", "ci-cgi"] [dependencies] cgi = "0.6" git2 = "0.14" querystring = "1.1.0" dirs = "4.0.0" +multimap = "0.8.3" +die = "0.2.0" +libc = "0.2" diff --git a/ci-web/commit-filter b/ci-web/commit-filter new file mode 100644 index 0000000..c2abea9 --- /dev/null +++ b/ci-web/commit-filter @@ -0,0 +1,70 @@ +<!-- +https://choosealicense.com/licenses/0bsd/ +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. + +add the following after the </h1>, before <table> +maybe relocate the script block to within <head></head> +--> + +<style> + .hide-without-js { + display: none; + } + .filtered { + display: none; + } + #filters { + margin: 1em 0; + } + #filters label{ + margin-left: 0.3em; + } +</style> +<div id="filters" class="hide-without-js"> + Filter by: +</div> +<template id="filter-template" style="display:none"> + <label class="checkbox-inline"><input type="checkbox" checked="checked" /></label> +</template> +<script> +(function () { + function get_row_status(el) { + return el.querySelector("td:nth-child(2)").textContent.trim() + } + function refresh_filters() { + const shown_statuses = new Set() + for (const el of document.querySelectorAll("#filters label")) { + if (el.querySelector("input").checked) { + shown_statuses.add(el.textContent.trim()) + } + } + const el_table = document.querySelector("table") + for (const el of el_table.querySelectorAll("tr")) { + const status = get_row_status(el) + if (shown_statuses.has(status)) { + el.classList.remove("filtered") + } else { + el.classList.add("filtered") + } + } + } + document.addEventListener("DOMContentLoaded", (event) => { + const js_class_name = "hide-without-js" + for (const el of document.getElementsByClassName(js_class_name)) { + el.classList.remove(js_class_name) + } + const result_types = new Set() + for (const el of document.querySelectorAll("tr")) { + result_types.add(get_row_status(el)) + } + const el_filters = document.querySelector("#filters") + const el_filter_template = document.querySelector("#filter-template").content + for (const result of result_types) { + const el_new_filter = el_filter_template.cloneNode(true) + el_new_filter.querySelector("label").appendChild(document.createTextNode(` ${result}`)) + el_new_filter.querySelector("input").addEventListener("change", refresh_filters) + el_filters.appendChild(el_new_filter) + } + }) +})() +</script>
\ No newline at end of file diff --git a/ci-web/src/get-test-job.rs b/ci-web/src/get-test-job.rs new file mode 100644 index 0000000..e3db5d3 --- /dev/null +++ b/ci-web/src/get-test-job.rs @@ -0,0 +1,218 @@ +extern crate libc; +use std::fs::{OpenOptions, create_dir_all}; +use std::os::unix::fs::OpenOptionsExt; +use std::io::ErrorKind; +use std::path::Path; +use std::process; +mod lib; +use lib::{Ktestrc, read_lines, ktestrc_read, git_get_commit}; + +extern crate multimap; +use multimap::MultiMap; +use die::die; + +fn get_subtests(test_path: &str) -> Vec<String> { + let test_name = Path::new(test_path).file_stem(); + + if let Some(test_name) = test_name { + let test_name = test_name.to_string_lossy(); + + let output = std::process::Command::new(&test_path) + .arg("list-tests") + .output() + .expect(&format!("failed to execute process {:?} ", &test_path)) + .stdout; + let output = String::from_utf8_lossy(&output); + + output + .split_whitespace() + .map(|i| format!("{}.{}", test_name, i)) + .collect() + } else { + Vec::new() + } +} + +fn lockfile_exists(rc: &Ktestrc, commit: &str, subtest: &str, create: bool) -> bool { + fn test_or_create(lockfile: &Path, create: bool) -> bool { + if !create { + lockfile.exists() + } else { + let dir = lockfile.parent(); + let r = create_dir_all(dir.unwrap()); + + if let Err(e) = r { + if e.kind() != ErrorKind::AlreadyExists { + die!("error creating {:?}: {}", dir, e); + } + } + + let mut options = OpenOptions::new(); + options.write(true); + options.custom_flags(libc::O_CREAT); + options.open(lockfile).is_ok() + } + } + + let lockfile = rc.ci_output_dir.join(commit).join(subtest); + let mut exists = test_or_create(&lockfile, create); + + if exists { + let timeout = std::time::Duration::new(3600, 0); + let now = std::time::SystemTime::now(); + let metadata = std::fs::metadata(&lockfile).unwrap(); + + if metadata.len() == 0&& + metadata.is_file() && + metadata.modified().unwrap() + timeout < now && + std::fs::remove_file(&lockfile).is_ok() { + exists = test_or_create(&lockfile, create); + } + } + + exists +} + +struct TestJob { + branch: String, + commit: String, + age: usize, + test: String, + subtests: Vec<String>, +} + +fn branch_get_next_test_job(rc: &Ktestrc, repo: &git2::Repository, + branch: &str, test_path: &str) -> Option<TestJob> { + let mut ret = TestJob { + branch: branch.to_string(), + commit: String::new(), + age: 0, + test: test_path.to_string(), + subtests: Vec::new(), + }; + + let subtests = get_subtests(test_path); + + let mut walk = repo.revwalk().unwrap(); + let reference = git_get_commit(&repo, branch.to_string()); + if reference.is_err() { + eprintln!("branch {} not found", branch); + return None; + } + let reference = reference.unwrap(); + + if let Err(e) = walk.push(reference.id()) { + eprintln!("Error walking {}: {}", branch, e); + return None; + } + + for commit in walk + .filter_map(|i| i.ok()) + .filter_map(|i| repo.find_commit(i).ok()) { + let commit = commit.id().to_string(); + ret.commit = commit.clone(); + + for subtest in subtests.iter() { + if !lockfile_exists(rc, &commit, subtest, false) { + ret.subtests.push(subtest.to_string()); + if ret.subtests.len() > 20 { + break; + } + } + } + + if !ret.subtests.is_empty() { + return Some(ret); + } + + ret.age += 1; + } + + None +} + +fn get_best_test_job(rc: &Ktestrc, repo: &git2::Repository, + branch_tests: &MultiMap<String, String>) -> Option<TestJob> { + let mut ret: Option<TestJob> = None; + + for (branch, testvec) in branch_tests.iter_all() { + for test in testvec { + let job = branch_get_next_test_job(rc, repo, branch, test); + + if let Some(job) = job { + match &ret { + Some(r) => if r.age > job.age { ret = Some(job) }, + None => ret = Some(job), + } + } + } + } + + ret +} + +fn create_job_lockfiles(rc: &Ktestrc, mut job: TestJob) -> Option<TestJob> { + job.subtests = job.subtests.iter() + .filter(|i| lockfile_exists(rc, &job.commit, &i, true)) + .map(|i| i.to_string()) + .collect(); + + if !job.subtests.is_empty() { Some(job) } else { None } +} + +fn main() { + let ktestrc = ktestrc_read(); + + let repo = git2::Repository::open(&ktestrc.ci_linux_repo); + if let Err(e) = repo { + eprintln!("Error opening {:?}: {}", ktestrc.ci_linux_repo, e); + eprintln!("Please specify correct JOBSERVER_LINUX_DIR"); + process::exit(1); + } + let repo = repo.unwrap(); + + let lines = read_lines(&ktestrc.ci_branches_to_test); + if let Err(e) = lines { + eprintln!("Error opening {:?}: {}", ktestrc.ci_branches_to_test, e); + eprintln!("Please specify correct JOBSERVER_BRANCHES_TO_TEST"); + process::exit(1); + } + let lines = lines.unwrap(); + + let lines = lines.filter_map(|i| i.ok()); + + let mut branch_tests: MultiMap<String, String> = MultiMap::new(); + + for l in lines { + let l: Vec<_> = l.split_whitespace().take(2).collect(); + + if l.len() == 2 { + let branch = l[0]; + let test = l[1]; + branch_tests.insert(branch.to_string(), test.to_string()); + } + } + + let mut job: Option<TestJob>; + + loop { + job = get_best_test_job(&ktestrc, &repo, &branch_tests); + + if job.is_none() { + break; + } + + job = create_job_lockfiles(&ktestrc, job.unwrap()); + if job.is_some() { + break; + } + } + + if let Some(job) = job { + print!("{} {} {}", job.branch, job.commit, job.test); + for t in job.subtests { + print!(" {}", t); + } + println!(""); + } +} diff --git a/ci-web/src/lib.rs b/ci-web/src/lib.rs new file mode 100644 index 0000000..9a8b40d --- /dev/null +++ b/ci-web/src/lib.rs @@ -0,0 +1,90 @@ +use std::fs::File; +use std::io::{self, BufRead}; +use std::path::{Path, PathBuf}; +extern crate dirs; + +pub fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>> +where P: AsRef<Path>, { + let file = File::open(filename)?; + Ok(io::BufReader::new(file).lines()) +} + +pub fn git_get_commit(repo: &git2::Repository, reference: String) -> Result<git2::Commit, git2::Error> { + let r = repo.revparse_single(&reference); + if let Err(e) = r { + eprintln!("Error from resolve_reference_from_short_name {} in {}: {}", reference, repo.path().display(), e); + return Err(e); + } + + let r = r.unwrap().peel_to_commit(); + if let Err(e) = r { + eprintln!("Error from peel_to_commit {} in {}: {}", reference, repo.path().display(), e); + return Err(e); + } + r +} + +pub struct Ktestrc { + pub ci_linux_repo: PathBuf, + pub ci_output_dir: PathBuf, + pub ci_branches_to_test: PathBuf, +} + +pub fn ktestrc_read() -> Ktestrc { + let mut ktestrc = Ktestrc { + ci_linux_repo: PathBuf::new(), + ci_output_dir: PathBuf::new(), + ci_branches_to_test: PathBuf::new(), + }; + + if let Some(home) = dirs::home_dir() { + ktestrc.ci_branches_to_test = home.join("BRANCHES-TO-TEST"); + } + + fn ktestrc_get(rc: &'static str, var: &'static str) -> Option<PathBuf> { + let cmd = format!(". {}; echo -n ${}", rc, var); + + let output = std::process::Command::new("/usr/bin/env") + .arg("bash") + .arg("-c") + .arg(&cmd) + .output() + .expect("failed to execute process /bin/sh") + .stdout; + + let output = String::from_utf8_lossy(&output); + let output = output.trim(); + + if !output.is_empty() { + Some(PathBuf::from(output)) + } else { + None + } + } + + if let Some(v) = ktestrc_get("/etc/ktestrc", "JOBSERVER_LINUX_DIR") { + ktestrc.ci_linux_repo = v; + } + + if let Some(v) = ktestrc_get("/etc/ktestrc", "JOBSERVER_OUTPUT_DIR") { + ktestrc.ci_output_dir = v; + } + + if let Some(v) = ktestrc_get("/etc/ktestrc", "JOBSERVER_BRANCHES_TO_TEST") { + ktestrc.ci_branches_to_test = v; + } + + if let Some(v) = ktestrc_get("$HOME/.ktestrc", "JOBSERVER_LINUX_DIR") { + ktestrc.ci_linux_repo = v; + } + + if let Some(v) = ktestrc_get("$HOME/.ktestrc", "JOBSERVER_OUTPUT_DIR") { + ktestrc.ci_output_dir = v; + } + + if let Some(v) = ktestrc_get("$HOME/.ktestrc", "JOBSERVER_BRANCHES_TO_TEST") { + ktestrc.ci_branches_to_test = v; + } + + ktestrc +} diff --git a/ci-web/src/main.rs b/ci-web/src/main.rs index 230689a..a18678e 100644 --- a/ci-web/src/main.rs +++ b/ci-web/src/main.rs @@ -1,14 +1,14 @@ -use git2::Repository; use std::fmt::Write; use std::fs::File; -use std::io::{self, Read, BufRead}; -use std::path::{Path, PathBuf}; -use std::process::Command; +use std::io::Read; +use std::path::Path; extern crate cgi; -extern crate dirs; extern crate querystring; -const COMMIT_FILTER: &str = include_str!("../../ci/commit-filter"); +mod lib; +use lib::*; + +const COMMIT_FILTER: &str = include_str!("../commit-filter"); const STYLESHEET: &str = "/bootstrap.min.css"; fn read_file(f: &Path) -> Option<String> { @@ -18,93 +18,6 @@ fn read_file(f: &Path) -> Option<String> { Some(ret) } -fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>> -where P: AsRef<Path>, { - let file = File::open(filename)?; - Ok(io::BufReader::new(file).lines()) -} - -fn git_get_commit(repo: &git2::Repository, reference: String) -> Result<git2::Commit, git2::Error> { - let r = repo.revparse_single(&reference); - if let Err(e) = r { - eprintln!("Error from resolve_reference_from_short_name {} in {}: {}", reference, repo.path().display(), e); - return Err(e); - } - - let r = r.unwrap().peel_to_commit(); - if let Err(e) = r { - eprintln!("Error from peel_to_commit {} in {}: {}", reference, repo.path().display(), e); - return Err(e); - } - r -} - -struct Ktestrc { - ci_linux_repo: PathBuf, - ci_output_dir: PathBuf, - ci_branches_to_test: PathBuf, -} - -fn ktestrc_read() -> Ktestrc { - let mut ktestrc = Ktestrc { - ci_linux_repo: PathBuf::new(), - ci_output_dir: PathBuf::new(), - ci_branches_to_test: PathBuf::new(), - }; - - if let Some(home) = dirs::home_dir() { - ktestrc.ci_branches_to_test = home.join("BRANCHES-TO-TEST"); - } - - fn ktestrc_get(rc: &'static str, var: &'static str) -> Option<PathBuf> { - let cmd = format!(". {}; echo -n ${}", rc, var); - - let output = Command::new("sh") - .arg("-c") - .arg(&cmd) - .output() - .expect("failed to execute process /bin/sh") - .stdout; - - let output = String::from_utf8_lossy(&output); - let output = output.trim(); - - if !output.is_empty() { - Some(PathBuf::from(output)) - } else { - None - } - } - - if let Some(v) = ktestrc_get("/etc/ktestrc", "JOBSERVER_LINUX_DIR") { - ktestrc.ci_linux_repo = v; - } - - if let Some(v) = ktestrc_get("/etc/ktestrc", "JOBSERVER_OUTPUT_DIR") { - ktestrc.ci_output_dir = v; - } - - if let Some(v) = ktestrc_get("/etc/ktestrc", "JOBSERVER_BRANCHES_TO_TEST") { - ktestrc.ci_branches_to_test = v; - } - - /* - if let Some(v) = ktestrc_get("$HOME/.ktestrc", "JOBSERVER_LINUX_DIR") { - ktestrc.ci_linux_repo = v; - } - - if let Some(v) = ktestrc_get("$HOME/.ktestrc", "JOBSERVER_OUTPUT_DIR") { - ktestrc.ci_output_dir = v; - } - - if let Some(v) = ktestrc_get("$HOME/.ktestrc", "JOBSERVER_BRANCHES_TO_TEST") { - ktestrc.ci_branches_to_test = v; - } - */ - - ktestrc -} - #[derive(PartialEq)] enum TestStatus { InProgress, @@ -325,21 +238,18 @@ fn ci_list_branches(ci: &Ci) -> cgi::Response { writeln!(&mut out, "<body>").unwrap(); writeln!(&mut out, "<table class=\"table\">").unwrap(); - if let Ok(lines) = read_lines(&ci.ktestrc.ci_branches_to_test) { - 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 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(); + let mut branches: Vec<_> = branches.iter().collect(); + branches.sort(); - for b in branches { - writeln!(&mut out, "<tr> <th> <a href={}?log={}>{}</a> </th> </tr>", ci.script_name, b, b).unwrap(); - } - } else { - writeln!(&mut out, "(BRANCHES-TO-TEST not found)").unwrap(); + for b in branches { + writeln!(&mut out, "<tr> <th> <a href={}?log={}>{}</a> </th> </tr>", ci.script_name, b, b).unwrap(); } writeln!(&mut out, "</table>").unwrap(); @@ -383,7 +293,7 @@ cgi::cgi_main! {|request: cgi::Request| -> cgi::Response { ktestrc.ci_branches_to_test.as_os_str())); } - let repo = Repository::open(&ktestrc.ci_linux_repo).unwrap(); + let repo = git2::Repository::open(&ktestrc.ci_linux_repo).unwrap(); let ci = Ci { ktestrc: ktestrc, |