summaryrefslogtreecommitdiff
path: root/ci-web
diff options
context:
space:
mode:
Diffstat (limited to 'ci-web')
-rw-r--r--ci-web/Cargo.toml12
-rw-r--r--ci-web/commit-filter70
-rw-r--r--ci-web/src/get-test-job.rs218
-rw-r--r--ci-web/src/lib.rs90
-rw-r--r--ci-web/src/main.rs124
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,