From 753288ba91fd571ee560f8b37ffe12b14201417f Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 2 Feb 2024 06:55:26 -0800 Subject: [PATCH 001/429] contest: ui: link to GitHub from contest Matthieu points out that people may get to contest from patchwork so the ability to click on the branch, like we have in status, is good to have. Signed-off-by: Jakub Kicinski --- contest/contest.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/contest/contest.js b/contest/contest.js index 29b2475..95360a1 100644 --- a/contest/contest.js +++ b/contest/contest.js @@ -99,7 +99,7 @@ function load_result_table(data_raw) var res = row.insertCell(6); date.innerHTML = v.end.toLocaleString(); - branch.innerHTML = v.branch; + branch.innerHTML = "" + v.branch + ""; remote.innerHTML = v.remote; exe.innerHTML = v.executor; group.innerHTML = r.group; @@ -109,6 +109,14 @@ function load_result_table(data_raw) }); } +function find_branch_urls(loaded_data) +{ + $.each(loaded_data, function(i, v) { + if (v.remote == "brancher") + branch_urls[v.branch] = v.results[0].link; + }); +} + function add_option_filter(data_raw, elem_id, field) { var elem = document.getElementById(elem_id); @@ -131,6 +139,7 @@ function results_update() } let xfr_todo = 2; +let branch_urls = {}; let loaded_data = null; let loaded_filters = null; @@ -164,6 +173,8 @@ function results_loaded(data_raw) }); data_raw.sort(function(a, b){return b.end - a.end;}); + find_branch_urls(data_raw); + loaded_data = data_raw; loaded_one(); } From 3e560e02881287d2b9297339867ce8b8a00e5b32 Mon Sep 17 00:00:00 2001 From: Hangbin Liu Date: Fri, 2 Feb 2024 17:11:50 +0800 Subject: [PATCH 002/429] ingest_mdir: set default branch to main Most repos are using main as default branch now. Signed-off-by: Hangbin Liu --- core/tree.py | 2 +- ingest_mdir.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/tree.py b/core/tree.py index 93bcc1f..3afb8ca 100644 --- a/core/tree.py +++ b/core/tree.py @@ -39,7 +39,7 @@ def __init__(self, name, pfx, fspath, remote=None, branch=None): self.branch = branch if remote and not branch: - self.branch = remote + "/master" + self.branch = remote + "/main" self._saved_path = None diff --git a/ingest_mdir.py b/ingest_mdir.py index f3c0a8a..4b1c677 100755 --- a/ingest_mdir.py +++ b/ingest_mdir.py @@ -33,7 +33,7 @@ parser.add_argument('--mdir', required=True, help='path to the directory with the patches') parser.add_argument('--tree', required=True, help='path to the tree to test on') parser.add_argument('--tree-name', default='unknown', help='the tree name to expect') -parser.add_argument('--tree-branch', default='master', +parser.add_argument('--tree-branch', default='main', help='the branch or commit to use as a base for applying patches') parser.add_argument('--result-dir', default=results_dir, help='the directory where results will be generated') From 64f8a76f742077dcc54dbe0f137639918d76aa30 Mon Sep 17 00:00:00 2001 From: Hangbin Liu Date: Fri, 2 Feb 2024 17:22:49 +0800 Subject: [PATCH 003/429] tests/patch: update Greg's script with new path Greg's script verify_fixes.sh and verify_signedoff.sh have been moved to new path. Signed-off-by: Hangbin Liu --- tests/patch/verify_fixes/info.json | 2 +- tests/patch/verify_signedoff/info.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/patch/verify_fixes/info.json b/tests/patch/verify_fixes/info.json index 114c7d3..181fc6c 100644 --- a/tests/patch/verify_fixes/info.json +++ b/tests/patch/verify_fixes/info.json @@ -1,5 +1,5 @@ { - "source": "https://raw.githubusercontent.com/gregkh/gregkh-linux/master/work/verify_fixes.sh", + "source": "https://raw.githubusercontent.com/gregkh/gregkh-linux/master/work/scripts/verify_fixes.sh", "run": ["verify_fixes.sh", "HEAD~..HEAD"], "pull-requests": true } diff --git a/tests/patch/verify_signedoff/info.json b/tests/patch/verify_signedoff/info.json index e044eaa..1539d03 100644 --- a/tests/patch/verify_signedoff/info.json +++ b/tests/patch/verify_signedoff/info.json @@ -1,5 +1,5 @@ { - "source": "https://raw.githubusercontent.com/gregkh/gregkh-linux/master/work/verify_signedoff.sh", + "source": "https://raw.githubusercontent.com/gregkh/gregkh-linux/master/work/scripts/verify_signedoff.sh", "run": ["verify_signedoff.sh", "HEAD~..HEAD"], "pull-requests": true } From 606826acaf01f9f935dcceadc11ebf5c707d87f0 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 2 Feb 2024 07:30:34 -0800 Subject: [PATCH 004/429] vm: add an optional hard stop for waits tc_actions test keeps hanging runners. Not clear why. Add a hard stop in case it's printing in a loop. Also add a couple of extra prints for debug of corner cases. Signed-off-by: Jakub Kicinski --- contest/remote/lib/vm.py | 7 ++++++- contest/remote/vmksft-p.py | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/contest/remote/lib/vm.py b/contest/remote/lib/vm.py index f2e35a2..758c328 100644 --- a/contest/remote/lib/vm.py +++ b/contest/remote/lib/vm.py @@ -210,6 +210,8 @@ def _read_pipe_nonblock(self, pipe): def drain_to_prompt(self, prompt="xx__-> ", dump_after=None): if dump_after is None: dump_after = int(self.config.get('vm', 'default_timeout')) + hard_stop = int(self.config.get('vm', 'hard_timeout', + fallback=(1 << 63))) waited = 0 total_wait = 0 stdout = "" @@ -238,8 +240,11 @@ def drain_to_prompt(self, prompt="xx__-> ", dump_after=None): waited += 0.03 sleep(0.03) + if total_wait > hard_stop: + waited = 1 << 63 if waited > dump_after: - print("WAIT TIMEOUT retcode:", self.p.returncode) + print("WAIT TIMEOUT retcode:", self.p.returncode, + "waited:", waited, "total:", total_wait) self.log_out += '\nWAIT TIMEOUT stdout\n' self.log_err += '\nWAIT TIMEOUT stderr\n' raise TimeoutError(stderr, stdout) diff --git a/contest/remote/vmksft-p.py b/contest/remote/vmksft-p.py index dfcdf13..943ce12 100755 --- a/contest/remote/vmksft-p.py +++ b/contest/remote/vmksft-p.py @@ -100,6 +100,7 @@ def _vm_thread(config, results_path, thr_id, in_queue, out_queue): retcode = vm.bash_prev_retcode() except TimeoutError: try: + print(f"INFO: thr-{thr_id} test timed out:", prog) vm.ctrl_c() vm.ctrl_c() vm.drain_to_prompt(dump_after=10) @@ -123,6 +124,7 @@ def _vm_thread(config, results_path, thr_id, in_queue, out_queue): result = 'fail' if vm.fail_state == 'oops': + print(f"INFO: thr-{thr_id} test crashed kernel:", prog) vm.extract_crash(results_path + f'/vm-crash-thr{thr_id}-{vm_id}') # Extraction will clear/discard false-positives (ignored traces) # check VM is still in failed state From e07428ea25025367a5ef0ba3849b410779583dd7 Mon Sep 17 00:00:00 2001 From: Pedro Tammela Date: Thu, 18 Jan 2024 10:47:17 -0300 Subject: [PATCH 005/429] contest: vm: handle empty virtme options virtme's argument parsing doesn't deal well with an empty string in the command line Signed-off-by: Pedro Tammela Signed-off-by: Jakub Kicinski --- contest/remote/lib/vm.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/contest/remote/lib/vm.py b/contest/remote/lib/vm.py index 758c328..2d2ad07 100644 --- a/contest/remote/lib/vm.py +++ b/contest/remote/lib/vm.py @@ -113,7 +113,10 @@ def start(self, cwd=None): cmd = cmd.split(' ') if cwd: cmd += ["--cwd", cwd] - cmd += self.config.get('vm', 'virtme_opt').split(',') + + opts = self.config.get('vm', 'virtme_opt', fallback="") + cmd += opts.split(',') if opts else [] + cpus = self.config.get('vm', 'cpus', fallback="") if cpus: cmd += ["--cpus", cpus] From 61f16dc405c53a9e77d66a5ddb09d8c37129f4f1 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 2 Feb 2024 14:36:29 -0800 Subject: [PATCH 006/429] ui: status: show full 6 branches Instead of showing 75 results show 6 branches. We're currently cutting off somewhat randomly. Signed-off-by: Jakub Kicinski --- status.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/status.js b/status.js index eacc377..e7f355f 100644 --- a/status.js +++ b/status.js @@ -466,12 +466,17 @@ function load_result_table(data_raw) v.start = new Date(v.start); v.end = new Date(v.end); + branches.add(v.branch); + if (v.remote == "brancher") branch_start[v.branch] = v.start; }); data_raw.sort(function(a, b){return b.end - a.end;}); - data_raw = data_raw.slice(0, 200); + + let recent_branches = new Set(Array.from(branches).sort().slice(-6)); + data_raw = $.grep(data_raw, + function(v, i) { return recent_branches.has(v.branch); }); var avgs = {}; $.each(data_raw, function(i, v) { @@ -498,8 +503,6 @@ function load_result_table(data_raw) data_raw.sort(function(a, b){return b.end - a.end;}); data_raw.sort(function(a, b){return b.branch > a.branch;}); - data_raw = data_raw.slice(0, 75); - reported_execs.add("brancher"); load_result_table_one(data_raw, table, true, avgs); reported_execs.delete("brancher"); @@ -508,6 +511,7 @@ function load_result_table(data_raw) let xfr_todo = 3; let all_results = null; +let branches = new Set(); let reported_execs = new Set(); let filtered_tests = new Array(); let branch_results = {}; From c8850b45dfa6853edd849d1c995e78a8cd9406a4 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 2 Feb 2024 15:29:39 -0800 Subject: [PATCH 007/429] ui: status: report AWOL executors Insert empty records for executors we have seen in the last 6 branches but which are missing results for some of them. Signed-off-by: Jakub Kicinski --- status.js | 52 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/status.js b/status.js index e7f355f..d8c0d45 100644 --- a/status.js +++ b/status.js @@ -425,7 +425,10 @@ function load_result_table_one(data_raw, table, reported, avgs) var remain = expect - passed; var color = "pink"; - if (remain > 0) { + if (v.end == 0) { + pend = "no result"; + color = "red"; + } else if (remain > 0) { pend = "pending (expected in " + (msec_to_str(remain)).toString() + ")"; color = "blue"; } else if (remain < -1000 * 60 * 60 * 2) { /* 2 h */ @@ -455,11 +458,15 @@ function load_result_table_one(data_raw, table, reported, avgs) }); } +function rem_exe(v) +{ + return v.remote + "/" + v.executor; +} + function load_result_table(data_raw) { var table = document.getElementById("contest"); var table_nr = document.getElementById("contest-purgatory"); - var branch_start = {}; $.each(data_raw, function(i, v) { @@ -472,12 +479,12 @@ function load_result_table(data_raw) branch_start[v.branch] = v.start; }); - data_raw.sort(function(a, b){return b.end - a.end;}); - + // Continue with only 6 most recent branches let recent_branches = new Set(Array.from(branches).sort().slice(-6)); data_raw = $.grep(data_raw, function(v, i) { return recent_branches.has(v.branch); }); + // Calculate expected runtimes var avgs = {}; $.each(data_raw, function(i, v) { if (!v.results) @@ -499,8 +506,43 @@ function load_result_table(data_raw) } }); + // Fill in runs for "AWOL" executors + let known_execs = {}; + let branch_execs = {}; + for (v of data_raw) { + let re = rem_exe(v); + + if (!(v.branch in branch_execs)) + branch_execs[v.branch] = new Set(); + branch_execs[v.branch].add(re); + + if (!(re in known_execs)) + known_execs[re] = { + "executor": v.executor, + "remote" : v.remote, + "branches" : new Set() + }; + known_execs[re].branches.add(v.branch); + } + + let known_exec_set = new Set(Object.keys(known_execs)); + for (br of recent_branches) { + for (re of known_exec_set) { + if (branch_execs[br].has(re)) + continue; + + data_raw.push({ + "executor" : known_execs[re].executor, + "remote" : known_execs[re].remote, + "branch" : br, + "end" : 0, + }); + } + } + + // Sort & display data_raw.sort(function(a, b){return avg_time_e(avgs, b) - avg_time_e(avgs, a);}); - data_raw.sort(function(a, b){return b.end - a.end;}); + data_raw.sort(function(a, b){return b.end == 0 || b.end - a.end;}); data_raw.sort(function(a, b){return b.branch > a.branch;}); reported_execs.add("brancher"); From 0150bb112466504f3b603c0449fe5e0cacaaa535 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 2 Feb 2024 16:23:53 -0800 Subject: [PATCH 008/429] ui: status: make the AWOL blue for the first 15min Make sure we don't flash red at the viewer at the start when branch was already cut but not all executors had a chance to react yet. Signed-off-by: Jakub Kicinski --- status.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/status.js b/status.js index d8c0d45..5fb6666 100644 --- a/status.js +++ b/status.js @@ -427,7 +427,10 @@ function load_result_table_one(data_raw, table, reported, avgs) if (v.end == 0) { pend = "no result"; - color = "red"; + if (passed > 1000 * 60 * 15 /* 15 min */) + color = "red"; + else + color = "blue"; } else if (remain > 0) { pend = "pending (expected in " + (msec_to_str(remain)).toString() + ")"; color = "blue"; From cb6e10fec4fc07ad494cd2c1c7e825a39531115d Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 2 Feb 2024 16:25:42 -0800 Subject: [PATCH 009/429] ui: status: fix the sorting Don't try to depend on stable sort, code the logic up properly. Signed-off-by: Jakub Kicinski --- status.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/status.js b/status.js index 5fb6666..c7dd96a 100644 --- a/status.js +++ b/status.js @@ -544,9 +544,23 @@ function load_result_table(data_raw) } // Sort & display - data_raw.sort(function(a, b){return avg_time_e(avgs, b) - avg_time_e(avgs, a);}); - data_raw.sort(function(a, b){return b.end == 0 || b.end - a.end;}); - data_raw.sort(function(a, b){return b.branch > a.branch;}); + data_raw.sort(function(a, b){ + if (b.branch != a.branch) + return b.branch > a.branch; + + // fake entry for "no result" always up top + if (b.end == 0) + return true; + + // both pending, sort by expected time + if (a.results == null && b.results == null) + return avg_time_e(avgs, b) - avg_time_e(avgs, a); + // pending before not pending + if (b.results == null) + return true; + + return b.end - a.end; + }); reported_execs.add("brancher"); load_result_table_one(data_raw, table, true, avgs); From 212563b79d30df65f1a933ecce23d0129104dbb7 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 2 Feb 2024 20:41:37 -0800 Subject: [PATCH 010/429] ui: status: fix the sorting for different browsers Web development! Firefox was fine with true / false but Chrome wants actual singed 1 / 0 / -1 values. Signed-off-by: Jakub Kicinski --- status.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/status.js b/status.js index c7dd96a..7f8d910 100644 --- a/status.js +++ b/status.js @@ -546,18 +546,18 @@ function load_result_table(data_raw) // Sort & display data_raw.sort(function(a, b){ if (b.branch != a.branch) - return b.branch > a.branch; + return b.branch > a.branch ? 1 : -1; // fake entry for "no result" always up top if (b.end == 0) - return true; + return 1; // both pending, sort by expected time if (a.results == null && b.results == null) return avg_time_e(avgs, b) - avg_time_e(avgs, a); // pending before not pending if (b.results == null) - return true; + return 1; return b.end - a.end; }); From 21d4b09b25be92c2611cc7852f2379d99b5fd67c Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 2 Feb 2024 22:24:40 -0800 Subject: [PATCH 011/429] ui: status: fix the sorting once more (and color) AWOL was showing under branch on Chrome. Plus we don't have start so we never crossed the 15min AWOL (meaning all AWOLs were blue, rather than turning red). Signed-off-by: Jakub Kicinski --- status.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/status.js b/status.js index 7f8d910..c107465 100644 --- a/status.js +++ b/status.js @@ -538,6 +538,7 @@ function load_result_table(data_raw) "executor" : known_execs[re].executor, "remote" : known_execs[re].remote, "branch" : br, + "start" : branch_start[br], "end" : 0, }); } @@ -549,7 +550,7 @@ function load_result_table(data_raw) return b.branch > a.branch ? 1 : -1; // fake entry for "no result" always up top - if (b.end == 0) + if (b.end === 0) return 1; // both pending, sort by expected time @@ -558,6 +559,8 @@ function load_result_table(data_raw) // pending before not pending if (b.results == null) return 1; + if (a.results == null) + return -1; return b.end - a.end; }); From 3237a73819de8dfeca3caa8d8575e1f1b1d2cad8 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 3 Feb 2024 09:43:57 -0800 Subject: [PATCH 012/429] ui: status: add warnings to fails for now We have no warnings reported, and the 0 just takes up space. Signed-off-by: Jakub Kicinski --- status.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/status.js b/status.js index c107465..d15b5bc 100644 --- a/status.js +++ b/status.js @@ -344,7 +344,7 @@ function load_result_table_one(data_raw, table, reported, avgs) if (!reported_execs.has(v.executor) && reported) return 1; - var pass = 0, skip = 0, warn = 0, fail = 0, total = 0, ignored = 0; + var pass = 0, skip = 0, fail = 0, total = 0, ignored = 0; var link = v.link; $.each(v.results, function(i, r) { if (pw_filted_r(v, r) != reported) { @@ -356,9 +356,7 @@ function load_result_table_one(data_raw, table, reported, avgs) pass++; } else if (r.result == "skip") { skip++; - } else if (r.result == "warn") { - warn++; - } else if (r.result == "fail") { + } else { fail++; } @@ -373,7 +371,6 @@ function load_result_table_one(data_raw, table, reported, avgs) var str_psf = {"str": "", "overall": ""}; colorify_str_psf(str_psf, "fail", fail, "red"); - colorify_str_psf(str_psf, "warn", warn, "orange"); colorify_str_psf(str_psf, "skip", skip, "blue"); colorify_str_psf(str_psf, "pass", pass, "green"); From 27e4b59cd49cd89d4815123f61615a62e3a60a76 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 3 Feb 2024 09:53:39 -0800 Subject: [PATCH 013/429] contest: ui: add links to history and matrix I always jump between these views, add links. They are useful when investigating flakiness. Signed-off-by: Jakub Kicinski --- contest/contest.html | 1 + contest/contest.js | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/contest/contest.html b/contest/contest.html index fa437d4..42e7c33 100644 --- a/contest/contest.html +++ b/contest/contest.html @@ -113,6 +113,7 @@ Group Test Result + Links diff --git a/contest/contest.js b/contest/contest.js index 95360a1..8cacd02 100644 --- a/contest/contest.js +++ b/contest/contest.js @@ -97,14 +97,21 @@ function load_result_table(data_raw) var group = row.insertCell(4); var test = row.insertCell(5); var res = row.insertCell(6); + let row_id = 7; + var outputs = row.insertCell(row_id++); + var flake = row.insertCell(row_id++); + var hist = row.insertCell(row_id++); date.innerHTML = v.end.toLocaleString(); branch.innerHTML = "" + v.branch + ""; remote.innerHTML = v.remote; exe.innerHTML = v.executor; group.innerHTML = r.group; - test.innerHTML = "" + r.test + ""; + test.innerHTML = "" + r.test + ""; res.innerHTML = colorify_str(r.result); + outputs.innerHTML = "outputs"; + hist.innerHTML = "history"; + flake.innerHTML = "matrix"; }); }); } From 175aa2a61d96753bd3ea81b625cb24194844a5ce Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 3 Feb 2024 11:02:15 -0800 Subject: [PATCH 014/429] contest: vm: uniformly prefix vm-printed messages In multi-threaded runner it's hard to identify the thread printing. Signed-off-by: Jakub Kicinski --- contest/remote/lib/vm.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/contest/remote/lib/vm.py b/contest/remote/lib/vm.py index 2d2ad07..b5f369b 100644 --- a/contest/remote/lib/vm.py +++ b/contest/remote/lib/vm.py @@ -62,11 +62,13 @@ def crash_finger_print(lines): class VM: - def __init__(self, config): + def __init__(self, config, vm_name=""): self.fail_state = "" self.p = None self.procs = [] self.config = config + self.vm_name = vm_name + self.print_pfx = (": " + vm_name) if vm_name else ":" self.cfg_boot_to = int(config.get('vm', 'boot_timeout')) @@ -103,7 +105,7 @@ def build(self, extra_configs, override_configs=None): if extra_configs: configs += extra_configs - print("INFO: building kernel") + print(f"INFO{self.print_pfx} building kernel") # Make sure we rebuild, config and module deps can be stale otherwise self.tree_cmd("make mrproper") self.tree_cmd("vng -v -b" + " -f ".join([""] + configs)) @@ -121,7 +123,7 @@ def start(self, cwd=None): if cpus: cmd += ["--cpus", cpus] - print("INFO: VM starting:", " ".join(cmd)) + print(f"INFO{self.print_pfx} VM starting:", " ".join(cmd)) self.p = self.tree_popen(cmd) for pipe in [self.p.stdout, self.p.stderr]: @@ -132,7 +134,7 @@ def start(self, cwd=None): init_prompt = self.config.get('vm', 'init_prompt') if init_prompt[-1] != ' ': init_prompt += ' ' - print(f"INFO: expecting prompt: '{init_prompt}'") + print(f"INFO{self.print_pfx} expecting prompt: '{init_prompt}'") try: self.drain_to_prompt(prompt=init_prompt, dump_after=self.cfg_boot_to) finally: @@ -140,7 +142,7 @@ def start(self, cwd=None): proc = psutil.Process(self.p.pid) self.procs = proc.children(recursive=True) + [proc] - print("INFO: reached initial prompt") + print(f"INFO{self.print_pfx} reached initial prompt") self.cmd("PS1='xx__-> '") self.drain_to_prompt() @@ -164,7 +166,7 @@ def stop(self): try: stdout, stderr = self.p.communicate(timeout=3) except subprocess.TimeoutExpired: - print("WARNING: process did not exit, sending a KILL to", self.p.pid, self.procs) + print(f"WARN{self.print_pfx} process did not exit, sending a KILL to", self.p.pid, self.procs) for p in self.procs: try: p.kill() @@ -177,7 +179,7 @@ def stop(self): stdout = stdout.decode("utf-8", "ignore") stderr = stderr.decode("utf-8", "ignore") - print("INFO: VM stopped") + print(f"INFO{self.print_pfx} VM stopped") self.log_out += stdout self.log_err += stderr @@ -246,7 +248,7 @@ def drain_to_prompt(self, prompt="xx__-> ", dump_after=None): if total_wait > hard_stop: waited = 1 << 63 if waited > dump_after: - print("WAIT TIMEOUT retcode:", self.p.returncode, + print(f"WARN{self.print_pfx} TIMEOUT retcode:", self.p.returncode, "waited:", waited, "total:", total_wait) self.log_out += '\nWAIT TIMEOUT stdout\n' self.log_err += '\nWAIT TIMEOUT stderr\n' @@ -309,7 +311,7 @@ def extract_crash(self, out_path): if in_crash: crash_lines.append(line) if not crash_lines: - print("WARNING: extract_crash found no crashes") + print(f"WARN{self.print_pfx} extract_crash found no crashes") return proc = self.tree_popen("./scripts/decode_stacktrace.sh vmlinux auto ./".split()) @@ -329,7 +331,7 @@ def extract_crash(self, out_path): ignore = set(self.filter_data["ignore-crashes"]) seen = set(finger_prints) if not seen - ignore: - print("INFO: all crashes were ignored") + print(f"INFO{self.print_pfx} all crashes were ignored") self.fail_state = "" def bash_prev_retcode(self): @@ -341,7 +343,7 @@ def bash_prev_retcode(self): def new_vm(results_path, vm_id, thr=None, vm=None, config=None, cwd=None): thr_pfx = f"thr{thr}-" if thr is not None else "" if vm is None: - vm = VM(config) + vm = VM(config, vm_name=f"{thr_pfx}{vm_id}") # For whatever reason starting sometimes hangs / crashes i = 0 while True: @@ -354,7 +356,7 @@ def new_vm(results_path, vm_id, thr=None, vm=None, config=None, cwd=None): i += 1 if i > 4: raise - print(f"WARNING: VM did not start, retrying {i}/4") + print(f"WARN{vm.print_pfx} VM did not start, retrying {i}/4") vm.dump_log(results_path + f'/vm-crashed-{thr_pfx}{vm_id}-{i}') vm.stop() From 9ce2a795cce2e376f1767664fb0aa1b7c4054b80 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 3 Feb 2024 11:07:23 -0800 Subject: [PATCH 015/429] contest: vm: move process killing handling to vm.py No strong reason, just feels right. Also bump the timeout to 12sec. Similarly no strong reason. Signed-off-by: Jakub Kicinski --- contest/remote/lib/vm.py | 8 ++++++++ contest/remote/vmksft-p.py | 9 ++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/contest/remote/lib/vm.py b/contest/remote/lib/vm.py index b5f369b..c449f97 100644 --- a/contest/remote/lib/vm.py +++ b/contest/remote/lib/vm.py @@ -197,6 +197,14 @@ def ctrl_c(self): self.p.stdin.write(b'\x03') self.p.stdin.flush() + def kill_current_cmd(self): + try: + self.ctrl_c() + self.ctrl_c() + self.drain_to_prompt(dump_after=12) + except TimeoutError: + print(f"WARN{self.print_pfx} failed to interrupt process") + def _read_pipe_nonblock(self, pipe): read_some = False output = "" diff --git a/contest/remote/vmksft-p.py b/contest/remote/vmksft-p.py index 943ce12..971cc3f 100755 --- a/contest/remote/vmksft-p.py +++ b/contest/remote/vmksft-p.py @@ -99,13 +99,8 @@ def _vm_thread(config, results_path, thr_id, in_queue, out_queue): vm.drain_to_prompt() retcode = vm.bash_prev_retcode() except TimeoutError: - try: - print(f"INFO: thr-{thr_id} test timed out:", prog) - vm.ctrl_c() - vm.ctrl_c() - vm.drain_to_prompt(dump_after=10) - except TimeoutError: - pass + print(f"INFO: thr-{thr_id} test timed out:", prog) + vm.kill_current_cmd() retcode = 1 t2 = datetime.datetime.now() From 92a438b860c8aadbfe4a14832787226050554120 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 3 Feb 2024 11:08:47 -0800 Subject: [PATCH 016/429] contest: vm: put VM in fail state after timeout If command times out and we don't manage to clean it up we gotta kill the VM. So make sure the fail_state is set to something. Signed-off-by: Jakub Kicinski --- contest/remote/lib/vm.py | 5 +++++ core/__init__.py | 1 + 2 files changed, 6 insertions(+) diff --git a/contest/remote/lib/vm.py b/contest/remote/lib/vm.py index c449f97..7b71868 100644 --- a/contest/remote/lib/vm.py +++ b/contest/remote/lib/vm.py @@ -260,8 +260,13 @@ def drain_to_prompt(self, prompt="xx__-> ", dump_after=None): "waited:", waited, "total:", total_wait) self.log_out += '\nWAIT TIMEOUT stdout\n' self.log_err += '\nWAIT TIMEOUT stderr\n' + if not self.fail_state: + self.fail_state = "timeout" raise TimeoutError(stderr, stdout) + if self.fail_state == "timeout": + self.fail_state = "" + return stdout, stderr def dump_log(self, dir_path, result=None, info=None): diff --git a/core/__init__.py b/core/__init__.py index 4390b4c..010d243 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -19,6 +19,7 @@ import os +from .lifetime import NipaLifetime from .logger import log, log_open_sec, log_end_sec, log_init from .patch import Patch from .test import Test From 3b5775971a224634332182763f0d1524b14f9e8d Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 3 Feb 2024 20:46:22 -0800 Subject: [PATCH 017/429] lifetime: create a common module for looping control and re-exec Create a module taking care of injecting loop delays, controlling exit and with the ability to re-exec the script when something of not changes or SIGUSR1 gets delivered. Signed-off-by: Jakub Kicinski --- contest/remote/core | 1 + contest/remote/kunit.py | 5 ++ contest/remote/lib/fetcher.py | 15 ++---- contest/remote/vmksft-p.py | 5 ++ contest/remote/vmksft.py | 5 ++ contest/remote/vmtest.py | 5 ++ core/lifetime.py | 86 +++++++++++++++++++++++++++++++++++ 7 files changed, 111 insertions(+), 11 deletions(-) create mode 120000 contest/remote/core create mode 100644 core/lifetime.py diff --git a/contest/remote/core b/contest/remote/core new file mode 120000 index 0000000..58377d5 --- /dev/null +++ b/contest/remote/core @@ -0,0 +1 @@ +../../core/ \ No newline at end of file diff --git a/contest/remote/kunit.py b/contest/remote/kunit.py index 2d9dd1b..412419a 100755 --- a/contest/remote/kunit.py +++ b/contest/remote/kunit.py @@ -7,6 +7,7 @@ import os import subprocess +from core import NipaLifetime from lib import Fetcher @@ -161,13 +162,17 @@ def main() -> None: base_dir = config.get('local', 'base_path') + life = NipaLifetime(config) + f = Fetcher(test, config, name=config.get('executor', 'name'), branches_url=config.get('remote', 'branches'), results_path=os.path.join(base_dir, config.get('local', 'json_path')), url_path=config.get('www', 'url') + '/' + config.get('local', 'json_path'), + life=life, tree_path=config.get('local', 'tree_path')) f.run() + life.exit() if __name__ == "__main__": diff --git a/contest/remote/lib/fetcher.py b/contest/remote/lib/fetcher.py index 713d6d8..070f02d 100644 --- a/contest/remote/lib/fetcher.py +++ b/contest/remote/lib/fetcher.py @@ -10,20 +10,19 @@ class Fetcher: def __init__(self, cb, cbarg, name, branches_url, results_path, url_path, tree_path, - check_sec=60, first_run="continue", single_shot=False): + life, first_run="continue"): self._cb = cb self._cbarg = cbarg self.name = name + self.life = life self._branches_url = branches_url - self._check_secs = check_sec self._results_path = results_path self._url_path = url_path self._results_manifest = os.path.join(results_path, 'results.json') self._tree_path = tree_path - self.single_shot = single_shot # Set last date to something old self._last_date = datetime.datetime.now(datetime.UTC) - datetime.timedelta(weeks=1) @@ -138,13 +137,7 @@ def _run_once(self): cwd=self._tree_path, shell=True, check=True) self._clean_old_branches(branches, to_test["branch"]) self._run_test(to_test) - return self.single_shot def run(self): - while True: - if self._run_once(): - return - try: - time.sleep(self._check_secs) - except KeyboardInterrupt: - return + while self.life.next_poll(): + self._run_once() diff --git a/contest/remote/vmksft-p.py b/contest/remote/vmksft-p.py index 971cc3f..1adc29b 100755 --- a/contest/remote/vmksft-p.py +++ b/contest/remote/vmksft-p.py @@ -12,6 +12,7 @@ import threading import time +from core import NipaLifetime from lib import CbArg from lib import Fetcher from lib import VM, new_vm, guess_indicators @@ -229,14 +230,18 @@ def main() -> None: base_dir = config.get('local', 'base_path') + life = NipaLifetime(config) + f = Fetcher(test, cbarg, name=config.get('executor', 'name'), branches_url=config.get('remote', 'branches'), results_path=os.path.join(base_dir, config.get('local', 'json_path')), url_path=config.get('www', 'url') + '/' + config.get('local', 'json_path'), tree_path=config.get('local', 'tree_path'), + life=life, first_run=config.get('executor', 'init', fallback="continue")) f.run() + life.exit() if __name__ == "__main__": diff --git a/contest/remote/vmksft.py b/contest/remote/vmksft.py index ec73db9..ced414a 100755 --- a/contest/remote/vmksft.py +++ b/contest/remote/vmksft.py @@ -8,6 +8,7 @@ import re import sys +from core import NipaLifetime from lib import CbArg from lib import Fetcher from lib import VM, new_vm, guess_indicators @@ -217,14 +218,18 @@ def main() -> None: base_dir = config.get('local', 'base_path') + life = NipaLifetime(config) + f = Fetcher(test, cbarg, name=config.get('executor', 'name'), branches_url=config.get('remote', 'branches'), results_path=os.path.join(base_dir, config.get('local', 'json_path')), url_path=config.get('www', 'url') + '/' + config.get('local', 'json_path'), tree_path=config.get('local', 'tree_path'), + life=life, first_run=config.get('executor', 'init', fallback="continue")) f.run() + life.exit() if __name__ == "__main__": diff --git a/contest/remote/vmtest.py b/contest/remote/vmtest.py index 3510775..b9d78c5 100755 --- a/contest/remote/vmtest.py +++ b/contest/remote/vmtest.py @@ -7,6 +7,7 @@ import sys import os +from core import NipaLifetime from lib import CbArg from lib import Fetcher from lib import VM, new_vm, guess_indicators @@ -142,14 +143,18 @@ def main() -> None: base_dir = config.get('local', 'base_path') + life = NipaLifetime(config) + f = Fetcher(test, cbarg, name=config.get('executor', 'name'), branches_url=config.get('remote', 'branches'), results_path=os.path.join(base_dir, config.get('local', 'json_path')), url_path=config.get('www', 'url') + '/' + config.get('local', 'json_path'), tree_path=config.get('local', 'tree_path'), + life=life, first_run=config.get('executor', 'init', fallback="continue")) f.run() + life.exit() if __name__ == "__main__": diff --git a/core/lifetime.py b/core/lifetime.py new file mode 100644 index 0000000..8be261a --- /dev/null +++ b/core/lifetime.py @@ -0,0 +1,86 @@ +# SPDX-License-Identifier: GPL-2.0 + +import subprocess +import signal +import sys +import time +import os + + +sig_initialized = False +got_sigusr1 = False + + +def sig_handler(signum, frame) -> None: + global got_sigusr1 + + got_sigusr1 |= signum == signal.SIGUSR1 + print('signal received, SIGUSR1:', got_sigusr1) + + +def sig_init(): + global sig_initialized + + if not sig_initialized: + signal.signal(signal.SIGUSR1, sig_handler) + + +def nipa_git_version(): + cwd = os.path.dirname(os.path.abspath(__file__)) + res = subprocess.run(["git", "show", "HEAD", "--format=quote", "--no-patch"], + capture_output=True, cwd=cwd, check=True) + return res.stdout.decode("utf-8").strip() + + +class NipaLifetime: + def __init__(self, config): + self.config = config + + # Load exit criteria + self.use_usrsig = config.getboolean('life', 'sigusr1', fallback=True) + if self.use_usrsig: + sig_init() + self._nipa_version = nipa_git_version() + self.use_nipa_version = config.getboolean('life', 'nipa_version', fallback=True) + if self.use_nipa_version: + self._nipa_version = nipa_git_version() + + print("NIPA version:", self._nipa_version) + + # Load params + self._sleep = config.getint('life', 'poll_ival', fallback=60) + self._single_shot = config.getboolean('life', 'single_shot', fallback=False) + # Set initial state + self._first_run = True + self._restart = False + + def next_poll(self): + global got_sigusr1 + + if self._first_run: + self._first_run = False + return True + elif self._single_shot: + return False + + if self.use_nipa_version and nipa_git_version() != self._nipa_version: + self._restart = True + + to_sleep = self._sleep + while not self._restart and to_sleep > 0: + if self.use_usrsig and got_sigusr1: + self._restart = True + break + try: + time.sleep(min(to_sleep, 1)) + except KeyboardInterrupt: + return False + to_sleep -= 1 + + return not self._restart + + def exit(self): + if self._restart: + print("NIPA restarting!") + os.execv(sys.executable, [sys.executable] + sys.argv) + print("NIPA quitting!") From b324de75231632e40dfa8f4c30e5594da4baebae Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 3 Feb 2024 21:44:30 -0800 Subject: [PATCH 018/429] tests: build_tools: baseline build We need pre-build to make sure we don't count _all_ warnings as preexisting, only the ones in touched files. Signed-off-by: Jakub Kicinski --- tests/patch/build_tools/build_tools.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/patch/build_tools/build_tools.sh b/tests/patch/build_tools/build_tools.sh index 591fa99..afeff5b 100755 --- a/tests/patch/build_tools/build_tools.sh +++ b/tests/patch/build_tools/build_tools.sh @@ -28,6 +28,13 @@ git log -1 --pretty='%h ("%s")' HEAD~ echo "Cleaning" make O=$output_dir $build_flags -C tools/testing/selftests/ clean +echo "Baseline building the tree" +make O=$output_dir $build_flags headers +for what in net net/forwarding net/tcp_ao; do + make O=$output_dir $build_flags -C tools/testing/selftests/ \ + TARGETS=$what +done + echo "Building the tree before the patch" git checkout -q HEAD~ From 468a45ec1522329efeed35e9b265f5314c450444 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sun, 4 Feb 2024 08:08:05 -0800 Subject: [PATCH 019/429] lifetime: allow caller to override wait time poller calculates the wait time for each loop, let it pass the sleep length in. Signed-off-by: Jakub Kicinski --- core/lifetime.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/lifetime.py b/core/lifetime.py index 8be261a..d10ae45 100644 --- a/core/lifetime.py +++ b/core/lifetime.py @@ -54,7 +54,7 @@ def __init__(self, config): self._first_run = True self._restart = False - def next_poll(self): + def next_poll(self, wait_time=None): global got_sigusr1 if self._first_run: @@ -67,6 +67,8 @@ def next_poll(self): self._restart = True to_sleep = self._sleep + if wait_time is not None: + to_sleep = wait_time while not self._restart and to_sleep > 0: if self.use_usrsig and got_sigusr1: self._restart = True From 93bd531085893fbdd98d2ccaf7d71fc25ed86506 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sun, 4 Feb 2024 08:15:42 -0800 Subject: [PATCH 020/429] pw_poller: use lifetime controller Teach poller to auto-restart when NIPA source changes. Signed-off-by: Jakub Kicinski --- pw_poller.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/pw_poller.py b/pw_poller.py index 293fe1a..2270f03 100755 --- a/pw_poller.py +++ b/pw_poller.py @@ -15,6 +15,7 @@ from typing import Dict from core import NIPA_DIR +from core import NipaLifetime from core import log, log_open_sec, log_end_sec, log_init from core import Tester from core import Tree @@ -29,13 +30,7 @@ class IncompleteSeries(Exception): class PwPoller: - def __init__(self) -> None: - config = configparser.ConfigParser() - config.read(['nipa.config', 'pw.config', 'poller.config']) - - log_init(config.get('log', 'type', fallback='org'), - config.get('log', 'file', fallback=os.path.join(NIPA_DIR, "poller.org"))) - + def __init__(self, config) -> None: self._worker_id = 0 self._async_workers = [] @@ -187,7 +182,7 @@ def process_series(self, pw_series) -> None: finally: log_end_sec() - def run(self) -> None: + def run(self, life) -> None: partial_series = {} prev_big_scan = datetime.datetime.fromtimestamp(self._state['last_poll']) @@ -198,7 +193,8 @@ def run(self) -> None: # apparently patchwork uses the time from the email headers and people back date their emails, a lot # We keep a history of the series we've seen in and since the last big poll to not process twice try: - while True: + secs = 0 + while life.next_poll(secs): this_poll_seen = set() req_time = datetime.datetime.now() @@ -252,11 +248,7 @@ def run(self) -> None: secs = 120 - (datetime.datetime.now() - req_time).total_seconds() if secs > 0: log("Sleep", secs) - time.sleep(secs) log_end_sec() - if os.path.exists('poller.quit'): - os.remove('poller.quit') - break finally: log_open_sec(f"Stopping threads") self._barrier.abort() @@ -277,5 +269,14 @@ def run(self) -> None: if __name__ == "__main__": os.umask(0o002) - poller = PwPoller() - poller.run() + + config = configparser.ConfigParser() + config.read(['nipa.config', 'pw.config', 'poller.config']) + + log_init(config.get('log', 'type', fallback='org'), + config.get('log', 'file', fallback=os.path.join(NIPA_DIR, "poller.org"))) + + life = NipaLifetime(config) + poller = PwPoller(config) + poller.run(life) + life.exit() From b9ed520b3eb996f3ed5d4a040e39f380061720a8 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sun, 4 Feb 2024 08:22:57 -0800 Subject: [PATCH 021/429] lifetime: use reference git format instead of quote "quote" is a format kernel devs use, it's defined in our .gitconfig. It won't work on random machines. Signed-off-by: Jakub Kicinski --- core/lifetime.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/lifetime.py b/core/lifetime.py index d10ae45..dd623e7 100644 --- a/core/lifetime.py +++ b/core/lifetime.py @@ -27,7 +27,7 @@ def sig_init(): def nipa_git_version(): cwd = os.path.dirname(os.path.abspath(__file__)) - res = subprocess.run(["git", "show", "HEAD", "--format=quote", "--no-patch"], + res = subprocess.run(["git", "show", "HEAD", "--format=reference", "--no-patch"], capture_output=True, cwd=cwd, check=True) return res.stdout.decode("utf-8").strip() From 8287097e4a0e8edb45c14636def511735499608c Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sun, 4 Feb 2024 09:53:06 -0800 Subject: [PATCH 022/429] contest: ui: skip empty rows in flakes There are some very old runners which still show up even tho they haven't produced a result in weeks. Don't show them. Signed-off-by: Jakub Kicinski --- contest/flakes.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/contest/flakes.js b/contest/flakes.js index e9c6df4..878b53b 100644 --- a/contest/flakes.js +++ b/contest/flakes.js @@ -87,12 +87,15 @@ function load_result_table(data_raw) // Sort from most to least flaky for (const [tn, entries] of Object.entries(test_row)) { - let count = 0, streak = 0; + let count = 0, streak = 0, total = 0; let prev = "pass"; for (let i = 0; i < branches.length; i++) { let current = entries[branches[i]]; + if (current != "") + total++; + if (current == "pass" && count == 0) streak++; @@ -101,6 +104,7 @@ function load_result_table(data_raw) count++; } } + test_row[tn]["total"] = total; test_row[tn]["cnt"] = count; test_row[tn]["streak"] = streak; } @@ -130,8 +134,11 @@ function load_result_table(data_raw) for (const tn of test_names) { let entries = test_row[tn]; - let row = table.insertRow(); + if (entries.total == 0) + continue; + + let row = table.insertRow(); let name = row.insertCell(0); name.innerHTML = tn; name.setAttribute("style", "padding: 0px"); From 03d3859312367a15ac31b09d7f0cab5c22db5c39 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sun, 4 Feb 2024 10:27:04 -0800 Subject: [PATCH 023/429] ui: status: deal with missing start times Right after the branch is created but before branch fetcher produced the fake "branch creation succeeded" result - we're in a situation where the brancher executor is missing a result. We insert an empty result for the missing run. Normally the start for that inserted result would be the start of branch... but there is no branch. So we end up setting result start to 'undefined'. Calling toLocaleString() on undefined will explode. Signed-off-by: Jakub Kicinski --- status.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/status.js b/status.js index d15b5bc..17a426e 100644 --- a/status.js +++ b/status.js @@ -443,7 +443,10 @@ function load_result_table_one(data_raw, table, reported, avgs) let res = row.insertCell(2); let br_res; - remote.innerHTML = v.start.toLocaleString(); + if (v.start) + remote.innerHTML = v.start.toLocaleString(); + else + remote.innerHTML = "unknown"; remote.setAttribute("colspan", "2"); branch.innerHTML = a + v.branch + ""; branch.setAttribute("colspan", "2"); From 4119e50000d106684c054ba917be7d3a628d26b4 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sun, 4 Feb 2024 10:37:53 -0800 Subject: [PATCH 024/429] pw_poller: drop patch ret code gathering I must have had some grand plans involving test return codes, but they are currently unused. Drop this, since it will make future work harder. Signed-off-by: Jakub Kicinski --- core/tester.py | 30 ++++++++---------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/core/tester.py b/core/tester.py index ca8b026..9ae4777 100644 --- a/core/tester.py +++ b/core/tester.py @@ -127,18 +127,16 @@ def _test_series(self, tree, series): elif os.path.exists(os.path.join(series_dir, ".tester_done")): core.log(f"Already tested in {series_dir}", "") core.log_end_sec() - return [], [] + return try: if series.is_pure_pull(): - ret = self._test_series_pull(tree, series, series_dir) + self._test_series_pull(tree, series, series_dir) else: - ret = self._test_series_patches(tree, series, series_dir) + self._test_series_patches(tree, series, series_dir) finally: core.log_end_sec() - return ret - def _test_series_patches(self, tree, series, series_dir): if not tree.check_applies(series): series_apply = os.path.join(series_dir, "apply") @@ -157,16 +155,13 @@ def _test_series_patches(self, tree, series, series_dir): fp.write("1") with open(os.path.join(series_apply, "desc"), "w+") as fp: fp.write(f"Patch does not apply to {tree.name}") - return [already_applied], [already_applied] + return - series_ret = [] - patch_ret = [] tree.reset(fetch=False) tree.apply(series) for test in self.series_tests: - ret = test.exec(tree, series, series_dir) - series_ret.append(ret) + test.exec(tree, series, series_dir) tree.reset(fetch=False) cnt = 1 @@ -174,8 +169,6 @@ def _test_series_patches(self, tree, series, series_dir): core.log_open_sec(f"Testing patch {cnt}/{len(series.patches)}| {patch.title}") cnt += 1 - current_patch_ret = [] - patch_dir = os.path.join(series_dir, str(patch.id)) if not os.path.exists(patch_dir): os.makedirs(patch_dir) @@ -184,14 +177,10 @@ def _test_series_patches(self, tree, series, series_dir): tree.apply(patch) for test in self.patch_tests: - ret = test.exec(tree, patch, patch_dir) - current_patch_ret.append(ret) + test.exec(tree, patch, patch_dir) finally: core.log_end_sec() - patch_ret.append(current_patch_ret) - - return series_ret, patch_ret def _test_series_pull(self, tree, series, series_dir): try: @@ -205,10 +194,9 @@ def _test_series_pull(self, tree, series, series_dir): fp.write("1") with open(os.path.join(series_apply, "desc"), "w+") as fp: fp.write(f"Pull to {tree.name} failed") - return [], [] + return patch = series.patches[0] - current_patch_ret = [] core.log_open_sec(f"Testing pull request {patch.title}") @@ -219,9 +207,7 @@ def _test_series_pull(self, tree, series, series_dir): try: for test in self.patch_tests: if test.is_pull_compatible(): - ret = test.exec(tree, patch, patch_dir) - current_patch_ret.append(ret) + test.exec(tree, patch, patch_dir) finally: core.log_end_sec() - return [], [current_patch_ret] From 0278573b7fb099083c95972adc3e13253d70df17 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sun, 4 Feb 2024 10:44:10 -0800 Subject: [PATCH 025/429] pw_poller: flip the nesting of test execution Instead of iterating: foreach patch: foreach test: test.run() do: foreach test: foreach patch: test.run() build tests need to put the tree into a fully built state first. A state where there's nothing to do for an incremental build. If we don't interleave the tests they can assume the tree is in whatever state it was when testing the previous patch finished plus git am next.patch, but be sure that absolutely nothing else has happened. Signed-off-by: Jakub Kicinski --- core/tester.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/core/tester.py b/core/tester.py index 9ae4777..78a1e27 100644 --- a/core/tester.py +++ b/core/tester.py @@ -162,25 +162,27 @@ def _test_series_patches(self, tree, series, series_dir): tree.apply(series) for test in self.series_tests: test.exec(tree, series, series_dir) - tree.reset(fetch=False) - cnt = 1 - for patch in series.patches: - core.log_open_sec(f"Testing patch {cnt}/{len(series.patches)}| {patch.title}") - cnt += 1 + tcnt = 0 + for test in self.patch_tests: + tcnt += 1 + tree.reset(fetch=False) - patch_dir = os.path.join(series_dir, str(patch.id)) - if not os.path.exists(patch_dir): - os.makedirs(patch_dir) + pcnt = 0 + for patch in series.patches: + pcnt += 1 + cnts = f"{tcnt}/{len(self.patch_tests)}|{pcnt}/{len(series.patches)}" + core.log_open_sec(f"Testing patch {cnts}| {patch.title}") - try: - tree.apply(patch) + patch_dir = os.path.join(series_dir, str(patch.id)) + if not os.path.exists(patch_dir): + os.makedirs(patch_dir) - for test in self.patch_tests: + try: + tree.apply(patch) test.exec(tree, patch, patch_dir) - finally: - core.log_end_sec() - + finally: + core.log_end_sec() def _test_series_pull(self, tree, series, series_dir): try: From cbab559584069f657f74b1dca71df0544d985900 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sun, 4 Feb 2024 11:24:39 -0800 Subject: [PATCH 026/429] pw_poller: export FIRST_IN_SERIES for patch tests Export FIRST_IN_SERIES=0/1 when testing patches, so that build tests can optimize their prep work. Signed-off-by: Jakub Kicinski --- core/patch.py | 3 +++ core/series.py | 1 + core/test.py | 11 ++++++++--- pw/pw_series.py | 2 +- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/core/patch.py b/core/patch.py index d552e02..4b47b97 100644 --- a/core/patch.py +++ b/core/patch.py @@ -35,6 +35,9 @@ def __init__(self, raw_patch, ident=None, title="", series=None): self.subject = "" self.series = series + # Whether the patch is first in the series, set by series.add_patch() + self.first_in_series = None + subj = re.search(r'Subject: \[.*\](.*)', raw_patch) if not subj: subj = re.search(r'Subject: (.*)', raw_patch) diff --git a/core/series.py b/core/series.py index dcec6c7..dd38143 100644 --- a/core/series.py +++ b/core/series.py @@ -39,6 +39,7 @@ def set_cover_letter(self, data): self.subject = subj.group(0)[9:] def add_patch(self, patch): + patch.first_in_series = len(self.patches) == 0 self.patches.append(patch) def is_pure_pull(self): diff --git a/core/test.py b/core/test.py index f32a59c..5e1c1b1 100644 --- a/core/test.py +++ b/core/test.py @@ -121,10 +121,15 @@ def _exec_run(self, tree, thing, result_dir): try: rfd, wfd = os.pipe() + env = { "DESC_FD": str(wfd), + "RESULTS_DIR": os.path.join(result_dir, self.name), + "BRANCH_BASE": tree.branch } + + if hasattr(thing, 'first_in_series'): + env["FIRST_IN_SERIES"] = int(thing.first_in_series) + out, err = CMD.cmd_run(self.info["run"], include_stderr=True, cwd=tree.path, - pass_fds=[wfd], add_env={"DESC_FD": str(wfd), - "RESULTS_DIR": os.path.join(result_dir, self.name), - "BRANCH_BASE": tree.branch}) + pass_fds=[wfd], add_env=env) except core.cmd.CmdError as e: retcode = e.retcode out = e.stdout diff --git a/pw/pw_series.py b/pw/pw_series.py index f974245..fdbee78 100644 --- a/pw/pw_series.py +++ b/pw/pw_series.py @@ -72,7 +72,7 @@ def __init__(self, pw, pw_series): for pid in pids: raw_patch = pw.get_mbox('patch', pid) - self.patches.append(Patch(raw_patch, pid)) + self.add_patch(Patch(raw_patch, pid)) if not pw_series['cover_letter']: if len(self.patches) == 1: From 762fec308b13c1f4f05a9c5a4088f2921f552d38 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sun, 4 Feb 2024 11:28:45 -0800 Subject: [PATCH 027/429] tests: build_clang: take advantage of $FIRST_IN_SERIES Only do the "baseline" build on the first patch in the series. Subsequent patches should have baseline from the previous run of the test. Print lengths of stdout to confirm we're rebuilding the same stuff. Signed-off-by: Jakub Kicinski --- tests/patch/build_clang/build_clang.sh | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/patch/build_clang/build_clang.sh b/tests/patch/build_clang/build_clang.sh index 3b59aed..7076ed8 100755 --- a/tests/patch/build_clang/build_clang.sh +++ b/tests/patch/build_clang/build_clang.sh @@ -25,10 +25,14 @@ HEAD=$(git rev-parse HEAD) echo "Tree base:" git log -1 --pretty='%h ("%s")' HEAD~ -echo "Baseline building the tree" +if [ x$FIRST_IN_SERIES == x0 ]; then + echo "Skip baseline build, not the first patch" +else + echo "Baseline building the tree" -prep_config -make LLVM=1 O=$output_dir $build_flags + prep_config + make LLVM=1 O=$output_dir $build_flags +fi git checkout -q HEAD~ @@ -68,6 +72,8 @@ if [ $current -gt $incumbent ]; then rc=1 fi +echo "Output lengths:" $(wc -l $tmpfile_n) $(wc -l $tmpfile_o) + rm $tmpfile_o $tmpfile_n exit $rc From 0eae73e66e9e20e9d3ea02c823c4ea3630bd1494 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sun, 4 Feb 2024 11:49:18 -0800 Subject: [PATCH 028/429] tests: build: drop duplicate -j $ncpu We already put -j $ncpu in the build_flags, no need to add it on particular call sites. Signed-off-by: Jakub Kicinski --- tests/patch/build_32bit/build_32bit.sh | 2 +- tests/patch/build_allmodconfig_warn/build_allmodconfig.sh | 2 +- tests/patch/build_clang/build_clang.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/patch/build_32bit/build_32bit.sh b/tests/patch/build_32bit/build_32bit.sh index d774e6c..7356c83 100755 --- a/tests/patch/build_32bit/build_32bit.sh +++ b/tests/patch/build_32bit/build_32bit.sh @@ -43,7 +43,7 @@ echo "Building the tree with the patch" git checkout -q $HEAD prep_config -make CC="$cc" O=$output_dir ARCH=i386 $build_flags -j $ncpu 2> >(tee $tmpfile_n >&2) || rc=1 +make CC="$cc" O=$output_dir ARCH=i386 $build_flags 2> >(tee $tmpfile_n >&2) || rc=1 current=$(grep -i -c "\(warn\|error\)" $tmpfile_n) diff --git a/tests/patch/build_allmodconfig_warn/build_allmodconfig.sh b/tests/patch/build_allmodconfig_warn/build_allmodconfig.sh index ff839dc..95857f6 100755 --- a/tests/patch/build_allmodconfig_warn/build_allmodconfig.sh +++ b/tests/patch/build_allmodconfig_warn/build_allmodconfig.sh @@ -43,7 +43,7 @@ echo "Building the tree with the patch" git checkout -q $HEAD prep_config -make CC="$cc" O=$output_dir $build_flags -j $ncpu 2> >(tee $tmpfile_n >&2) || rc=1 +make CC="$cc" O=$output_dir $build_flags 2> >(tee $tmpfile_n >&2) || rc=1 current=$(grep -i -c "\(warn\|error\)" $tmpfile_n) diff --git a/tests/patch/build_clang/build_clang.sh b/tests/patch/build_clang/build_clang.sh index 7076ed8..53b1fa8 100755 --- a/tests/patch/build_clang/build_clang.sh +++ b/tests/patch/build_clang/build_clang.sh @@ -47,7 +47,7 @@ echo "Building the tree with the patch" git checkout -q $HEAD prep_config -make LLVM=1 O=$output_dir $build_flags -j $ncpu 2> >(tee $tmpfile_n >&2) || rc=1 +make LLVM=1 O=$output_dir $build_flags 2> >(tee $tmpfile_n >&2) || rc=1 current=$(grep -i -c "\(warn\|error\)" $tmpfile_n) From 2fd29c32839d748c1417dc421a2143ba2cda3d31 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sun, 4 Feb 2024 12:10:08 -0800 Subject: [PATCH 029/429] status: adjust after the poller changes Adjust the outputs for status page after poller reorder of patch and test iteration. Signed-off-by: Jakub Kicinski --- status.html | 3 ++- status.js | 13 ++++++++----- system-status.py | 17 ++++++++++++++--- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/status.html b/status.html index 1b977af..9997fe0 100644 --- a/status.html +++ b/status.html @@ -82,9 +82,10 @@

Processing times (by patch post time)

Tree Qlen + Tid + Test Pid Patch - Test
diff --git a/status.js b/status.js index 17a426e..9fe1e56 100644 --- a/status.js +++ b/status.js @@ -189,15 +189,18 @@ function load_runners(data_raw) $.each(data_raw, function(i, v) { var row = table.insertRow(); - var name = row.insertCell(0); - var qlen = row.insertCell(1); - var pid = row.insertCell(2); - var patch = row.insertCell(3); - var test = row.insertCell(4); + let cell_id = 0; + var name = row.insertCell(cell_id++); + var qlen = row.insertCell(cell_id++); + var tid = row.insertCell(cell_id++); + var test = row.insertCell(cell_id++); + var pid = row.insertCell(cell_id++); + var patch = row.insertCell(cell_id++); name.innerHTML = i; pid.innerHTML = v.progress; patch.innerHTML = v.patch; + tid.innerHTML = v["test-progress"]; test.innerHTML = v.test; qlen.innerHTML = v.backlog; }); diff --git a/system-status.py b/system-status.py index aebe12d..01fe169 100755 --- a/system-status.py +++ b/system-status.py @@ -49,13 +49,19 @@ def add_one_tree(result, pfx, name): lines = fp.readlines() last = None test = '' + test_prog = '' blog = '' progress = '' for line in lines: if 'Testing patch' in line: patch = pre_strip(line, 'Testing patch') - progress = patch[:patch.find('|')] - patch = patch[patch.find('|') + 2:] + + test_sep = patch.find('|') + patch_sep = patch.find('|', test_sep + 1) + + test_prog = patch[:test_sep] + progress = patch[test_sep + 1:patch_sep] + patch = patch[patch_sep + 2:] last = re.sub(char_filter, "", patch) test = '' elif '* Testing pull request' in line: @@ -73,8 +79,13 @@ def add_one_tree(result, pfx, name): last = None progress = '' test = '' + test_prog = '' blog = '' - result['runners'][name] = {"patch": last, "progress": progress, "test": test, "backlog": blog} + result['runners'][name] = {"patch": last, + "progress": progress, + "test": test, + "test-progress": test_prog, + "backlog": blog} def add_one_runtime(fname, total, res): From ce6e93061c3ed7cb425cdf0d81932ca7c0625210 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sun, 4 Feb 2024 12:22:49 -0800 Subject: [PATCH 030/429] core: test: make sure that FIRST_IN_SERIES is str env does not like dealing with integers. Signed-off-by: Jakub Kicinski --- core/test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/test.py b/core/test.py index 5e1c1b1..89f1f52 100644 --- a/core/test.py +++ b/core/test.py @@ -126,7 +126,7 @@ def _exec_run(self, tree, thing, result_dir): "BRANCH_BASE": tree.branch } if hasattr(thing, 'first_in_series'): - env["FIRST_IN_SERIES"] = int(thing.first_in_series) + env["FIRST_IN_SERIES"] = str(int(thing.first_in_series)) out, err = CMD.cmd_run(self.info["run"], include_stderr=True, cwd=tree.path, pass_fds=[wfd], add_env=env) From 40609cab599182d84b48399f41d23a3e4ca57d96 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Mon, 5 Feb 2024 06:35:10 -0800 Subject: [PATCH 031/429] deploy: move systemd files into a new dir We need more structure to also hold remote configs in the same spot. Signed-off-by: Jakub Kicinski --- {systemd => deploy/systemd}/nipa-checks.service | 0 {systemd => deploy/systemd}/nipa-checks.timer | 0 {systemd => deploy/systemd}/nipa-clean-logs.service | 0 {systemd => deploy/systemd}/nipa-clean-output.service | 0 {systemd => deploy/systemd}/nipa-poller.service | 0 {systemd => deploy/systemd}/nipa-status.service | 0 {systemd => deploy/systemd}/nipa-status.timer | 0 {systemd => deploy/systemd}/nipa-upload.service | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename {systemd => deploy/systemd}/nipa-checks.service (100%) rename {systemd => deploy/systemd}/nipa-checks.timer (100%) rename {systemd => deploy/systemd}/nipa-clean-logs.service (100%) rename {systemd => deploy/systemd}/nipa-clean-output.service (100%) rename {systemd => deploy/systemd}/nipa-poller.service (100%) rename {systemd => deploy/systemd}/nipa-status.service (100%) rename {systemd => deploy/systemd}/nipa-status.timer (100%) rename {systemd => deploy/systemd}/nipa-upload.service (100%) diff --git a/systemd/nipa-checks.service b/deploy/systemd/nipa-checks.service similarity index 100% rename from systemd/nipa-checks.service rename to deploy/systemd/nipa-checks.service diff --git a/systemd/nipa-checks.timer b/deploy/systemd/nipa-checks.timer similarity index 100% rename from systemd/nipa-checks.timer rename to deploy/systemd/nipa-checks.timer diff --git a/systemd/nipa-clean-logs.service b/deploy/systemd/nipa-clean-logs.service similarity index 100% rename from systemd/nipa-clean-logs.service rename to deploy/systemd/nipa-clean-logs.service diff --git a/systemd/nipa-clean-output.service b/deploy/systemd/nipa-clean-output.service similarity index 100% rename from systemd/nipa-clean-output.service rename to deploy/systemd/nipa-clean-output.service diff --git a/systemd/nipa-poller.service b/deploy/systemd/nipa-poller.service similarity index 100% rename from systemd/nipa-poller.service rename to deploy/systemd/nipa-poller.service diff --git a/systemd/nipa-status.service b/deploy/systemd/nipa-status.service similarity index 100% rename from systemd/nipa-status.service rename to deploy/systemd/nipa-status.service diff --git a/systemd/nipa-status.timer b/deploy/systemd/nipa-status.timer similarity index 100% rename from systemd/nipa-status.timer rename to deploy/systemd/nipa-status.timer diff --git a/systemd/nipa-upload.service b/deploy/systemd/nipa-upload.service similarity index 100% rename from systemd/nipa-upload.service rename to deploy/systemd/nipa-upload.service From 1ae2427e93f86d73d246c0f2c111ced102d0c9dc Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Mon, 5 Feb 2024 09:38:50 -0800 Subject: [PATCH 032/429] contest: exec: fix after adding lifetime exec was not converted to use lifetime. Signed-off-by: Jakub Kicinski --- contest/remote/exec.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/contest/remote/exec.py b/contest/remote/exec.py index 2799674..c3ff4ca 100755 --- a/contest/remote/exec.py +++ b/contest/remote/exec.py @@ -6,6 +6,7 @@ import os import subprocess +from core import NipaLifetime from lib import Fetcher @@ -81,13 +82,18 @@ def main() -> None: base_dir = config.get('local', 'base_path') + life = NipaLifetime(config) + f = Fetcher(test, config, name=config.get('executor', 'name'), branches_url=config.get('remote', 'branches'), results_path=os.path.join(base_dir, config.get('local', 'json_path')), url_path=config.get('www', 'url') + '/' + config.get('local', 'json_path'), - tree_path=config.get('local', 'tree_path')) + tree_path=config.get('local', 'tree_path'), + life=life, + first_run=config.get('executor', 'init', fallback="continue")) f.run() + life.exit() if __name__ == "__main__": From 21d189b1927cf5ddd307cb9fdf10db5a122efe67 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 8 Feb 2024 08:06:03 -0800 Subject: [PATCH 033/429] contest: vmksft-p: support loadavg based waits We have a bit of a thundering herd problem with all testers running builds when branch appears and consuming 100% of CPU. This makes non-slow runners appear slow. Support waiting for loadavg to drop before we start tests. Signed-off-by: Jakub Kicinski --- contest/remote/lib/__init__.py | 1 + contest/remote/lib/loadavg.py | 15 +++++++++++++++ contest/remote/vmksft-p.py | 5 +++++ 3 files changed, 21 insertions(+) create mode 100644 contest/remote/lib/loadavg.py diff --git a/contest/remote/lib/__init__.py b/contest/remote/lib/__init__.py index 2067e09..8964b8a 100644 --- a/contest/remote/lib/__init__.py +++ b/contest/remote/lib/__init__.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: GPL-2.0 from .fetcher import Fetcher +from .loadavg import wait_loadavg from .vm import VM, new_vm, guess_indicators from .cbarg import CbArg diff --git a/contest/remote/lib/loadavg.py b/contest/remote/lib/loadavg.py new file mode 100644 index 0000000..7600e7d --- /dev/null +++ b/contest/remote/lib/loadavg.py @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: GPL-2.0 + +import os +import time + + +def wait_loadavg(target, check_ival=30): + while target is not None: + load, _, _ = os.getloadavg() + + if load <= target: + break + + print(f"Waiting for loadavg to decrease: {load} > {target}") + time.sleep(check_ival) diff --git a/contest/remote/vmksft-p.py b/contest/remote/vmksft-p.py index 1adc29b..f67a8c9 100755 --- a/contest/remote/vmksft-p.py +++ b/contest/remote/vmksft-p.py @@ -13,6 +13,7 @@ import time from core import NipaLifetime +from lib import wait_loadavg from lib import CbArg from lib import Fetcher from lib import VM, new_vm, guess_indicators @@ -193,9 +194,13 @@ def test(binfo, rinfo, cbarg): i += 1 in_queue.put((i, prog, )) + # In case we have multiple tests kicking off on the same machine, + # add optional wait to make sure others have finished building + load_tgt = config.getfloat("cfg", "wait_loadavg", fallback=None) thr_cnt = int(config.get("cfg", "thread_cnt")) delay = float(config.get("cfg", "thread_spawn_delay", fallback=0)) for i in range(thr_cnt): + wait_loadavg(load_tgt) print("INFO: starting VM", i) threads.append(threading.Thread(target=vm_thread, args=[config, results_path, i, in_queue, out_queue])) From 8052d52b648fe21541c4cd5e7e33bcc92ef7c60b Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 8 Feb 2024 08:12:21 -0800 Subject: [PATCH 034/429] contest: cocci: decrease the number of jobs and wait for loadavg to die down Cocci consumes a ton of CPU, make it only use at most CPUs - 8 jobs. Make it wait until loadavg is below 8. Signed-off-by: Jakub Kicinski --- contest/tests/cocci-check.sh | 53 ++++++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/contest/tests/cocci-check.sh b/contest/tests/cocci-check.sh index a382e48..ae205e7 100755 --- a/contest/tests/cocci-check.sh +++ b/contest/tests/cocci-check.sh @@ -29,18 +29,48 @@ clean_up_output() { sed -i '/An error occurred when attempting /d' $file } +# Figure out the number of physical cores, save 8 or half for other stuff +THREADS_PER_CORE=$(LANG=C lscpu | grep "Thread(s) per core: " | tr -cd "[:digit:]") +NPROC=$(getconf _NPROCESSORS_ONLN) +JOBS=$((NPROC / THREADS_PER_CORE)) + +if [ $JOBS -gt 16 ]; then + JOBS=$((JOBS - 8)) +else + JOBS=$((JOBS / 2)) +fi + echo " === Start ===" echo "Base: $BASE" echo "Branch: $BRANCH ($branch_rev)" +echo "Jobs: $JOBS" +echo + +echo " === Waiting for loadavg to die down ===" +while true; do + # Sleep first to make sure others get a chance to start + sleep 120 + + load=$(cat /proc/loadavg | sed -e 's/\([0-9.]\) .*/\1/;s/\.//;s/^0*//') + [ $load -lt 800 ] && break +done + +echo "Starting at $(date)" echo +IGNORED=( scripts/coccinelle/misc/minmax.cocci ) +for ign_file in ${IGNORED[@]}; do + echo "Ignoring " $ign_file + mv $ign_file $ign_file.ignore +done + echo " === Checking the base tree ===" git checkout -q $BASE -make coccicheck MODE=report SPFLAGS="$SPFLAGS" > $out_o +make coccicheck MODE=report J=$JOBS SPFLAGS="$SPFLAGS" > $out_o echo " === Building the new tree ===" git checkout -q $BRANCH -make coccicheck MODE=report SPFLAGS="$SPFLAGS" > $out_n +make coccicheck MODE=report J=$JOBS SPFLAGS="$SPFLAGS" > $out_n dirty=( $(grep -c . $out_o) $(grep -i -c "warn" $out_o) $(grep -i -c "error" $out_o) $(grep -c . $out_n) $(grep -i -c "warn" $out_n) $(grep -i -c "error" $out_n) @@ -73,6 +103,25 @@ elif [ ${current[0]} -gt ${incumbent[0]} ]; then rc=5 fi +if [ $rc -ne 0 ]; then + echo "Per-file breakdown" 1>&2 + tmpfile_fo=$(mktemp) + tmpfile_fn=$(mktemp) + + grep -i "^$PWD" $tmpfile_o | sed -n 's@\(^\.\./[/a-zA-Z0-9_.-]*.[ch]\):.*@\1@p' | sort | uniq -c \ + > $tmpfile_fo + grep -i "^$PWD" $tmpfile_n | sed -n 's@\(^\.\./[/a-zA-Z0-9_.-]*.[ch]\):.*@\1@p' | sort | uniq -c \ + > $tmpfile_fn + + diff -U 0 $tmpfile_fo $tmpfile_fn 1>&2 + rm $tmpfile_fo $tmpfile_fn +fi + +for ign_file in ${IGNORED[@]}; do + echo "Un-ignoring " $ign_file + mv $ign_file.ignore $ign_file +done + echo echo " === Summary === " echo "Incumbent: ${incumbent[@]}" From 6aecd00f326cb27d8fa30400720ddc566636e2d2 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 8 Feb 2024 14:14:24 -0800 Subject: [PATCH 035/429] contest: ui: add link to repro wiki Signed-off-by: Jakub Kicinski --- contest/contest.html | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/contest/contest.html b/contest/contest.html index 42e7c33..c0740dc 100644 --- a/contest/contest.html +++ b/contest/contest.html @@ -33,12 +33,22 @@ } .column { - flex: 50%; + flex: max-content; padding: 1em; } + .box { + position: absolute; + right: 1em; + } + .box p { + border: 1px solid grey; + padding: 1em; + border-radius: 0.2em; + } +
Filtering: @@ -101,6 +111,11 @@ Loading...
+
From 303e75e5a43aa92c1e16fbc08ef6d7fcac648768 Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Thu, 8 Feb 2024 15:53:15 +0100 Subject: [PATCH 036/429] contest: vm: add missing doc for ld_paths The option was supported, but not documented. Signed-off-by: Matthieu Baerts (NGI0) --- contest/remote/lib/vm.py | 1 + 1 file changed, 1 insertion(+) diff --git a/contest/remote/lib/vm.py b/contest/remote/lib/vm.py index 7b71868..1469740 100644 --- a/contest/remote/lib/vm.py +++ b/contest/remote/lib/vm.py @@ -29,6 +29,7 @@ paths=/extra/exec/PATH:/another/bin [vm] paths=/extra/exec/PATH:/another/bin +ld_paths=/extra/lib/PATH:/another/lib configs=relative/path/config,another/config init_prompt=expected_on-boot# virtme_opt=--opt,--another one From e652354ceb35728e8a2798dcef2e4a7df0685b70 Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Thu, 8 Feb 2024 15:55:33 +0100 Subject: [PATCH 037/429] contest: vm: add missing doc for exports The option was supported, but not documented. Signed-off-by: Matthieu Baerts (NGI0) --- contest/remote/lib/vm.py | 1 + 1 file changed, 1 insertion(+) diff --git a/contest/remote/lib/vm.py b/contest/remote/lib/vm.py index 1469740..ab845c5 100644 --- a/contest/remote/lib/vm.py +++ b/contest/remote/lib/vm.py @@ -30,6 +30,7 @@ [vm] paths=/extra/exec/PATH:/another/bin ld_paths=/extra/lib/PATH:/another/lib +exports=VAR1=val1,VAR2=val2 configs=relative/path/config,another/config init_prompt=expected_on-boot# virtme_opt=--opt,--another one From ea0b029a530038e9438b52e537126a20cc0b95b6 Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Thu, 8 Feb 2024 16:02:20 +0100 Subject: [PATCH 038/429] contest: vm: extract 'set env' code I'm going to add more code around, probably best to extract it from the 'start'. Signed-off-by: Matthieu Baerts (NGI0) --- contest/remote/lib/vm.py | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/contest/remote/lib/vm.py b/contest/remote/lib/vm.py index ab845c5..5b23722 100644 --- a/contest/remote/lib/vm.py +++ b/contest/remote/lib/vm.py @@ -112,6 +112,25 @@ def build(self, extra_configs, override_configs=None): self.tree_cmd("make mrproper") self.tree_cmd("vng -v -b" + " -f ".join([""] + configs)) + def _set_env(self): + # Install extra PATHs + if self.config.get('vm', 'paths', fallback=None): + self.cmd("export PATH=" + self.config.get('vm', 'paths') + ':$PATH') + self.drain_to_prompt() + + if self.config.get('vm', 'ld_paths', fallback=None): + self.cmd("export LD_LIBRARY_PATH=" + self.config.get('vm', 'ld_paths') + ':$LD_LIBRARY_PATH') + self.drain_to_prompt() + + exports = self.config.get('vm', 'exports', fallback=None) + if exports: + for export in exports.split(','): + self.cmd("export " + export) + self.drain_to_prompt() + + self.cmd("env") + self.drain_to_prompt() + def start(self, cwd=None): cmd = "vng -v -r arch/x86/boot/bzImage --user root" cmd = cmd.split(' ') @@ -148,20 +167,7 @@ def start(self, cwd=None): self.cmd("PS1='xx__-> '") self.drain_to_prompt() - # Install extra PATHs - if self.config.get('vm', 'paths', fallback=None): - self.cmd("export PATH=" + self.config.get('vm', 'paths') + ':$PATH') - self.drain_to_prompt() - if self.config.get('vm', 'ld_paths', fallback=None): - self.cmd("export LD_LIBRARY_PATH=" + self.config.get('vm', 'ld_paths') + ':$LD_LIBRARY_PATH') - self.drain_to_prompt() - exports = self.config.get('vm', 'exports', fallback=None) - if exports: - for export in exports.split(','): - self.cmd("export " + export) - self.drain_to_prompt() - self.cmd("env") - self.drain_to_prompt() + self._set_env() def stop(self): self.cmd("exit") From e8c5ca97f1144e218e8158e52e6022553ab9eb94 Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Thu, 8 Feb 2024 17:12:01 +0100 Subject: [PATCH 039/429] contest: vm: 'slowdown' option to increase timeout When this option is set, two different env vars are set: - KSFT_MACHINE_SLOW: some tests check if the env var is defined (no matter the value it is set to), and are then more tolerant with issues. No need to set this env var in the 'exports' option everywhere needed then. - kselftest_override_timeout: the kselftest timeout is multiplied by the float value linked to this new 'slowdown' option. The kselftest's 'settings' file is read to get the custom timeout option. If not set, the default timeout of 45 seconds is used. Increasing the timeout is useful when the environment is "abnormally" slow, e.g. when using a bunch of debug kconfig. This is probably better to override the timeout instead of setting a huge value supporting such envs, not to have a too high value for "normal" environments, loosing the interest of "quickly" stopping a stalled test. Note that the _get_ksft_timeout() method could maybe be moved to vmksft. I didn't do it as it is not clear to me if vmksft-p will be merged with vmksft or if the latter will be removed. Anyway, this ksft specific code is in a separated method. So if we want to move stuff, it should not be too hard. Signed-off-by: Matthieu Baerts (NGI0) --- contest/remote/lib/vm.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/contest/remote/lib/vm.py b/contest/remote/lib/vm.py index 5b23722..d1b9f22 100644 --- a/contest/remote/lib/vm.py +++ b/contest/remote/lib/vm.py @@ -36,6 +36,7 @@ virtme_opt=--opt,--another one default_timeout=15 boot_timeout=45 +slowdown=2.5 # mark the machine as slow and multiply the ksft timeout by 2.5 """ @@ -112,6 +113,26 @@ def build(self, extra_configs, override_configs=None): self.tree_cmd("make mrproper") self.tree_cmd("vng -v -b" + " -f ".join([""] + configs)) + def _get_ksft_timeout(self): + default_timeout = 45 # from tools/testing/selftests/kselftest/runner.sh + + target = self.config.get('ksft', 'target', fallback=None) + tree_path = self.config.get('local', 'tree_path', fallback=None) + if not target or not tree_path: + return default_timeout + + settings_path = f'{tree_path}/tools/testing/selftests/{target}/settings' + if not os.path.isfile(settings_path): + return default_timeout + + with open(settings_path, 'r') as fp: + lines = fp.readlines() + for l in lines: + if l.startswith('timeout='): + return int(l.split('=')[1]) + + return default_timeout + def _set_env(self): # Install extra PATHs if self.config.get('vm', 'paths', fallback=None): @@ -128,6 +149,17 @@ def _set_env(self): self.cmd("export " + export) self.drain_to_prompt() + slowdown = self.config.getfloat('vm', 'slowdown', fallback=0) + if slowdown: + self.cmd("export KSFT_MACHINE_SLOW=yes") + self.drain_to_prompt() + + # only when needed, to avoid 'overriding timeout' message + if slowdown > 1: + timeout = self._get_ksft_timeout() * slowdown + self.cmd(f"export kselftest_override_timeout={timeout}") + self.drain_to_prompt() + self.cmd("env") self.drain_to_prompt() From d9d62c429320d200ec961e4df0ae8d43c759f0d5 Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Fri, 9 Feb 2024 10:16:31 +0100 Subject: [PATCH 040/429] contest: vm: round timeout value Even if 'timeout' supports float numbers, it is strange to see ... overriding timeout to 3600.0 ... in the logs. Signed-off-by: Matthieu Baerts (NGI0) --- contest/remote/lib/vm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contest/remote/lib/vm.py b/contest/remote/lib/vm.py index d1b9f22..c91b8d5 100644 --- a/contest/remote/lib/vm.py +++ b/contest/remote/lib/vm.py @@ -157,7 +157,7 @@ def _set_env(self): # only when needed, to avoid 'overriding timeout' message if slowdown > 1: timeout = self._get_ksft_timeout() * slowdown - self.cmd(f"export kselftest_override_timeout={timeout}") + self.cmd(f"export kselftest_override_timeout={round(timeout)}") self.drain_to_prompt() self.cmd("env") From 12fc2e13bc59b060e2d5f880347cdc11dc484ccf Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Tue, 6 Feb 2024 17:43:49 +0100 Subject: [PATCH 041/429] contest: vmksft: drop dash from test name This dash is optional in TAP 13/14 specs: TestPoint := ("not ")? "ok" (" " Number)? ((" -")? (" " Description) )? (" " Directive)? (...) So instead of having a name like: "- mptcp_join: delete and re-add" It is now: "mptcp_join: delete and re-add" Link: https://testanything.org/tap-version-14-specification.html Signed-off-by: Matthieu Baerts (NGI0) --- contest/remote/vmksft.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contest/remote/vmksft.py b/contest/remote/vmksft.py index ced414a..412ad13 100755 --- a/contest/remote/vmksft.py +++ b/contest/remote/vmksft.py @@ -56,7 +56,7 @@ def ktap_split(full_run): test = None test_id = 0 - result_re = re.compile(r"(not )?ok (\d+) ([^#]*[^ ])( # )?([^ ].*)?$") + result_re = re.compile(r"(not )?ok (\d+)( -)? ([^#]*[^ ])( # )?([^ ].*)?$") for line in full_run.split('\n'): if test is None: @@ -82,10 +82,10 @@ def ktap_split(full_run): v = result_re.match(line).groups() test["output"] = "\n".join(test["output"]) test["sid"] = int(v[1]) - test["name"] = v[2] - if len(v) > 4: - test["comment"] = v[4] - if v[4] == "SKIP" and test["result"] == "pass": + test["name"] = v[3] + if len(v) > 5: + test["comment"] = v[5] + if v[5] == "SKIP" and test["result"] == "pass": test["result"] = "skip" tests.append(test) test = None From a51de8826acc93f764cc612c5f53a28161bd2467 Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Tue, 6 Feb 2024 18:53:22 +0100 Subject: [PATCH 042/429] contest: vmksft: support parsing nested tests Some selftests are executing a lot of different subtests. On the other hand, the kselftests infrastructure only supports TAP 13 format, which doesn't support subtests. If one subtest fails, the whole selftest is marked as failed. It starts to be really annoying when one subtest starts to be unstable and marked as "ignored": all the other subtests are then ignored as well. It is then important to parse subtests to be able to track each subtest individually, and not loose info about the others when one is unstable. A workaround to support subtests with TAP 13 is to embed the subtests result in the comments. This is what is done with MPTCP selftests. That's not specific to MPTCP, because TC and others written in C and using kselftest_harness.h are also doing that. Because it will increase the number of tests, there is a new option, disabled by default, to enable this feature or not per target: [ksft] nested_tests = on When nested TAP results are detected, new tests are being created on top of the main/parent one. At the end of the comments, the parsing continue on the main/parent test. Note that the 'output' of these subtests will not be duplicated in the main test and the subtests ones: what is specific to the subtests will only be in the 'output' of the subtests, not the main one. Signed-off-by: Matthieu Baerts (NGI0) --- contest/remote/vmksft.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/contest/remote/vmksft.py b/contest/remote/vmksft.py index 412ad13..1e54f32 100755 --- a/contest/remote/vmksft.py +++ b/contest/remote/vmksft.py @@ -42,6 +42,7 @@ boot_timeout=45 [ksft] targets=net +nested_tests=off / on Expected: @@ -51,14 +52,28 @@ """ -def ktap_split(full_run): +def ktap_split(full_run, parse_nested_tests): tests = [] test = None test_id = 0 + test_main = None result_re = re.compile(r"(not )?ok (\d+)( -)? ([^#]*[^ ])( # )?([^ ].*)?$") for line in full_run.split('\n'): + if parse_nested_tests: + # nested tests support: we parse the comments from 'TAP version' + if test_main: + if line.startswith("# "): + line = line[2:] + else: + # back to the main test + test = test_main + test_main = None + elif line.startswith("# TAP version "): + test_main = test + test = None + if test is None: test = { "tid": test_id, @@ -168,7 +183,9 @@ def test(binfo, rinfo, cbarg): full_run = vm.log_out vm.dump_log(results_path + '/full', result=retcode, info={"vm_state": vm.fail_state}) - tests = ktap_split(full_run) + parse_nested_tests = config.getboolean('ksft', 'nested_tests', + fallback=False) + tests = ktap_split(full_run, parse_nested_tests) if tests: pfx = ktap_extract_pfx(tests) grp_name = namify(pfx) From 343403f48da4ab1950654eab3286c4e8a03bf35d Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Thu, 8 Feb 2024 20:14:28 +0100 Subject: [PATCH 043/429] contest: vmksft-p: support parsing nested tests Some selftests are executing a lot of different subtests. On the other hand, the kselftests infrastructure only supports TAP 13 format, which doesn't support subtests. If one subtest fails, the whole selftest is marked as failed. It starts to be really annoying when one subtest starts to be unstable and marked as "ignored": all the other subtests are then ignored as well. It is then important to parse subtests to be able to track each subtest individually, and not loose info about the others when one is unstable. A workaround to support subtests with TAP 13 is to embed the subtests result in the comments. This is what is done with MPTCP selftests. That's not specific to MPTCP, because TC and others written in C and using kselftest_harness.h are also doing that. The output to parse looks like that: TAP version 13 1..1 # timeout set to (...) # selftests: (...) # # (...) # # # TAP version 13 | # 1..X | # ok 1 (...) | # nok 2 (...) | <== That's what we want to parse # ok 3 (...) # SKIP | # (...) | # ok X (...) | ok 1 selftests: {target}: {prog} The last line is not parsed, this is handled by the existing code, also checking for VM crash, etc. This means that in case of crash, the kselftest will be marked as 'fail', but subtests (if any) will be parsed: the ones previous to the crash are still valid. It might help to know what was OK, and what was not. Because it will increase the number of tests, there is a new option, disabled by default, to enable this feature or not per target, where it makes sense: [ksft] nested_tests = on Signed-off-by: Matthieu Baerts (NGI0) --- contest/remote/vmksft-p.py | 48 +++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/contest/remote/vmksft-p.py b/contest/remote/vmksft-p.py index f67a8c9..9a97335 100755 --- a/contest/remote/vmksft-p.py +++ b/contest/remote/vmksft-p.py @@ -47,6 +47,7 @@ boot_timeout=45 [ksft] targets=net +nested_tests=off / on Expected: @@ -73,6 +74,42 @@ def get_prog_list(vm, target): return [e.split(":")[1].strip() for e in targets] +def _parse_nested_tests(full_run): + tests = [] + nested_tests = False + + result_re = re.compile(r"(not )?ok (\d+)( -)? ([^#]*[^ ])( # )?([^ ].*)?$") + + for line in full_run.split('\n'): + # nested subtests support: we parse the comments from 'TAP version' + if nested_tests: + if line.startswith("# "): + line = line[2:] + else: + nested_tests = False + elif line.startswith("# TAP version "): + nested_tests = True + continue + + if not nested_tests: + continue + + if line.startswith("ok "): + result = "pass" + elif line.startswith("not ok "): + result = "fail" + else: + continue + + v = result_re.match(line).groups() + name = v[3] + if len(v) > 5 and v[5]: + if v[5].lower().startswith('skip') and result == "pass": + result = "skip" + tests.append((name, result)) + + return tests + def _vm_thread(config, results_path, thr_id, in_queue, out_queue): target = config.get('ksft', 'target') vm = None @@ -136,6 +173,14 @@ def _vm_thread(config, results_path, thr_id, in_queue, out_queue): out_queue.put({'prog': prog, 'test': test_name, 'file_name': file_name, 'result': result, 'time': (t2 - t1).seconds}) + if config.getboolean('ksft', 'nested_tests', fallback=False): + # this will only parse nested tests inside the TAP comments + tests = _parse_nested_tests(vm.log_out) + + for r_name, r_result in tests: + out_queue.put({'prog': prog, 'test': namify(r_name), + 'file_name': file_name, 'result': r_result}) + if vm.fail_state: print(f"INFO: thr-{thr_id} VM kernel crashed, destroying it") vm.stop() @@ -214,7 +259,8 @@ def test(binfo, rinfo, cbarg): cases = [] while not out_queue.empty(): r = out_queue.get() - cbarg.prev_runtime[r["prog"]] = r["time"] + if 'time' in r: + cbarg.prev_runtime[r["prog"]] = r["time"] cases.append({'test': r['test'], 'group': grp_name, 'result': r["result"], 'link': link + '/' + r['file_name']}) if not in_queue.empty(): From bfbd1353c3b7a7e60c07e0501e73f03662e07512 Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Fri, 9 Feb 2024 18:22:58 +0100 Subject: [PATCH 044/429] contest: vmksft-p: dump logs after subtests parsing 'vm.dump_log()' will clear 'vm.log_out', used to parse nested tests. To fix that, simply move vm.dump_log() call after the nested tests parsing. It looks find to do that at the end. While at it, also add a log message to mentioned the number of subtests that have been parsed. Signed-off-by: Matthieu Baerts (NGI0) --- contest/remote/vmksft-p.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/contest/remote/vmksft-p.py b/contest/remote/vmksft-p.py index 9a97335..170bfce 100755 --- a/contest/remote/vmksft-p.py +++ b/contest/remote/vmksft-p.py @@ -164,9 +164,6 @@ def _vm_thread(config, results_path, thr_id, in_queue, out_queue): # check VM is still in failed state if vm.fail_state: result = "fail" - vm.dump_log(results_path + '/' + file_name, result=retcode, - info={"thr-id": thr_id, "vm-id": vm_id, "time": (t2 - t1).seconds, - "found": indicators, "vm_state": vm.fail_state}) print(f"INFO: thr-{thr_id} {prog} >> retcode:", retcode, "result:", result, "found", indicators) @@ -181,6 +178,12 @@ def _vm_thread(config, results_path, thr_id, in_queue, out_queue): out_queue.put({'prog': prog, 'test': namify(r_name), 'file_name': file_name, 'result': r_result}) + print(f"INFO: thr-{thr_id} {prog} >> nested tests: {len(tests)} subtests") + + vm.dump_log(results_path + '/' + file_name, result=retcode, + info={"thr-id": thr_id, "vm-id": vm_id, "time": (t2 - t1).seconds, + "found": indicators, "vm_state": vm.fail_state}) + if vm.fail_state: print(f"INFO: thr-{thr_id} VM kernel crashed, destroying it") vm.stop() From 290b9b05022f78e97c789d658f703c205e6b1045 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 8 Feb 2024 15:13:52 -0800 Subject: [PATCH 045/429] contest: vm: auto-lower timeout to 5min if we see a crash net and forwarding need very long timeouts, if a bad change gets in we end up waiting until timeout in multiple tests. Most likely for hours. Signed-off-by: Jakub Kicinski --- contest/remote/lib/vm.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/contest/remote/lib/vm.py b/contest/remote/lib/vm.py index c91b8d5..4e60a0f 100644 --- a/contest/remote/lib/vm.py +++ b/contest/remote/lib/vm.py @@ -261,8 +261,9 @@ def _read_pipe_nonblock(self, pipe): return read_some, output def drain_to_prompt(self, prompt="xx__-> ", dump_after=None): + _dump_after = dump_after if dump_after is None: - dump_after = int(self.config.get('vm', 'default_timeout')) + dump_after = self.config.getint('vm', 'default_timeout') hard_stop = int(self.config.get('vm', 'hard_timeout', fallback=(1 << 63))) waited = 0 @@ -281,6 +282,10 @@ def drain_to_prompt(self, prompt="xx__-> ", dump_after=None): if read_some: if stdout.endswith(prompt): break + if self.fail_state == "oops" and _dump_after is None and dump_after > 300: + dump_after = 300 + self.log_out += '\nDETECTED CRASH, lowering timeout\n' + # A bit of a hack, sometimes kernel spew will clobber # the prompt. Until we have a good way of sending kernel # logs elsewhere try to get a new prompt by sending a new line. From 1438bd9341841fde8bd6204a9cbb967b3e56afd3 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 9 Feb 2024 09:48:42 -0800 Subject: [PATCH 046/429] contest: ui: centralize and redesign the UI "pw reporting" logic Move the result filtering to nipa.js. Switch to using a new "ignore-results" table in the JSON file, which allows filtering on any number of fields. Note that the actual PW reporter (the Python service) is not changed yet! It will be changed once the flakes are shaken out and when time allows... Signed-off-by: Jakub Kicinski --- contest/contest.js | 36 +++--------------------------------- contest/flakes.js | 36 +++--------------------------------- contest/nipa.js | 29 +++++++++++++++++++++++++++++ status.html | 3 ++- status.js | 24 +++--------------------- 5 files changed, 40 insertions(+), 88 deletions(-) diff --git a/contest/contest.js b/contest/contest.js index 8cacd02..8a0151f 100644 --- a/contest/contest.js +++ b/contest/contest.js @@ -10,35 +10,6 @@ function colorify_str(value) return ret + value + ''; } -function pw_filter_r(v, r, drop_reported) -{ - if (loaded_filters == null) - return false; - - var reported_exec = false; - for (const exec of loaded_filters.executors) { - if (v.executor == exec) { - reported_exec = true; - break; - } - } - - if (reported_exec == false && drop_reported == true) - return false; - - var reported_test = true; - for (const test of loaded_filters["ignore-tests"]) { - if (r.group == test.group && r.test == test.test) { - reported_test = false; - break; - } - } - if ((reported_test && reported_exec) == drop_reported) - return true; - - return false; -} - function load_result_table(data_raw) { var table = document.getElementById("results"); @@ -83,9 +54,9 @@ function load_result_table(data_raw) return 1; if (result_filter[r.result] == false) return 1; - if (pw_y == false && pw_filter_r(v, r, true)) + if (pw_y == false && nipa_pw_reported(v, r) == true) return 1; - if (pw_n == false && pw_filter_r(v, r, false)) + if (pw_n == false && nipa_pw_reported(v, r) == false) return 1; var row = table.insertRow(); @@ -148,7 +119,6 @@ function results_update() let xfr_todo = 2; let branch_urls = {}; let loaded_data = null; -let loaded_filters = null; function loaded_one() { @@ -168,7 +138,7 @@ function loaded_one() function filters_loaded(data_raw) { - loaded_filters = data_raw; + nipa_set_filters_json(data_raw); loaded_one(); } diff --git a/contest/flakes.js b/contest/flakes.js index 878b53b..2bde51b 100644 --- a/contest/flakes.js +++ b/contest/flakes.js @@ -12,35 +12,6 @@ function colorify(cell, value) cell.setAttribute("style", ret); } -function pw_filter_r(v, r, drop_reported) -{ - if (loaded_filters == null) - return false; - - var reported_exec = false; - for (const exec of loaded_filters.executors) { - if (v.executor == exec) { - reported_exec = true; - break; - } - } - - if (reported_exec == false && drop_reported == true) - return false; - - var reported_test = true; - for (const test of loaded_filters["ignore-tests"]) { - if (r.group == test.group && r.test == test.test) { - reported_test = false; - break; - } - } - if ((reported_test && reported_exec) == drop_reported) - return true; - - return false; -} - function get_sort_key() { if (document.getElementById("sort-streak").checked) @@ -67,9 +38,9 @@ function load_result_table(data_raw) $.each(data_raw, function(i, v) { $.each(v.results, function(j, r) { - if (pw_y == false && pw_filter_r(v, r, true)) + if (pw_y == false && nipa_pw_reported(v, r) == true) return 1; - if (pw_n == false && pw_filter_r(v, r, false)) + if (pw_n == false && nipa_pw_reported(v, r) == false) return 1; const tn = v.remote + '/' + r.group + '/' + r.test; @@ -157,7 +128,6 @@ function results_update() let xfr_todo = 2; let loaded_data = null; -let loaded_filters = null; function loaded_one() { @@ -172,7 +142,7 @@ function loaded_one() function filters_loaded(data_raw) { - loaded_filters = data_raw; + nipa_set_filters_json(data_raw); loaded_one(); } diff --git a/contest/nipa.js b/contest/nipa.js index 64d3dd5..fe00773 100644 --- a/contest/nipa.js +++ b/contest/nipa.js @@ -31,3 +31,32 @@ function nipa_filters_set_from_url() } } } + +// ------------------ + +let nipa_filters_json = null; + +function nipa_set_filters_json(filters_json) +{ + nipa_filters_json = filters_json; +} + +// v == result info, r == particular result / test case +function nipa_pw_reported(v, r) +{ + for (const filter of nipa_filters_json["ignore-results"]) { + if (!("remote" in filter) || filter.remote == v.remote) { + if (!("executor" in filter) || filter.executor == v.executor) { + if (!("branch" in filter) || filter.branch == v.branch) { + if (!("group" in filter) || filter.group == r.group) { + if (!("test" in filter) || filter.test == r.test) { + return false; + } + } + } + } + } + } + + return true; +} diff --git a/status.html b/status.html index 9997fe0..d4c7dbd 100644 --- a/status.html +++ b/status.html @@ -7,6 +7,7 @@ + - +

diff --git a/contest/contest.html b/contest/contest.html index f050f96..389f91a 100644 --- a/contest/contest.html +++ b/contest/contest.html @@ -12,53 +12,12 @@ + +
diff --git a/contest/flakes.html b/contest/flakes.html index 2c7c411..2f59925 100644 --- a/contest/flakes.html +++ b/contest/flakes.html @@ -12,48 +12,12 @@ + +
diff --git a/nipa.css b/nipa.css new file mode 100644 index 0000000..5da0f0b --- /dev/null +++ b/nipa.css @@ -0,0 +1,64 @@ +table { + font-family: arial, sans-serif; + border-collapse: collapse; + width: 100%; +} + +td, th { + border: 1px solid #eeeeee; + text-align: left; + padding: 8px; +} + +tr:nth-child(even) { + background-color: #eeeeee; +} + +.row { + display: flex; +} + +.column { + flex: 50%; + padding: 1em; +} + +.box { + position: absolute; + right: 1em; +} + +.box p { + border: 1px solid grey; + padding: 1em; + border-radius: 0.2em; +} + +#contest-filters { + margin: 1em; + padding: 1em; + border: solid grey 1px; +} + +#flake-link { + margin: 1em; +} + +@media (prefers-color-scheme: dark) { + body { + color: #b8b8b8; + background: #202020; + } + a { + color: #809fff; + } + tr, th, td { + border-color: #181818; + } + tr:nth-child(even) { + background-color: #282828; + } + tr:nth-child(odd) { + background-color: #303030; + } +} diff --git a/status.html b/status.html index 3bce3eb..5fa090e 100644 --- a/status.html +++ b/status.html @@ -12,62 +12,18 @@ + +
From 59d1b6c83e6d87919122e3b54feaacd3170eb475 Mon Sep 17 00:00:00 2001 From: Pedro Tammela Date: Wed, 27 Mar 2024 15:27:56 -0400 Subject: [PATCH 086/429] vm: return build status on build() Returns true on build success false on build failure Signed-off-by: Pedro Tammela --- contest/remote/lib/vm.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/contest/remote/lib/vm.py b/contest/remote/lib/vm.py index 8fb9f07..fb0af0a 100644 --- a/contest/remote/lib/vm.py +++ b/contest/remote/lib/vm.py @@ -95,6 +95,7 @@ def tree_cmd(self, cmd): self.log_err += stderr.decode("utf-8", "ignore") proc.stdout.close() proc.stderr.close() + return proc.returncode def build(self, extra_configs, override_configs=None): if self.log_out or self.log_err: @@ -111,7 +112,13 @@ def build(self, extra_configs, override_configs=None): print(f"INFO{self.print_pfx} building kernel") # Make sure we rebuild, config and module deps can be stale otherwise self.tree_cmd("make mrproper") - self.tree_cmd("vng -v -b" + " -f ".join([""] + configs)) + + rc = self.tree_cmd("vng -v -b" + " -f ".join([""] + configs)) + if rc != 0: + print(f"INFO{self.print_pfx} kernel build failed") + return False + + return True def _get_ksft_timeout(self): default_timeout = 45 # from tools/testing/selftests/kselftest/runner.sh From 43c46f7b784465786ec2d9886395bd69aaacd821 Mon Sep 17 00:00:00 2001 From: Pedro Tammela Date: Wed, 27 Mar 2024 15:30:04 -0400 Subject: [PATCH 087/429] vmksft-p: report build failures On a build failure return a meaningful result field instead of an empty one: "results": [ { "test": "build", "group": "selftests-tc-testing", "result": "fail", "link": "" } ], Which in turn gets reported correctly in the status dashboard. Signed-off-by: Pedro Tammela --- contest/remote/vmksft-p.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/contest/remote/vmksft-p.py b/contest/remote/vmksft-p.py index f30f167..40ab078 100755 --- a/contest/remote/vmksft-p.py +++ b/contest/remote/vmksft-p.py @@ -244,9 +244,18 @@ def test(binfo, rinfo, cbarg): rinfo['run-cookie'] rinfo['link'] = link target = config.get('ksft', 'target') + grp_name = "selftests-" + namify(target) vm = VM(config) - vm.build([f"tools/testing/selftests/{target}/config"]) + + if vm.build([f"tools/testing/selftests/{target}/config"]) == False: + return [{ + 'test': 'build', + 'group': grp_name, + 'result': 'fail', + 'link': '', + }] + shutil.copy(os.path.join(config.get('local', 'tree_path'), '.config'), results_path + '/config') vm.tree_cmd("make headers") @@ -286,7 +295,6 @@ def test(binfo, rinfo, cbarg): for i in range(thr_cnt): threads[i].join() - grp_name = "selftests-" + namify(target) cases = [] while not out_queue.empty(): r = out_queue.get() From 1b9c21a0f7cec34ce007cc15d96dd75dc7809b1d Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 28 Mar 2024 12:22:19 -0700 Subject: [PATCH 088/429] ui: set canvas background color to stand out Signed-off-by: Jakub Kicinski --- nipa.css | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nipa.css b/nipa.css index 5da0f0b..97ac84a 100644 --- a/nipa.css +++ b/nipa.css @@ -49,6 +49,9 @@ tr:nth-child(even) { color: #b8b8b8; background: #202020; } + canvas { + background-color: #303030; + } a { color: #809fff; } From 1e022a2cfbfebaf735dbbd7074ba9e473a9c41a8 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 28 Mar 2024 13:18:25 -0700 Subject: [PATCH 089/429] contest: gh: add a very simple GH connector Add a runner which pushes to a remote and expects GH to run some actions. The expectations is that there will be a permanently open PR from the branch to which we are pushing, and the push will make GH run actions. Which we can then fetch back. Signed-off-by: Jakub Kicinski --- contest/remote/gh.py | 198 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100755 contest/remote/gh.py diff --git a/contest/remote/gh.py b/contest/remote/gh.py new file mode 100755 index 0000000..0e428d3 --- /dev/null +++ b/contest/remote/gh.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 + +import configparser +import datetime +import os +import requests +import subprocess +import sys +import time + +from core import NipaLifetime +from lib import Fetcher, CbArg + +""" +[executor] +name= +group= +test= +[remote] +branches= +[local] +tree_path= +base_path= +results_path= +json_path= +[www] +url= + +[gh] +token=api-token +base=base/branch +link=https://full/link +out_remote=remote-name +out_branch=remote-branch +wait_first=secs-to-first-check +wait_poll=secs-between-rechecks +wait_max=secs-to-wait +[ci] +owner=gh-owner +repo=gh-repo +runs_ref=refs/pull/... +""" + +def get(url, token): + headers = {"Accept": "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + "Authorization": token} + return requests.get(url, headers=headers) + + +def get_results(config, cbarg, prev_run): + token = config.get('gh', 'token') + repo_url = f"https://api.github.com/repos/%7Bconfig.get('ci', 'owner')}/{config.get('ci', 'repo')}" + ref = config.get('ci', 'runs_ref') + + resp = get(repo_url + '/actions/runs', token) + runs = resp.json() + found = None + for run in runs.get('workflow_runs'): + if ref in [r['ref'] for r in run['referenced_workflows']]: + if found is None or found["id"] < run["id"]: + found = run + if found is None: + print("Run not found!") + return None + if prev_run == found["id"]: + print("Found old run:", prev_run) + return None + cbarg.prev_runid = found["id"] + + resp = get(repo_url + f'/actions/runs/{found["id"]}/jobs', token) + jobs = resp.json() + + if 'jobs' not in jobs: + print("bad jobs") + print(jobs) + return None + + decoder = { + 'success': 0, + 'skipped': 1, + None: 2, + 'failure': 3, + 'cancelled': 4, + 'unknown': 5, + } + encoder = { + 0: 'pass', + 1: 'pass', + 2: None, + 3: 'fail', + 4: 'fail', + 5: 'fail', + } + + result = -1 + for job in jobs["jobs"]: + c = job["conclusion"] + if job["conclusion"] in decoder: + result = max(result, decoder[c]) + else: + print("Unknown result:", c) + result = 5 + return encoder[result] + + +def test_run(binfo, rinfo, cbarg, config, start): + tree_path = config.get('local', 'tree_path') + base = config.get('gh', 'base') + + subprocess.run('git checkout ' + base, cwd=tree_path, shell=True, check=True) + res = subprocess.run('git merge ' + binfo['branch'], cwd=tree_path, shell=True) + if res.returncode != 0: + # If rerere fixed it, just commit + res = subprocess.run('git diff -s --exit-code', cwd=tree_path, shell=True) + if res.returncode != 0: + return 'skip' + subprocess.run('git commit --no-edit', cwd=tree_path, shell=True, check=True) + + out_remote = config.get('gh', 'out_remote') + out_branch = config.get('gh', 'out_branch') + + subprocess.run(f'git push -f {out_remote} HEAD:{out_branch}', + cwd=tree_path, shell=True, check=True) + + end = start + datetime.timedelta(seconds=config.getint('gh', 'wait_max')) + time.sleep(config.getint('gh', 'wait_first')) + + prev_runid = 0 + if hasattr(cbarg, "prev_runid"): + prev_runid = cbarg.prev_runid + + while datetime.datetime.now() < end: + res = get_results(config, cbarg, prev_runid) + if res: + print("Got result:", res) + return res + + print("Not completed, waiting") + time.sleep(config.getint('gh', 'wait_poll')) + + return 'skip' + + +def test(binfo, rinfo, cbarg): + start = datetime.datetime.now() + print("Run at", start) + + cbarg.refresh_config() + config = cbarg.config + + results_path = os.path.join(config.get('local', 'base_path'), + config.get('local', 'results_path'), + rinfo['run-cookie']) + os.makedirs(results_path) + + res = test_run(binfo, rinfo, cbarg, config, start) + + link = config.get('gh', 'link') + if hasattr(cbarg, "prev_runid"): + link = "https://github.com/" + \ + config.get('ci', 'owner') + "/" + \ + config.get('ci', 'repo') + "/" + \ + "actions/runs/" + str(cbarg.prev_runid) + + return [{'test': config.get('executor', 'test'), + 'group': config.get('executor', 'group'), + 'result': res, 'link': link}] + + +def main() -> None: + cfg_paths = ['remote.config', 'gh.config'] + if len(sys.argv) > 1: + cfg_paths += sys.argv[1:] + + cbarg = CbArg(cfg_paths) + config = cbarg.config + + base_dir = config.get('local', 'base_path') + + life = NipaLifetime(config) + + f = Fetcher(test, cbarg, + name=config.get('executor', 'name'), + branches_url=config.get('remote', 'branches'), + results_path=os.path.join(base_dir, config.get('local', 'json_path')), + url_path=config.get('www', 'url') + '/' + config.get('local', 'json_path'), + tree_path=config.get('local', 'tree_path'), + patches_path=config.get('local', 'patches_path', fallback=None), + life=life, + first_run=config.get('executor', 'init', fallback="continue")) + f.run() + life.exit() + + +if __name__ == "__main__": + main() From 1542aac32260ac57bbc87dae45778cc7bf61872d Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 28 Mar 2024 14:40:30 -0700 Subject: [PATCH 090/429] contest: build-doc: make a copy of the latest output Sometimes during review it's useful to look at the output. The full output is quite large so make just one copy in the result dir. Signed-off-by: Jakub Kicinski --- contest/tests/build-doc.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/contest/tests/build-doc.sh b/contest/tests/build-doc.sh index f200027..323b473 100755 --- a/contest/tests/build-doc.sh +++ b/contest/tests/build-doc.sh @@ -32,6 +32,15 @@ if [ $current -gt $incumbent ]; then rc=1 fi +# Copy the latest outputs +if [ "x${RESULTS_DIR}" != x ]; then + OUTPUT_TMP=$(dirname ${RESULTS_DIR})/output + + rm -rf "${OUTPUT_TMP}" + mkdir -p "${OUTPUT_TMP}" + cp -r Documentation/output/* "${OUTPUT_TMP}"/ +fi + echo echo " === Summary === " echo "Incumbent: $incumbent" From 19237e7ab48a0c28db6e92513e93678479e900a5 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 29 Mar 2024 07:38:08 -0700 Subject: [PATCH 091/429] ui: collapse processing times onto one canvas Since support for multiple build workers was added build processing times are much less of a concern. There's no need to let them take up the entire top row of the status page. I usually look at what the workers are busy with and their queue length than the result latency. Combine the processing times to one graph, make it a scatter plot rather than line graph for clarity. Move the workers above it. Signed-off-by: Jakub Kicinski --- status.html | 34 ++++++++++++++-------------------- status.js | 31 +++++++++++++++++++++++-------- 2 files changed, 37 insertions(+), 28 deletions(-) diff --git a/status.html b/status.html index 5fa090e..7b54b25 100644 --- a/status.html +++ b/status.html @@ -28,26 +28,7 @@
-

Processing times (by check post time)

- -
-
-

Processing times (by patch post time)

- -
-
-
-
- - - - - - - - -
ServiceStatusTasksCPU coresMemory Use
-
+

Build processing

@@ -58,6 +39,18 @@

Processing times (by patch post time)

Tree Patch
+
+ +
+ + + + + + + + +
ServiceStatusTasksCPU coresMemory Use


@@ -81,6 +74,7 @@

Recent crashes

+

Continuous testing results

diff --git a/status.js b/status.js index edc3fae..b4dfba1 100644 --- a/status.js +++ b/status.js @@ -1,4 +1,4 @@ -function load_times(data, canva_id, patch_time) +function load_times_series(data, patch_time) { const minute = 1000 * 60; const hour = minute * 60; @@ -41,16 +41,32 @@ function load_times(data, canva_id, patch_time) // Sort by labels entries.sort(function(a, b){return a.l - b.l;}); + return entries; +} + +function load_times(data, canva_id) +{ + let e1, e2; + + e1 = load_times_series(data, true); + e2 = load_times_series(data, false); + const ctx = document.getElementById(canva_id); new Chart(ctx, { - type: 'line', + type: 'scatter', data: { - labels: entries.map(function(e){return e.l;}), + labels: e1.map(function(e){return e.l;}), datasets: [{ - tension: 0.1, - label: 'Patch age at check delivery', - data: entries.map(function(e){return e.v;}) + backgroundColor: "rgba(0, 0, 0, 0)", + pointBorderColor: "rgba(0, 64, 255, 0.7)", + label: 'Processing time by patch post time', + data: e1.map(function(e){return e.v;}) + }, { + backgroundColor: "rgba(0, 0, 0, 0)", + pointBorderColor: "rgba(255, 64, 0, 0.7)", + label: 'Processing time by check delivery time', + data: e2.map(function(e){return e.v;}) }] }, options: { @@ -100,8 +116,7 @@ function run_it(data_raw) data.push(v); }); - load_times(data, 'process-time', false); - load_times(data, 'process-time-p', true); + load_times(data, 'process-time'); } function colorify_str_any(value, color_map) From c7bdef32352b7cc2beab95e7258e5cfde6a7f4bf Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 3 Apr 2024 15:06:35 -0700 Subject: [PATCH 092/429] tests: kdoc: use -Wall People started complaining about lack of Return: statements. Signed-off-by: Jakub Kicinski --- tests/patch/kdoc/kdoc.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/patch/kdoc/kdoc.sh b/tests/patch/kdoc/kdoc.sh index 88c574c..0d75e96 100755 --- a/tests/patch/kdoc/kdoc.sh +++ b/tests/patch/kdoc/kdoc.sh @@ -14,14 +14,14 @@ HEAD=$(git rev-parse HEAD) echo "Checking the tree before the patch" git checkout -q HEAD~ -./scripts/kernel-doc -none $files 2> >(tee $tmpfile_o >&2) +./scripts/kernel-doc -Wall -none $files 2> >(tee $tmpfile_o >&2) incumbent=$(grep -v 'Error: Cannot open file ' $tmpfile_o | wc -l) echo "Checking the tree with the patch" git checkout -q $HEAD -./scripts/kernel-doc -none $files 2> >(tee $tmpfile_n >&2) +./scripts/kernel-doc -Wall -none $files 2> >(tee $tmpfile_n >&2) current=$(grep -v 'Error: Cannot open file ' $tmpfile_n | wc -l) From 92803b3f13324914c0a7ffeec837a4bc49eaa25a Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 11 Apr 2024 10:12:08 -0700 Subject: [PATCH 093/429] contest: try to strip meaningless part of crash fingerprint Most of the crashes (but not all) start with "dump_stack_lvl" which is obviously not useful. Support reading "skip prefix" lists from the filter file. This way we can strip the lockdep or KASAN internals from the report. Signed-off-by: Jakub Kicinski --- contest/remote/lib/vm.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/contest/remote/lib/vm.py b/contest/remote/lib/vm.py index fb0af0a..8874ec5 100644 --- a/contest/remote/lib/vm.py +++ b/contest/remote/lib/vm.py @@ -51,7 +51,7 @@ def decode_and_filter(buf): return "".join([x for x in buf if (x in ['\n'] or unicodedata.category(x)[0]!="C")]) -def crash_finger_print(lines): +def crash_finger_print(filters, lines): needles = [] need_re = re.compile(r'.*( |0:)([a-z0-9_]+)\+0x[0-9a-f]+/0x[0-9a-f]+.*') for line in lines: @@ -61,6 +61,15 @@ def crash_finger_print(lines): needles.append(m.groups()[1]) if len(needles) == 4: break + + # Filter may contain a list of needles we want to skip + # Assume it's well sorted, so we don't need LPM... + if filters and 'crash-prefix-skip' in filters: + for skip_pfx in filters['crash-prefix-skip']: + if needles[:len(skip_pfx)] == skip_pfx: + needles = needles[len(skip_pfx):] + break + return ":".join(needles) @@ -367,7 +376,8 @@ def extract_crash(self, out_path): in_crash &= '] ---[ end trace ' not in line in_crash &= '] ' not in line if not in_crash: - finger_prints.append(crash_finger_print(crash_lines[start:])) + finger_prints.append(crash_finger_print(self.filter_data, + crash_lines[start:])) else: in_crash |= '] Hardware name: ' in line if in_crash: From 6b5de8198bd1b43e8a515df519f2d6d555b1b17c Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sun, 14 Apr 2024 17:35:31 -0700 Subject: [PATCH 094/429] contest: results-fetcher: protect from badly encoded manifests Manifests are not updated atomically so the decode may occasionally fail. Return false, i.e. no update seen. Signed-off-by: Jakub Kicinski --- contest/results-fetcher.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/contest/results-fetcher.py b/contest/results-fetcher.py index 3f428bb..56e6a62 100755 --- a/contest/results-fetcher.py +++ b/contest/results-fetcher.py @@ -42,7 +42,11 @@ def fetch_remote_run(run_info, remote_state): def fetch_remote(remote, seen): print("Fetching remote", remote['url']) r = requests.get(remote['url']) - manifest = json.loads(r.content.decode('utf-8')) + try: + manifest = json.loads(r.content.decode('utf-8')) + except json.decoder.JSONDecodeError: + print('Failed to decode manifest from remote:', remote['name']) + return False remote_state = seen[remote['name']] fetched = False From 4cc6383e92282282f867a28b833d2a0062b02479 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 17 Apr 2024 16:20:47 -0700 Subject: [PATCH 095/429] series_format: fix cover letter in description We only check for pull cover not normal cover letter so the description ends up saying "(and no cover letter)" even if there was one. Signed-off-by: Jakub Kicinski --- tests/series/series_format/test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/series/series_format/test.py b/tests/series/series_format/test.py index 392d3a4..f5151fa 100644 --- a/tests/series/series_format/test.py +++ b/tests/series/series_format/test.py @@ -24,7 +24,9 @@ def patch_count(tree, thing, result_dir) -> Tuple[int, str]: if len(thing.patches) <= 15: return 0, "" if thing.cover_pull: - return 250, "Series longer than 15 patches" + return 250, "Series longer than 15 patches (PR)" + if thing.cover_letter: + return 1, "Series longer than 15 patches" # Really no good if there's no cover letter. return 1, "Series longer than 15 patches (and no cover letter)" From 377e31dfb9683fb50f778f8f5ac865c05b28a110 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 18 Apr 2024 06:52:43 -0700 Subject: [PATCH 096/429] core: protect git from parallel updates Git gets upset when we run parallel commands (even though they run on different work trees): error: cannot lock ref 'refs/remotes/net-next/main': is at 81b095[...] but expected 62d6d9[...] Add a lock to the tree. Signed-off-by: Jakub Kicinski --- core/tree.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/core/tree.py b/core/tree.py index 8cbb7e0..b5d6f60 100644 --- a/core/tree.py +++ b/core/tree.py @@ -4,6 +4,7 @@ """ The git tree module """ +import multiprocessing import os import tempfile from typing import List @@ -36,13 +37,18 @@ class Tree: Git tree class which controls a git tree """ def __init__(self, name, pfx, fspath, remote=None, branch=None, - wt_id=None): + wt_id=None, parent=None): self.name = name self.pfx = pfx self.path = os.path.abspath(fspath) self.remote = remote self.branch = branch + if parent: + self.lock = parent.lock + else: + self.lock = multiprocessing.RLock() + if remote and not branch: self.branch = remote + "/main" @@ -63,10 +69,14 @@ def work_tree(self, worker_id): new_name = self.name + f'-{worker_id}' return Tree(new_name, self.pfx, new_path, self.remote, self.branch, - wt_id=worker_id) + wt_id=worker_id, parent=self) def git(self, args: List[str]): - return CMD.cmd_run(["git"] + args, cwd=self.path) + self.lock.acquire(timeout=300) + try: + return CMD.cmd_run(["git"] + args, cwd=self.path) + finally: + self.lock.release() def git_am(self, patch): return self.git(["am", "-s", "--", patch]) From 33802ccf4db473035d2c95b7aa11d50b43d6deaf Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 18 Apr 2024 07:06:02 -0700 Subject: [PATCH 097/429] mailbot: fix kbuild reactions in BPF The current code doesn't work we get: ERROR: too many actions for un-authorized user Actions: pw-bot: changes-requested pw-bot: changes-requested We need to flush the actions. Signed-off-by: Jakub Kicinski --- mailbot.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mailbot.py b/mailbot.py index 205a0f2..b1d7a09 100755 --- a/mailbot.py +++ b/mailbot.py @@ -422,6 +422,11 @@ def extract_actions(self, pw): self.dr_act = [] self.pw_act = [] + def flush_actions(self): + self.actions = [] + self.dr_act = [] + self.pw_act = [] + # # PW stuff @@ -605,6 +610,7 @@ def do_mail_file(msg_path, pw, dr): do_mail(msg, pw, dr) except MlDelayActions as e: global delay_actions + msg.flush_actions() # avoid duplicates, actions will get re-parsed delay_actions.append((e.when, msg, )) From c29027cc7c9c632d487a20825955db9a2295e81f Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 18 Apr 2024 07:40:07 -0700 Subject: [PATCH 098/429] mailbot: support automatically setting series for other trees to Awaiting Upstream netdev gets patches for other trees, auto-discard those. Signed-off-by: Jakub Kicinski --- mailbot.py | 43 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/mailbot.py b/mailbot.py index b1d7a09..2be1756 100755 --- a/mailbot.py +++ b/mailbot.py @@ -29,6 +29,7 @@ maintainers = None authorized_users = set() auto_changes_requested = set() +auto_awaiting_upstream = set() delay_actions = [] # contains tuples of (datetime, email) @@ -314,6 +315,28 @@ def _resolve_authorized(self, pw): def user_bot(self): return self.msg.get('From') in auto_changes_requested + def auto_awaiting_upstream(self): + # Try to operate only on the first message in the thread + if self.get('References', ""): + return False + subject = self.get('Subject') + if subject[0] != '[': + return False + + tags_end = subject.rfind(']') + if tags_end == -1: + return False + tags = subject[1:tags_end] + + global auto_awaiting_upstream + for designation in auto_awaiting_upstream: + if designation in tags: + return True + return False + + def auto_actions(self): + return self.user_bot() or self.auto_awaiting_upstream() + def self_reply(self, pw): return self.get_thread_author(pw) == self.msg.get("From") @@ -370,7 +393,7 @@ def get_thread_author(self, pw): return self._series_author def has_actions(self): - if self.user_bot(): + if self.auto_actions(): return True body_str = self._body() @@ -401,6 +424,9 @@ def extract_actions(self, pw): self.actions.append(line) self.dr_act.append(line[8:].strip()) elif self.user_bot(): + self.actions.append('pw-bot: awaiting-upstream') + self.pw_act.append('awaiting-upstream') + elif self.auto_awaiting_upstream(): self.actions.append('pw-bot: changes-requested') self.pw_act.append('changes-requested') @@ -536,8 +562,8 @@ def do_mail(msg, pw, dr): series_id = msg.get_thread_series(pw) if not series_id: - print('', 'ERROR: could not find patchwork series') - return + print('', 'INFO: could not find patchwork series, retry in an hour') + raise MlDelayActions("not in PW", datetime.now() + datetime.timedelta(hours=1)) series = PwSeries(pw, series_id) patches = [p['id'] for p in series.patches] @@ -596,7 +622,7 @@ def do_mail_file(msg_path, pw, dr): print('', '', 'INFO: no actions, skip') return - if not msg.user_authorized(pw) and not msg.user_bot() and not msg.self_reply(pw): + if not msg.user_authorized(pw) and not msg.auto_actions() and not msg.self_reply(pw): print('', '', 'INFO: not an authorized user, skip') return print('', 'Authorized:', msg.user_authorized()) @@ -619,7 +645,7 @@ def do_mail_delayed(msg, pw, dr): print('', 'Subject:', msg.get('Subject')) print('', 'From:', msg.get('From')) - if not msg.user_authorized(pw) and not msg.user_bot(): + if not msg.user_authorized(pw) and not msg.auto_actions(): print('', '', 'INFO: not an authorized user, skip') return print('', 'Authorized:', msg.user_authorized()) @@ -632,8 +658,7 @@ def do_mail_delayed(msg, pw, dr): try: do_mail(msg, pw, dr) except MlDelayActions as e: - global delay_actions - delay_actions.append((e.when, msg, )) + print("ERROR: message delayed for the second time", str(e)) def check_new(tree, pw, dr): @@ -668,6 +693,10 @@ def main(): users = config.get('mailbot', 'error-bots') auto_changes_requested.update(set(users.split(','))) + global auto_awaiting_upstream + users = config.get('mailbot', 'awaiting-upstream') + auto_awaiting_upstream.update(set(users.split(','))) + tree_dir = config.get('dirs', 'trees', fallback=os.path.join(NIPA_DIR, "../")) mail_repos = {} for tree in config['mail-repos']: From 36271a3712ca549fa03668a8161f1125d26f5a9a Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 18 Apr 2024 11:57:03 -0700 Subject: [PATCH 099/429] mailbot: fix the cr vs au switcheroo Bots want changes requested, other trees want awaiting upstream. Signed-off-by: Jakub Kicinski --- mailbot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mailbot.py b/mailbot.py index 2be1756..bf03c77 100755 --- a/mailbot.py +++ b/mailbot.py @@ -423,10 +423,10 @@ def extract_actions(self, pw): elif line.startswith('doc-bot:'): self.actions.append(line) self.dr_act.append(line[8:].strip()) - elif self.user_bot(): + elif self.auto_awaiting_upstream(): self.actions.append('pw-bot: awaiting-upstream') self.pw_act.append('awaiting-upstream') - elif self.auto_awaiting_upstream(): + elif self.user_bot(): self.actions.append('pw-bot: changes-requested') self.pw_act.append('changes-requested') From 13f382aa1122b314418c1a3c2fc2fa6a0fe9f292 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 18 Apr 2024 11:59:01 -0700 Subject: [PATCH 100/429] mailbot: shorten the logs for no-action Vast majority of emails have no action, make the logs for that case less verbose. Signed-off-by: Jakub Kicinski --- mailbot.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mailbot.py b/mailbot.py index bf03c77..7c299cf 100755 --- a/mailbot.py +++ b/mailbot.py @@ -614,14 +614,14 @@ def do_mail(msg, pw, dr): def do_mail_file(msg_path, pw, dr): msg = MlEmail(msg_path) + if not msg.has_actions(): + print('INFO: no actions, skip:', msg.get('Message-ID')) + return + print('Message-ID:', msg.get('Message-ID')) print('', 'Subject:', msg.get('Subject')) print('', 'From:', msg.get('From')) - if not msg.has_actions(): - print('', '', 'INFO: no actions, skip') - return - if not msg.user_authorized(pw) and not msg.auto_actions() and not msg.self_reply(pw): print('', '', 'INFO: not an authorized user, skip') return From 3941e2bbff95db34f3aa61a3d46a829cc4f6ebd1 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 18 Apr 2024 12:30:48 -0700 Subject: [PATCH 101/429] mailbot: fix datetime.now() Signed-off-by: Jakub Kicinski --- mailbot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mailbot.py b/mailbot.py index 7c299cf..cff04af 100755 --- a/mailbot.py +++ b/mailbot.py @@ -563,7 +563,7 @@ def do_mail(msg, pw, dr): series_id = msg.get_thread_series(pw) if not series_id: print('', 'INFO: could not find patchwork series, retry in an hour') - raise MlDelayActions("not in PW", datetime.now() + datetime.timedelta(hours=1)) + raise MlDelayActions("not in PW", datetime.datetime.now() + datetime.timedelta(hours=1)) series = PwSeries(pw, series_id) patches = [p['id'] for p in series.patches] From 18bc5a1433084584c270c7f1cb97483fd6db89df Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 20 Apr 2024 19:36:40 -0700 Subject: [PATCH 102/429] contest: vmksft-p: dump logs on build failure Generate some logs on build failure so we can tell why it failed. Signed-off-by: Jakub Kicinski --- contest/remote/vmksft-p.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contest/remote/vmksft-p.py b/contest/remote/vmksft-p.py index 40ab078..bbec108 100755 --- a/contest/remote/vmksft-p.py +++ b/contest/remote/vmksft-p.py @@ -249,11 +249,12 @@ def test(binfo, rinfo, cbarg): vm = VM(config) if vm.build([f"tools/testing/selftests/{target}/config"]) == False: + vm.dump_log(results_path + '/build') return [{ 'test': 'build', 'group': grp_name, 'result': 'fail', - 'link': '', + 'link': link + '/build', }] shutil.copy(os.path.join(config.get('local', 'tree_path'), '.config'), From 3d1f0c9b8363b2620911151e3a7bcf02390843c4 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Mon, 22 Apr 2024 06:13:41 -0700 Subject: [PATCH 103/429] mailbot: allow auto-awaiting-upstream even tho we consider it self-reply We consider the await-upstream patches as self-reply, so because the self-reply handling is earlier in the if / elif ladder we never enter the awaiting-upstream branch. Signed-off-by: Jakub Kicinski --- mailbot.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mailbot.py b/mailbot.py index cff04af..3068b5e 100755 --- a/mailbot.py +++ b/mailbot.py @@ -423,13 +423,14 @@ def extract_actions(self, pw): elif line.startswith('doc-bot:'): self.actions.append(line) self.dr_act.append(line[8:].strip()) - elif self.auto_awaiting_upstream(): - self.actions.append('pw-bot: awaiting-upstream') - self.pw_act.append('awaiting-upstream') elif self.user_bot(): self.actions.append('pw-bot: changes-requested') self.pw_act.append('changes-requested') + if len(self.pw_act) == 0 and self.auto_awaiting_upstream(): + self.actions.append('pw-bot: awaiting-upstream') + self.pw_act.append('awaiting-upstream') + if not self.user_authorized(pw): bad = False if len(self.dr_act) or len(self.pw_act) > 1: From 8a23d3212719126b4b7e0b17316579be99ec01bd Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Tue, 23 Apr 2024 07:41:27 -0700 Subject: [PATCH 104/429] mailbot: process delay_actions in order of time We used to delay all actions by the same amount of time. So the processing check treats the action list as a queue. Now we delay by 1h or 24h, so we need to make sure the list is indeed sorted. Note that the list contains tuples (time, msg obj), but time should never be equal, so msg objects won't get compared. Signed-off-by: Jakub Kicinski --- mailbot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mailbot.py b/mailbot.py index 3068b5e..fb094b9 100755 --- a/mailbot.py +++ b/mailbot.py @@ -736,6 +736,7 @@ def main(): check_new(t, pw, dr) global delay_actions + delay_actions.sort() while len(delay_actions) and (delay_actions[0][0] - req_time).total_seconds() < 0: msg = delay_actions[0][1] delay_actions = delay_actions[1:] From 16315312d3007760e5fef94b7f154dcad68ca912 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Tue, 23 Apr 2024 07:47:20 -0700 Subject: [PATCH 105/429] mailbot: use 'auto' as actor changing the series state to AU It may be confusing if we log the author of the posting as the one changing its state, when its in fact the bot automatically marking it as awaiting-upstream. Signed-off-by: Jakub Kicinski --- mailbot.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mailbot.py b/mailbot.py index fb094b9..4d92d98 100755 --- a/mailbot.py +++ b/mailbot.py @@ -592,7 +592,10 @@ def do_mail(msg, pw, dr): name = series["name"] if not name: name = '? ' + msg.get('Subject') - log = [name, msg.get('From'), series.state(), pw_act_map[act], series["id"], mid] + actor = msg.get('From') + if msg.auto_awaiting_upstream(): + actor = "auto" + log = [name, actor, series.state(), pw_act_map[act], series["id"], mid] pw_state_log(log) else: print('', '', "ERROR: action not in the map:", f"'{act}'") From d6d0270472905e05c177e1cb279a7827d09eda0b Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Tue, 23 Apr 2024 08:07:27 -0700 Subject: [PATCH 106/429] mailbot: fix empty subject problems Apparently sometimes Subject parsing doesn't work. Signed-off-by: Jakub Kicinski --- mailbot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mailbot.py b/mailbot.py index 4d92d98..5abc59e 100755 --- a/mailbot.py +++ b/mailbot.py @@ -320,7 +320,7 @@ def auto_awaiting_upstream(self): if self.get('References', ""): return False subject = self.get('Subject') - if subject[0] != '[': + if not subject or subject[0] != '[': return False tags_end = subject.rfind(']') From 39d9c5a39ebd9efdb9acaa037bfd8f0ec935b778 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 24 Apr 2024 06:56:39 -0700 Subject: [PATCH 107/429] contest: vm: make sure we try to load the crash filters Filter data is loaded on demand. We need to demand it, otherwise fingerprint trimming won't work (unless we happened to need filters earlier for the same VM instance). Signed-off-by: Jakub Kicinski --- contest/remote/lib/vm.py | 1 + 1 file changed, 1 insertion(+) diff --git a/contest/remote/lib/vm.py b/contest/remote/lib/vm.py index 8874ec5..e036198 100644 --- a/contest/remote/lib/vm.py +++ b/contest/remote/lib/vm.py @@ -376,6 +376,7 @@ def extract_crash(self, out_path): in_crash &= '] ---[ end trace ' not in line in_crash &= '] ' not in line if not in_crash: + self._load_filters() finger_prints.append(crash_finger_print(self.filter_data, crash_lines[start:])) else: From 956c9012eabdda5ef3dfaadaedf24e5a72e6a743 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 24 Apr 2024 19:25:03 -0700 Subject: [PATCH 108/429] contest: vm: try to keep the crash fingerprint len at 5 If we start skipping we want to get deeper into the stack, not have a shorter finger print. Signed-off-by: Jakub Kicinski --- contest/remote/lib/vm.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/contest/remote/lib/vm.py b/contest/remote/lib/vm.py index e036198..5ef0530 100644 --- a/contest/remote/lib/vm.py +++ b/contest/remote/lib/vm.py @@ -51,25 +51,32 @@ def decode_and_filter(buf): return "".join([x for x in buf if (x in ['\n'] or unicodedata.category(x)[0]!="C")]) +def finger_print_skip_pfx_len(filters, needles): + # Filter may contain a list of needles we want to skip + # Assume it's well sorted, so we don't need LPM... + if filters and 'crash-prefix-skip' in filters: + for skip_pfx in filters['crash-prefix-skip']: + if len(needles) < len(skip_pfx): + continue + if needles[:len(skip_pfx)] == skip_pfx: + return len(skip_pfx) + return 0 + + def crash_finger_print(filters, lines): needles = [] need_re = re.compile(r'.*( |0:)([a-z0-9_]+)\+0x[0-9a-f]+/0x[0-9a-f]+.*') + skip = 0 for line in lines: m = need_re.match(line) if not m: continue needles.append(m.groups()[1]) - if len(needles) == 4: + skip = finger_print_skip_pfx_len(filters, needles) + if len(needles) - skip == 5: break - # Filter may contain a list of needles we want to skip - # Assume it's well sorted, so we don't need LPM... - if filters and 'crash-prefix-skip' in filters: - for skip_pfx in filters['crash-prefix-skip']: - if needles[:len(skip_pfx)] == skip_pfx: - needles = needles[len(skip_pfx):] - break - + needles = needles[skip:] return ":".join(needles) From 1d85ac28aa6b6fbbf7082954427f6519c39c0f7f Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Mon, 29 Apr 2024 11:13:12 -0700 Subject: [PATCH 109/429] contest: brancher: record which pulls succeeded Currently when net and net-next conflict - we stop merging net, and test purely net-next. It's good to know when this happens, dump this info into branch info. Signed-off-by: Jakub Kicinski --- pw_brancher.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pw_brancher.py b/pw_brancher.py index f0860a3..79f1f10 100755 --- a/pw_brancher.py +++ b/pw_brancher.py @@ -179,14 +179,18 @@ def create_new(pw, config, state, tree, tgt_remote) -> None: tree.git_reset(tree.branch, hard=True) log_end_sec() + state["info"][branch_name] = {"base-pulls":{}} + pull_list = config.get("target", "pull", fallback=None) if pull_list: log_open_sec("Pulling in other trees") for url in pull_list.split(','): try: tree.pull(url, reset=False) + state["info"][branch_name]["base-pulls"][url] = "okay" except PullError: log("PULL FAILED") + state["info"][branch_name]["base-pulls"][url] = "fail" pass log_end_sec() @@ -194,7 +198,7 @@ def create_new(pw, config, state, tree, tgt_remote) -> None: state["hashes"][branch_name] = tree.head_hash() series, prs = apply_pending_patches(pw, config, tree) - state["info"][branch_name] = {"series": series, "prs": prs} + state["info"][branch_name] |= {"series": series, "prs": prs} extras = apply_local_patches(config, tree) state["info"][branch_name]["extras"] = extras From d1d0eb5f41e2e3e07dd576e4868ba601b0718901 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Mon, 29 Apr 2024 11:27:25 -0700 Subject: [PATCH 110/429] core: tree: teach git pull how to trust rerere resolutions If the pull failed but tree is clean - rerere probably fixed it for us. If that's okay with the caller - use the resolution and act as if there was no conflict. Signed-off-by: Jakub Kicinski --- core/tree.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/core/tree.py b/core/tree.py index b5d6f60..5f39f0f 100644 --- a/core/tree.py +++ b/core/tree.py @@ -233,21 +233,30 @@ def check_applies(self, thing): return ret - def _pull_safe(self, pull_url): + def _pull_safe(self, pull_url, trust_rerere): try: self.git_pull(pull_url) except CMD.CmdError as e: + try: + # If rerere fixed it, just commit + if trust_rerere: + self.git(['diff', '-s', '--exit-code']) # will raise if rerere didn't fix it + self.git(['commit', '--no-edit']) + return + except CMD.CmdError: + pass + try: self.git(["merge", "--abort"]) except CMD.CmdError: pass raise PullError(e) from e - def pull(self, pull_url, reset=True): + def pull(self, pull_url, reset=True, trust_rerere=None): core.log_open_sec("Pulling " + pull_url) try: if reset: self.reset() - self._pull_safe(pull_url) + self._pull_safe(pull_url, trust_rerere) finally: core.log_end_sec() From 5e4043dd3f698fb53210c6d6f06aa50b488eaac6 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Mon, 29 Apr 2024 11:30:12 -0700 Subject: [PATCH 111/429] contest: brancher: try to use rerere if pull failed Before completely giving up try to use git rerere for conflict resolution. Signed-off-by: Jakub Kicinski --- pw_brancher.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pw_brancher.py b/pw_brancher.py index 79f1f10..acf9f5a 100755 --- a/pw_brancher.py +++ b/pw_brancher.py @@ -189,9 +189,12 @@ def create_new(pw, config, state, tree, tgt_remote) -> None: tree.pull(url, reset=False) state["info"][branch_name]["base-pulls"][url] = "okay" except PullError: - log("PULL FAILED") - state["info"][branch_name]["base-pulls"][url] = "fail" - pass + try: + tree.pull(url, reset=False, trust_rerere=True) + state["info"][branch_name]["base-pulls"][url] = "resolved" + except PullError: + log("PULL FAILED") + state["info"][branch_name]["base-pulls"][url] = "fail" log_end_sec() From afb8d6d5b6ed6cfb11b6a87a768b0fc645f7a523 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Mon, 29 Apr 2024 14:17:47 -0700 Subject: [PATCH 112/429] ui: status: show pull status in the result table Use recently added pull status from branches-info.json to indicate if net is currently getting pulled. Signed-off-by: Jakub Kicinski --- status.js | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/status.js b/status.js index b4dfba1..94498ff 100644 --- a/status.js +++ b/status.js @@ -478,14 +478,16 @@ function load_result_table_one(data_raw, table, reported, avgs) } } else { let res = row.insertCell(2); - let br_res; + let br_res, br_pull = ""; if (v.start) remote.innerHTML = v.start.toLocaleString(); else remote.innerHTML = "unknown"; remote.setAttribute("colspan", "2"); - branch.innerHTML = a + v.branch + ""; + if (v.pull_status != "okay") + br_pull = " (pull: " + v.pull_status + ")"; + branch.innerHTML = a + v.branch + "" + br_pull; branch.setAttribute("colspan", "2"); br_res = ''; br_res += colorify_basic(branch_results[v.branch]); @@ -504,16 +506,35 @@ function load_result_table(data_raw) { var table = document.getElementById("contest"); var table_nr = document.getElementById("contest-purgatory"); + var branch_pull_status = {}; var branch_start = {}; + // Parse branch info to extract pull status + $.each(branches_info, function(i, v) { + let summary = null; + $.each(v['base-pulls'], function(url, res) { + if (res == "okay" && !summary) { + summary = res; + } else if (res == "resolved" && (!summary || summary == "okay")) { + summary = res; + } else { + summary = res; + } + }); + branch_pull_status[i] = summary; + }); + + // Decorate branchers and collect branch_start $.each(data_raw, function(i, v) { v.start = new Date(v.start); v.end = new Date(v.end); branches.add(v.branch); - if (v.remote == "brancher") + if (v.remote == "brancher") { branch_start[v.branch] = v.start; + v.pull_status = branch_pull_status[v.branch]; + } }); // Continue with only 6 most recent branches @@ -604,8 +625,9 @@ function load_result_table(data_raw) load_fails(data_raw); } -let xfr_todo = 3; +let xfr_todo = 4; let all_results = null; +let branches_info = null; let branches = new Set(); let branch_results = {}; @@ -700,6 +722,12 @@ function filters_doit(data_raw) loaded_one(); } +function branches_loaded(data_raw) +{ + branches_info = data_raw; + loaded_one(); +} + function do_it() { $(document).ready(function() { @@ -717,4 +745,7 @@ function do_it() $(document).ready(function() { $.get("contest/all-results.json", results_loaded) }); + $(document).ready(function() { + $.get("static/nipa/branches-info.json", branches_loaded) + }); } From d4461ffca6689ba7d7ce6ce6c0ebbbf932a6ed76 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Mon, 29 Apr 2024 17:01:31 -0700 Subject: [PATCH 113/429] contest: gh: support pagination Try older "pages" of results if we don't get the data in action runs. Signed-off-by: Jakub Kicinski --- contest/remote/gh.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/contest/remote/gh.py b/contest/remote/gh.py index 0e428d3..c7ab14f 100755 --- a/contest/remote/gh.py +++ b/contest/remote/gh.py @@ -49,12 +49,12 @@ def get(url, token): return requests.get(url, headers=headers) -def get_results(config, cbarg, prev_run): +def get_results(config, cbarg, prev_run, page=1): token = config.get('gh', 'token') repo_url = f"https://api.github.com/repos/%7Bconfig.get('ci', 'owner')}/{config.get('ci', 'repo')}" ref = config.get('ci', 'runs_ref') - resp = get(repo_url + '/actions/runs', token) + resp = get(repo_url + f'/actions/runs?page={page}', token) runs = resp.json() found = None for run in runs.get('workflow_runs'): @@ -62,7 +62,9 @@ def get_results(config, cbarg, prev_run): if found is None or found["id"] < run["id"]: found = run if found is None: - print("Run not found!") + if page < 10: + return get_results(config, cbarg, prev_run, page=(page + 1)) + print(f"Run not found, tried all {page} pages!") return None if prev_run == found["id"]: print("Found old run:", prev_run) From 1508eb7c93cc67501c3e8e897cd103e2c2fe21e0 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Tue, 30 Apr 2024 07:37:08 -0700 Subject: [PATCH 114/429] contests: ui: flakes: add a link to contest.html Support clicking on a test name in flakes to go to list of runs for a given test (in contest). Signed-off-by: Jakub Kicinski --- contest/flakes.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/contest/flakes.js b/contest/flakes.js index 2bde51b..5c1011d 100644 --- a/contest/flakes.js +++ b/contest/flakes.js @@ -35,6 +35,7 @@ function load_result_table(data_raw) let needle = document.getElementById("tn-needle").value; var test_row = {}; + let tn_urls = {}; $.each(data_raw, function(i, v) { $.each(v.results, function(j, r) { @@ -47,6 +48,8 @@ function load_result_table(data_raw) if (needle && !tn.includes(needle)) return 1; + tn_urls[tn] = "executor=" + v.executor + "&test=" + r.test; + if (!(tn in test_row)) { test_row[tn] = {}; for (let i = 1; i <= branches.length; i++) @@ -111,7 +114,7 @@ function load_result_table(data_raw) let row = table.insertRow(); let name = row.insertCell(0); - name.innerHTML = tn; + name.innerHTML = "" + tn + ""; name.setAttribute("style", "padding: 0px"); for (let i = 0; i < branches.length; i++) { From 2332e07008de0479f552399a86e08aec390c58a9 Mon Sep 17 00:00:00 2001 From: Daniel Xu Date: Wed, 1 May 2024 15:07:16 -0600 Subject: [PATCH 115/429] README: Link to wiki Help users locate the user-facing docs. --- README.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.rst b/README.rst index aea1c66..7c5efbf 100644 --- a/README.rst +++ b/README.rst @@ -13,6 +13,9 @@ Currently this project only includes simple checks and build testing, all Linux kernel-centric. Patches are not tested against existing kernel selftests. +Please see `the wiki `_ +for how to interact with NIPA. + Goals ===== From 5ee9eaf1c1a50aeb31e9d3677c36bdc266db48f5 Mon Sep 17 00:00:00 2001 From: Daniel Xu Date: Wed, 1 May 2024 15:08:48 -0600 Subject: [PATCH 116/429] README: Fix link Previous link did not render correctly. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 7c5efbf..4d29e37 100644 --- a/README.rst +++ b/README.rst @@ -202,7 +202,7 @@ netdev policy. signed ~~~~~~ -Check for patch attestation (as generated by [patatt](https://github.com/mricon/patatt)). Warn when there +Check for patch attestation (as generated by `patatt `_). Warn when there is no signature or if the key for a signature isn't available. Fail if the signature doesn't match the attestation. From b45cb28a948356b23519e9d79c3661f5c4aa1e11 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 3 May 2024 10:41:38 -0700 Subject: [PATCH 117/429] ui: flake: color flakes and fails differently Separate the colors of flakes (fail + pass on retry) from the failures (fail + fail or no retry). Move the coloring to CSS - set class instead of color inline. Signed-off-by: Jakub Kicinski --- contest/flakes.html | 12 ++++++++++++ contest/flakes.js | 15 +++++---------- nipa.css | 5 +++++ 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/contest/flakes.html b/contest/flakes.html index 2f59925..2540fec 100644 --- a/contest/flakes.html +++ b/contest/flakes.html @@ -61,6 +61,18 @@ Loading... +
+
+
Branch
+ + + + + + +
pass
skip
fail
flake
+
+
diff --git a/contest/flakes.js b/contest/flakes.js index 5c1011d..5ae2d12 100644 --- a/contest/flakes.js +++ b/contest/flakes.js @@ -1,15 +1,8 @@ function colorify(cell, value) { - if (value == "") { - ret = ""; - } else if (value == "pass") { - ret = "background-color:green"; - } else if (value == "skip") { - ret = "background-color:blue"; - } else { - ret = "background-color:red"; - } - cell.setAttribute("style", ret); + if (value == "pass" || value == "skip" || + value == "fail" || value == "flake") + cell.setAttribute("class", "box-" + value); } function get_sort_key() @@ -56,6 +49,8 @@ function load_result_table(data_raw) test_row[tn][branches[i - 1]] = ""; } test_row[tn][v.branch] = r.result; + if (r.result == "fail" && r.retry == "pass") + test_row[tn][v.branch] = "flake"; }); }); diff --git a/nipa.css b/nipa.css index 97ac84a..13e66b7 100644 --- a/nipa.css +++ b/nipa.css @@ -14,6 +14,11 @@ tr:nth-child(even) { background-color: #eeeeee; } +.box-pass { background-color: green; } +.box-skip { background-color: royalblue; } +.box-flake { background-color: red; } +.box-fail { background-color: #d06060; } + .row { display: flex; } From 432d3bc69f55f018e95a432863888cbfc977f64c Mon Sep 17 00:00:00 2001 From: Daniel Xu Date: Fri, 3 May 2024 16:21:45 -0700 Subject: [PATCH 118/429] ui: status: Label patch processing graph axes This commit labels the axes with units, so it's more obvious what the graph is trying to depict. Also move the legend inside the graph to conserve valuable vertical screen space. --- status.js | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/status.js b/status.js index 94498ff..9fa35ab 100644 --- a/status.js +++ b/status.js @@ -60,19 +60,34 @@ function load_times(data, canva_id) datasets: [{ backgroundColor: "rgba(0, 0, 0, 0)", pointBorderColor: "rgba(0, 64, 255, 0.7)", - label: 'Processing time by patch post time', + label: 'By patch post time', data: e1.map(function(e){return e.v;}) }, { backgroundColor: "rgba(0, 0, 0, 0)", pointBorderColor: "rgba(255, 64, 0, 0.7)", - label: 'Processing time by check delivery time', + label: 'By check delivery time', data: e2.map(function(e){return e.v;}) }] }, options: { + plugins: { + title: { + display: true, + text: 'Patch processing times over last 7 days', + padding: 0 + }, + legend: { + position: 'chartArea', + }, + }, scales: { y: { type: 'linear', + title: { + display: true, + text: 'Hours', + padding: 0 + }, ticks: { stepSize: 3 }, @@ -80,6 +95,11 @@ function load_times(data, canva_id) }, x: { type: 'linear', + title: { + display: true, + text: 'Hours ago', + padding: 0 + }, ticks: { stepSize: 24 }, From aeac3f5773f26da84df893e51316a0a71c3ce4a3 Mon Sep 17 00:00:00 2001 From: Daniel Xu Date: Fri, 3 May 2024 15:22:28 -0700 Subject: [PATCH 119/429] ui: Move all UI files to ui/ directory During deployment all the JS/HTML files were placed in the same directory anyways. So align with production to simplify development. --- checks.html => ui/checks.html | 0 checks.js => ui/checks.js | 0 {contest => ui}/contest.html | 0 {contest => ui}/contest.js | 0 {contest => ui}/flakes.html | 0 {contest => ui}/flakes.js | 0 nipa.css => ui/nipa.css | 0 {contest => ui}/nipa.js | 0 status.html => ui/status.html | 0 status.js => ui/status.js | 0 10 files changed, 0 insertions(+), 0 deletions(-) rename checks.html => ui/checks.html (100%) rename checks.js => ui/checks.js (100%) rename {contest => ui}/contest.html (100%) rename {contest => ui}/contest.js (100%) rename {contest => ui}/flakes.html (100%) rename {contest => ui}/flakes.js (100%) rename nipa.css => ui/nipa.css (100%) rename {contest => ui}/nipa.js (100%) rename status.html => ui/status.html (100%) rename status.js => ui/status.js (100%) diff --git a/checks.html b/ui/checks.html similarity index 100% rename from checks.html rename to ui/checks.html diff --git a/checks.js b/ui/checks.js similarity index 100% rename from checks.js rename to ui/checks.js diff --git a/contest/contest.html b/ui/contest.html similarity index 100% rename from contest/contest.html rename to ui/contest.html diff --git a/contest/contest.js b/ui/contest.js similarity index 100% rename from contest/contest.js rename to ui/contest.js diff --git a/contest/flakes.html b/ui/flakes.html similarity index 100% rename from contest/flakes.html rename to ui/flakes.html diff --git a/contest/flakes.js b/ui/flakes.js similarity index 100% rename from contest/flakes.js rename to ui/flakes.js diff --git a/nipa.css b/ui/nipa.css similarity index 100% rename from nipa.css rename to ui/nipa.css diff --git a/contest/nipa.js b/ui/nipa.js similarity index 100% rename from contest/nipa.js rename to ui/nipa.js diff --git a/status.html b/ui/status.html similarity index 100% rename from status.html rename to ui/status.html diff --git a/status.js b/ui/status.js similarity index 100% rename from status.js rename to ui/status.js From 4a4085eaa4aaf01dd8fd5f51a58d7ba745b2bb6f Mon Sep 17 00:00:00 2001 From: Daniel Xu Date: Fri, 3 May 2024 15:23:55 -0700 Subject: [PATCH 120/429] scripts: Add ui_assets.sh script to manage assets This script helps download all the necessary UI assets such that you can run the UI locally in your browser. It's a bit of a hack given it needs to be manually kept in sync with all the JS, but somewhat necessary to do local development. --- .gitignore | 4 ++- scripts/ui_assets.sh | 66 ++++++++++++++++++++++++++++++++++++++++++++ ui/checks.js | 3 ++ ui/contest.js | 3 ++ ui/flakes.js | 3 ++ ui/status.js | 3 ++ 6 files changed, 81 insertions(+), 1 deletion(-) create mode 100755 scripts/ui_assets.sh diff --git a/.gitignore b/.gitignore index 7ea23b2..0fc0baf 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ system-status.cfg poller.state* *.config *.org +*.xz +ui/contest/ +ui/static/ __pycache__/ results/ -*.xz \ No newline at end of file diff --git a/scripts/ui_assets.sh b/scripts/ui_assets.sh new file mode 100755 index 0000000..28f0980 --- /dev/null +++ b/scripts/ui_assets.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# +# This script manages all the assets a UI running locally +# on your computer would need. +# +# Note we are downloading real assets from a production instance. +# +# Usage examples: +# +# ./scripts/ui_assets.sh download +# ./scripts/ui_assets.sh clean + +set -eu + +PROD=https://netdev.bots.linux.dev +LOCAL=./ui +ASSETS=( + "static/nipa/checks.json" + "static/nipa/systemd.json" + "static/nipa/branch-results.json" + "static/nipa/branches-info.json" + "contest/filters.json" + "contest/all-results.json" +) + +function usage() { + echo "Usage: ${0} download|clean" +} + +function download() { + mkdir -p "${LOCAL}/static/nipa" + mkdir -p "${LOCAL}/contest" + for asset in "${ASSETS[@]}"; do + curl "${PROD}/${asset}" -o "${LOCAL}/${asset}" + done +} + +function clean() { + for asset in "${ASSETS[@]}"; do + rm -f "${LOCAL}/${asset}" + done + rm -r "${LOCAL}/static" + rm -r "${LOCAL}/contest" +} + +# Change dir to project root +cd "$(git rev-parse --show-toplevel)" + +if [[ $# -ne 1 ]]; then + usage + exit 1 +fi + +case $1 in + download) + download + ;; + clean) + clean + ;; + *) + echo >&2 "Error: Unrecognized subcommand $1" + usage + exit 1 + ;; +esac diff --git a/ui/checks.js b/ui/checks.js index 7d2c949..a810fa2 100644 --- a/ui/checks.js +++ b/ui/checks.js @@ -402,6 +402,9 @@ function run_it(data_raw) function do_it() { + /* + * Please remember to keep these assets in sync with `scripts/ui_assets.sh` + */ $(document).ready(function() { $.get("static/nipa/checks.json", run_it) }); diff --git a/ui/contest.js b/ui/contest.js index 8d03f96..d82e832 100644 --- a/ui/contest.js +++ b/ui/contest.js @@ -161,6 +161,9 @@ function results_loaded(data_raw) function do_it() { + /* + * Please remember to keep these assets in sync with `scripts/ui_assets.sh` + */ $(document).ready(function() { $.get("contest/filters.json", filters_loaded) }); diff --git a/ui/flakes.js b/ui/flakes.js index 5ae2d12..ea52669 100644 --- a/ui/flakes.js +++ b/ui/flakes.js @@ -158,6 +158,9 @@ function results_loaded(data_raw) function do_it() { + /* + * Please remember to keep these assets in sync with `scripts/ui_assets.sh` + */ $(document).ready(function() { $.get("contest/filters.json", filters_loaded) }); diff --git a/ui/status.js b/ui/status.js index 9fa35ab..60e5932 100644 --- a/ui/status.js +++ b/ui/status.js @@ -750,6 +750,9 @@ function branches_loaded(data_raw) function do_it() { + /* + * Please remember to keep these assets in sync with `scripts/ui_assets.sh` + */ $(document).ready(function() { $.get("static/nipa/checks.json", run_it) }); From 79c400195f406f9dfe8ec7710fe932f0c42172ca Mon Sep 17 00:00:00 2001 From: Daniel Xu Date: Fri, 3 May 2024 15:28:40 -0700 Subject: [PATCH 121/429] scripts: Add ui.sh This script runs your local checkout of the UI. --- scripts/ui.sh | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100755 scripts/ui.sh diff --git a/scripts/ui.sh b/scripts/ui.sh new file mode 100755 index 0000000..aabef77 --- /dev/null +++ b/scripts/ui.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# +# This script opens your local checkout of the UI in your browser. +# +# Note you need to have run `scripts/ui_assets.sh download` at least +# once prior to running this script. +# +# Usage example: +# +# ./scripts/ui.sh + +set -eu + +# Change dir to project root +cd "$(git rev-parse --show-toplevel)" + +# Quick sanity check +if [[ ! -d ./ui/static ]]; then + echo >&2 "Error: you haven't run scripts/ui_assets.sh yet" + exit 1 +fi + +# Need to run a local webserver to avoid CORS violations +python -m http.server -d ./ui -b localhost 8080 &> /dev/null & +pid=$! +trap 'kill ${pid}' EXIT + +# Best effort in case someone is on OSX or SSH forwarding +if ! xdg-open http://localhost:8080/status.html &> /dev/null; then + echo "UI is available at http://localhost:8080/status.html" +fi + +echo "Press Enter to stop serving and exit..." +read -r -p "" From 654e8cd9e1e125157b0bea09def3e98ac0a0614a Mon Sep 17 00:00:00 2001 From: Daniel Xu Date: Tue, 7 May 2024 10:40:02 -0600 Subject: [PATCH 122/429] ui: Put all favicon images into ui/ Note 3 of the favicons used in prod were not checked in. --- {contest => ui}/favicon-contest.png | Bin ui/favicon-flakes.png | Bin 0 -> 5120 bytes ui/favicon-stats.png | Bin 0 -> 4742 bytes ui/favicon-status.png | Bin 0 -> 4929 bytes 4 files changed, 0 insertions(+), 0 deletions(-) rename {contest => ui}/favicon-contest.png (100%) create mode 100644 ui/favicon-flakes.png create mode 100644 ui/favicon-stats.png create mode 100644 ui/favicon-status.png diff --git a/contest/favicon-contest.png b/ui/favicon-contest.png similarity index 100% rename from contest/favicon-contest.png rename to ui/favicon-contest.png diff --git a/ui/favicon-flakes.png b/ui/favicon-flakes.png new file mode 100644 index 0000000000000000000000000000000000000000..5934fdf5738dafbe8fcb3f6a5b42628827ea9052 GIT binary patch literal 5120 zcmeHLdsGu=77rk(^Z=q#pRCj16PaWtnLH)(3P_aj5-_q-nOEX0~M|$&kJReQ0IvJcH?ik<&er@^rkHG0(eu|Dw#_f>BYSuH^G{ojv zXoz+&dLGYlwKOTow*eaPqGRY3cXv!kP*wycog3j@A~F+h3QDn zjm_o5cIf+E-oAF!;>ea;4Te8Aj5(F${?*OPN9tk+&cC~7RsHhNw$TSqtq#5T$C#iq zK9|_H(^ouy@9S?atv+wxlnEV5*g8(24!V)KIq-)iWmnoNYhnB0bVcRzecFnQ8*9(D z1h>_1_El9@Z*x?_rJ5l0q5NEH;|}T_hyT40dTy}Kf)5O6iBBtUr00XVOFrx8iS0ht z-h2nXRV%HH3H$LA!EK|icBn~Z3n7Ji7 zIPgd4dvia@s;#@4pOJ0>lt z`z!JNIsf!@kFE zdij{W)#0%y&bl)vwtY5_=QouBb*R(EDM_B=3D8R`SB@aeio^q_+eAMLmf&0V5C_bau~BsHl;(w zcj79+m=g>6kW+=tQt@@#L@3H^r6Ej!2@p8i!OR!&r+P!7R!Xl-R>yQgfGZV$9?M#k zLZRJm7udxDv(+F(6$*tA5eY>i7-+z@JQIsMV3RF~gK%M}X&Y%}EG%O-K^!Jdm~&Yb zpAY7tSN<6-T5T7+$=1mN;6v!ZEkaa)2#rQzPYW9xJs*H{I`o?swiM9L!erWJ&b5+s z^nBXH2K9uX$S!+Jt~J{k4n+#-Y}yD^ZD3Wj*OJpT+QcpkjsgQ?v^cE**}XJbM&C_V zFW=Jf<&;!G%Slv9NJKrLG$tF1n@F020^kA$;LxN#>5l?V-!We3MmRk zm`sMt^r%82qIy6iSQ${2cy>>(I4FvPl4BAnjf!AWk4Rulf{0;)!X>a=ieso4Ln(z` z>V%?5WsKQs#6db4BW|FD7L&m_!4a;ENYtqKA_3AZNzBGsJup!5;}}z}qkAHSG14 zDQ0uFiqE+Oah6@vTF_1u&f;pEr2!}+!juTA6iHJMnGyqI8H~u4NDqB8#pv_?oi^7z zP^c^Av5XC@pXU_0I%+1JY`;1O^I z%G1deG|`!;Nxil0^Jp#$h)4`03K&6CP*f=vE5#TLm;;0f-+YK7sR)yyI06$gu3FlUCxda6DE%I9Y_R-Zx*K0BGTFQOZ)koKBG4NW-ebx0pqszPd zrIy{sL)^162<9Rsh+Uc`)PYTirkTXU?o97qOQVrScUytjsMhZ}EQ)o>u}&uX;M z{qMMq^a~j!Ks17l*0E7$Blm8_b8h)*3bHfoJc!%!&syX=4E%Fap;1Sq=o;=< z?2T_ghDIkWIT^PmEi#h8BZ!~8!g%MdCg8htPuJ}B515i&UG`DgYW~%2f#(a)6HA`$ zDNlioOLd+!`<1qgt(o84Q^xVYQg^g=;z}C_72+c@dgiD>-k5A zmeDWot{ZpJ!#y~9e>3EDN4d95e4srjb39Vml`w2d&$<^x(e53;vW)lz!sx$0z^fVb{O+tb&&ieoZ~TE})|}x~{CIFfLnA zNZM|C`+xUo{Qly}YjeXr&$P5;r6yD^PZ(n)1ha3h_ZyS%KWNH9udUz7JOZbmxR7Pf zyIjAqK4+rahH4^>S6TP{1c=>9m{%2OzL*qV(G+;PZiJ<^X(m)OQk-SJIBp}qqM|Z7V zd|*X;@VO{(v51`gR`&{;gSAJ>b!vrCvsa|7FRFyz`Lv>$bo(?^L_K5EjXcs=~J1_1LA0EB!8S!&5J=H@Wsr(4)2u zYWe9MUr1A2FJC!STgofTxPV{Z$zo$J1t1U zfr^IX&tFd)bLeAV&vA|Sb+=m=c&j`j1+7tNWAddA^858qN^WwH?FEfx4t_^&eBO%j4i|y;@2&$s8wA(bh~WL8SnC@3+|^TtBGu%uO|ee&GjvR zZCY`;`u56cIkju|ex3{?HY%`eNJTPwP#E zUQ90u7e5^L^Mx6j@@p3tzI*ausef!hlv+`oJ>!0PW9H_Q@yAa5D1GU*UX(-Zetar& zTMfJu3fr)5?!wvUBX?x%3G|vH-eT|iQ&P2zxpC!(sAc|sSDzD)-FnyGJz3Emm!GUc zeW_~UxAjx}n#WG#*S`$OD7@&=vUc3Do}JYyH(jXwc*Us(f3(bJ<;pWWVtnNtm&m=| zw{|3b6T7IPE%!P1)#3SQwpY)Wnbx$u_j_ET|8aDCa82zDG5KoCi3;+xSg+UI3-e?s&q_7iL4Blxm z3x=E~RGJiCrcgrRCJO=am^>zmL^#L{4jkeMNi4Wdtdd3cLx3+SoJvt3u&fElM$jZF^$PaNnsf1p@H}e zW`$x9-e~P-0r0_cU}hGZiLwj^)^HCi6_Eiz`UCn^533riXO@bvnrs#=5s^U{shPte zaP6SK*=Er@)4{bYLQfcgsTGXM9x-H;T%jEFpeaZp4Q8hoAbW%*Me2sg8W9_QFwyJ_LlUJPqjbPQ3dfK}n`20!CJjU!h0$!X z1tKnA#O1JgY(NSxc*rQ8uvkGQ(wJljU^t6aUGJsf&u2TTul!IKv;T8g;Ocu4t zq?f{UND%Egs8)dGgkuyY!zcoPq8zRm6^T){n#~q-xneE{L4(EUFnbeD>N5YAHNAKs zN#B%5lU6W(rc>0nqT-13zOTMdJ?UIZ5ae7GVockYf)&dkaA%wVt52m(#f&Kg=pOy$ zIv^*1r4)2Ro`xT+6CrF4KNyq(p+zt*2SWrpfkww;>$n(SI}+V$(ouHILWHFN9syUN zJe^!Yvz-$)XQZ_~m7r$<5Qd;4WQZ_cKVhu?gjsaY7|2+{`Y%o-PJ>}h2Ke>KK=T5< zkTuv0`#GcA&TshZpNrpc1OPoc$XoF{O4le|Z^gh{8IM-iC|z&Gz*`xQR@eWHF3+Lk z6k!CrAUilL@$&5pz(H%AW>J)k(Mw;sv07O%%h-Fl`$w2SQ8P3fjfkSqZN`>gnVnVL(WN_xbCzpk(m!0p}S#G?6 z27VG+{7E^_^YiAjYR=Nb^CoRw716jeH@Nir?^5r&+MeumTeW}12Srd~tB0S;WMj=M z5sXF#2kUPKfy|PrpBSuU|er1)?cYo%; z_%&XV*zAk;<2Q=)?NO$Vr>5+NQxDs|M2>$bcoTgn*q>Q~&%e+z_ z%)UOp_f^m9pA%wk#-BbKIkU?zE#@YJd++v`pp9>?USsBl`L7beb2x2R^7ZFFwqbj)P zf^y5T=E9e*FWeL%Z1(zR}%PvRCtC zIyP7OMc|36^&#GY<-v_UdvYH%yDZuj5V84M=S(%D;6O)tLCw;IHOl1=N~M#Ed@KEn zPV5`wR+CpU{^cKBBjY8f>!06CR9n?sh@?E3nL+ lRd^uy?%Ws6cdp!GO|QJUyhyM8JE#bTJYuoz*zZ@Z`wtqy=Gy=O literal 0 HcmV?d00001 diff --git a/ui/favicon-status.png b/ui/favicon-status.png new file mode 100644 index 0000000000000000000000000000000000000000..322488e07009991ec23134ebd43d371a164bcab9 GIT binary patch literal 4929 zcmeHKc~BH*7H^OO9YOI5$gv4e3sTES5=ZEL1mbN+g=n+xMhtcTt^!tyF-w$_Ge4l^`=3dy_eUVBI z%%&3xcV5%zvg$YA{&-l!dlP7DTiQ|IFN#NOwh1=mwp}+Cmwui9Wj8;x#xr9WFZ14> ze>Qa>w=+zF(wZyp@7$C=(I>|-rZBz)g4{k})ap2mTKzI_kZsxW4^=T|!vgCv5Pie~cA8Up2)?^VcP}c+1gBOm1#si|SJ4=RaGjHFjo73=9N?qfOC!;(w12=D_8|Eei z)ZG7hf?Md1bs!*^fSTHT+OBG^wSF-_kf0Y#4h`Ok-z_+|esN03 ziuT2}1+800iMMM;M;F&6_{~UbxVSeeL>-tU+3~Qb?ELgQ$fbp!UuwB?OXltO%O?6z zVvw4*CUup^C|>8jzw!QnZOciRNoA^Twg=@rAAM@9EvdHtNI?Kze<wmhA5x~XY*bZx8Dnti=#qpH>B z<)0^_5_3O+bIMaaZd~rz`mnYUSzavp%zQtmEEHO>Kk135T<$UDPa97^uy@rwsCr)g zY1)|}zQDG6=KOCILZN^t8ZfvVsPMppe;4D?u)$Sy)dS8MBJt zRegpJGn9&-BGIB+tD4SaV)AWtV*WfGnZJ}&Q2g1Uybz}n0GMc&fSsl+vt8*_@m;t| z@XU#YeAs2eE>-bUwQ;c8VxwVPfD2G0(#hnC_@O*F#6}sE2@z3!5a3P4&tzGvQYdsd z90G?}V6hp6m_nftq9UP4ga8Y~o@ZtWCt|iw|Q#)7{du_>RjW({|gQLL6n5-@@K=x~vEMs^@)@!kG z8do|40|D;+xUX5i%-v-Sth8EXgoVuE!qY^k_}uzR%0e=f()B14p>j%rixDY_$q}4D zaYP}f2}Djw6||hhqH0F=gTX9+V&b5H>= zVp4?+l`13>kc~*9ki0TVrfqi6i5w<|3dFce!<9t|k^#gLT%Q5}mmK7xRNH8Rwb*nP zOO}eyg#>e+{c0^3CyHQ+2!f>nC@K;wF;t0RI!va-L`ty&L1jvGfW3uc40->}nj0QC zq_^ZTj2*0>=Mwc!R3e?-`_}uK#kfWZhFwDeYS^2CoyesrSDXN=S4CzLW+M%DkG^(& zDQAAC6%2A*FOeA(2p~{~;51Dlgjhr%QiD`)z%hfEko2}fKf2vwU>$^w4mScG0au_s zU0lIaU4@$VdMu7inyUgJ3_$@&fG|uc7AwVgA7R3`pQ1=A!etnNB6=A&Fe0fG(UY_R z!AP-Kk5QN&@9UiZJw*&fe%lj$DTWBSZSiGMLxjI6d#}NOeH2jBD+Bv8*zSe>`+Xm0 zAl|q6>ua~SxdaRkF7ihF4$?J9*BdeLM#h8PHAvSRG4MvlgWdIiql@?I#7LXLhn@qR z2V3obi3Mj#PyO8J2uKR;hGvxOSA&BmG|U>azz#vf$8eV$w5`k^2)$U1Hqz@3&l~a% zz7+ezEg%}tMy9fAi-|jUK(6m$nt~k+n+bE@!zn9A05G>IO+>gZ_3Zu4*!(ZU#-4xw zVDkl!n912jEPINm!>{(RvTD?9H~ZOp!LVR;Q*%eh?jgfUwwDaA9_DM69v?BGJOA{a zQh#g5qWrGs%bxdiU0>!XLbf0CPiu3ZT${APcT3}B5BPAasVF1k`jFr-x83v0IzCT! z?*DaLAzA_L614kIC*49#J|nxYo~cSoA2-##`eKj!-mqQCpKcJe${ro-++W<`9y8|V zf$+6aH~iZ7`{K>$$Nom2wi>5?@y&%4ZZ95ODz&3O9ulB|Z}q_Cq$%bOMWiFfNKIL75ISaT4+ zJSQDLe)$7vcD8Tbg~cEBJhl=c=Q{5=$`5)bjPxxm^8KnYm8$nj|JJ^4hr9YCXwjlg zvCC%0sFzHM`Ki)Fmzta%Ux&L@q?DaFUtvv|WiM0QK01E*%9zJZA+vT?I@cA*4#us! z`%v(x$uDT@)3e@{#`=PjcfI||?V}GLJ^Z4y;7IxUo{J~-|965a-eV#Hf_o77uKwv_kM0=>?e&suDfw8y!o4KIVd%xiJTX4XwH&V{{gKc3OxV- literal 0 HcmV?d00001 From 8ec2104a21896d5e116014eddac78d9af6f189b9 Mon Sep 17 00:00:00 2001 From: Daniel Xu Date: Mon, 6 May 2024 12:26:25 -0600 Subject: [PATCH 123/429] ui: Add sitemap to top of all pages Currently it is hard to discover all the available features/pages of the UI. Add a sitemap to the top of every page to make discovery/exploration easier. --- ui/checks.html | 3 +++ ui/contest.html | 3 ++- ui/flakes.html | 2 ++ ui/nipa.js | 7 +++++++ ui/sitemap.html | 5 +++++ ui/status.html | 2 ++ 6 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 ui/sitemap.html diff --git a/ui/checks.html b/ui/checks.html index 9b65e9e..2667774 100644 --- a/ui/checks.html +++ b/ui/checks.html @@ -7,13 +7,16 @@ + +

diff --git a/ui/contest.html b/ui/contest.html index 389f91a..d9c247c 100644 --- a/ui/contest.html +++ b/ui/contest.html @@ -10,6 +10,7 @@ @@ -20,7 +21,7 @@ -
+
Filtering: diff --git a/ui/flakes.html b/ui/flakes.html index 2540fec..777eda9 100644 --- a/ui/flakes.html +++ b/ui/flakes.html @@ -10,6 +10,7 @@ @@ -20,6 +21,7 @@ +
Filtering: diff --git a/ui/nipa.js b/ui/nipa.js index fe00773..3671ea7 100644 --- a/ui/nipa.js +++ b/ui/nipa.js @@ -60,3 +60,10 @@ function nipa_pw_reported(v, r) return true; } + +function nipa_load_sitemap() +{ + $(document).ready(function() { + $("#sitemap").load("sitemap.html") + }); +} diff --git a/ui/sitemap.html b/ui/sitemap.html new file mode 100644 index 0000000..5572ccd --- /dev/null +++ b/ui/sitemap.html @@ -0,0 +1,5 @@ + diff --git a/ui/status.html b/ui/status.html index 7b54b25..6aa39de 100644 --- a/ui/status.html +++ b/ui/status.html @@ -10,6 +10,7 @@ @@ -26,6 +27,7 @@ +

Build processing

From c2dee8d3234a0cfa6df0e0416d4858cf8d53e320 Mon Sep 17 00:00:00 2001 From: Daniel Xu Date: Tue, 7 May 2024 12:53:00 -0600 Subject: [PATCH 124/429] ui: Remove unused variable Variable was assigned but not used anywhere else. --- ui/status.js | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/status.js b/ui/status.js index 60e5932..e4c31fb 100644 --- a/ui/status.js +++ b/ui/status.js @@ -699,7 +699,6 @@ function filters_doit(data_raw) let cf_execs = document.getElementById("cf-execs"); let cf_tests = document.getElementById("cf-tests"); var output, sep = ""; - var execs = "Executors reported "; output = "Executors reported: "; $.each(data_raw.executors, function(i, v) { From c338ed1bca880583eeed20f7acc18d1fe4aa67e7 Mon Sep 17 00:00:00 2001 From: Daniel Xu Date: Tue, 7 May 2024 12:55:03 -0600 Subject: [PATCH 125/429] ui: Move link to flaky tests to sitemap It's more visible / accessible up there. --- ui/nipa.css | 4 ---- ui/sitemap.html | 3 ++- ui/status.html | 4 ---- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/ui/nipa.css b/ui/nipa.css index 13e66b7..98f96b9 100644 --- a/ui/nipa.css +++ b/ui/nipa.css @@ -45,10 +45,6 @@ tr:nth-child(even) { border: solid grey 1px; } -#flake-link { - margin: 1em; -} - @media (prefers-color-scheme: dark) { body { color: #b8b8b8; diff --git a/ui/sitemap.html b/ui/sitemap.html index 5572ccd..9857144 100644 --- a/ui/sitemap.html +++ b/ui/sitemap.html @@ -1,5 +1,6 @@ diff --git a/ui/status.html b/ui/status.html index 6aa39de..4a0473b 100644 --- a/ui/status.html +++ b/ui/status.html @@ -93,10 +93,6 @@

Continuous testing results

Patchwork reporting

-

Ignored tests:

From 8092d8685066aefc38bfcd5b72cfa3786f34309c Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Tue, 7 May 2024 12:56:17 -0700 Subject: [PATCH 126/429] ui: status: correct reported execs / remotes The JSON format has changed for filtering executors to filtering remotes. Adjust the UI. Signed-off-by: Jakub Kicinski --- ui/status.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/status.js b/ui/status.js index e4c31fb..e3af5aa 100644 --- a/ui/status.js +++ b/ui/status.js @@ -700,8 +700,8 @@ function filters_doit(data_raw) let cf_tests = document.getElementById("cf-tests"); var output, sep = ""; - output = "Executors reported: "; - $.each(data_raw.executors, function(i, v) { + output = "Remotes reported: "; + $.each(data_raw.remotes, function(i, v) { output += sep + v; sep = ", "; }); From 95a9150d87dbd1473e1ed83e1b4be7195ff8eb89 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 9 May 2024 10:27:09 -0700 Subject: [PATCH 127/429] contest: vm: try to extract ref_tracker leaks Signed-off-by: Jakub Kicinski --- contest/remote/lib/vm.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contest/remote/lib/vm.py b/contest/remote/lib/vm.py index 5ef0530..651e99b 100644 --- a/contest/remote/lib/vm.py +++ b/contest/remote/lib/vm.py @@ -277,7 +277,7 @@ def _read_pipe_nonblock(self, pipe): return read_some, output read_some = True output = decode_and_filter(buf) - if output.find("] RIP: ") != -1 or output.find("] Call Trace:") != -1: + if output.find("] RIP: ") != -1 or output.find("] Call Trace:") != -1 or output.find('] ref_tracker: ') != -1: self.fail_state = "oops" except BlockingIOError: pass @@ -382,12 +382,14 @@ def extract_crash(self, out_path): if in_crash: in_crash &= '] ---[ end trace ' not in line in_crash &= '] ' not in line + in_crash &= line[-2:] != '] ' if not in_crash: self._load_filters() finger_prints.append(crash_finger_print(self.filter_data, crash_lines[start:])) else: in_crash |= '] Hardware name: ' in line + in_crash |= '] ref_tracker: ' in line if in_crash: start = len(crash_lines) crash_lines += last5 From 52dbb1ce8f6cedf4dc1af776daa47934a6c3dee0 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 10 May 2024 19:48:28 -0700 Subject: [PATCH 128/429] ui: flakes: fix radio button selection by URL The "hasAttribute(checked)" only works for checkboxes. Radio buttons don't have that attr on FF unless they are checked. Signed-off-by: Jakub Kicinski --- ui/nipa.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/nipa.js b/ui/nipa.js index 3671ea7..4055b89 100644 --- a/ui/nipa.js +++ b/ui/nipa.js @@ -21,7 +21,7 @@ function nipa_filters_set_from_url() if (!url_val) continue; - if (elem.hasAttribute("checked")) { + if (elem.hasAttribute("checked") || elem.type == "radio") { if (url_val == "0") elem.checked = false; else if (url_val == "1") From 10e0778279bc74cdf88f87b36d02707170c9d41f Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Tue, 14 May 2024 17:53:24 -0700 Subject: [PATCH 129/429] docs: add CI diagram as SVG for the wiki Signed-off-by: Jakub Kicinski --- docs/ci.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/ci.svg diff --git a/docs/ci.svg b/docs/ci.svg new file mode 100644 index 0000000..ca0c094 --- /dev/null +++ b/docs/ci.svg @@ -0,0 +1 @@ + \ No newline at end of file From a6a920f2b4f27b778b3e923627b031b412c18242 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 16 May 2024 09:44:25 -0700 Subject: [PATCH 130/429] mailbot: use a regex for the cover letter check The cover letter check misses cases when character before the 0s is not a space. Use a regex, it's getting too large. Signed-off-by: Jakub Kicinski --- mailbot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mailbot.py b/mailbot.py index 5abc59e..738c18f 100755 --- a/mailbot.py +++ b/mailbot.py @@ -6,6 +6,7 @@ import csv import datetime import os +import re import requests import signal import time @@ -350,7 +351,7 @@ def dkim_ok(self): def _resolve_thread(self, pw): subject = self.get('Subject') - if subject.find(' 0/') != -1 or subject.find(' 00/') != -1: + if re.match(r"\W0+/", subject): obj_type = 'covers' else: obj_type = 'patches' From fb7c45fd3b68b379b7bceb8f79c8df06aaf53ee0 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 17 May 2024 12:01:56 -0700 Subject: [PATCH 131/429] contest: vmksft-p: fix reporting runtime Signed-off-by: Jakub Kicinski --- contest/remote/vmksft-p.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contest/remote/vmksft-p.py b/contest/remote/vmksft-p.py index bbec108..48ecba0 100755 --- a/contest/remote/vmksft-p.py +++ b/contest/remote/vmksft-p.py @@ -204,7 +204,7 @@ def _vm_thread(config, results_path, thr_id, hard_stop, in_queue, out_queue): print(f"INFO: thr-{thr_id} {prog} >> nested tests: {len(tests)} subtests") vm.dump_log(results_path + '/' + file_name, result=retcode, - info={"thr-id": thr_id, "vm-id": vm_id, "time": (t2 - t1).seconds, + info={"thr-id": thr_id, "vm-id": vm_id, "time": (t2 - t1).total_seconds(), "found": indicators, "vm_state": vm.fail_state}) if vm.fail_state: From c1deab99da74fb23c46f8e5272575df03a078ab9 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 17 May 2024 12:50:46 -0700 Subject: [PATCH 132/429] doc-bot: update form letter for 6.10 Signed-off-by: Jakub Kicinski --- form-letters/net-next-closed | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/form-letters/net-next-closed b/form-letters/net-next-closed index 68dfa4e..187ad5b 100644 --- a/form-letters/net-next-closed +++ b/form-letters/net-next-closed @@ -1,8 +1,8 @@ -The merge window for v6.9 has begun and we have already posted our pull +The merge window for v6.10 has begun and we have already posted our pull request. Therefore net-next is closed for new drivers, features, code refactoring and optimizations. We are currently accepting bug fixes only. -Please repost when net-next reopens after March 25th. +Please repost when net-next reopens after May 26th. RFC patches sent for review only are obviously welcome at any time. From 09739f545f7c7c56be10262f15f79d7d2c7bc255 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 17 May 2024 13:24:31 -0700 Subject: [PATCH 133/429] mailbot: fix re API use --- mailbot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mailbot.py b/mailbot.py index 738c18f..0e37633 100755 --- a/mailbot.py +++ b/mailbot.py @@ -351,7 +351,7 @@ def dkim_ok(self): def _resolve_thread(self, pw): subject = self.get('Subject') - if re.match(r"\W0+/", subject): + if re.search(r"\W0+/", subject): obj_type = 'covers' else: obj_type = 'patches' From 45bf742a25f328a3385bbb2466a9da32ef875acf Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 17 May 2024 13:51:04 -0700 Subject: [PATCH 134/429] brancher: log branches into CouchDB There's a CouchDB running on the same server. Log the info into it (in addition to producing the JSONs). Signed-off-by: Jakub Kicinski --- pw_brancher.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/pw_brancher.py b/pw_brancher.py index acf9f5a..e0ad5f3 100755 --- a/pw_brancher.py +++ b/pw_brancher.py @@ -2,11 +2,13 @@ # SPDX-License-Identifier: GPL-2.0 import configparser +import couchdb import datetime import json import os import time from typing import List, Tuple +import uuid from core import NIPA_DIR from core import log, log_open_sec, log_end_sec, log_init @@ -30,9 +32,14 @@ [output] branches=branches.json info=branches-info.json +[db] +name=db-name +user=name +pwd=pass """ +db = None ignore_delegate = {} gate_checks = {} @@ -169,6 +176,20 @@ def apply_local_patches(config, tree) -> List: return extras +def db_insert(config, state, name): + global db + + pub_url = config.get('target', 'public_url') + row = {'_id': uuid.uuid4().hex, + "branch": name, + "date": state["branches"][name], + "base": state["hashes"].get(name, None), + "url": pub_url + " " + name} + row |= state["info"][name] + + db.save(row) + + def create_new(pw, config, state, tree, tgt_remote) -> None: now = datetime.datetime.now(datetime.UTC) pfx = config.get("target", "branch_pfx") @@ -208,6 +229,8 @@ def create_new(pw, config, state, tree, tgt_remote) -> None: state["branches"][branch_name] = now.isoformat() + db_insert(config, state, branch_name) + log_open_sec("Pushing out") tree.git_push(tgt_remote, "HEAD:" + branch_name) log_end_sec() @@ -311,6 +334,14 @@ def prep_remote(config, tree) -> str: return "brancher" +def open_db(config): + user = config.get("db", "user") + pwd = config.get("db", "pwd") + name = config.get("db", "name") + server = couchdb.Server(f'http://%7Buser%7D:%7Bpwd%7D@127.0.0.1:5984/') + return server[name] + + def main() -> None: config = configparser.ConfigParser() config.read(['nipa.config', 'pw.config', 'brancher.config']) @@ -339,6 +370,8 @@ def main() -> None: ignore_delegate = set(config.get('filters', 'ignore_delegate', fallback="").split(',')) global gate_checks gate_checks = set(config.get('filters', 'gate_checks', fallback="").split(',')) + global db + db = open_db(config) tree_obj = None tree_dir = config.get('dirs', 'trees', fallback=os.path.join(NIPA_DIR, "../")) From 65681939f61e5d95523772a7b5986cab9d262953 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 17 May 2024 13:00:13 -0700 Subject: [PATCH 135/429] contest: fetcher: add fetcher state class To make it easier to carry state around create fetcher state class. Signed-off-by: Jakub Kicinski --- contest/results-fetcher.py | 62 +++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/contest/results-fetcher.py b/contest/results-fetcher.py index 56e6a62..ba4085d 100755 --- a/contest/results-fetcher.py +++ b/contest/results-fetcher.py @@ -23,6 +23,15 @@ """ +class FetcherState: + def __init__(self): + self.config = configparser.ConfigParser() + self.config.read(['fetcher.config']) + + # "fetched" is more of a "need state rebuild" + self.fetched = True + + def write_json_atomic(path, data): tmp = path + '.new' with open(tmp, 'w') as fp: @@ -39,36 +48,34 @@ def fetch_remote_run(run_info, remote_state): json.dump(data, fp) -def fetch_remote(remote, seen): +def fetch_remote(fetcher, remote, seen): print("Fetching remote", remote['url']) r = requests.get(remote['url']) try: manifest = json.loads(r.content.decode('utf-8')) except json.decoder.JSONDecodeError: print('Failed to decode manifest from remote:', remote['name']) - return False + return remote_state = seen[remote['name']] - fetched = False for run in manifest: if run['branch'] in remote_state['seen']: continue if not run['url']: # Executor has not finished, yet - fetched |= run['branch'] not in remote_state['wip'] + fetcher.fetched |= run['branch'] not in remote_state['wip'] continue print('Fetching run', run['branch']) fetch_remote_run(run, remote_state) - fetched = True + fetcher.fetched = True with open(os.path.join(remote_state['dir'], 'results.json'), "w") as fp: json.dump(manifest, fp) - return fetched -def build_combined(config, remote_db): - r = requests.get(config.get('input', 'branch_url')) +def build_combined(fetcher, remote_db): + r = requests.get(fetcher.config.get('input', 'branch_url')) branches = json.loads(r.content.decode('utf-8')) branch_info = {} for br in branches: @@ -77,7 +84,7 @@ def build_combined(config, remote_db): combined = [] for remote in remote_db: name = remote['name'] - dir = os.path.join(config.get('output', 'dir'), name) + dir = os.path.join(fetcher.config.get('output', 'dir'), name) print('Combining from remote', name) manifest = os.path.join(dir, 'results.json') @@ -110,18 +117,18 @@ def build_combined(config, remote_db): return combined -def build_seen(config, remote_db): +def build_seen(fetcher, remote_db): seen = {} for remote in remote_db: seen[remote['name']] = {'seen': set(), 'wip': set()} # Prepare local state name = remote['name'] - dir = os.path.join(config.get('output', 'dir'), name) + dir = os.path.join(fetcher.config.get('output', 'dir'), name) seen[name]['dir'] = dir os.makedirs(dir, exist_ok=True) - url = config.get('output', 'url_pfx') + '/' + name + url = fetcher.config.get('output', 'url_pfx') + '/' + name seen[name]['url'] = url # Read the files @@ -143,36 +150,29 @@ def build_seen(config, remote_db): return seen -def one_check(config, remote_db, seen): - fetched = False - for remote in remote_db: - fetched |= fetch_remote(remote, seen) - return fetched - - def main() -> None: - config = configparser.ConfigParser() - config.read(['fetcher.config']) + fetcher = FetcherState() - with open(config.get('input', 'remote_db'), "r") as fp: + with open(fetcher.config.get('input', 'remote_db'), "r") as fp: remote_db = json.load(fp) - fetched = True while True: - if fetched: - seen = build_seen(config, remote_db) + if fetcher.fetched: + seen = build_seen(fetcher, remote_db) + fetcher.fetched = False - fetched = one_check(config, remote_db, seen) + for remote in remote_db: + fetch_remote(fetcher, remote, seen) - if fetched: + if fetcher.fetched: print('Generating combined') - results = build_combined(config, remote_db) + results = build_combined(fetcher, remote_db) - combined = os.path.join(config.get('output', 'dir'), - config.get('output', 'combined')) + combined = os.path.join(fetcher.config.get('output', 'dir'), + fetcher.config.get('output', 'combined')) write_json_atomic(combined, results) - time.sleep(int(config.get('cfg', 'refresh'))) + time.sleep(int(fetcher.config.get('cfg', 'refresh'))) if __name__ == "__main__": From 1c1d96cbbaa9c9ba41421dbd135bd5399125542f Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 17 May 2024 13:25:23 -0700 Subject: [PATCH 136/429] contest: fetcher: populate CouchDB with the results Feed the results into CouchDB as we get them. For now produce all-results.json as well. Signed-off-by: Jakub Kicinski --- contest/results-fetcher.py | 82 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 79 insertions(+), 3 deletions(-) diff --git a/contest/results-fetcher.py b/contest/results-fetcher.py index ba4085d..c3ef9a3 100755 --- a/contest/results-fetcher.py +++ b/contest/results-fetcher.py @@ -2,11 +2,13 @@ # SPDX-License-Identifier: GPL-2.0 import configparser +import couchdb import datetime import json import os import requests import time +import uuid """ @@ -20,6 +22,11 @@ dir=/path/to/output url_pfx=relative/within/server combined=name-of-manifest.json +[db] +results-name=db-name +branches-name=db-name +user=name +pwd=pass """ @@ -31,6 +38,71 @@ def __init__(self): # "fetched" is more of a "need state rebuild" self.fetched = True + user = self.config.get("db", "user") + pwd = self.config.get("db", "pwd") + server = couchdb.Server(f'http://%7Buser%7D:%7Bpwd%7D@127.0.0.1:5984/') + self.res_db = server[self.config.get("db", "results-name", fallback="results")] + self.brn_db = server[self.config.get("db", "branches-name", fallback="branches")] + + def _one(self, rows): + rows = list(rows) + if len(rows) != 1: + raise Exception("Expected 1 row, found", rows) + return rows[0] + + def get_branch(self, name): + branch_info = self.brn_db.find({ + 'selector': { + 'branch': name + } + }) + return self._one(branch_info) + + def get_wip_row(self, remote, run): + rows = self.res_db.find({ + 'selector': { + 'branch': run["branch"], + 'remote': remote["name"], + 'executor': run["executor"], + 'url': None + } + }) + for row in rows: + return row + + def insert_wip(self, remote, run): + existing = self.get_wip_row(remote, run) + + branch_info = self.get_branch(run["branch"]) + + data = run.copy() + if existing: + data['_id'] = existing['_id'] + data['_rev'] = existing['_rev'] + else: + data['_id'] = uuid.uuid4().hex + data["remote"] = remote["name"] + when = datetime.datetime.fromisoformat(branch_info['date']) + data["start"] = str(when) + when += datetime.timedelta(hours=2, minutes=58) + data["end"] = str(when) + data["results"] = None + + self.res_db.save(data) + + def insert_real(self, remote, run): + existing = self.get_wip_row(remote, run) + + data = run.copy() + if existing: + data['_id'] = existing['_id'] + data['_rev'] = existing['_rev'] + else: + data['_id'] = uuid.uuid4().hex + data["remote"] = remote["name"] + + self.res_db.save(data) + def write_json_atomic(path, data): tmp = path + '.new' @@ -39,10 +111,12 @@ def write_json_atomic(path, data): os.rename(tmp, path) -def fetch_remote_run(run_info, remote_state): +def fetch_remote_run(fetcher, remote, run_info, remote_state): r = requests.get(run_info['url']) data = json.loads(r.content.decode('utf-8')) + fetcher.insert_real(remote, data) + file = os.path.join(remote_state['dir'], os.path.basename(run_info['url'])) with open(file, "w") as fp: json.dump(data, fp) @@ -62,11 +136,13 @@ def fetch_remote(fetcher, remote, seen): if run['branch'] in remote_state['seen']: continue if not run['url']: # Executor has not finished, yet - fetcher.fetched |= run['branch'] not in remote_state['wip'] + if run['branch'] not in remote_state['wip']: + fetcher.insert_wip(remote, run) + fetcher.fetched = True continue print('Fetching run', run['branch']) - fetch_remote_run(run, remote_state) + fetch_remote_run(fetcher, remote, run, remote_state) fetcher.fetched = True with open(os.path.join(remote_state['dir'], 'results.json'), "w") as fp: From 17a364f1266c67bec5faf8661f3bdc81cc3cad4b Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 17 May 2024 16:35:36 -0700 Subject: [PATCH 137/429] contest: backend: add trivial query endpoint Add a simple flask app to query CouchDB. /query=branches={branch-cnt} /query=branch-name={name} We have two views, one for counting and one for fetching docs. Actually fetching the rows is kinda slow but user probably doesn't want more than 100 branches, so we're fine. Signed-off-by: Jakub Kicinski --- contest/backend/query.py | 65 ++++++++++++++++++++++++++++++++++++++++ ui/contest.js | 2 +- ui/status.js | 2 +- 3 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 contest/backend/query.py diff --git a/contest/backend/query.py b/contest/backend/query.py new file mode 100644 index 0000000..9cf361a --- /dev/null +++ b/contest/backend/query.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 + + +from flask import Flask +from flask import request +import couchdb +import os +import datetime + + +app = Flask("NIPA contest query") + +user = os.getenv('DB_USER') +pwd = os.getenv('DB_PWD') +couch = couchdb.Server(f'http://%7Buser%7D:%7Bpwd%7D@127.0.0.1:5984/') +res_db = couch["results"] + + +def branches_to_rows(br_cnt): + data = res_db.view('branch/rows', None, + group=True, descending=True, limit=br_cnt) + cnt = 0 + for row in data: + cnt += row.value + return cnt + + +@app.route('/') +def hello(): + return '

boo!

' + + +@app.route('/results') +def results(): + global couch + + t1 = datetime.datetime.now() + + br_name = request.args.get('branch-name') + if br_name: + t1 = datetime.datetime.now() + rows = [r.value for r in res_db.view('branch/row_fetch', None, + key=br_name, limit=100)] + t2 = datetime.datetime.now() + print("Query for exact branch took: ", str(t2-t1)) + return rows + + br_cnt = request.args.get('branches') + try: + br_cnt = int(br_cnt) + except: + br_cnt = None + if not br_cnt: + br_cnt = 10 + + need_rows = branches_to_rows(br_cnt) + t2 = datetime.datetime.now() + data = [r.value for r in res_db.view('branch/row_fetch', None, + descending=True, limit=need_rows)] + + t3 = datetime.datetime.now() + print(f"Query for {br_cnt} branches, {need_rows} records took: {str(t3-t1)} ({str(t2-t1)}+{str(t3-t2)})") + + return data diff --git a/ui/contest.js b/ui/contest.js index d82e832..469d990 100644 --- a/ui/contest.js +++ b/ui/contest.js @@ -168,6 +168,6 @@ function do_it() $.get("contest/filters.json", filters_loaded) }); $(document).ready(function() { - $.get("contest/all-results.json", results_loaded) + $.get("query/results?branches=100", results_loaded) }); } diff --git a/ui/status.js b/ui/status.js index e3af5aa..69709b1 100644 --- a/ui/status.js +++ b/ui/status.js @@ -765,7 +765,7 @@ function do_it() $.get("static/nipa/branch-results.json", branch_res_doit) }); $(document).ready(function() { - $.get("contest/all-results.json", results_loaded) + $.get("query/results?branches=6", results_loaded) }); $(document).ready(function() { $.get("static/nipa/branches-info.json", branches_loaded) From 6b9b8e455bf92404d20defc194e02ba2660c79e5 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Mon, 20 May 2024 16:40:04 -0700 Subject: [PATCH 138/429] ui: use the queries in a more serious fashion Let the user control how many rows we query. Signed-off-by: Jakub Kicinski --- ui/contest.html | 13 +++++- ui/contest.js | 114 ++++++++++++++++++++++++++++++++++++++++++------ ui/flakes.html | 3 +- ui/flakes.js | 23 ++++++++-- ui/nipa.js | 9 +++- 5 files changed, 138 insertions(+), 24 deletions(-) diff --git a/ui/contest.html b/ui/contest.html index d9c247c..54534d3 100644 --- a/ui/contest.html +++ b/ui/contest.html @@ -11,7 +11,6 @@ - +
+
+ Loading: + +
+ +
 
+ +
+ +
Filtering: diff --git a/ui/contest.js b/ui/contest.js index 469d990..2e5f629 100644 --- a/ui/contest.js +++ b/ui/contest.js @@ -30,14 +30,16 @@ function load_result_table(data_raw) $("#results tr").slice(1).remove(); let warn_box = document.getElementById("fl-warn-box"); - if (!exec_filter && !test_filter && !branch_filter) { - warn_box.innerHTML = "Set an executor, branch or test filter. Otherwise this page will set your browser on fire..."; - return; - } else { - warn_box.innerHTML = ""; - } + warn_box.innerHTML = ""; + + let row_count = 0; $.each(data_raw, function(i, v) { + if (row_count >= 5000) { + warn_box.innerHTML = "Reached 5000 rows. Set an executor, branch or test filter. Otherwise this page will set your browser on fire..."; + return 0; + } + if (branch_filter && branch_filter != v.branch) return 1; @@ -86,6 +88,8 @@ function load_result_table(data_raw) outputs.innerHTML = "outputs"; hist.innerHTML = "history"; flake.innerHTML = "matrix"; + + row_count++; }); }); } @@ -103,6 +107,13 @@ function add_option_filter(data_raw, elem_id, field) var elem = document.getElementById(elem_id); var values = new Set(); + // Re-create "all" + const opt = document.createElement('option'); + opt.value = ""; + opt.innerHTML = "-- all --"; + elem.appendChild(opt); + + // Create the dynamic entries $.each(data_raw, function(i, v) { values.add(v[field]); }); @@ -123,17 +134,44 @@ let xfr_todo = 2; let branch_urls = {}; let loaded_data = null; -function loaded_one() +function reload_select_filters(first_load) { - if (--xfr_todo) - return; + let old_values = new Object(); + + // Save old values before we wipe things out + for (const elem_id of ["branch", "executor", "remote"]) { + var elem = document.getElementById(elem_id); + old_values[elem_id] = elem.value; + } + + // Keep the "all" option, remove the rest + $("select option").remove(); // We have all JSONs now, do processing. add_option_filter(loaded_data, "branch", "branch"); add_option_filter(loaded_data, "executor", "executor"); add_option_filter(loaded_data, "remote", "remote"); - nipa_filters_set_from_url(); + // On first load we use URL, later we try to keep settings user tweaked + if (first_load) + nipa_filters_set_from_url(); + + for (const elem_id of ["branch", "executor", "remote"]) { + var elem = document.getElementById(elem_id); + + if (!first_load) + elem.value = old_values[elem_id]; + if (elem.selectedIndex == -1) + elem.selectedIndex = 0; + } +} + +function loaded_one() +{ + if (--xfr_todo) + return; + + reload_select_filters(true); nipa_filters_enable(results_update); results_update(); @@ -155,19 +193,67 @@ function results_loaded(data_raw) find_branch_urls(data_raw); + const had_data = loaded_data; loaded_data = data_raw; - loaded_one(); + if (!had_data) { + loaded_one(); + } else if (!xfr_todo) { + reload_select_filters(false); + results_update(); + } +} + +function reload_data(event) +{ + const br_cnt = document.getElementById("ld_cnt"); + const br_name = document.getElementById("ld_branch"); + + if (event) { + if (event.target == br_name) + br_cnt.value = 1; + else if (event.target == br_cnt) + br_name.value = ""; + } + + let req_url = "query/results?"; + if (br_name.value) { + req_url += "branch-name=" + br_name.value; + } else { + req_url += "branches=" + br_cnt.value; + } + + $(document).ready(function() { + $.get(req_url, results_loaded) + }); + + let warn_box = document.getElementById("fl-warn-box"); + warn_box.innerHTML = "Loading..."; } function do_it() { + const urlParams = new URLSearchParams(window.location.search); + + nipa_input_set_from_url("https://github.com/ld-pw"); + /* The filter is called "branch" the load selector is called "ld_branch" + * auto-copy will not work, but we want them to match, initially. + */ + if (urlParams.get("branch")) { + document.getElementById("ld_branch").value = urlParams.get("branch"); + document.getElementById("ld_cnt").value = 1; + } + + const ld_pw = document.querySelectorAll("[name=ld-pw]"); + for (const one of ld_pw) { + one.addEventListener("change", reload_data); + one.disabled = false; + } + /* * Please remember to keep these assets in sync with `scripts/ui_assets.sh` */ $(document).ready(function() { $.get("contest/filters.json", filters_loaded) }); - $(document).ready(function() { - $.get("query/results?branches=100", results_loaded) - }); + reload_data(null); } diff --git a/ui/flakes.html b/ui/flakes.html index 777eda9..14d463f 100644 --- a/ui/flakes.html +++ b/ui/flakes.html @@ -11,7 +11,6 @@ - +
diff --git a/ui/flakes.js b/ui/flakes.js index ea52669..299f608 100644 --- a/ui/flakes.js +++ b/ui/flakes.js @@ -152,19 +152,34 @@ function results_loaded(data_raw) }); data_raw.sort(function(a, b){return b.end - a.end;}); + const had_data = loaded_data; loaded_data = data_raw; - loaded_one(); + if (!had_data) { + loaded_one(); + } else if (!xfr_todo) { + results_update(); + } +} + +function reload_data() +{ + let br_cnt = document.getElementById("br-cnt"); + $(document).ready(function() { + $.get("query/results?branches=" + br_cnt.value, results_loaded) + }); } function do_it() { + let br_cnt = document.getElementById("br-cnt"); + + br_cnt.addEventListener("change", reload_data); + /* * Please remember to keep these assets in sync with `scripts/ui_assets.sh` */ $(document).ready(function() { $.get("contest/filters.json", filters_loaded) }); - $(document).ready(function() { - $.get("contest/all-results.json", results_loaded) - }); + reload_data(); } diff --git a/ui/nipa.js b/ui/nipa.js index 4055b89..fac0fec 100644 --- a/ui/nipa.js +++ b/ui/nipa.js @@ -10,10 +10,10 @@ function nipa_filters_enable(update_cb) } } -function nipa_filters_set_from_url() +function nipa_input_set_from_url(https://github.com/name) { const urlParams = new URLSearchParams(window.location.search); - const filters = document.querySelectorAll("[name=fl-pw]"); + const filters = document.querySelectorAll("[name="+ name + "]"); for (const elem of filters) { let url_val = urlParams.get(elem.id); @@ -32,6 +32,11 @@ function nipa_filters_set_from_url() } } +function nipa_filters_set_from_url() +{ + nipa_input_set_from_url("https://github.com/fl-pw"); +} + // ------------------ let nipa_filters_json = null; From cef6747eba6b9e38b49a225e03cf2239c9dda869 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Mon, 20 May 2024 21:35:31 -0700 Subject: [PATCH 139/429] ui: status: optionally hide all-pass results The results on the status page are getting cluttered. There's too many small runners taking up space. Hide "all-pass" runs by default. Add a checkbox to show them. Since there's a lot fewer rows now we can bump the number of displayed branches from 6 (18 hours) to 10 (30 hours). Signed-off-by: Jakub Kicinski --- ui/nipa.css | 13 ++++++ ui/status.html | 4 ++ ui/status.js | 119 +++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 123 insertions(+), 13 deletions(-) diff --git a/ui/nipa.css b/ui/nipa.css index 98f96b9..a32c650 100644 --- a/ui/nipa.css +++ b/ui/nipa.css @@ -14,6 +14,16 @@ tr:nth-child(even) { background-color: #eeeeee; } +.end-row td { + border-width: 1px 1px 6px 1px; + border-color: white; +} + +.summary-row td { + text-align: right; + font-style: italic; +} + .box-pass { background-color: green; } .box-skip { background-color: royalblue; } .box-flake { background-color: red; } @@ -65,4 +75,7 @@ tr:nth-child(even) { tr:nth-child(odd) { background-color: #303030; } + .end-row td { + border-color: #202020; + } } diff --git a/ui/status.html b/ui/status.html index 4a0473b..aa78258 100644 --- a/ui/status.html +++ b/ui/status.html @@ -77,6 +77,10 @@

Recent crashes

Continuous testing results

+
+ + +
diff --git a/ui/status.js b/ui/status.js index 69709b1..79b4b96 100644 --- a/ui/status.js +++ b/ui/status.js @@ -398,8 +398,70 @@ function load_fails(data_raw) }); } +function add_summaries(table, summary, reported) +{ + let row = table.insertRow(); + let i = 0; + + let cell = row.insertCell(i++); // branch + cell.innerHTML = "summary"; + + cell = row.insertCell(i++); // remote + let count_line = summary["remote-cnt"] + " remotes"; + if (summary["hidden"]) { + if (summary["hidden"] == summary["remote-cnt"]) + count_line += " (all hidden)"; + else + count_line += " (" + summary["hidden"] + " hidden)"; + } + + cell.innerHTML = count_line; + + cell = row.insertCell(i++); // time + cell.innerHTML = msec_to_str(summary["time-pass"]); + + let str_psf = {"str": "", "overall": ""}; + + colorify_str_psf(str_psf, "fail", summary["fail"], "red"); + colorify_str_psf(str_psf, "skip", summary["skip"], "#809fff"); + colorify_str_psf(str_psf, "pass", summary["total"], "green"); + + var link_to_contest = "" + str_psf.str + ""; + + cell = row.insertCell(i++); // tests + cell.innerHTML = link_to_contest; + + cell = row.insertCell(i++); // result + cell.setAttribute("style", "text-align: left; font-weight: bold; font-style: normal;"); + cell.innerHTML = colorify_basic(branch_results[summary.branch]); + + row.setAttribute("class", "summary-row"); +} + +function reset_summary(summary) +{ + summary["branch"] = null; + summary["remote-cnt"] = 0; + summary["time-pass"] = 0; + summary["total"] = 0; + summary["skip"] = 0; + summary["fail"] = 0; + summary["hidden"] = 0; +} + function load_result_table_one(data_raw, table, reported, avgs) { + const summarize = document.getElementById("contest-summary").checked; + let summary = {}; + + reset_summary(summary); + $.each(data_raw, function(i, v) { var pass = 0, skip = 0, fail = 0, total = 0, ignored = 0; var link = v.link; @@ -425,6 +487,29 @@ function load_result_table_one(data_raw, table, reported, avgs) if (!total && ignored && v.executor != "brancher") return 1; + var t_start = new Date(v.start); + var t_end = new Date(v.end); + + if (v.remote == "brancher") { + summary["branch"] = v.branch; + add_summaries(table, summary, reported); + reset_summary(summary); + } else { + summary["total"] += total; + if (total) { + summary["remote-cnt"] += 1; + if (summary["time-pass"] < t_end - t_start) + summary["time-pass"] = t_end - t_start; + } + + summary["skip"] += skip; + summary["fail"] += fail; + if (summarize && total && total == pass) { + summary["hidden"] += 1; + return 1; + } + } + var str_psf = {"str": "", "overall": ""}; colorify_str_psf(str_psf, "fail", fail, "red"); @@ -444,8 +529,6 @@ function load_result_table_one(data_raw, table, reported, avgs) var branch = row.insertCell(0); var remote = row.insertCell(1); - var t_start = new Date(v.start); - var t_end = new Date(v.end); var a = ""; if (v.remote != "brancher") { @@ -498,7 +581,7 @@ function load_result_table_one(data_raw, table, reported, avgs) } } else { let res = row.insertCell(2); - let br_res, br_pull = ""; + let br_pull = ""; if (v.start) remote.innerHTML = v.start.toLocaleString(); @@ -509,10 +592,8 @@ function load_result_table_one(data_raw, table, reported, avgs) br_pull = " (pull: " + v.pull_status + ")"; branch.innerHTML = a + v.branch + "" + br_pull; branch.setAttribute("colspan", "2"); - br_res = ''; - br_res += colorify_basic(branch_results[v.branch]); - br_res += ''; - res.innerHTML = br_res; + res.innerHTML = ""; + row.setAttribute("class", "end-row"); } }); } @@ -522,7 +603,7 @@ function rem_exe(v) return v.remote + "/" + v.executor; } -function load_result_table(data_raw) +function load_result_table(data_raw, reload) { var table = document.getElementById("contest"); var table_nr = document.getElementById("contest-purgatory"); @@ -558,7 +639,7 @@ function load_result_table(data_raw) }); // Continue with only 6 most recent branches - let recent_branches = new Set(Array.from(branches).sort().slice(-6)); + let recent_branches = new Set(Array.from(branches).sort().slice(-10)); data_raw = $.grep(data_raw, function(v, i) { return recent_branches.has(v.branch); }); @@ -640,9 +721,12 @@ function load_result_table(data_raw) return b.end - a.end; }); + $("#contest tr").slice(1).remove(); + $("#contest-purgatory tr").slice(1).remove(); load_result_table_one(data_raw, table, true, avgs); load_result_table_one(data_raw, table_nr, false, avgs); - load_fails(data_raw); + if (!reload) + load_fails(data_raw); } let xfr_todo = 4; @@ -651,10 +735,19 @@ let branches_info = null; let branches = new Set(); let branch_results = {}; +function reload_results() +{ + load_result_table(all_results, true); +} + function loaded_one() { - if (!--xfr_todo) - load_result_table(all_results); + if (!--xfr_todo) { + load_result_table(all_results, false); + + let summary_checkbox = document.getElementById("contest-summary"); + summary_checkbox.addEventListener("change", reload_results); + } } function results_loaded(data_raw) @@ -765,7 +858,7 @@ function do_it() $.get("static/nipa/branch-results.json", branch_res_doit) }); $(document).ready(function() { - $.get("query/results?branches=6", results_loaded) + $.get("query/results?branches=10", results_loaded) }); $(document).ready(function() { $.get("static/nipa/branches-info.json", branches_loaded) From 09420046766214a258c131a684b75b82ec74b429 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Mon, 20 May 2024 21:45:35 -0700 Subject: [PATCH 140/429] ui: status: link to test from crashes Add a URL to the test that crashed for ease of navigation. Signed-off-by: Jakub Kicinski --- ui/status.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/ui/status.js b/ui/status.js index 79b4b96..e589e88 100644 --- a/ui/status.js +++ b/ui/status.js @@ -370,6 +370,20 @@ function avg_time_e(avgs, v) avgs[ent_name]["sum"] / avgs[ent_name]["cnt"]; } +function wrap_link(objA, objB, text) +{ + let url = null; + + if ("link" in objA) + url = objA.link; + else if ("link" in objB) + url = objB.link; + else + return text; + + return "" + text + ""; +} + function load_fails(data_raw) { var fail_table = document.getElementById("recent-fails"); @@ -390,7 +404,7 @@ function load_fails(data_raw) if ("crashes" in r) { for (crash of r.crashes) { let i = 0, row = crash_table.insertRow(); - row.insertCell(i++).innerHTML = r.test; + row.insertCell(i++).innerHTML = wrap_link(r, v, r.test); row.insertCell(i++).innerHTML = crash; } } From 583b1e3362995cbac95779e70247b2a6d8c116b6 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Mon, 20 May 2024 21:51:18 -0700 Subject: [PATCH 141/429] ui: status: move the recent failures to the right column Now that results get summarized the right column has more space than the left. We can fit recent crashes on the right for better "at a glance" usability. Signed-off-by: Jakub Kicinski --- ui/status.html | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/ui/status.html b/ui/status.html index aa78258..b9e10a3 100644 --- a/ui/status.html +++ b/ui/status.html @@ -56,17 +56,6 @@

Build processing



-

Recent failures

-
Branch
- - - - - - - -
BranchRemoteTestResultRetry
-

Recent crashes

@@ -90,6 +79,17 @@

Continuous testing results

Result
+
+

Recent failures

+ + + + + + + + +
BranchRemoteTestResultRetry
From 3b28d09ca31f509c9e1e08e4bd0db1f87786b7ed Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 25 May 2024 17:36:53 -0700 Subject: [PATCH 142/429] ui: status: use one column on mobile Status and checks are unreadable on mobile. Use one column on high-DPI screens. Signed-off-by: Jakub Kicinski --- ui/contest.html | 12 ++++++------ ui/flakes.html | 18 +++++++++--------- ui/nipa.css | 17 ++++++++++++++--- 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/ui/contest.html b/ui/contest.html index 54534d3..b38920a 100644 --- a/ui/contest.html +++ b/ui/contest.html @@ -21,7 +21,7 @@
-
+
Loading: @@ -35,8 +35,8 @@
Filtering: -
-
+
+
@@ -54,7 +54,7 @@
-
+
-
+
@@ -89,7 +89,7 @@
-
+
Loading...
diff --git a/ui/flakes.html b/ui/flakes.html index 14d463f..390bc18 100644 --- a/ui/flakes.html +++ b/ui/flakes.html @@ -21,29 +21,29 @@
-
+
Filtering: -
-
+
+
-
+

-
+

-
+
Sort: @@ -58,12 +58,12 @@
-
+
Loading...
-
-
+
+
diff --git a/ui/nipa.css b/ui/nipa.css index a32c650..c96c360 100644 --- a/ui/nipa.css +++ b/ui/nipa.css @@ -29,11 +29,22 @@ tr:nth-child(even) { .box-flake { background-color: red; } .box-fail { background-color: #d06060; } -.row { - display: flex; +@media screen and (max-resolution: 116dpi) { + .row { + display: flex; + } + + .column { + flex: 50%; + padding: 1em; + } } -.column { +/* layout inside fieldsets even on small screens */ +.row-small { + display: flex; +} +.column-small { flex: 50%; padding: 1em; } From a471f10fb2481d253b296b2d796efe61c9c77756 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 1 Jun 2024 12:58:46 -0700 Subject: [PATCH 143/429] ui: status: set preferred y axis length for builder times It's easier to judge if we're in trouble "at a glance" if the Y axis of the times is fixed. Basically now if we have data points next to the top of the graph, it's not good. Signed-off-by: Jakub Kicinski --- ui/status.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/status.js b/ui/status.js index e589e88..ea17736 100644 --- a/ui/status.js +++ b/ui/status.js @@ -91,6 +91,7 @@ function load_times(data, canva_id) ticks: { stepSize: 3 }, + suggestedMax: 12, beginAtZero: true }, x: { @@ -103,6 +104,7 @@ function load_times(data, canva_id) ticks: { stepSize: 24 }, + suggestedMax: 12, reverse: true } } From c3a11c7fc7c1b46af7b92fe97d9cdf327a037641 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 1 Jun 2024 13:00:09 -0700 Subject: [PATCH 144/429] ui: status: drop trimming branch count Now that the DB query sets the branch count we don't have to trim data_raw any more, of filter branches. Signed-off-by: Jakub Kicinski --- ui/status.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/ui/status.js b/ui/status.js index ea17736..64fe419 100644 --- a/ui/status.js +++ b/ui/status.js @@ -654,11 +654,6 @@ function load_result_table(data_raw, reload) } }); - // Continue with only 6 most recent branches - let recent_branches = new Set(Array.from(branches).sort().slice(-10)); - data_raw = $.grep(data_raw, - function(v, i) { return recent_branches.has(v.branch); }); - // Calculate expected runtimes var avgs = {}; $.each(data_raw, function(i, v) { @@ -701,7 +696,7 @@ function load_result_table(data_raw, reload) } let known_exec_set = new Set(Object.keys(known_execs)); - for (br of recent_branches) { + for (br of branches) { for (re of known_exec_set) { if (branch_execs[br].has(re)) continue; From 481ac09083b123a6f1f0807e75b689ca9e137524 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 1 Jun 2024 13:02:37 -0700 Subject: [PATCH 145/429] ui: status: add a list of missing tests Now that we print branch test count summary it's very evident that the total test count wobbles by 1 or 2 on every run. Add a list of tests which are missing results for some branches. Signed-off-by: Jakub Kicinski --- ui/nipa.js | 5 ++++ ui/status.html | 8 +++++++ ui/status.js | 64 +++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/ui/nipa.js b/ui/nipa.js index fac0fec..9d6bad1 100644 --- a/ui/nipa.js +++ b/ui/nipa.js @@ -1,3 +1,8 @@ +function nipa_test_fullname(v, r) +{ + return v.remote + "/" + v.executor + "/" + r.group + "/" + r.test; +} + function nipa_filters_enable(update_cb) { let warn_box = document.getElementById("fl-warn-box"); diff --git a/ui/status.html b/ui/status.html index b9e10a3..b7c93ab 100644 --- a/ui/status.html +++ b/ui/status.html @@ -90,6 +90,14 @@

Recent failures

pass
Retry
+
+

Tests with missing results

+ + + + + +
Test# missing
diff --git a/ui/status.js b/ui/status.js index 64fe419..99bac7d 100644 --- a/ui/status.js +++ b/ui/status.js @@ -414,6 +414,62 @@ function load_fails(data_raw) }); } +function load_partial_tests(data) +{ + let table = document.getElementById("test-presence"); + let pending_executors = {}; + let count_map = {}; + let total = 0; + + $.each(data, function(i, v) { + // Ignore tests from AWOL executors, that should be rare + if (v.executor in awol_executors) + return 1; + + if (v.executor == "brancher") { + total++; + return 1; + } + + // Track pending executors + if (v.results == null) { + let name = rem_exe(v); + + if (name in pending_executors) + pending_executors[name]++; + else + pending_executors[name] = 1; + } + + $.each(v.results, function(i, r) { + let name = nipa_test_fullname(v, r); + + if (name in count_map) + count_map[name]++; + else + count_map[name] = 1; + }); + }); + + for (const name of Object.keys(count_map)) { + let missing = total - count_map[name]; + + if (!missing) + continue; + for (const pending in Object.keys(pending_executors)) { + if (name.startsWith(pending)) { + if (missing == pending_executors[pending]) + continue; + break; + } + } + + let row = table.insertRow(); + row.insertCell(0).innerHTML = name; + row.insertCell(1).innerHTML = missing; + } +} + function add_summaries(table, summary, reported) { let row = table.insertRow(); @@ -619,6 +675,8 @@ function rem_exe(v) return v.remote + "/" + v.executor; } +var awol_executors; + function load_result_table(data_raw, reload) { var table = document.getElementById("contest"); @@ -696,6 +754,7 @@ function load_result_table(data_raw, reload) } let known_exec_set = new Set(Object.keys(known_execs)); + awol_executors = new Set(); for (br of branches) { for (re of known_exec_set) { if (branch_execs[br].has(re)) @@ -708,6 +767,7 @@ function load_result_table(data_raw, reload) "start" : branch_start[br], "end" : 0, }); + awol_executors.add(known_execs[re].executor); } } @@ -736,8 +796,10 @@ function load_result_table(data_raw, reload) $("#contest-purgatory tr").slice(1).remove(); load_result_table_one(data_raw, table, true, avgs); load_result_table_one(data_raw, table_nr, false, avgs); - if (!reload) + if (!reload) { load_fails(data_raw); + load_partial_tests(data_raw); + } } let xfr_todo = 4; From 8ccbce57730a801fd695db0f5407bb55ca4d95d3 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 1 Jun 2024 13:06:20 -0700 Subject: [PATCH 146/429] ui: status: move recent crashes to the right column Now that we fold results the right column is much sparser, and has some empty space. Move the recent crashes there, it also makes sense since it goes nicely with recent failures. Signed-off-by: Jakub Kicinski --- ui/status.html | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ui/status.html b/ui/status.html index b7c93ab..e392274 100644 --- a/ui/status.html +++ b/ui/status.html @@ -55,14 +55,6 @@

Build processing


-
-

Recent crashes

- - - - - -
TestCrashes

Continuous testing results

@@ -91,6 +83,14 @@

Recent failures


+

Recent crashes

+ + + + + +
TestCrashes
+

Tests with missing results

From becf42ebbcb5a79081b1fc69fca3c6eef5ddf56b Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Tue, 4 Jun 2024 06:22:26 -0700 Subject: [PATCH 147/429] ui: status: fix counting pending results as not missing Signed-off-by: Jakub Kicinski --- ui/status.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ui/status.js b/ui/status.js index 99bac7d..2b41ff1 100644 --- a/ui/status.js +++ b/ui/status.js @@ -456,13 +456,15 @@ function load_partial_tests(data) if (!missing) continue; - for (const pending in Object.keys(pending_executors)) { + for (const pending of Object.keys(pending_executors)) { if (name.startsWith(pending)) { if (missing == pending_executors[pending]) - continue; + missing = 0; break; } } + if (!missing) + continue; let row = table.insertRow(); row.insertCell(0).innerHTML = name; From 21059fbe9b588aacf5e3d842d998e3163cbe2cd7 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 7 Jun 2024 14:48:51 -0700 Subject: [PATCH 148/429] contest: kunit: add option to split expected bad with pipe Some tests have a space in their name, so splitting them with the default str.split() will not work. Try pipe (|) first, if it doesn't give us 3 tokens fall back to split(). Signed-off-by: Jakub Kicinski --- contest/remote/kunit.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contest/remote/kunit.py b/contest/remote/kunit.py index e313d12..4f4b02d 100755 --- a/contest/remote/kunit.py +++ b/contest/remote/kunit.py @@ -72,7 +72,9 @@ def load_expected(config): for l in lines: if not l: continue - words = l.split() + words = l.strip().split('|') + if len(words) != 3: + words = l.split() if words[0] not in expected: expected[words[0]] = {} grp = expected[words[0]] From bf87f3d3f3f756cd9d0e617bc9e71d7df92e5ec2 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 7 Jun 2024 13:29:23 -0700 Subject: [PATCH 149/429] contest: kunit: add sub-cases Format the results as the new "l2" thing, with an optional "results" entry in every case, which will hold sub-cases. The first level of results is unchanged* (and must contain the overall "result"), the second layer of results only have test name and result. No futher nesting. * There's actually a change in the results with this patch, because the previous parsing was broken. I _intended_ to descend recursively to subgroups but it only worked for the first layer (it tried to go two levels deep when recursing). Signed-off-by: Jakub Kicinski --- contest/remote/kunit.py | 54 +++++++++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/contest/remote/kunit.py b/contest/remote/kunit.py index 4f4b02d..5e39b04 100755 --- a/contest/remote/kunit.py +++ b/contest/remote/kunit.py @@ -81,32 +81,54 @@ def load_expected(config): if words[1] not in grp: grp[words[1]] = {} grp[words[1]] = str_to_code[words[2]] - return expected + return expected -def summary_result(expected, got, link, sub_path=""): +def summary_flat(expected, got, sub_path=""): if sub_path: sub_path += '.' + overall_code = 0 results = [] bad_tests = [] + for case in got["test_cases"]: + code = str_to_code[case["status"]] + + exp = expected.get(got["name"], {}).get(case["name"]) + if exp and exp == code: + continue + + overall_code = max(code, overall_code) + results.append({'test': sub_path + case["name"], + 'result': code_to_str[code]}) + if code: + bad_tests.append(f"{got['name']} {case['name']} {case['status']}") + for sub_group in got["sub_groups"]: - for case in sub_group["test_cases"]: - code = str_to_code[case["status"]] + ov, bt, res = summary_flat(expected, sub_group, sub_path + sub_group["name"]) + overall_code = max(ov, overall_code) + results += res + bad_tests += bt - exp = expected.get(sub_group["name"], {}).get(case["name"]) - if exp and exp == code: - continue + return overall_code, bad_tests, results - results.append({'test': case["name"], - 'group': sub_path + sub_group["name"], - 'result': code_to_str[code], 'link': link}) - if code: - bad_tests.append(f"{sub_group['name']} {case['name']} {case['status']}") - for grp in sub_group["sub_groups"]: - bt, res = summary_result(expected, grp, link, sub_path + grp["name"]) - results += res - bad_tests += bt + +def summary_result(expected, got, link, sub_path=""): + results = [] + bad_tests = [] + for sub_group in got["sub_groups"]: + code, bt, res = summary_flat(expected, sub_group) + + data = { + 'test': sub_group["name"], + 'group': 'kunit', + 'result': code_to_str[code], + 'results': res, + 'link': link + } + results.append(data) + + bad_tests += bt return bad_tests, results From fb68c46012e06e0024a67b97ca1a2c841967ef63 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 7 Jun 2024 12:48:20 -0700 Subject: [PATCH 150/429] contest: kunit: support [executor.init] config option Read the executor init from the config, so that we can force the run on restart (kunit is a very quick runner, good for testing). Signed-off-by: Jakub Kicinski --- contest/remote/kunit.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contest/remote/kunit.py b/contest/remote/kunit.py index 5e39b04..feb2216 100755 --- a/contest/remote/kunit.py +++ b/contest/remote/kunit.py @@ -18,6 +18,7 @@ name=executor group=test-group test=test-name +init=force / continue / next [remote] branches=https://url-to-branches-manifest [local] @@ -196,7 +197,8 @@ def main() -> None: url_path=config.get('www', 'url') + '/' + config.get('local', 'json_path'), patches_path=config.get('local', 'patches_path', fallback=None), life=life, - tree_path=config.get('local', 'tree_path')) + tree_path=config.get('local', 'tree_path'), + first_run=config.get('executor', 'init', fallback="continue")) f.run() life.exit() From 33ee652fd4fe0a00b2980b4f687d3b38713ff071 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sun, 9 Jun 2024 11:08:16 -0700 Subject: [PATCH 151/429] notes: add notes on creating the DB Record some notes about creating a PostgreSQL DB for NIPA. Signed-off-by: Jakub Kicinski --- deploy/contest/db | 57 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 deploy/contest/db diff --git a/deploy/contest/db b/deploy/contest/db new file mode 100644 index 0000000..71a4b43 --- /dev/null +++ b/deploy/contest/db @@ -0,0 +1,57 @@ +# Results DB deployment notes + +sudo dnf -y install postgresql postgresql-server python-psycopg2 + +/usr/bin/postgresql-setup --initdb + +sudo systemctl start postgresql +sudo systemctl enable postgresql + +sudo su - postgres + +# Do the same for fedora, nipa-upload and nipa-brancher +# Actually 'nipa' itself may actually not need these, TBH +createuser nipa +createdb nipa + +psql + GRANT ALL PRIVILEGES ON DATABASE "nipa" to nipa; + ALTER USER nipa WITH PASSWORD 'new_password'; + \c nipa postgres + GRANT ALL ON SCHEMA public TO "nipa"; + GRANT ALL ON ALL TABLES IN SCHEMA public TO "nipa"; + \q + +# Read-only users +createuser flask +psql + \c nipa postgres + GRANT SELECT ON ALL TABLES IN SCHEMA public TO "flask"; + \q + +exit + +# back as fedora +psql --dbname=nipa + +CREATE TABLE results ( + branch varchar(80), + remote varchar(80), + executor varchar(80), + t_start timestamp, + t_end timestamp, + json_normal text, + json_full text +); + +CREATE INDEX ON branches (branch DESC); + +CREATE TABLE branches ( + branch varchar(80), + t_date timestamp, + base varchar(80), + url varchar(200), + info text +); + +CREATE INDEX by_branch ON results (branch DESC); From fd49ac54d580dfabd71b89e0fe34cfa3b501b7a7 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 7 Jun 2024 08:35:40 -0700 Subject: [PATCH 152/429] contest: brancher: log branches and info into PostgreSQL CouchDB is not ideal in terms of perf for most common queries. Also record the branches in PostgreSQL so we can migrate to that slowly. Signed-off-by: Jakub Kicinski --- pw_brancher.py | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/pw_brancher.py b/pw_brancher.py index e0ad5f3..54b478c 100755 --- a/pw_brancher.py +++ b/pw_brancher.py @@ -6,6 +6,7 @@ import datetime import json import os +import psycopg2 import time from typing import List, Tuple import uuid @@ -33,12 +34,14 @@ branches=branches.json info=branches-info.json [db] -name=db-name +db=db-name +name=table-name user=name pwd=pass """ +psql_conn = None db = None ignore_delegate = {} gate_checks = {} @@ -177,7 +180,7 @@ def apply_local_patches(config, tree) -> List: def db_insert(config, state, name): - global db + global db, psql_conn pub_url = config.get('target', 'public_url') row = {'_id': uuid.uuid4().hex, @@ -189,6 +192,24 @@ def db_insert(config, state, name): db.save(row) + cur = None + try: + row = {"branch": name, + "date": state["branches"][name], + "base": state["hashes"].get(name, None), + "url": pub_url + " " + name} + + cur = psql_conn.cursor() + arg = cur.mogrify("(%s,%s,%s,%s,%s)", (row["branch"], row["date"], row["base"], row["url"], + json.dumps(row | state["info"][name]))) + cur.execute("INSERT INTO branches VALUES " + arg.decode('utf-8')) + print("PSQL save success!") + except Exception as e: + if cur: + cur.close() + print("PSQL save FAIL!") + print(e) + def create_new(pw, config, state, tree, tgt_remote) -> None: now = datetime.datetime.now(datetime.UTC) @@ -337,9 +358,14 @@ def prep_remote(config, tree) -> str: def open_db(config): user = config.get("db", "user") pwd = config.get("db", "pwd") + db_name = config.get("db", "db") name = config.get("db", "name") + + conn = psycopg2.connect(database=db_name) + conn.autocommit = True + server = couchdb.Server(f'http://%7Buser%7D:%7Bpwd%7D@127.0.0.1:5984/') - return server[name] + return conn, server[name] def main() -> None: @@ -370,8 +396,8 @@ def main() -> None: ignore_delegate = set(config.get('filters', 'ignore_delegate', fallback="").split(',')) global gate_checks gate_checks = set(config.get('filters', 'gate_checks', fallback="").split(',')) - global db - db = open_db(config) + global psql_conn, db + psql_conn, db = open_db(config) tree_obj = None tree_dir = config.get('dirs', 'trees', fallback=os.path.join(NIPA_DIR, "../")) From 16644c8c42cecc6e97e8a0483acab9fd99f2041a Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 7 Jun 2024 08:35:40 -0700 Subject: [PATCH 153/429] contest: fetcher: log results and info into PostgreSQL CouchDB is not ideal in terms of perf for most common queries. Also record the results in PostgreSQL so we can migrate to that slowly. Results are split into "normal" and "full", where normal is what most UIs want. We can return that precomputed, and already minimized in terms of transfer size. Signed-off-by: Jakub Kicinski --- contest/results-fetcher.py | 105 +++++++++++++++++++++++++++++++------ 1 file changed, 88 insertions(+), 17 deletions(-) diff --git a/contest/results-fetcher.py b/contest/results-fetcher.py index c3ef9a3..26cd627 100755 --- a/contest/results-fetcher.py +++ b/contest/results-fetcher.py @@ -2,12 +2,15 @@ # SPDX-License-Identifier: GPL-2.0 import configparser +import copy import couchdb import datetime import json import os +import psycopg2 import requests import time +import traceback import uuid @@ -23,8 +26,9 @@ url_pfx=relative/within/server combined=name-of-manifest.json [db] -results-name=db-name -branches-name=db-name +db=db-name +results-name=table-name +branches-name=table-name user=name pwd=pass """ @@ -38,25 +42,89 @@ def __init__(self): # "fetched" is more of a "need state rebuild" self.fetched = True + self.tbl_res = self.config.get("db", "results-name", fallback="results") + self.tbl_brn = self.config.get("db", "branches-name", fallback="branches") + user = self.config.get("db", "user") pwd = self.config.get("db", "pwd") server = couchdb.Server(f'http://%7Buser%7D:%7Bpwd%7D@127.0.0.1:5984/') - self.res_db = server[self.config.get("db", "results-name", fallback="results")] - self.brn_db = server[self.config.get("db", "branches-name", fallback="branches")] + self.res_db = server[self.tbl_res] + self.brn_db = server[self.tbl_brn] - def _one(self, rows): - rows = list(rows) - if len(rows) != 1: - raise Exception("Expected 1 row, found", rows) - return rows[0] + db_name = self.config.get("db", "db") + self.psql_conn = psycopg2.connect(database=db_name) + self.psql_conn.autocommit = True def get_branch(self, name): - branch_info = self.brn_db.find({ - 'selector': { - 'branch': name - } - }) - return self._one(branch_info) + with self.psql_conn.cursor() as cur: + cur.execute(f"SELECT info FROM {self.tbl_brn} WHERE branch = '{name}'") + rows = cur.fetchall() + return json.loads(rows[0][0]) + + def psql_run_selector(self, cur, remote, run): + return cur.mogrify("WHERE branch = %s AND remote = %s AND executor = %s", + (run['branch'], remote["name"], run["executor"],)).decode('utf-8') + + def psql_has_wip(self, remote, run): + with self.psql_conn.cursor() as cur: + cur.execute(f"SELECT branch FROM {self.tbl_res} " + self.psql_run_selector(cur, remote, run)) + rows = cur.fetchall() + return rows and len(rows) > 0 + + def insert_result_psql(self, cur, data): + normal, full = self.psql_json_split(data) + arg = cur.mogrify("(%s,%s,%s,%s,%s,%s,%s)", (data["branch"], data["remote"], data["executor"], + data["start"], data["end"], normal, full)) + cur.execute(f"INSERT INTO {self.tbl_res} VALUES " + arg.decode('utf-8')) + + def insert_wip_psql(self, remote, run, branch_info): + if self.psql_has_wip(remote, run): + # no point, we have no interesting info to add + return + + data = run.copy() + data["remote"] = remote["name"] + when = datetime.datetime.fromisoformat(branch_info['date']) + data["start"] = str(when) + when += datetime.timedelta(hours=2, minutes=58) + data["end"] = str(when) + data["results"] = None + + with self.psql_conn.cursor() as cur: + self.insert_result_psql(cur, data) + + def psql_json_split(self, data): + # return "normal" and "full" as json string or None + # "full" will be None if they are the same to save storage + if data.get("results") is None: + return json.dumps(data), None + + normal = copy.deepcopy(data) + full = None + + for row in normal["results"]: + if "results" in row: + full = True + del row["results"] + + if full: + full = json.dumps(data) + return json.dumps(normal), full + + def insert_real_psql(self, remote, run): + data = run.copy() + data["remote"] = remote["name"] + + with self.psql_conn.cursor() as cur: + if not self.psql_has_wip(remote, run): + self.insert_result_psql(cur, data) + else: + normal, full = self.psql_json_split(data) + vals = cur.mogrify("SET t_start = %s, t_end = %s, json_normal = %s, json_full = %s", + (data["start"], data["end"], normal, full)).decode('utf-8') + selector = self.psql_run_selector(cur, remote, run) + q = f"UPDATE {self.tbl_res} " + vals + ' ' + selector + cur.execute(q) def get_wip_row(self, remote, run): rows = self.res_db.find({ @@ -71,10 +139,12 @@ def get_wip_row(self, remote, run): return row def insert_wip(self, remote, run): - existing = self.get_wip_row(remote, run) - branch_info = self.get_branch(run["branch"]) + self.insert_wip_psql(remote, run, branch_info) + + existing = self.get_wip_row(remote, run) + data = run.copy() if existing: data['_id'] = existing['_id'] @@ -91,6 +161,7 @@ def insert_wip(self, remote, run): self.res_db.save(data) def insert_real(self, remote, run): + self.insert_real_psql(remote, run) existing = self.get_wip_row(remote, run) data = run.copy() From 15d89a701fd5107b20eb967cdec6d208376fdacc Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 7 Jun 2024 12:06:26 -0700 Subject: [PATCH 154/429] contest: backend: switch queries to PostgreSQL Use PostgreSQL for queries. When "normal" results are fetched just concatenated the jsons from DB without parsing. This speeds up the processing significantly. Signed-off-by: Jakub Kicinski --- contest/backend/query.py | 123 ++++++++++++++++++++++++++++++++------- 1 file changed, 103 insertions(+), 20 deletions(-) diff --git a/contest/backend/query.py b/contest/backend/query.py index 9cf361a..3d271e7 100644 --- a/contest/backend/query.py +++ b/contest/backend/query.py @@ -3,9 +3,12 @@ from flask import Flask +from flask import Response from flask import request -import couchdb +import json +import psycopg2 import os +import re import datetime @@ -13,17 +16,10 @@ user = os.getenv('DB_USER') pwd = os.getenv('DB_PWD') -couch = couchdb.Server(f'http://%7Buser%7D:%7Bpwd%7D@127.0.0.1:5984/') -res_db = couch["results"] - -def branches_to_rows(br_cnt): - data = res_db.view('branch/rows', None, - group=True, descending=True, limit=br_cnt) - cnt = 0 - for row in data: - cnt += row.value - return cnt +db_name = os.getenv('DB_NAME') +psql = psycopg2.connect(database=db_name) +psql.autocommit = True @app.route('/') @@ -31,21 +27,73 @@ def hello(): return '

boo!

' +@app.route('/branches') +def branches(): + global psql + + with psql.cursor() as cur: + cur.execute(f"SELECT branch, t_date, base, url FROM branches ORDER BY branch DESC LIMIT 40") + rows = [{"branch": r[0], "date": r[1].isoformat() + "+00:00", "base": r[2], "url": r[3]} for r in cur.fetchall()] + rows.reverse() + return rows + + +def branches_to_rows(br_cnt, remote): + global psql + + cnt = 0 + with psql.cursor() as cur: + if remote: + q = f"SELECT branch,count(*),remote FROM results GROUP BY branch, remote ORDER BY branch DESC LIMIT {br_cnt}" + else: + q = f"SELECT branch,count(*) FROM results GROUP BY branch ORDER BY branch DESC LIMIT {br_cnt}" + cur.execute(q) + for r in cur.fetchall(): + cnt += r[1] + return cnt + + +def result_as_l2(raw): + row = json.loads(raw) + flat = [] + + for l1 in row["results"]: + if "results" not in l1: + flat.append(l1) + else: + for case in l1["results"]: + data = l1.copy() + del data["results"] + data |= case + data["test"] = l1["test"] + '.' + case["test"] + flat.append(data) + row["results"] = flat + return json.dumps(row) + + @app.route('/results') def results(): - global couch - - t1 = datetime.datetime.now() + global psql br_name = request.args.get('branch-name') if br_name: + if re.match(r'^[\w_ -]+$', br_name) is None: + return {} + t1 = datetime.datetime.now() - rows = [r.value for r in res_db.view('branch/row_fetch', None, - key=br_name, limit=100)] + with psql.cursor() as cur: + cur.execute(f"SELECT json_normal FROM results WHERE branch = '{br_name}' LIMIT 100") + rows = [json.loads(r[0]) for r in cur.fetchall()] t2 = datetime.datetime.now() print("Query for exact branch took: ", str(t2-t1)) return rows + t1 = datetime.datetime.now() + + remote = request.args.get('remote') + if remote and re.match(r'^[\w_ -]+$', remote) is None: + remote = None + form = request.args.get('format') br_cnt = request.args.get('branches') try: br_cnt = int(br_cnt) @@ -54,12 +102,47 @@ def results(): if not br_cnt: br_cnt = 10 - need_rows = branches_to_rows(br_cnt) + need_rows = branches_to_rows(br_cnt, remote) + t2 = datetime.datetime.now() - data = [r.value for r in res_db.view('branch/row_fetch', None, - descending=True, limit=need_rows)] + + where = f"WHERE remote = '{remote}'" if remote else '' + if not form or form == "normal": + with psql.cursor() as cur: + cur.execute(f"SELECT json_normal FROM results {where} ORDER BY branch DESC LIMIT {need_rows}") + rows = "[" + ",".join([r[0] for r in cur.fetchall()]) + "]" + elif form == "l2": + with psql.cursor() as cur: + cur.execute(f"SELECT json_normal, json_full FROM results {where} ORDER BY branch DESC LIMIT {need_rows}") + rows = "[" + for r in cur.fetchall(): + if rows[-1] != '[': + rows += ',' + if r[1] and len(r[1]) > 50: + rows += result_as_l2(r[1]) + else: + rows += r[0] + rows += ']' + else: + rows = "[]" t3 = datetime.datetime.now() print(f"Query for {br_cnt} branches, {need_rows} records took: {str(t3-t1)} ({str(t2-t1)}+{str(t3-t2)})") - return data + return Response(rows, mimetype='application/json') + + +@app.route('/remotes') +def remotes(): + global psql + + t1 = datetime.datetime.now() + + with psql.cursor() as cur: + cur.execute(f"SELECT remote FROM results GROUP BY remote LIMIT 50") + rows = [r[0] for r in cur.fetchall()] + + t2 = datetime.datetime.now() + print(f"Query for remotes: {str(t2-t1)}") + + return rows From be8e62f6a8c3a9af704e1463ebac800498466f02 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sun, 9 Jun 2024 11:03:45 -0700 Subject: [PATCH 155/429] ui: add placeholder for device results Add an empty page where device test results will one day go. Signed-off-by: Jakub Kicinski --- ui/devices.html | 28 ++++++++++++++++++++++++++++ ui/devices.js | 3 +++ ui/favicon-nic.png | Bin 0 -> 803 bytes ui/sitemap.html | 3 ++- 4 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 ui/devices.html create mode 100644 ui/devices.js create mode 100644 ui/favicon-nic.png diff --git a/ui/devices.html b/ui/devices.html new file mode 100644 index 0000000..bcc0c25 --- /dev/null +++ b/ui/devices.html @@ -0,0 +1,28 @@ + + + + + NIPA device tests + + + + + + + + + + +
+
+

Device test results

+
+

Starting with Linux v6.12 (October 2024) all officially supported NIC drivers in Linux will be required to be continuously tested.

+

See the announcement on the mailing list.

+

No results have been reported so far.

+
+
+ + diff --git a/ui/devices.js b/ui/devices.js new file mode 100644 index 0000000..3e49077 --- /dev/null +++ b/ui/devices.js @@ -0,0 +1,3 @@ +function do_it() +{ +} diff --git a/ui/favicon-nic.png b/ui/favicon-nic.png new file mode 100644 index 0000000000000000000000000000000000000000..434e0b864020a44fd0688b9195ac18958cca62ee GIT binary patch literal 803 zcmV+;1Kj+HP)EX>4Tx04R}tkv&L4Q5c4wd#$WUB65gmxP#S_OGPxA1rij96=*edf6@f+Rqj<< z9D`P&p`RtIp{2RjhSnem`hjR}Xew%nxN%faBKwx}@SgAeINy6fE@yPpX$gU8w9UUYE2&Fs!Mg+BCD;8ra^3G^@I1k#RrhDhF!=$VQe6ui zLiZM&U)42j7cRD;?=h#^YDMw@axw{Tr{GIK?+tXWRCBA{b8-)hMVPyW{R0?ELHBBt zz1tge`?sf--w%)ga+@luAeOcHEP)Dwja{ hf8cj2prN6m3TOD~QOBpCk#qn6002ovPDHLkV1lI^Q{Vsq literal 0 HcmV?d00001 diff --git a/ui/sitemap.html b/ui/sitemap.html index 9857144..2ce877a 100644 --- a/ui/sitemap.html +++ b/ui/sitemap.html @@ -2,5 +2,6 @@ Status | Result log | Check stats | - Flaky tests + Flaky tests | + Device results From cabbe75d8bb06d4270dad1c111cfda37ba7098af Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 7 Jun 2024 19:56:03 -0700 Subject: [PATCH 156/429] ui: status: make crash fingerprint more friendly for small screens The crash finterprints are pretty wide. For some reason they are also displayed in a slightly larger font than the results in tables. Insert word-breaks into them at the colon characters to prevent them from stretching the layout. Signed-off-by: Jakub Kicinski --- ui/status.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/status.js b/ui/status.js index 2b41ff1..a8b0135 100644 --- a/ui/status.js +++ b/ui/status.js @@ -901,7 +901,8 @@ function filters_doit(data_raw) output = "Crashes ignored:
"; $.each(data_raw["ignore-crashes"], function(i, v) { - output += v + "
"; + let breakable = v.replace(/:/g, ":"); + output += "" + breakable + "
"; }); cf_crashes.innerHTML = output; From 8fb947b5b93ba6934d2c2df1774bde7d8e6460ad Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sun, 9 Jun 2024 11:26:43 -0700 Subject: [PATCH 157/429] ui: improve using args from URL Extend the "checked" handling to checkbox. Teach the code to insert an option into select if doesn't exist (sometimes the options are populated after data is loaded). Signed-off-by: Jakub Kicinski --- ui/nipa.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/ui/nipa.js b/ui/nipa.js index 9d6bad1..00a2b73 100644 --- a/ui/nipa.js +++ b/ui/nipa.js @@ -26,11 +26,23 @@ function nipa_input_set_from_url(https://github.com/name) if (!url_val) continue; - if (elem.hasAttribute("checked") || elem.type == "radio") { + if (elem.hasAttribute("checked") || + elem.type == "radio" || elem.type == "checkbox") { if (url_val == "0") elem.checked = false; else if (url_val == "1") elem.checked = true; + } else if (elem.type == "select-one") { + let option = elem.querySelector('[value="' + url_val + '"]'); + + if (!option) { + const opt = document.createElement('option'); + opt.value = url_val; + opt.innerHTML = url_val; + opt.setAttribute("style", "display: none;"); + elem.appendChild(opt); + } + elem.value = url_val; } else { elem.value = url_val; } From a94bbd729d029498daf01f8f3cf22e086a84d65e Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sun, 9 Jun 2024 11:31:10 -0700 Subject: [PATCH 158/429] ui: move loading options for select to common code Move the code from contest to nipa.js and teach to load table as well as object by attr. Signed-off-by: Jakub Kicinski --- ui/contest.js | 29 +++-------------------------- ui/nipa.js | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/ui/contest.js b/ui/contest.js index 2e5f629..9128d07 100644 --- a/ui/contest.js +++ b/ui/contest.js @@ -102,29 +102,6 @@ function find_branch_urls(loaded_data) }); } -function add_option_filter(data_raw, elem_id, field) -{ - var elem = document.getElementById(elem_id); - var values = new Set(); - - // Re-create "all" - const opt = document.createElement('option'); - opt.value = ""; - opt.innerHTML = "-- all --"; - elem.appendChild(opt); - - // Create the dynamic entries - $.each(data_raw, function(i, v) { - values.add(v[field]); - }); - for (const value of values) { - const opt = document.createElement('option'); - opt.value = value; - opt.innerHTML = value; - elem.appendChild(opt); - } -} - function results_update() { load_result_table(loaded_data); @@ -148,9 +125,9 @@ function reload_select_filters(first_load) $("select option").remove(); // We have all JSONs now, do processing. - add_option_filter(loaded_data, "branch", "branch"); - add_option_filter(loaded_data, "executor", "executor"); - add_option_filter(loaded_data, "remote", "remote"); + nipa_filter_add_options(loaded_data, "branch", "branch"); + nipa_filter_add_options(loaded_data, "executor", "executor"); + nipa_filter_add_options(loaded_data, "remote", "remote"); // On first load we use URL, later we try to keep settings user tweaked if (first_load) diff --git a/ui/nipa.js b/ui/nipa.js index 00a2b73..7c92d3b 100644 --- a/ui/nipa.js +++ b/ui/nipa.js @@ -54,6 +54,32 @@ function nipa_filters_set_from_url() nipa_input_set_from_url("https://github.com/fl-pw"); } +function nipa_filter_add_options(data_raw, elem_id, field) +{ + var elem = document.getElementById(elem_id); + var values = new Set(); + + // Re-create "all" + const opt = document.createElement('option'); + opt.value = ""; + opt.innerHTML = "-- all --"; + elem.appendChild(opt); + + // Create the dynamic entries + $.each(data_raw, function(i, v) { + if (field) + values.add(v[field]); + else + values.add(v); + }); + for (const value of values) { + const opt = document.createElement('option'); + opt.value = value; + opt.innerHTML = value; + elem.appendChild(opt); + } +} + // ------------------ let nipa_filters_json = null; From 8da3e216620c24e3246213162f30204b66e77679 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 8 Jun 2024 08:02:04 -0700 Subject: [PATCH 159/429] ui: adjust color and font Trivial tweak. Signed-off-by: Jakub Kicinski --- ui/nipa.css | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ui/nipa.css b/ui/nipa.css index c96c360..81eea5e 100644 --- a/ui/nipa.css +++ b/ui/nipa.css @@ -1,5 +1,8 @@ +body { + font-family: "roboto mono", helvetica, nunito; +} + table { - font-family: arial, sans-serif; border-collapse: collapse; width: 100%; } @@ -69,7 +72,7 @@ tr:nth-child(even) { @media (prefers-color-scheme: dark) { body { color: #b8b8b8; - background: #202020; + background: #1c1c1c; } canvas { background-color: #303030; From 84724f6907f36ff4c4cfd007297b0b497bc1e513 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sun, 9 Jun 2024 11:39:13 -0700 Subject: [PATCH 160/429] ui: consistently disable filters when loading Disable loading argument inputs when data is getting loaded. If someone changes this fields while loading the UI may get confused. Signed-off-by: Jakub Kicinski --- ui/contest.html | 4 ++-- ui/contest.js | 15 +++++---------- ui/flakes.html | 10 +++++++--- ui/flakes.js | 13 ++++++++----- ui/nipa.js | 32 ++++++++++++++++++++++++++------ 5 files changed, 48 insertions(+), 26 deletions(-) diff --git a/ui/contest.html b/ui/contest.html index b38920a..bdcbbfe 100644 --- a/ui/contest.html +++ b/ui/contest.html @@ -26,11 +26,11 @@ Loading:
- +
 

- +
Filtering: diff --git a/ui/contest.js b/ui/contest.js index 9128d07..a6bf1f8 100644 --- a/ui/contest.js +++ b/ui/contest.js @@ -149,7 +149,8 @@ function loaded_one() return; reload_select_filters(true); - nipa_filters_enable(results_update); + nipa_filters_enable(reload_data, "ld-pw"); + nipa_filters_enable(results_update, "fl-pw"); results_update(); } @@ -178,6 +179,8 @@ function results_loaded(data_raw) reload_select_filters(false); results_update(); } + + nipa_filters_enable(null, ["ld-pw", "fl-pw"]); } function reload_data(event) @@ -199,12 +202,10 @@ function reload_data(event) req_url += "branches=" + br_cnt.value; } + nipa_filters_disable(["ld-pw", "fl-pw"]); $(document).ready(function() { $.get(req_url, results_loaded) }); - - let warn_box = document.getElementById("fl-warn-box"); - warn_box.innerHTML = "Loading..."; } function do_it() @@ -220,12 +221,6 @@ function do_it() document.getElementById("ld_cnt").value = 1; } - const ld_pw = document.querySelectorAll("[name=ld-pw]"); - for (const one of ld_pw) { - one.addEventListener("change", reload_data); - one.disabled = false; - } - /* * Please remember to keep these assets in sync with `scripts/ui_assets.sh` */ diff --git a/ui/flakes.html b/ui/flakes.html index 390bc18..5823819 100644 --- a/ui/flakes.html +++ b/ui/flakes.html @@ -22,6 +22,13 @@
+
+ Loading: +
 
+ +
+ +
Filtering:
@@ -32,9 +39,6 @@
-
- -
diff --git a/ui/flakes.js b/ui/flakes.js index 299f608..64c5872 100644 --- a/ui/flakes.js +++ b/ui/flakes.js @@ -133,8 +133,7 @@ function loaded_one() return; // We have all JSONs now, do processing. - nipa_filters_set_from_url(); - nipa_filters_enable(results_update); + nipa_input_set_from_url("https://github.com/fl-pw"); results_update(); } @@ -159,11 +158,15 @@ function results_loaded(data_raw) } else if (!xfr_todo) { results_update(); } + + nipa_filters_enable(null, ["ld-pw", "fl-pw"]); } function reload_data() { let br_cnt = document.getElementById("br-cnt"); + + nipa_filters_disable(["ld-pw", "fl-pw"]); $(document).ready(function() { $.get("query/results?branches=" + br_cnt.value, results_loaded) }); @@ -171,9 +174,9 @@ function reload_data() function do_it() { - let br_cnt = document.getElementById("br-cnt"); - - br_cnt.addEventListener("change", reload_data); + nipa_filters_enable(reload_data, "ld-pw"); + nipa_filters_enable(results_update, "fl-pw"); + nipa_input_set_from_url("https://github.com/ld-pw"); /* * Please remember to keep these assets in sync with `scripts/ui_assets.sh` diff --git a/ui/nipa.js b/ui/nipa.js index 7c92d3b..9ccdd31 100644 --- a/ui/nipa.js +++ b/ui/nipa.js @@ -3,18 +3,38 @@ function nipa_test_fullname(v, r) return v.remote + "/" + v.executor + "/" + r.group + "/" + r.test; } -function nipa_filters_enable(update_cb) +function __nipa_filters_set(update_cb, set_name, enabled) { - let warn_box = document.getElementById("fl-warn-box"); - warn_box.innerHTML = ""; + if (set_name.constructor === Array) { + for (name of set_name) + __nipa_filters_set(update_cb, name, enabled); + return; + } - const fl_pw = document.querySelectorAll("[name=fl-pw]"); + const fl_pw = document.querySelectorAll("[name=" + set_name + "]"); for (const one of fl_pw) { - one.addEventListener("change", update_cb); - one.disabled = false; + if (update_cb) + one.addEventListener("change", update_cb); + one.disabled = enabled; } } +function nipa_filters_enable(update_cb, set_name) +{ + let warn_box = document.getElementById("fl-warn-box"); + warn_box.innerHTML = ""; + + __nipa_filters_set(update_cb, set_name, false); +} + +function nipa_filters_disable(set_name) +{ + let warn_box = document.getElementById("fl-warn-box"); + warn_box.innerHTML = "Loading..."; + + __nipa_filters_set(null, set_name, true); +} + function nipa_input_set_from_url(https://github.com/name) { const urlParams = new URLSearchParams(window.location.search); From d7fac765861e22477cec2ad7704c28885d69858b Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sun, 9 Jun 2024 11:44:48 -0700 Subject: [PATCH 161/429] ui: flakes: support filtering by remote To lower the transfer size support filtering loaded data by remote. Use a separate query to fetch them, we can't depend on loaded data because it may be filtered.. Signed-off-by: Jakub Kicinski --- ui/flakes.html | 3 +++ ui/flakes.js | 22 +++++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/ui/flakes.html b/ui/flakes.html index 5823819..68be1a8 100644 --- a/ui/flakes.html +++ b/ui/flakes.html @@ -28,6 +28,9 @@
+
 
+ +
Filtering: diff --git a/ui/flakes.js b/ui/flakes.js index 64c5872..fd180a3 100644 --- a/ui/flakes.js +++ b/ui/flakes.js @@ -124,7 +124,7 @@ function results_update() load_result_table(loaded_data); } -let xfr_todo = 2; +let xfr_todo = 3; let loaded_data = null; function loaded_one() @@ -162,13 +162,26 @@ function results_loaded(data_raw) nipa_filters_enable(null, ["ld-pw", "fl-pw"]); } +function remotes_loaded(data_raw) +{ + nipa_filter_add_options(data_raw, "ld-remote", null); + loaded_one(); +} + function reload_data() { - let br_cnt = document.getElementById("br-cnt"); + const br_cnt = document.getElementById("br-cnt"); + const remote = document.getElementById("ld-remote"); + + let req_url = "query/results"; + req_url += "?branches=" + br_cnt.value; + + if (remote.value) + req_url += "&remote=" + remote.value; nipa_filters_disable(["ld-pw", "fl-pw"]); $(document).ready(function() { - $.get("query/results?branches=" + br_cnt.value, results_loaded) + $.get(req_url, results_loaded) }); } @@ -184,5 +197,8 @@ function do_it() $(document).ready(function() { $.get("contest/filters.json", filters_loaded) }); + $(document).ready(function() { + $.get("query/remotes", remotes_loaded) + }); reload_data(); } From 7d8e296321a6b958ed234e423ec35f3269bee9b5 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sun, 9 Jun 2024 11:44:53 -0700 Subject: [PATCH 162/429] ui: support displaying sub-cases Allow requesting results with detailed sub-cases from the backend. Signed-off-by: Jakub Kicinski --- ui/contest.html | 3 +++ ui/contest.js | 11 +++++++++-- ui/flakes.html | 3 +++ ui/flakes.js | 8 +++++++- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/ui/contest.html b/ui/contest.html index bdcbbfe..50c649a 100644 --- a/ui/contest.html +++ b/ui/contest.html @@ -31,6 +31,9 @@
+
 
+ +
Filtering: diff --git a/ui/contest.js b/ui/contest.js index a6bf1f8..7c8706e 100644 --- a/ui/contest.js +++ b/ui/contest.js @@ -34,6 +34,10 @@ function load_result_table(data_raw) let row_count = 0; + let form = ""; + if (document.getElementById("ld-cases").checked) + form = "&ld-cases=1"; + $.each(data_raw, function(i, v) { if (row_count >= 5000) { warn_box.innerHTML = "Reached 5000 rows. Set an executor, branch or test filter. Otherwise this page will set your browser on fire..."; @@ -86,8 +90,8 @@ function load_result_table(data_raw) retry.innerHTML = colorify_str(r.retry); res.innerHTML = colorify_str(r.result); outputs.innerHTML = "outputs"; - hist.innerHTML = "history"; - flake.innerHTML = "matrix"; + hist.innerHTML = "history"; + flake.innerHTML = "matrix"; row_count++; }); @@ -185,6 +189,7 @@ function results_loaded(data_raw) function reload_data(event) { + const format_l2 = document.getElementById("ld-cases"); const br_cnt = document.getElementById("ld_cnt"); const br_name = document.getElementById("ld_branch"); @@ -201,6 +206,8 @@ function reload_data(event) } else { req_url += "branches=" + br_cnt.value; } + if (format_l2.checked) + req_url += '&format=l2'; nipa_filters_disable(["ld-pw", "fl-pw"]); $(document).ready(function() { diff --git a/ui/flakes.html b/ui/flakes.html index 68be1a8..aa81608 100644 --- a/ui/flakes.html +++ b/ui/flakes.html @@ -31,6 +31,9 @@
 
+
 
+ +
Filtering: diff --git a/ui/flakes.js b/ui/flakes.js index fd180a3..2938c60 100644 --- a/ui/flakes.js +++ b/ui/flakes.js @@ -101,6 +101,9 @@ function load_result_table(data_raw) cell.setAttribute("style", "writing-mode: tb-rl; font-size: 0.8em; padding: 0px;"); } + let form = ""; + if (document.getElementById("ld-cases").checked) + form = "&ld-cases=1"; for (const tn of test_names) { let entries = test_row[tn]; @@ -109,7 +112,7 @@ function load_result_table(data_raw) let row = table.insertRow(); let name = row.insertCell(0); - name.innerHTML = "" + tn + ""; + name.innerHTML = "" + tn + ""; name.setAttribute("style", "padding: 0px"); for (let i = 0; i < branches.length; i++) { @@ -170,12 +173,15 @@ function remotes_loaded(data_raw) function reload_data() { + const format_l2 = document.getElementById("ld-cases"); const br_cnt = document.getElementById("br-cnt"); const remote = document.getElementById("ld-remote"); let req_url = "query/results"; req_url += "?branches=" + br_cnt.value; + if (format_l2.checked) + req_url += "&format=l2"; if (remote.value) req_url += "&remote=" + remote.value; From 5bbb27ff17dea1380bc310316c6e14242b342b1b Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Tue, 11 Jun 2024 22:01:46 +0200 Subject: [PATCH 163/429] contest: backend: remove unused vars The user and password for the DB are no longer needed. Fixes: 15d89a7 ("contest: backend: switch queries to PostgreSQL") Signed-off-by: Matthieu Baerts (NGI0) --- contest/backend/query.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/contest/backend/query.py b/contest/backend/query.py index 3d271e7..c6ed285 100644 --- a/contest/backend/query.py +++ b/contest/backend/query.py @@ -14,9 +14,6 @@ app = Flask("NIPA contest query") -user = os.getenv('DB_USER') -pwd = os.getenv('DB_PWD') - db_name = os.getenv('DB_NAME') psql = psycopg2.connect(database=db_name) psql.autocommit = True From 1fd1f828c7a011285ac84446f8a88fed2af8c881 Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Tue, 11 Jun 2024 22:33:14 +0200 Subject: [PATCH 164/429] system-status: log DB size The DB might get info about subtests when available. It is then important to monitor the DB size, to quickly take actions if it grows too fast. Here, the system-status script will check the size once per day. The size of the last 40 days are written in the JSON, that could be displayed in the status page later. Signed-off-by: Matthieu Baerts (NGI0) --- deploy/contest/db | 7 +++++++ system-status.py | 44 +++++++++++++++++++++++++++++++++++++++++++- ui/favicon-nic.png | Bin 803 -> 1047 bytes 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/deploy/contest/db b/deploy/contest/db index 71a4b43..4fed820 100644 --- a/deploy/contest/db +++ b/deploy/contest/db @@ -20,6 +20,7 @@ psql \c nipa postgres GRANT ALL ON SCHEMA public TO "nipa"; GRANT ALL ON ALL TABLES IN SCHEMA public TO "nipa"; + GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO "nipa"; \q # Read-only users @@ -55,3 +56,9 @@ CREATE TABLE branches ( ); CREATE INDEX by_branch ON results (branch DESC); + +CREATE TABLE db_monitor ( + id serial primary key, + ts timestamp not null, + size int not null check (size > 0) +); diff --git a/system-status.py b/system-status.py index cbb551f..cc9421e 100755 --- a/system-status.py +++ b/system-status.py @@ -4,6 +4,7 @@ import datetime import lzma import os +import psycopg2 import re import requests import sys @@ -162,28 +163,66 @@ def add_remote_services(result, remote): result["remote"][remote["name"]] = data +def add_db(result, cfg): + db_name = cfg["db"]["name"] + tbl = cfg["db"]["table"] + + psql = psycopg2.connect(database=db_name) + psql.autocommit = True + + with psql.cursor() as cur: + cur.execute(f"SELECT pg_database_size('{db_name}')") + size = cur.fetchall()[0][0] + print("DB size", size) + + arg = cur.mogrify("(NOW(),%s)", (size, )) + cur.execute(f"INSERT INTO {tbl}(ts, size) VALUES" + arg.decode('utf-8')) + + with psql.cursor() as cur: + cur.execute(f"SELECT ts,size FROM {tbl} ORDER BY id DESC LIMIT 40") + result["db"]["data"] = [ {'ts': t.isoformat(), 'size': s} for t, s in cur.fetchall() ] + + def main(): with open(sys.argv[1], 'r') as fp: cfg = json.load(fp) log_files = {} run_logs = 'log-files' in cfg + + db = {} + run_db = 'db' in cfg + if os.path.isfile(sys.argv[2]): with open(sys.argv[2], 'r') as fp: prev = json.load(fp) + if "log-files" in prev and "prev-date" in prev["log-files"]: prev_date = datetime.datetime.fromisoformat(prev["log-files"]["prev-date"]) run_logs = datetime.datetime.now() - prev_date > datetime.timedelta(hours=3) print("Since log scan", datetime.datetime.now() - prev_date, "Will rescan:", run_logs) prev_date = prev["log-files"]["prev-date"] log_files = {"prev-date": prev_date, "data": prev["log-files"]["data"]} + + if "db" in prev and "prev-date" in prev["db"]: + prev_date = datetime.datetime.fromisoformat(prev["db"]["prev-date"]) + run_db = datetime.datetime.now() - prev_date > datetime.timedelta(hours=24) + print("Since db monitor", datetime.datetime.now() - prev_date, "Will rescan:", run_db) + prev_date = prev["db"]["prev-date"] + db = {"prev-date": prev_date, "data": prev["db"]["data"]} + if run_logs: prev_date = datetime.datetime.now().isoformat() log_files = {"prev-date": prev_date, } + if run_db: + prev_date = datetime.datetime.now().isoformat() + db = {"prev-date": prev_date, } + result = {'services': {}, 'runners': {}, 'remote': {}, 'date': datetime.datetime.now().isoformat(), - "log-files": log_files} + "log-files": log_files, + "db": db} if "trees" in cfg: for name in cfg["trees"]: add_one_tree(result, cfg["tree-path"], name) @@ -197,6 +236,9 @@ def main(): for remote in cfg["remote"]: add_remote_services(result, remote) + if "db" in cfg and run_db: + add_db(result, cfg) + with open(sys.argv[2], 'w') as fp: json.dump(result, fp) diff --git a/ui/favicon-nic.png b/ui/favicon-nic.png index 434e0b864020a44fd0688b9195ac18958cca62ee..bf341665c00523f38e4457398bf91c84453cc4d5 100644 GIT binary patch delta 546 zcmV+-0^R+i2A2r1-T?;^IX7((*3OgV0Vsd45uTWU@E_f)GJ$R5_7usu~1eYS>Wpv%EL$(Lw zI*AjLnz(_!OJ?RJ`Tyq4doK}$w!eV`yab$ZDF80l_Xq%dPg?!+J30XP_Jyy&hY^3g z5OC!%_2Z-byy|SW10FD80J!UWDAQ@K`JQy=cXY(>d(!>2pm)P*4VVF@!ud5Y0DAQ% zYpDQ_vBR5P*Nmk&0BH~+hcGemo-wW&K!cbM6Gr3^?ip8XJ2dW(gN-91GYX%_1>-NW znd!5@kP2WvBm$;KPE^?UG|Q>S;9spj}Ou?sz=$Bd`hI=EJ@9iPXB1EdO2x;E1CLgiH$aL)L%Or_#U6 zfx+`ZdPGLLxFb+Y3bm**7qfOX$M22{JObz6J4jM8mfi{D--4`PL&0|b-?3Z~`2O}; kA=lE6p!ENN(j)K}nYzCYI=l|eFaQ7m07*qoM6N<$f*;%olK=n! delta 300 zcmV+{0n`4M2%`qD-T?;@Fbof$I{K640Vsa~Nkl?tnwQ4aof_Y2wL*PIDd;ceZhQ`0c z@H}r>6vY|zhDQjwPkQGm`#n5sSSH3EbY5Jp-`AThafZ~y=iB5;e#B`|;3 z4PYPbI!;F^vU&Eo3H{P+@luAeOcHEP)Dwja{f8cj2prII{p$cdC=~2h0ppkR{0000 Date: Tue, 11 Jun 2024 16:34:50 -0700 Subject: [PATCH 165/429] ui: status: display DB size Show the size of the DB on a graph. Signed-off-by: Jakub Kicinski --- ui/status.html | 9 ++++++++- ui/status.js | 29 +++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/ui/status.html b/ui/status.html index e392274..1724600 100644 --- a/ui/status.html +++ b/ui/status.html @@ -54,7 +54,14 @@

Build processing


- +
+
+ +
+
+ +
+

Continuous testing results

diff --git a/ui/status.js b/ui/status.js index a8b0135..02c3970 100644 --- a/ui/status.js +++ b/ui/status.js @@ -305,11 +305,40 @@ function load_runtime(data_raw) }); } +function load_db_size(data) +{ + const ctx = document.getElementById("db-size"); + + new Chart(ctx, { + type: 'line', + data: { + labels: data.map(function(e){return new Date(e.ts).toDateString();}), + datasets: [{ + label: 'size in kB', + data: data.map(function(e){return Math.floor(e.size / 1024);}), + }] + }, + options: { + responsive: true, + plugins: { + legend: { + position: 'bottom', + }, + title: { + display: true, + text: 'DB size' + } + } + } + }); +} + function status_system(data_raw) { systemd(data_raw, data_raw["services"], data_raw["remote"]); load_runners(data_raw["runners"]); load_runtime(data_raw["log-files"]); + load_db_size(data_raw["db"]["data"]); } function msec_to_str(msec) { From de0b3c84d57d92d35d3f91ea33cb07fff3b04962 Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Wed, 12 Jun 2024 13:04:49 +0200 Subject: [PATCH 166/429] system-status: db: reverse order The list we get from the DB is reversed, to be able to easily limit it to 40 entries. To present the data in the right order in the JSON, we need to reverse them again. The UI could also reverse them, but it sounds better to write the data in the right order instead. Signed-off-by: Matthieu Baerts (NGI0) --- system-status.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system-status.py b/system-status.py index cc9421e..d852506 100755 --- a/system-status.py +++ b/system-status.py @@ -180,7 +180,7 @@ def add_db(result, cfg): with psql.cursor() as cur: cur.execute(f"SELECT ts,size FROM {tbl} ORDER BY id DESC LIMIT 40") - result["db"]["data"] = [ {'ts': t.isoformat(), 'size': s} for t, s in cur.fetchall() ] + result["db"]["data"] = [ {'ts': t.isoformat(), 'size': s} for t, s in reversed(cur.fetchall()) ] def main(): From 2e33d65fc6b2166218d84416409069e6053c43ec Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Tue, 11 Jun 2024 23:16:49 +0200 Subject: [PATCH 167/429] contest: remote: export 'namify' to fetcher It sounds interesting to avoid using spaces and other unwanted chars in test names. This is currently done for the kselftests, but others might need the same treatment, e.g. in kUnit tests. To help with that, the 'namify' helper is exported to fetcher.py. Signed-off-by: Matthieu Baerts (NGI0) --- contest/remote/lib/fetcher.py | 8 ++++++++ contest/remote/vmksft-p.py | 9 +-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/contest/remote/lib/fetcher.py b/contest/remote/lib/fetcher.py index ca1f126..370a6f0 100644 --- a/contest/remote/lib/fetcher.py +++ b/contest/remote/lib/fetcher.py @@ -3,6 +3,7 @@ import datetime import json import os +import re import requests import subprocess import time @@ -154,3 +155,10 @@ def _run_once(self): def run(self): while self.life.next_poll(): self._run_once() + + +def namify(what): + name = re.sub(r'[^0-9a-zA-Z]+', '-', what) + if name[-1] == '-': + name = name[:-1] + return name diff --git a/contest/remote/vmksft-p.py b/contest/remote/vmksft-p.py index 48ecba0..1ed1dcb 100755 --- a/contest/remote/vmksft-p.py +++ b/contest/remote/vmksft-p.py @@ -15,7 +15,7 @@ from core import NipaLifetime from lib import wait_loadavg from lib import CbArg -from lib import Fetcher +from lib import Fetcher, namify from lib import VM, new_vm, guess_indicators @@ -58,13 +58,6 @@ """ -def namify(what): - name = re.sub(r'[^0-9a-zA-Z]+', '-', what) - if name[-1] == '-': - name = name[:-1] - return name - - def get_prog_list(vm, target): tmpdir = tempfile.mkdtemp() vm.tree_cmd(f"make -C tools/testing/selftests/ TARGETS={target} INSTALL_PATH={tmpdir} install") From c081fc08e3309c0d2d6d829d30fdeceaa150e6b4 Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Tue, 11 Jun 2024 23:20:47 +0200 Subject: [PATCH 168/429] contest: kunit: 'namify' the test names The new subtests have spaces in their name, e.g. example.example_params_test.example value 0 It sounds better to avoid these spaces and other unwanted chars, as the test name is used to identify the test. This will break the tracking of failed tests, but it has just been broken a few days ago, probably the good time to do that again. Signed-off-by: Matthieu Baerts (NGI0) --- contest/remote/kunit.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/contest/remote/kunit.py b/contest/remote/kunit.py index feb2216..a70e13e 100755 --- a/contest/remote/kunit.py +++ b/contest/remote/kunit.py @@ -8,7 +8,7 @@ import subprocess from core import NipaLifetime -from lib import Fetcher +from lib import Fetcher, namify """ @@ -99,11 +99,12 @@ def summary_flat(expected, got, sub_path=""): if exp and exp == code: continue + name = namify(case["name"]) overall_code = max(code, overall_code) - results.append({'test': sub_path + case["name"], + results.append({'test': sub_path + name, 'result': code_to_str[code]}) if code: - bad_tests.append(f"{got['name']} {case['name']} {case['status']}") + bad_tests.append(f"{got['name']} {name} {case['status']}") for sub_group in got["sub_groups"]: ov, bt, res = summary_flat(expected, sub_group, sub_path + sub_group["name"]) From 0f14903c5f1f39ed211cbfb704a45e97a782df81 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 12 Jun 2024 07:57:07 -0700 Subject: [PATCH 169/429] ui: status: move the graphs into their own row Three diagrams fit nicely, given their geometry. When we only have 2 they get large, if we have 4 (2x2) X labels squeeze out the actual graph. Signed-off-by: Jakub Kicinski --- ui/status.html | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/ui/status.html b/ui/status.html index 1724600..f35188c 100644 --- a/ui/status.html +++ b/ui/status.html @@ -42,8 +42,6 @@

Build processing


- -
@@ -53,15 +51,6 @@

Build processing

Service Memory Use
-
-
-
- -
-
- -
-

Continuous testing results

@@ -107,6 +96,17 @@

Tests with missing results

+
+
+ +
+
+ +
+
+ +
+
From 472716ba45020b25bddeb2204857803857a3441c Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 12 Jun 2024 08:12:25 -0700 Subject: [PATCH 170/429] contest: lib: export namify We do selective imports, __init__ needs to import things. Signed-off-by: Jakub Kicinski --- contest/remote/lib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contest/remote/lib/__init__.py b/contest/remote/lib/__init__.py index 8964b8a..1585fc3 100644 --- a/contest/remote/lib/__init__.py +++ b/contest/remote/lib/__init__.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: GPL-2.0 -from .fetcher import Fetcher +from .fetcher import Fetcher, namify from .loadavg import wait_loadavg from .vm import VM, new_vm, guess_indicators from .cbarg import CbArg From dab1c594a5e9012dbf9f49f4b4f9c01cdff73d62 Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Wed, 12 Jun 2024 17:25:36 +0200 Subject: [PATCH 171/429] contest: vmksft-p: nested: support sub-cases format Similar to commit bf87f3d ("contest: kunit: add sub-cases"), sub-cases are now stored in a list, attached to the parent's result, using the 'results' key. The nested tests are then now parsed before adding the parent's results in the queues, to be able to attach them if any. By doing that, we should be able to re-enable sub-tests parsing for selftests supporting them: MPTCP, TC, the ones using kselftest_harness.h, etc. Note that it is also easy to add a new option to only store failed tests if needed: just adding a check before 'tests.append()' in _parse_nested_tests(). Signed-off-by: Matthieu Baerts (NGI0) --- contest/remote/vmksft-p.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/contest/remote/vmksft-p.py b/contest/remote/vmksft-p.py index 1ed1dcb..c8b3ab7 100755 --- a/contest/remote/vmksft-p.py +++ b/contest/remote/vmksft-p.py @@ -100,7 +100,8 @@ def _parse_nested_tests(full_run): if len(v) > 5 and v[5]: if v[5].lower().startswith('skip') and result == "pass": result = "skip" - tests.append((name, result)) + + tests.append({'test': namify(name), 'result': result}) return tests @@ -181,21 +182,19 @@ def _vm_thread(config, results_path, thr_id, hard_stop, in_queue, out_queue): if crashes: outcome['crashes'] = crashes + if config.getboolean('ksft', 'nested_tests', fallback=False): + # this will only parse nested tests inside the TAP comments + nested_tests = _parse_nested_tests(vm.log_out) + if nested_tests: + outcome['results'] = nested_tests + + print(f"INFO: thr-{thr_id} {prog} >> nested tests: {len(nested_tests)}") + if not is_retry and result == 'fail': in_queue.put(outcome) else: out_queue.put(outcome) - if config.getboolean('ksft', 'nested_tests', fallback=False) and not is_retry: - # this will only parse nested tests inside the TAP comments - tests = _parse_nested_tests(vm.log_out) - - for r_name, r_result in tests: - out_queue.put({'prog': prog, 'test': namify(r_name), - 'file_name': file_name, 'result': r_result}) - - print(f"INFO: thr-{thr_id} {prog} >> nested tests: {len(tests)} subtests") - vm.dump_log(results_path + '/' + file_name, result=retcode, info={"thr-id": thr_id, "vm-id": vm_id, "time": (t2 - t1).total_seconds(), "found": indicators, "vm_state": vm.fail_state}) @@ -300,7 +299,7 @@ def test(binfo, rinfo, cbarg): 'result': r["result"], 'link': link + '/' + r['file_name'] } - for key in ['retry', 'crashes']: + for key in ['retry', 'crashes', 'results']: if key in r: outcome[key] = r[key] cases.append(outcome) From 4db9bb6b375c637a33df260f26486453c07d4f0c Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 12 Jun 2024 16:14:06 -0700 Subject: [PATCH 172/429] contest: backend: improve filter / argument handling Avoid splintering processing between requests with different filters. Try to collect WHERE conditions into a common query instead. This fixes the l2 format support when querying results for a single branch (by name). Signed-off-by: Jakub Kicinski --- contest/backend/query.py | 53 ++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/contest/backend/query.py b/contest/backend/query.py index c6ed285..be68d63 100644 --- a/contest/backend/query.py +++ b/contest/backend/query.py @@ -72,45 +72,50 @@ def result_as_l2(raw): def results(): global psql + limit = 0 + where = [] + + form = request.args.get('format') + remote = request.args.get('remote') + if remote and re.match(r'^[\w_ -]+$', remote) is None: + remote = None + br_name = request.args.get('branch-name') if br_name: if re.match(r'^[\w_ -]+$', br_name) is None: return {} + br_cnt = br_name + limit = 100 + where.append(f"branch = '{br_name}'") + t1 = t2 = datetime.datetime.now() + else: t1 = datetime.datetime.now() - with psql.cursor() as cur: - cur.execute(f"SELECT json_normal FROM results WHERE branch = '{br_name}' LIMIT 100") - rows = [json.loads(r[0]) for r in cur.fetchall()] - t2 = datetime.datetime.now() - print("Query for exact branch took: ", str(t2-t1)) - return rows - t1 = datetime.datetime.now() + br_cnt = request.args.get('branches') + try: + br_cnt = int(br_cnt) + except: + br_cnt = None + if not br_cnt: + br_cnt = 10 - remote = request.args.get('remote') - if remote and re.match(r'^[\w_ -]+$', remote) is None: - remote = None - form = request.args.get('format') - br_cnt = request.args.get('branches') - try: - br_cnt = int(br_cnt) - except: - br_cnt = None - if not br_cnt: - br_cnt = 10 + limit = branches_to_rows(br_cnt, remote) + + t2 = datetime.datetime.now() - need_rows = branches_to_rows(br_cnt, remote) + if remote: + where.append(f"remote = '{remote}'") - t2 = datetime.datetime.now() + where = "WHERE " + " AND ".join(where) if where else "" - where = f"WHERE remote = '{remote}'" if remote else '' if not form or form == "normal": with psql.cursor() as cur: - cur.execute(f"SELECT json_normal FROM results {where} ORDER BY branch DESC LIMIT {need_rows}") + cur.execute(f"SELECT json_normal FROM results {where} ORDER BY branch DESC LIMIT {limit}") rows = "[" + ",".join([r[0] for r in cur.fetchall()]) + "]" elif form == "l2": with psql.cursor() as cur: - cur.execute(f"SELECT json_normal, json_full FROM results {where} ORDER BY branch DESC LIMIT {need_rows}") + cur.execute(f"SELECT json_normal, json_full FROM results {where} ORDER BY branch DESC LIMIT {limit}") rows = "[" for r in cur.fetchall(): if rows[-1] != '[': @@ -124,7 +129,7 @@ def results(): rows = "[]" t3 = datetime.datetime.now() - print(f"Query for {br_cnt} branches, {need_rows} records took: {str(t3-t1)} ({str(t2-t1)}+{str(t3-t2)})") + print(f"Query for {br_cnt} branches, {limit} records took: {str(t3-t1)} ({str(t2-t1)}+{str(t3-t2)})") return Response(rows, mimetype='application/json') From 289bb8e79f1d8f396bc992a3122b38e37a2a0d44 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 12 Jun 2024 17:47:28 -0700 Subject: [PATCH 173/429] mailbot: support MlEmail sorting Every now and then some bot blasts a lot of emails at once and we end up with the same exact dates in the defer queue. The defer queue has tuples of data and MlEmail so when dates are the same it tries to compare email instances. We don't care which one gets to the start of the queue first, but let's not crash. Signed-off-by: Jakub Kicinski --- mailbot.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/mailbot.py b/mailbot.py index 0e37633..33f8058 100755 --- a/mailbot.py +++ b/mailbot.py @@ -262,6 +262,15 @@ def __init__(self, msg_path): self._series_author = None self._authorized = None + def __eq__(self, other): + return True + + def __lt__(self, other): + return False + + def __gt__(self, other): + return False + def get(self, item, failobj=None): return self.msg.get(item, failobj) From 1123563a29cba369ada9250289f953542f3e537f Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 15 Jun 2024 12:39:24 -0700 Subject: [PATCH 174/429] system-status: add disk use Since we're already recording DB size, also record and display the disk use, on the same graph. Signed-off-by: Jakub Kicinski --- deploy/contest/db | 4 +++- system-status.py | 22 ++++++++++++++++++---- ui/status.js | 25 ++++++++++++++++++++++--- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/deploy/contest/db b/deploy/contest/db index 4fed820..9dc3aee 100644 --- a/deploy/contest/db +++ b/deploy/contest/db @@ -60,5 +60,7 @@ CREATE INDEX by_branch ON results (branch DESC); CREATE TABLE db_monitor ( id serial primary key, ts timestamp not null, - size int not null check (size > 0) + size int not null check (size > 0), + disk_pct REAL, + disk_pct_metal REAL ); diff --git a/system-status.py b/system-status.py index d852506..d553aaf 100755 --- a/system-status.py +++ b/system-status.py @@ -39,6 +39,13 @@ def add_one_service(result, name): result['time-mono'] = time.monotonic_ns() // 1000 +def add_disk_size(result, path): + output = subprocess.check_output(f"df {path} --output=avail,size".split()).decode('utf-8') + sizes = output.split('\n')[1].split() + sizes = [int(s) for s in sizes] + result["disk-use"] = round(sizes[0] / sizes[1] * 100, 2) + + def pre_strip(line, needle): return line[line.find(needle) + len(needle):].strip() @@ -175,12 +182,17 @@ def add_db(result, cfg): size = cur.fetchall()[0][0] print("DB size", size) - arg = cur.mogrify("(NOW(),%s)", (size, )) - cur.execute(f"INSERT INTO {tbl}(ts, size) VALUES" + arg.decode('utf-8')) + remote_disk = 0 + for _, remote in result["remote"].items(): + remote_disk = remote["disk-use"] + + arg = cur.mogrify("(NOW(),%s,%s,%s)", (size, result["disk-use"], remote_disk)) + cur.execute(f"INSERT INTO {tbl}(ts, size, disk_pct, disk_pct_metal) VALUES" + arg.decode('utf-8')) with psql.cursor() as cur: - cur.execute(f"SELECT ts,size FROM {tbl} ORDER BY id DESC LIMIT 40") - result["db"]["data"] = [ {'ts': t.isoformat(), 'size': s} for t, s in reversed(cur.fetchall()) ] + cur.execute(f"SELECT ts,size,disk_pct,disk_pct_metal FROM {tbl} ORDER BY id DESC LIMIT 40") + result["db"]["data"] = [ {'ts': t.isoformat(), 'size': s, 'disk': d, 'disk_remote': dr} + for t, s, d, dr in reversed(cur.fetchall()) ] def main(): @@ -236,6 +248,8 @@ def main(): for remote in cfg["remote"]: add_remote_services(result, remote) + add_disk_size(result, "/") + if "db" in cfg and run_db: add_db(result, cfg) diff --git a/ui/status.js b/ui/status.js index 02c3970..38a3936 100644 --- a/ui/status.js +++ b/ui/status.js @@ -314,8 +314,17 @@ function load_db_size(data) data: { labels: data.map(function(e){return new Date(e.ts).toDateString();}), datasets: [{ - label: 'size in kB', + yAxisID: 'A', + label: 'DB size in kB', data: data.map(function(e){return Math.floor(e.size / 1024);}), + }, { + yAxisID: 'B', + label: 'free disk %', + data: data.map(function(e){return e.disk;}), + }, { + yAxisID: 'B', + label: 'metal free disk %', + data: data.map(function(e){return e.disk_remote;}), }] }, options: { @@ -326,9 +335,19 @@ function load_db_size(data) }, title: { display: true, - text: 'DB size' + text: 'Storage use' } - } + }, + scales: { + A: { + display: true + }, + B: { + position: 'right', + display: true, + beginAtZero: true + } + }, } }); } From da4c724779b0e3ebc9e20d319ae1207e3a846c84 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 15 Jun 2024 15:07:17 -0700 Subject: [PATCH 175/429] tests: build: rework relink touching and pay attention to Makefiles Add changes to Makefiles as reason to relink. Some drivers add all files and then in a last patch add to Kconfig and Makefile without adding files. To avoid repeating conditions compute a "path to touch", if no relink is needed - touch /dev/null which won't have any effect. Signed-off-by: Jakub Kicinski --- tests/patch/build_32bit/build_32bit.sh | 13 ++++++++----- .../build_allmodconfig_warn/build_allmodconfig.sh | 13 ++++++++----- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/tests/patch/build_32bit/build_32bit.sh b/tests/patch/build_32bit/build_32bit.sh index 4af9f4e..1236d28 100755 --- a/tests/patch/build_32bit/build_32bit.sh +++ b/tests/patch/build_32bit/build_32bit.sh @@ -38,11 +38,16 @@ fi # so all module and linker related warnings will pop up in the "after" # but not "before". To avoid this we need to force re-linking on # the "before", too. -if ! git log --diff-filter=A HEAD~.. --exit-code >>/dev/null; then +touch_relink=/dev/null +if ! git log --diff-filter=A HEAD~.. --exit-code >>/dev/null || \ + ! git log HEAD~.. --exit-code -- */Makefile >>/dev/null +then echo "Trying to force re-linking, new files were added" - touch ${output_dir}/include/generated/utsrelease.h + touch_relink=${output_dir}/include/generated/utsrelease.h fi +touch $touch_relink + git checkout -q HEAD~ echo "Building the tree before the patch" @@ -56,9 +61,7 @@ echo "Building the tree with the patch" git checkout -q $HEAD # Also force rebuild "after" in case the file added isn't important. -if ! git log --diff-filter=A HEAD~.. --exit-code >>/dev/null; then - touch ${output_dir}/include/generated/utsrelease.h -fi +touch $touch_relink prep_config make CC="$cc" O=$output_dir ARCH=i386 $build_flags 2> >(tee $tmpfile_n >&2) || rc=1 diff --git a/tests/patch/build_allmodconfig_warn/build_allmodconfig.sh b/tests/patch/build_allmodconfig_warn/build_allmodconfig.sh index 1db47e8..ea9c60b 100755 --- a/tests/patch/build_allmodconfig_warn/build_allmodconfig.sh +++ b/tests/patch/build_allmodconfig_warn/build_allmodconfig.sh @@ -38,11 +38,16 @@ fi # so all module and linker related warnings will pop up in the "after" # but not "before". To avoid this we need to force re-linking on # the "before", too. -if ! git log --diff-filter=A HEAD~.. --exit-code >>/dev/null; then +touch_relink=/dev/null +if ! git log --diff-filter=A HEAD~.. --exit-code >>/dev/null || \ + ! git log HEAD~.. --exit-code -- */Makefile >>/dev/null +then echo "Trying to force re-linking, new files were added" - touch ${output_dir}/include/generated/utsrelease.h + touch_relink=${output_dir}/include/generated/utsrelease.h fi +touch $touch_relink + git checkout -q HEAD~ echo "Building the tree before the patch" @@ -56,9 +61,7 @@ echo "Building the tree with the patch" git checkout -q $HEAD # Also force rebuild "after" in case the file added isn't important. -if ! git log --diff-filter=A HEAD~.. --exit-code >>/dev/null; then - touch ${output_dir}/include/generated/utsrelease.h -fi +touch $touch_relink prep_config make CC="$cc" O=$output_dir $build_flags 2> >(tee $tmpfile_n >&2) || rc=1 From b8c422665ef0deda470194df758fe3db1fd1a6f2 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 15 Jun 2024 15:13:46 -0700 Subject: [PATCH 176/429] tests: build: ignore the x86/boot static symbol noise Whenever a use a symbol gets added module relinking runs and for some reason also rebuilds a few objects in arch/x86/boot/. Those symbols trigger warnings about object which can be static, except they can't because asm uses them: arch/x86/boot/version.c:18:12: warning: symbol 'kernel_version' was not declared. Should it be static? arch/x86/boot/compressed/misc.c:535:6: warning: symbol '__fortify_panic' was not declared. Should it be static? ignore these. This is ugly but I have no idea how to fix this better. Signed-off-by: Jakub Kicinski --- tests/patch/build_32bit/build_32bit.sh | 10 +++++++++- .../build_allmodconfig_warn/build_allmodconfig.sh | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/tests/patch/build_32bit/build_32bit.sh b/tests/patch/build_32bit/build_32bit.sh index 1236d28..39bf3b2 100755 --- a/tests/patch/build_32bit/build_32bit.sh +++ b/tests/patch/build_32bit/build_32bit.sh @@ -16,6 +16,13 @@ prep_config() { ./scripts/config --file $output_dir/.config -d werror } +clean_up_output() { + local file=$1 + + # modpost triggers this randomly on use of existing symbols + sed -i '/arch\/x86\/boot.* warning: symbol .* was not declared. Should it be static?/d' $file +} + echo "Using $build_flags redirect to $tmpfile_o and $tmpfile_n" echo "CC=$cc" $cc --version | head -n1 @@ -54,6 +61,7 @@ echo "Building the tree before the patch" prep_config make CC="$cc" O=$output_dir ARCH=i386 $build_flags 2> >(tee $tmpfile_o >&2) +clean_up_output $tmpfile_o incumbent=$(grep -i -c "\(warn\|error\)" $tmpfile_o) echo "Building the tree with the patch" @@ -65,7 +73,7 @@ touch $touch_relink prep_config make CC="$cc" O=$output_dir ARCH=i386 $build_flags 2> >(tee $tmpfile_n >&2) || rc=1 - +clean_up_output $tmpfile_n current=$(grep -i -c "\(warn\|error\)" $tmpfile_n) echo "Errors and warnings before: $incumbent this patch: $current" >&$DESC_FD diff --git a/tests/patch/build_allmodconfig_warn/build_allmodconfig.sh b/tests/patch/build_allmodconfig_warn/build_allmodconfig.sh index ea9c60b..73d240c 100755 --- a/tests/patch/build_allmodconfig_warn/build_allmodconfig.sh +++ b/tests/patch/build_allmodconfig_warn/build_allmodconfig.sh @@ -16,6 +16,13 @@ prep_config() { ./scripts/config --file $output_dir/.config -d werror } +clean_up_output() { + local file=$1 + + # modpost triggers this randomly on use of existing symbols + sed -i '/arch\/x86\/boot.* warning: symbol .* was not declared. Should it be static?/d' $file +} + echo "Using $build_flags redirect to $tmpfile_o and $tmpfile_n" echo "CC=$cc" $cc --version | head -n1 @@ -54,6 +61,7 @@ echo "Building the tree before the patch" prep_config make CC="$cc" O=$output_dir $build_flags 2> >(tee $tmpfile_o >&2) +clean_up_output $tmpfile_o incumbent=$(grep -i -c "\(warn\|error\)" $tmpfile_o) echo "Building the tree with the patch" @@ -65,7 +73,7 @@ touch $touch_relink prep_config make CC="$cc" O=$output_dir $build_flags 2> >(tee $tmpfile_n >&2) || rc=1 - +clean_up_output $tmpfile_n current=$(grep -i -c "\(warn\|error\)" $tmpfile_n) echo "Errors and warnings before: $incumbent this patch: $current" >&$DESC_FD From 63faa0d9e5c52d984995d2d275619df9a68f2f75 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 15 Jun 2024 15:18:58 -0700 Subject: [PATCH 177/429] tests: build: don't run sparse in the 32b build I can't remember sparse catching extra build errors on 32b. It's just a waste of time to run it for both normal x86 and 32b build. The 64b build takes more time, anyway, (34.6% vs 25.3% of total) but every little helps. Signed-off-by: Jakub Kicinski --- tests/patch/build_32bit/build_32bit.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/patch/build_32bit/build_32bit.sh b/tests/patch/build_32bit/build_32bit.sh index 39bf3b2..c83df67 100755 --- a/tests/patch/build_32bit/build_32bit.sh +++ b/tests/patch/build_32bit/build_32bit.sh @@ -6,7 +6,7 @@ cc="ccache gcc" output_dir=build_32bit/ ncpu=$(grep -c processor /proc/cpuinfo) -build_flags="-Oline -j $ncpu W=1 C=1" +build_flags="-Oline -j $ncpu W=1" tmpfile_o=$(mktemp) tmpfile_n=$(mktemp) rc=0 From 78e7b0ad38ed4a46917084cbfce874a442a355ab Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 15 Jun 2024 15:28:52 -0700 Subject: [PATCH 178/429] contest: brancher: remove couchdb CouchDB is gone, stop trying to save to it. Signed-off-by: Jakub Kicinski --- pw_brancher.py | 41 +++++++---------------------------------- 1 file changed, 7 insertions(+), 34 deletions(-) diff --git a/pw_brancher.py b/pw_brancher.py index 54b478c..18ab1c6 100755 --- a/pw_brancher.py +++ b/pw_brancher.py @@ -2,7 +2,6 @@ # SPDX-License-Identifier: GPL-2.0 import configparser -import couchdb import datetime import json import os @@ -35,14 +34,10 @@ info=branches-info.json [db] db=db-name -name=table-name -user=name -pwd=pass """ psql_conn = None -db = None ignore_delegate = {} gate_checks = {} @@ -180,35 +175,19 @@ def apply_local_patches(config, tree) -> List: def db_insert(config, state, name): - global db, psql_conn + global psql_conn pub_url = config.get('target', 'public_url') - row = {'_id': uuid.uuid4().hex, - "branch": name, + row = {"branch": name, "date": state["branches"][name], "base": state["hashes"].get(name, None), "url": pub_url + " " + name} row |= state["info"][name] - db.save(row) - - cur = None - try: - row = {"branch": name, - "date": state["branches"][name], - "base": state["hashes"].get(name, None), - "url": pub_url + " " + name} - - cur = psql_conn.cursor() + with psql_conn.cursor() as cur: arg = cur.mogrify("(%s,%s,%s,%s,%s)", (row["branch"], row["date"], row["base"], row["url"], - json.dumps(row | state["info"][name]))) + json.dumps(row))) cur.execute("INSERT INTO branches VALUES " + arg.decode('utf-8')) - print("PSQL save success!") - except Exception as e: - if cur: - cur.close() - print("PSQL save FAIL!") - print(e) def create_new(pw, config, state, tree, tgt_remote) -> None: @@ -356,16 +335,10 @@ def prep_remote(config, tree) -> str: def open_db(config): - user = config.get("db", "user") - pwd = config.get("db", "pwd") db_name = config.get("db", "db") - name = config.get("db", "name") - conn = psycopg2.connect(database=db_name) conn.autocommit = True - - server = couchdb.Server(f'http://%7Buser%7D:%7Bpwd%7D@127.0.0.1:5984/') - return conn, server[name] + return conn def main() -> None: @@ -396,8 +369,8 @@ def main() -> None: ignore_delegate = set(config.get('filters', 'ignore_delegate', fallback="").split(',')) global gate_checks gate_checks = set(config.get('filters', 'gate_checks', fallback="").split(',')) - global psql_conn, db - psql_conn, db = open_db(config) + global psql_conn + psql_conn = open_db(config) tree_obj = None tree_dir = config.get('dirs', 'trees', fallback=os.path.join(NIPA_DIR, "../")) From 221b7a936bfdefebde1254c33cd5398fa301bbf9 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 15 Jun 2024 15:28:52 -0700 Subject: [PATCH 179/429] contest: results-fetcher: remove couchdb CouchDB is gone, stop trying to save to it. Signed-off-by: Jakub Kicinski --- contest/results-fetcher.py | 63 +++----------------------------------- 1 file changed, 4 insertions(+), 59 deletions(-) diff --git a/contest/results-fetcher.py b/contest/results-fetcher.py index 26cd627..f54fadc 100755 --- a/contest/results-fetcher.py +++ b/contest/results-fetcher.py @@ -3,7 +3,6 @@ import configparser import copy -import couchdb import datetime import json import os @@ -29,8 +28,6 @@ db=db-name results-name=table-name branches-name=table-name -user=name -pwd=pass """ @@ -45,12 +42,6 @@ def __init__(self): self.tbl_res = self.config.get("db", "results-name", fallback="results") self.tbl_brn = self.config.get("db", "branches-name", fallback="branches") - user = self.config.get("db", "user") - pwd = self.config.get("db", "pwd") - server = couchdb.Server(f'http://%7Buser%7D:%7Bpwd%7D@127.0.0.1:5984/') - self.res_db = server[self.tbl_res] - self.brn_db = server[self.tbl_brn] - db_name = self.config.get("db", "db") self.psql_conn = psycopg2.connect(database=db_name) self.psql_conn.autocommit = True @@ -77,11 +68,13 @@ def insert_result_psql(self, cur, data): data["start"], data["end"], normal, full)) cur.execute(f"INSERT INTO {self.tbl_res} VALUES " + arg.decode('utf-8')) - def insert_wip_psql(self, remote, run, branch_info): + def insert_wip(self, remote, run): if self.psql_has_wip(remote, run): # no point, we have no interesting info to add return + branch_info = self.get_branch(run["branch"]) + data = run.copy() data["remote"] = remote["name"] when = datetime.datetime.fromisoformat(branch_info['date']) @@ -111,7 +104,7 @@ def psql_json_split(self, data): full = json.dumps(data) return json.dumps(normal), full - def insert_real_psql(self, remote, run): + def insert_real(self, remote, run): data = run.copy() data["remote"] = remote["name"] @@ -126,54 +119,6 @@ def insert_real_psql(self, remote, run): q = f"UPDATE {self.tbl_res} " + vals + ' ' + selector cur.execute(q) - def get_wip_row(self, remote, run): - rows = self.res_db.find({ - 'selector': { - 'branch': run["branch"], - 'remote': remote["name"], - 'executor': run["executor"], - 'url': None - } - }) - for row in rows: - return row - - def insert_wip(self, remote, run): - branch_info = self.get_branch(run["branch"]) - - self.insert_wip_psql(remote, run, branch_info) - - existing = self.get_wip_row(remote, run) - - data = run.copy() - if existing: - data['_id'] = existing['_id'] - data['_rev'] = existing['_rev'] - else: - data['_id'] = uuid.uuid4().hex - data["remote"] = remote["name"] - when = datetime.datetime.fromisoformat(branch_info['date']) - data["start"] = str(when) - when += datetime.timedelta(hours=2, minutes=58) - data["end"] = str(when) - data["results"] = None - - self.res_db.save(data) - - def insert_real(self, remote, run): - self.insert_real_psql(remote, run) - existing = self.get_wip_row(remote, run) - - data = run.copy() - if existing: - data['_id'] = existing['_id'] - data['_rev'] = existing['_rev'] - else: - data['_id'] = uuid.uuid4().hex - data["remote"] = remote["name"] - - self.res_db.save(data) - def write_json_atomic(path, data): tmp = path + '.new' From 57a1f10adf5d6001236ce5562cd9d387648e9409 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 15 Jun 2024 16:01:18 -0700 Subject: [PATCH 180/429] poller: make sure hash contains uses fresh trees Since workers no longer update the outer tree - make sure tree.contains() does a reset (which implies a fetch). The main process only uses tree.contains() and tree.check_applies() the former already does a reset. Signed-off-by: Jakub Kicinski --- core/tree.py | 1 + 1 file changed, 1 insertion(+) diff --git a/core/tree.py b/core/tree.py index 5f39f0f..d7640c4 100644 --- a/core/tree.py +++ b/core/tree.py @@ -161,6 +161,7 @@ def remotes(self): def contains(self, commit): core.log_open_sec("Checking for commit " + commit) try: + self.reset() self.git_merge_base(commit, 'HEAD', is_ancestor=True) ret = True except CMD.CmdError: From a314bac73151ea21c81150a4bee71a4da63cd697 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 15 Jun 2024 16:10:48 -0700 Subject: [PATCH 181/429] tests: build: disable more werrors There are some subsystems with their own WERRORs which is dumb. Signed-off-by: Jakub Kicinski --- tests/patch/build_32bit/build_32bit.sh | 2 ++ tests/patch/build_allmodconfig_warn/build_allmodconfig.sh | 2 ++ tests/patch/build_clang/build_clang.sh | 2 ++ 3 files changed, 6 insertions(+) diff --git a/tests/patch/build_32bit/build_32bit.sh b/tests/patch/build_32bit/build_32bit.sh index c83df67..97333c5 100755 --- a/tests/patch/build_32bit/build_32bit.sh +++ b/tests/patch/build_32bit/build_32bit.sh @@ -14,6 +14,8 @@ rc=0 prep_config() { make CC="$cc" O=$output_dir ARCH=i386 allmodconfig ./scripts/config --file $output_dir/.config -d werror + ./scripts/config --file $output_dir/.config -d drm_werror + ./scripts/config --file $output_dir/.config -d kvm_werror } clean_up_output() { diff --git a/tests/patch/build_allmodconfig_warn/build_allmodconfig.sh b/tests/patch/build_allmodconfig_warn/build_allmodconfig.sh index 73d240c..ec20518 100755 --- a/tests/patch/build_allmodconfig_warn/build_allmodconfig.sh +++ b/tests/patch/build_allmodconfig_warn/build_allmodconfig.sh @@ -14,6 +14,8 @@ rc=0 prep_config() { make CC="$cc" O=$output_dir allmodconfig ./scripts/config --file $output_dir/.config -d werror + ./scripts/config --file $output_dir/.config -d drm_werror + ./scripts/config --file $output_dir/.config -d kvm_werror } clean_up_output() { diff --git a/tests/patch/build_clang/build_clang.sh b/tests/patch/build_clang/build_clang.sh index 9da64a5..3991f63 100755 --- a/tests/patch/build_clang/build_clang.sh +++ b/tests/patch/build_clang/build_clang.sh @@ -14,6 +14,8 @@ rc=0 prep_config() { make LLVM=1 O=$output_dir allmodconfig ./scripts/config --file $output_dir/.config -d werror + ./scripts/config --file $output_dir/.config -d drm_werror + ./scripts/config --file $output_dir/.config -d kvm_werror } echo "Using $build_flags redirect to $tmpfile_o and $tmpfile_n" From dc7450fc63308977b088e1d7106386e5f5424f32 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 15 Jun 2024 16:26:11 -0700 Subject: [PATCH 182/429] poller: add more debug to stopping poller doesn't seem to dump state correctly when exiting. It splats: Traceback (most recent call last): File "nipa/pw_poller.py", line 277, in poller.run(life) File "nipa/pw_poller.py", line 256, in run worker.join() File "/usr/lib64/python3.12/threading.py", line 1147, in join self._wait_for_tstate_lock() File "/usr/lib64/python3.12/threading.py", line 1167, in _wait_for_tstate_lock if lock.acquire(block, timeout): ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Add a log message and a handler, just in case. Signed-off-by: Jakub Kicinski --- core/tester.py | 2 ++ pw_poller.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/core/tester.py b/core/tester.py index f29cc12..49d3e7f 100644 --- a/core/tester.py +++ b/core/tester.py @@ -96,6 +96,8 @@ def run(self) -> None: self.done_queue.put(s) core.log("Tester done processing") + core.log("Tester exiting") + def load_tests(self, name): core.log_open_sec(name.capitalize() + " tests") tests_subdir = os.path.join(self.config.get('dirs', 'tests'), name) diff --git a/pw_poller.py b/pw_poller.py index 38faa3e..fb034a5 100755 --- a/pw_poller.py +++ b/pw_poller.py @@ -246,6 +246,8 @@ def run(self, life) -> None: if secs > 0: log("Sleep", secs) log_end_sec() + except KeyboardInterrupt: + pass # finally will still run, but don't splat finally: log_open_sec(f"Stopping threads") for worker in self._workers: From e2c20b48eabcdc47bbe7ae7be0e5c509c00aa682 Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Tue, 18 Jun 2024 15:15:48 +0200 Subject: [PATCH 183/429] contest: vm: add cmd to start VM in logs In the logs, we can see the command used to build the VM, the selftests, etc. but not the one to run the VM. The command is now added to 'log_out', which will be dumped later to the 'stdout' file. Note that we could also move the line adding '> TREE CMD:' to log_out from tree_cmd() to tree_popen(), but that will also add the decoded_stacktrace.sh command to log_out, which will not contain the output of this command as it is redirected to another file. So maybe better not to modify 'tree_popen()' I suppose. Signed-off-by: Matthieu Baerts (NGI0) --- contest/remote/lib/vm.py | 1 + 1 file changed, 1 insertion(+) diff --git a/contest/remote/lib/vm.py b/contest/remote/lib/vm.py index 651e99b..c28fef3 100644 --- a/contest/remote/lib/vm.py +++ b/contest/remote/lib/vm.py @@ -200,6 +200,7 @@ def start(self, cwd=None): cmd += ["--cpus", cpus] print(f"INFO{self.print_pfx} VM starting:", " ".join(cmd)) + self.log_out += "# " + " ".join(cmd) + "\n" self.p = self.tree_popen(cmd) for pipe in [self.p.stdout, self.p.stderr]: From be5f03c6fbeba90bd733986a72d35b9cd6c30a63 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 15 Jun 2024 18:24:23 -0700 Subject: [PATCH 184/429] contest: vm: try to catch kmemleaks Check if VM has kmemleak support and dump its output after each test. Signed-off-by: Jakub Kicinski --- contest/remote/lib/vm.py | 18 +++++++++++++++++- contest/remote/vmksft-p.py | 2 ++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/contest/remote/lib/vm.py b/contest/remote/lib/vm.py index c28fef3..b6c4a5d 100644 --- a/contest/remote/lib/vm.py +++ b/contest/remote/lib/vm.py @@ -92,6 +92,7 @@ def __init__(self, config, vm_name=""): self.cfg_boot_to = int(config.get('vm', 'boot_timeout')) self.filter_data = None + self.has_kmemleak = None self.log_out = "" self.log_err = "" @@ -223,6 +224,11 @@ def start(self, cwd=None): self.cmd("PS1='xx__-> '") self.drain_to_prompt() + off = len(self.log_out) + self.cmd("ls /sys/kernel/debug/") + self.drain_to_prompt() + self.has_kmemleak = "kmemleak" in self.log_out[off:] + self._set_env() def stop(self): @@ -278,7 +284,10 @@ def _read_pipe_nonblock(self, pipe): return read_some, output read_some = True output = decode_and_filter(buf) - if output.find("] RIP: ") != -1 or output.find("] Call Trace:") != -1 or output.find('] ref_tracker: ') != -1: + if output.find("] RIP: ") != -1 or \ + output.find("] Call Trace:") != -1 or \ + output.find('] ref_tracker: ') != -1 or \ + output.find('unreferenced object 0x') != -1: self.fail_state = "oops" except BlockingIOError: pass @@ -425,6 +434,13 @@ def extract_crash(self, out_path): self.fail_state = "" return finger_prints + def check_health(self): + if self.fail_state: + return + if self.has_kmemleak: + self.cmd("echo scan > /sys/kernel/debug/kmemleak && cat /sys/kernel/debug/kmemleak") + self.drain_to_prompt() + def bash_prev_retcode(self): self.cmd("echo $?") stdout, stderr = self.drain_to_prompt() diff --git a/contest/remote/vmksft-p.py b/contest/remote/vmksft-p.py index c8b3ab7..58e5a40 100755 --- a/contest/remote/vmksft-p.py +++ b/contest/remote/vmksft-p.py @@ -162,6 +162,8 @@ def _vm_thread(config, results_path, thr_id, hard_stop, in_queue, out_queue): if indicators["fail"]: result = 'fail' + vm.check_health() + crashes = None if vm.fail_state == 'oops': print(f"INFO: thr-{thr_id} test crashed kernel:", prog) From da2d8b2fc34b710386e773500edd6daf9847e88e Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 15 Jun 2024 18:31:06 -0700 Subject: [PATCH 185/429] contest: vm: dedup crash fingerprints For refleaks and kmemleaks there may be a lot of duplicated objects and stack traces. Dedup the reported crashes. Signed-off-by: Jakub Kicinski --- contest/remote/lib/vm.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/contest/remote/lib/vm.py b/contest/remote/lib/vm.py index b6c4a5d..9bb7082 100644 --- a/contest/remote/lib/vm.py +++ b/contest/remote/lib/vm.py @@ -385,7 +385,7 @@ def extract_crash(self, out_path): in_crash = False start = 0 crash_lines = [] - finger_prints = [] + finger_prints = set() last5 = [""] * 5 combined = self.log_out.split('\n') + self.log_err.split('\n') for line in combined: @@ -395,8 +395,8 @@ def extract_crash(self, out_path): in_crash &= line[-2:] != '] ' if not in_crash: self._load_filters() - finger_prints.append(crash_finger_print(self.filter_data, - crash_lines[start:])) + finger_prints.add(crash_finger_print(self.filter_data, + crash_lines[start:])) else: in_crash |= '] Hardware name: ' in line in_crash |= '] ref_tracker: ' in line @@ -428,11 +428,10 @@ def extract_crash(self, out_path): self._load_filters() if self.filter_data is not None and 'ignore-crashes' in self.filter_data: ignore = set(self.filter_data["ignore-crashes"]) - seen = set(finger_prints) - if not seen - ignore: + if not finger_prints - ignore: print(f"INFO{self.print_pfx} all crashes were ignored") self.fail_state = "" - return finger_prints + return list(finger_prints) def check_health(self): if self.fail_state: From d4b0beac9950e66f6af61a068c96c61c039d9d6b Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 15 Jun 2024 18:50:07 -0700 Subject: [PATCH 186/429] contest: vmksft-p: hack-in support for extra targets Try to support multiple entries in targets. Signed-off-by: Jakub Kicinski --- contest/remote/lib/vm.py | 11 +++++++---- contest/remote/vmksft-p.py | 31 +++++++++++++++++-------------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/contest/remote/lib/vm.py b/contest/remote/lib/vm.py index 9bb7082..18b23d5 100644 --- a/contest/remote/lib/vm.py +++ b/contest/remote/lib/vm.py @@ -105,8 +105,10 @@ def tree_popen(self, cmd): stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) def tree_cmd(self, cmd): - self.log_out += "> TREE CMD: " + cmd + "\n" - proc = self.tree_popen(cmd.split()) + if isinstance(cmd, str): + cmd = cmd.split() + self.log_out += "> TREE CMD: " + " ".join(cmd) + "\n" + proc = self.tree_popen(cmd) stdout, stderr = proc.communicate() self.log_out += stdout.decode("utf-8", "ignore") self.log_err += stderr.decode("utf-8", "ignore") @@ -140,10 +142,11 @@ def build(self, extra_configs, override_configs=None): def _get_ksft_timeout(self): default_timeout = 45 # from tools/testing/selftests/kselftest/runner.sh - target = self.config.get('ksft', 'target', fallback=None) + targets = self.config.get('ksft', 'target', fallback=None) tree_path = self.config.get('local', 'tree_path', fallback=None) - if not target or not tree_path: + if not targets or not tree_path: return default_timeout + target = targets.split()[0] settings_path = f'{tree_path}/tools/testing/selftests/{target}/settings' if not os.path.isfile(settings_path): diff --git a/contest/remote/vmksft-p.py b/contest/remote/vmksft-p.py index 58e5a40..9999b86 100755 --- a/contest/remote/vmksft-p.py +++ b/contest/remote/vmksft-p.py @@ -47,7 +47,7 @@ default_timeout=15 boot_timeout=45 [ksft] -targets=net +target=net nested_tests=off / on @@ -58,14 +58,15 @@ """ -def get_prog_list(vm, target): +def get_prog_list(vm, targets): tmpdir = tempfile.mkdtemp() - vm.tree_cmd(f"make -C tools/testing/selftests/ TARGETS={target} INSTALL_PATH={tmpdir} install") + targets = " ".join(targets) + vm.tree_cmd(['make', '-C', 'tools/testing/selftests/', 'TARGETS=' + targets, 'INSTALL_PATH=' + tmpdir, 'install']) with open(os.path.join(tmpdir, 'kselftest-list.txt'), "r") as fp: targets = fp.readlines() vm.tree_cmd("rm -rf " + tmpdir) - return [e.split(":")[1].strip() for e in targets] + return [(e.split(":")[0].strip(), e.split(":")[1].strip()) for e in targets] def _parse_nested_tests(full_run): @@ -106,7 +107,6 @@ def _parse_nested_tests(full_run): return tests def _vm_thread(config, results_path, thr_id, hard_stop, in_queue, out_queue): - target = config.get('ksft', 'target') vm = None vm_id = -1 @@ -119,6 +119,7 @@ def _vm_thread(config, results_path, thr_id, hard_stop, in_queue, out_queue): test_id = work_item['tid'] prog = work_item['prog'] + target = work_item['target'] test_name = namify(prog) file_name = f"{test_id}-{test_name}" is_retry = 'result' in work_item @@ -138,7 +139,7 @@ def _vm_thread(config, results_path, thr_id, hard_stop, in_queue, out_queue): print(f"INFO: thr-{thr_id} testing == " + prog) t1 = datetime.datetime.now() - vm.cmd(f'make -C tools/testing/selftests TARGETS={target} TEST_PROGS={prog} TEST_GEN_PROGS="" run_tests') + vm.cmd(f'make -C tools/testing/selftests TARGETS="{target}" TEST_PROGS={prog} TEST_GEN_PROGS="" run_tests') try: vm.drain_to_prompt(deadline=deadline) retcode = vm.bash_prev_retcode() @@ -179,7 +180,8 @@ def _vm_thread(config, results_path, thr_id, hard_stop, in_queue, out_queue): outcome = work_item outcome['retry'] = result else: - outcome = {'tid': test_id, 'prog': prog, 'test': test_name, 'file_name': file_name, + outcome = {'tid': test_id, 'prog': prog, 'target': target, + 'test': test_name, 'file_name': file_name, 'result': result, 'time': (t2 - t1).total_seconds()} if crashes: outcome['crashes'] = crashes @@ -237,12 +239,12 @@ def test(binfo, rinfo, cbarg): config.get('local', 'results_path') + '/' + \ rinfo['run-cookie'] rinfo['link'] = link - target = config.get('ksft', 'target') - grp_name = "selftests-" + namify(target) + targets = config.get('ksft', 'target').split() + grp_name = "selftests-" + namify(targets[0]) vm = VM(config) - if vm.build([f"tools/testing/selftests/{target}/config"]) == False: + if vm.build([f"tools/testing/selftests/{targets[0]}/config"]) == False: vm.dump_log(results_path + '/build') return [{ 'test': 'build', @@ -254,11 +256,12 @@ def test(binfo, rinfo, cbarg): shutil.copy(os.path.join(config.get('local', 'tree_path'), '.config'), results_path + '/config') vm.tree_cmd("make headers") - vm.tree_cmd(f"make -C tools/testing/selftests/{target}/") + for target in targets: + vm.tree_cmd(f"make -C tools/testing/selftests/{target}/") vm.dump_log(results_path + '/build') - progs = get_prog_list(vm, target) - progs.sort(reverse=True, key=lambda prog : cbarg.prev_runtime.get(prog, 0)) + progs = get_prog_list(vm, targets) + progs.sort(reverse=True, key=lambda prog : cbarg.prev_runtime.get(prog[1], 0)) dl_min = config.getint('executor', 'deadline_minutes', fallback=999999) hard_stop = datetime.datetime.fromisoformat(binfo["date"]) @@ -271,7 +274,7 @@ def test(binfo, rinfo, cbarg): i = 0 for prog in progs: i += 1 - in_queue.put({'tid': i, 'prog': prog}) + in_queue.put({'tid': i, 'prog': prog[1], 'target': prog[0]}) # In case we have multiple tests kicking off on the same machine, # add optional wait to make sure others have finished building From 7ba93710435332b7cfd5f2a28418b3b68796ebd9 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sun, 16 Jun 2024 09:20:47 -0700 Subject: [PATCH 187/429] contest: move crash extraction code and support kmemleak Move crash extraction helpers from vm.py to its own file, and add unit tests to make developing this code much quicker. Fix the support of kmemleak report parsing while at it. Signed-off-by: Jakub Kicinski --- contest/remote/lib/__init__.py | 1 + contest/remote/lib/crash.py | 566 +++++++++++++++++++++++++++++++++ contest/remote/lib/vm.py | 78 +---- 3 files changed, 578 insertions(+), 67 deletions(-) create mode 100755 contest/remote/lib/crash.py diff --git a/contest/remote/lib/__init__.py b/contest/remote/lib/__init__.py index 1585fc3..2268a29 100644 --- a/contest/remote/lib/__init__.py +++ b/contest/remote/lib/__init__.py @@ -4,3 +4,4 @@ from .loadavg import wait_loadavg from .vm import VM, new_vm, guess_indicators from .cbarg import CbArg +from .crash import has_crash, extract_crash diff --git a/contest/remote/lib/crash.py b/contest/remote/lib/crash.py new file mode 100755 index 0000000..e67afaa --- /dev/null +++ b/contest/remote/lib/crash.py @@ -0,0 +1,566 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 + +import re +import unittest + + +def has_crash(output): + return output.find("] RIP: ") != -1 or \ + output.find("] Call Trace:") != -1 or \ + output.find('] ref_tracker: ') != -1 or \ + output.find('unreferenced object 0x') != -1 + + +def finger_print_skip_pfx_len(filters, needles): + # Filter may contain a list of needles we want to skip + # Assume it's well sorted, so we don't need LPM... + if filters and 'crash-prefix-skip' in filters: + for skip_pfx in filters['crash-prefix-skip']: + if len(needles) < len(skip_pfx): + continue + if needles[:len(skip_pfx)] == skip_pfx: + return len(skip_pfx) + return 0 + + +def crash_finger_print(filters, lines): + needles = [] + need_re = re.compile(r'.*( |0:|>\] )([a-z0-9_]+)\+0x[0-9a-f]+/0x[0-9a-f]+.*') + skip = 0 + for line in lines: + m = need_re.match(line) + if not m: + continue + needles.append(m.groups()[1]) + skip = finger_print_skip_pfx_len(filters, needles) + if len(needles) - skip == 5: + break + + needles = needles[skip:] + return ":".join(needles) + + +def extract_crash(outputs, prompt, get_filters): + in_crash = False + start = 0 + crash_lines = [] + finger_prints = set() + last5 = [""] * 5 + outputs = outputs.split('\n') + for line in outputs: + if in_crash: + in_crash &= '] ---[ end trace ' not in line + in_crash &= '] ' not in line + in_crash &= line[-2:] != '] ' + in_crash &= not line.startswith(prompt) + if not in_crash: + finger_prints.add(crash_finger_print(get_filters(), + crash_lines[start:])) + else: + in_crash |= '] Hardware name: ' in line + in_crash |= '] ref_tracker: ' in line + in_crash |= line.startswith('unreferenced object 0x') + if in_crash: + start = len(crash_lines) + crash_lines += last5 + + # Keep last 5 to get some of the stuff before stack trace + last5 = last5[1:] + ["| " + line] + + if in_crash: + crash_lines.append(line) + + return crash_lines, finger_prints + + +############################################################# +# END OF CODE --- START OF UNIT TESTS +############################################################# + + +class TestCrashes(unittest.TestCase): + def test_memleak(self): + self.assertTrue(has_crash(TestCrashes.kmemleak)) + lines, fingers = extract_crash(TestCrashes.kmemleak, "xx__->", lambda : None) + self.assertGreater(len(lines), 8) + self.assertEqual(fingers, + {'kmalloc_trace_noprof:tcp_ao_alloc_info:do_tcp_setsockopt:do_sock_setsockopt:__sys_setsockopt'}) + + def test_bad_irq(self): + self.assertTrue(has_crash(TestCrashes.bad_irq)) + lines, fingers = extract_crash(TestCrashes.bad_irq, "xx__->", lambda : None) + self.assertGreater(len(lines), 10) + self.assertEqual(fingers, + {'dump_stack_lvl:__report_bad_irq:note_interrupt:handle_irq_event:handle_edge_irq'}) + + def test_bad_irq_trim(self): + self.assertTrue(has_crash(TestCrashes.bad_irq)) + lines, fingers = extract_crash(TestCrashes.bad_irq, "xx__->", + lambda : {'crash-prefix-skip': [["dump_stack_lvl","__report_bad_irq"]]}) + self.assertGreater(len(lines), 10) + self.assertEqual(fingers, + {'note_interrupt:handle_irq_event:handle_edge_irq:__common_interrupt:common_interrupt'}) + + def test_refleak(self): + self.assertTrue(has_crash(TestCrashes.refleak)) + lines, fingers = extract_crash(TestCrashes.refleak, "xx__->", lambda : None) + self.assertGreater(len(lines), 50) + self.assertEqual(fingers, + {'dev_hard_start_xmit:__dev_queue_xmit:ip6_finish_output2:ip6_finish_output:netdev_get_by_index', + '___sys_sendmsg:__sys_sendmsg:do_syscall_64:dst_init:dst_alloc', + 'dst_init:dst_alloc:ip6_dst_alloc:ip6_rt_pcpu_alloc:ip6_pol_route', + '___sys_sendmsg:__sys_sendmsg:do_syscall_64:ipv6_add_dev:addrconf_notify', + 'dev_hard_start_xmit:__dev_queue_xmit:arp_solicit:neigh_probe:dst_init'}) + + ######################################################### + ### Sample outputs + ######################################################### + kmemleak = """xx__-> echo $? +0 +xx__-> echo scan > /sys/kernel/debug/kmemleak && cat /sys/kernel/debug/kmemleak +unreferenced object 0xffff888003692380 (size 128): + comm "unsigned-md5_ip", pid 762, jiffies 4294831244 + hex dump (first 32 bytes): + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ + backtrace (crc 2128895f): + [] kmalloc_trace_noprof+0x236/0x290 + [] tcp_ao_alloc_info+0x44/0xf0 + [] tcp_ao_info_cmd.constprop.0+0x423/0x8e0 + [] do_tcp_setsockopt+0xa64/0x2320 + [] do_sock_setsockopt+0x149/0x3a0 + [] __sys_setsockopt+0x104/0x1a0 + [] __x64_sys_setsockopt+0xbd/0x160 + [] do_syscall_64+0xc1/0x1d0 + [] entry_SYSCALL_64_after_hwframe+0x77/0x7f +xx__-> + """ + + bad_irq = """[ 1000.092583][ T3849] tc (3849) used greatest stack depth: 23216 bytes left +[ 1081.418714][ C3] irq 4: nobody cared (try booting with the "irqpoll" option) +[ 1081.419111][ C3] CPU: 3 PID: 3703 Comm: perl Not tainted 6.10.0-rc3-virtme #1 +[ 1081.419389][ C3] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS rel-1.16.3-0-ga6ed6b701f0a-prebuilt.qemu.org 04/01/2014 +[ 1081.419773][ C3] Call Trace: +[ 1081.419909][ C3] +[ 1081.420008][ C3] dump_stack_lvl+0x82/0xd0 +[ 1081.420197][ C3] __report_bad_irq+0x5f/0x180 +[ 1081.420371][ C3] note_interrupt+0x6b3/0x860 +[ 1081.420556][ C3] handle_irq_event+0x16d/0x1c0 +[ 1081.420728][ C3] handle_edge_irq+0x1fa/0xb60 +[ 1081.420912][ C3] __common_interrupt+0x82/0x170 +[ 1081.421128][ C3] common_interrupt+0x7e/0x90 +[ 1081.421330][ C3] +[ 1081.421430][ C3] +[ 1081.421526][ C3] asm_common_interrupt+0x26/0x40 +[ 1081.421711][ C3] RIP: 0010:_raw_spin_unlock_irqrestore+0x43/0x70 +[ 1081.421951][ C3] Code: 10 e8 d1 1a 92 fd 48 89 ef e8 49 8b 92 fd 81 e3 00 02 00 00 75 1d 9c 58 f6 c4 02 75 29 48 85 db 74 01 fb 65 ff 0d 95 7a 06 54 <74> 0e 5b 5d c3 cc cc cc cc e8 7f 01 b6 fd eb dc 0f 1f 44 00 00 5b +[ 1081.422616][ C3] RSP: 0018:ffffc90000bdfac0 EFLAGS: 00000286 +[ 1081.422862][ C3] RAX: 0000000000000006 RBX: 0000000000000200 RCX: 1ffffffff5e2ff1a +[ 1081.423147][ C3] RDX: 0000000000000000 RSI: 0000000000000000 RDI: ffffffffabfd4d81 +[ 1081.423422][ C3] RBP: ffffffffafa41060 R08: 0000000000000001 R09: fffffbfff5e2b0a8 +[ 1081.423701][ C3] R10: ffffffffaf158547 R11: 0000000000000000 R12: 0000000000000001 +[ 1081.423991][ C3] R13: 0000000000000286 R14: ffffffffafa41060 R15: ffff888006683800 +[ 1081.424296][ C3] ? _raw_spin_unlock_irqrestore+0x51/0x70 +[ 1081.424542][ C3] uart_write+0x13d/0x330 +[ 1081.424695][ C3] process_output_block+0x13e/0x790 +[ 1081.424885][ C3] ? lockdep_hardirqs_on_prepare+0x275/0x410 +[ 1081.425144][ C3] n_tty_write+0x412/0x7a0 +[ 1081.425344][ C3] ? __pfx_n_tty_write+0x10/0x10 +[ 1081.425535][ C3] ? trace_lock_acquire+0x14d/0x1f0 +[ 1081.425722][ C3] ? __pfx_woken_wake_function+0x10/0x10 +[ 1081.425909][ C3] ? iterate_tty_write+0x95/0x540 +[ 1081.426098][ C3] ? lock_acquire+0x32/0xc0 +[ 1081.426285][ C3] ? iterate_tty_write+0x95/0x540 +[ 1081.426473][ C3] iterate_tty_write+0x228/0x540 +[ 1081.426660][ C3] ? tty_ldisc_ref_wait+0x28/0x80 +[ 1081.426850][ C3] file_tty_write.constprop.0+0x1db/0x370 +[ 1081.427037][ C3] vfs_write+0xa18/0x10b0 +[ 1081.427184][ C3] ? __pfx_lock_acquire.part.0+0x10/0x10 +[ 1081.427369][ C3] ? __pfx_vfs_write+0x10/0x10 +[ 1081.427557][ C3] ? clockevents_program_event+0xf6/0x300 +[ 1081.427750][ C3] ? __fget_light+0x53/0x1e0 +[ 1081.427938][ C3] ? clockevents_program_event+0x1ea/0x300 +[ 1081.428170][ C3] ksys_write+0xf5/0x1e0 +[ 1081.428319][ C3] ? __pfx_ksys_write+0x10/0x10 +[ 1081.428515][ C3] do_syscall_64+0xc1/0x1d0 +[ 1081.428696][ C3] entry_SYSCALL_64_after_hwframe+0x77/0x7f +[ 1081.428915][ C3] RIP: 0033:0x7f3d90a53957 +[ 1081.429131][ C3] Code: 0b 00 f7 d8 64 89 02 48 c7 c0 ff ff ff ff eb b7 0f 1f 00 f3 0f 1e fa 64 8b 04 25 18 00 00 00 85 c0 75 10 b8 01 00 00 00 0f 05 <48> 3d 00 f0 ff ff 77 51 c3 48 83 ec 28 48 89 54 24 18 48 89 74 24 +[ 1081.429726][ C3] RSP: 002b:00007ffe774784d8 EFLAGS: 00000246 ORIG_RAX: 0000000000000001 +[ 1081.429987][ C3] RAX: ffffffffffffffda RBX: 00005596b8d2a1d0 RCX: 00007f3d90a53957 +[ 1081.430242][ C3] RDX: 0000000000000001 RSI: 00005596b8d2a1d0 RDI: 0000000000000001 +[ 1081.430494][ C3] RBP: 0000000000000001 R08: 0000000000000000 R09: 0000000000002000 +[ 1081.430753][ C3] R10: 0000000000000001 R11: 0000000000000246 R12: 00005596b8d165c0 +[ 1081.431012][ C3] R13: 00005596b8cf72a0 R14: 0000000000000001 R15: 00005596b8d165c0 +[ 1081.431290][ C3] +[ 1081.431421][ C3] handlers: +[ 1081.431553][ C3] [] serial8250_interrupt +[ 1081.432206][ C3] Disabling IRQ #4 +""" + + refleak = """ +[ 1055.837009][ T75] veth_A-C: left allmulticast mode +[ 1055.837273][ T75] veth_A-C: left promiscuous mode +[ 1055.837697][ T75] br0: port 1(veth_A-C) entered disabled state +[ 1619.761346][T10781] Initializing XFRM netlink socket +[ 1868.101248][T12484] unregister_netdevice: waiting for veth_A-R1 to become free. Usage count = 5 +[ 1868.101753][T12484] ref_tracker: veth_A-R1@ffff8880060c45e0 has 1/4 users at +[ 1868.101753][T12484] dst_init+0x84/0x4a0 +[ 1868.101753][T12484] dst_alloc+0x97/0x150 +[ 1868.101753][T12484] ip6_dst_alloc+0x23/0x90 +[ 1868.101753][T12484] ip6_rt_pcpu_alloc+0x1e6/0x520 +[ 1868.101753][T12484] ip6_pol_route+0x56f/0x840 +[ 1868.101753][T12484] fib6_rule_lookup+0x334/0x630 +[ 1868.101753][T12484] ip6_route_output_flags+0x259/0x480 +[ 1868.101753][T12484] ip6_dst_lookup_tail.constprop.0+0x700/0xb60 +[ 1868.101753][T12484] ip6_dst_lookup_flow+0x88/0x190 +[ 1868.101753][T12484] udp_tunnel6_dst_lookup+0x2b0/0x4d0 +[ 1868.101753][T12484] vxlan_xmit_one+0xd41/0x4500 [vxlan] +[ 1868.101753][T12484] vxlan_xmit+0x9b6/0xf10 [vxlan] +[ 1868.101753][T12484] dev_hard_start_xmit+0x10e/0x360 +[ 1868.101753][T12484] __dev_queue_xmit+0xe76/0x1740 +[ 1868.101753][T12484] arp_solicit+0x4aa/0xe20 +[ 1868.101753][T12484] neigh_probe+0xaa/0xf0 +[ 1868.101753][T12484] +[ 1868.104788][T12484] ref_tracker: veth_A-R1@ffff8880060c45e0 has 1/4 users at +[ 1868.104788][T12484] dst_init+0x84/0x4a0 +[ 1868.104788][T12484] dst_alloc+0x97/0x150 +[ 1868.104788][T12484] ip6_dst_alloc+0x23/0x90 +[ 1868.104788][T12484] ip6_rt_pcpu_alloc+0x1e6/0x520 +[ 1868.104788][T12484] ip6_pol_route+0x56f/0x840 +[ 1868.104788][T12484] fib6_rule_lookup+0x334/0x630 +[ 1868.104788][T12484] ip6_route_output_flags+0x259/0x480 +[ 1868.104788][T12484] ip6_dst_lookup_tail.constprop.0+0x700/0xb60 +[ 1868.104788][T12484] ip6_dst_lookup_flow+0x88/0x190 +[ 1868.104788][T12484] udp_tunnel6_dst_lookup+0x2b0/0x4d0 +[ 1868.104788][T12484] vxlan_xmit_one+0xd41/0x4500 [vxlan] +[ 1868.104788][T12484] vxlan_xmit+0x9b6/0xf10 [vxlan] +[ 1868.104788][T12484] dev_hard_start_xmit+0x10e/0x360 +[ 1868.104788][T12484] __dev_queue_xmit+0xe76/0x1740 +[ 1868.104788][T12484] ip6_finish_output2+0x59b/0xff0 +[ 1868.104788][T12484] ip6_finish_output+0x553/0xdf0 +[ 1868.104788][T12484] +[ 1868.107473][T12484] ref_tracker: veth_A-R1@ffff8880060c45e0 has 1/4 users at +[ 1868.107473][T12484] netdev_get_by_index+0x5e/0x80 +[ 1868.107473][T12484] fib6_nh_init+0x3dd/0x15c0 +[ 1868.107473][T12484] nh_create_ipv6+0x377/0x600 +[ 1868.107473][T12484] nexthop_create+0x311/0x650 +[ 1868.107473][T12484] rtm_new_nexthop+0x3f0/0x5c0 +[ 1868.107473][T12484] rtnetlink_rcv_msg+0x2fb/0xc10 +[ 1868.107473][T12484] netlink_rcv_skb+0x130/0x360 +[ 1868.107473][T12484] netlink_unicast+0x449/0x710 +[ 1868.107473][T12484] netlink_sendmsg+0x723/0xbe0 +[ 1868.107473][T12484] ____sys_sendmsg+0x800/0xa90 +[ 1868.107473][T12484] ___sys_sendmsg+0xee/0x170 +[ 1868.107473][T12484] __sys_sendmsg+0xc2/0x150 +[ 1868.107473][T12484] do_syscall_64+0xc1/0x1d0 +[ 1868.107473][T12484] entry_SYSCALL_64_after_hwframe+0x77/0x7f +[ 1868.107473][T12484] +[ 1868.109800][T12484] ref_tracker: veth_A-R1@ffff8880060c45e0 has 1/4 users at +[ 1868.109800][T12484] ipv6_add_dev+0x3b9/0x11c0 +[ 1868.109800][T12484] addrconf_notify+0x344/0xd60 +[ 1868.109800][T12484] notifier_call_chain+0xcd/0x150 +[ 1868.109800][T12484] register_netdevice+0x1091/0x1690 +[ 1868.109800][T12484] veth_newlink+0x401/0x830 +[ 1868.109800][T12484] rtnl_newlink_create+0x341/0x850 +[ 1868.109800][T12484] __rtnl_newlink+0xac9/0xd80 +[ 1868.109800][T12484] rtnl_newlink+0x63/0xa0 +[ 1868.109800][T12484] rtnetlink_rcv_msg+0x2fb/0xc10 +[ 1868.109800][T12484] netlink_rcv_skb+0x130/0x360 +[ 1868.109800][T12484] netlink_unicast+0x449/0x710 +[ 1868.109800][T12484] netlink_sendmsg+0x723/0xbe0 +[ 1868.109800][T12484] ____sys_sendmsg+0x800/0xa90 +[ 1868.109800][T12484] ___sys_sendmsg+0xee/0x170 +[ 1868.109800][T12484] __sys_sendmsg+0xc2/0x150 +[ 1868.109800][T12484] do_syscall_64+0xc1/0x1d0 +[ 1868.109800][T12484] +[ 1878.221231][T12484] unregister_netdevice: waiting for veth_A-R1 to become free. Usage count = 5 +[ 1878.221630][T12484] ref_tracker: veth_A-R1@ffff8880060c45e0 has 1/4 users at +[ 1878.221630][T12484] dst_init+0x84/0x4a0 +[ 1878.221630][T12484] dst_alloc+0x97/0x150 +[ 1878.221630][T12484] ip6_dst_alloc+0x23/0x90 +[ 1878.221630][T12484] ip6_rt_pcpu_alloc+0x1e6/0x520 +[ 1878.221630][T12484] ip6_pol_route+0x56f/0x840 +[ 1878.221630][T12484] fib6_rule_lookup+0x334/0x630 +[ 1878.221630][T12484] ip6_route_output_flags+0x259/0x480 +[ 1878.221630][T12484] ip6_dst_lookup_tail.constprop.0+0x700/0xb60 +[ 1878.221630][T12484] ip6_dst_lookup_flow+0x88/0x190 +[ 1878.221630][T12484] udp_tunnel6_dst_lookup+0x2b0/0x4d0 +[ 1878.221630][T12484] vxlan_xmit_one+0xd41/0x4500 [vxlan] +[ 1878.221630][T12484] vxlan_xmit+0x9b6/0xf10 [vxlan] +[ 1878.221630][T12484] dev_hard_start_xmit+0x10e/0x360 +[ 1878.221630][T12484] __dev_queue_xmit+0xe76/0x1740 +[ 1878.221630][T12484] arp_solicit+0x4aa/0xe20 +[ 1878.221630][T12484] neigh_probe+0xaa/0xf0 +[ 1878.221630][T12484] +[ 1878.223972][T12484] ref_tracker: veth_A-R1@ffff8880060c45e0 has 1/4 users at +[ 1878.223972][T12484] dst_init+0x84/0x4a0 +[ 1878.223972][T12484] dst_alloc+0x97/0x150 +[ 1878.223972][T12484] ip6_dst_alloc+0x23/0x90 +[ 1878.223972][T12484] ip6_rt_pcpu_alloc+0x1e6/0x520 +[ 1878.223972][T12484] ip6_pol_route+0x56f/0x840 +[ 1878.223972][T12484] fib6_rule_lookup+0x334/0x630 +[ 1878.223972][T12484] ip6_route_output_flags+0x259/0x480 +[ 1878.223972][T12484] ip6_dst_lookup_tail.constprop.0+0x700/0xb60 +[ 1878.223972][T12484] ip6_dst_lookup_flow+0x88/0x190 +[ 1878.223972][T12484] udp_tunnel6_dst_lookup+0x2b0/0x4d0 +[ 1878.223972][T12484] vxlan_xmit_one+0xd41/0x4500 [vxlan] +[ 1878.223972][T12484] vxlan_xmit+0x9b6/0xf10 [vxlan] +[ 1878.223972][T12484] dev_hard_start_xmit+0x10e/0x360 +[ 1878.223972][T12484] __dev_queue_xmit+0xe76/0x1740 +[ 1878.223972][T12484] ip6_finish_output2+0x59b/0xff0 +[ 1878.223972][T12484] ip6_finish_output+0x553/0xdf0 +[ 1878.223972][T12484] +[ 1878.226285][T12484] ref_tracker: veth_A-R1@ffff8880060c45e0 has 1/4 users at +[ 1878.226285][T12484] netdev_get_by_index+0x5e/0x80 +[ 1878.226285][T12484] fib6_nh_init+0x3dd/0x15c0 +[ 1878.226285][T12484] nh_create_ipv6+0x377/0x600 +[ 1878.226285][T12484] nexthop_create+0x311/0x650 +[ 1878.226285][T12484] rtm_new_nexthop+0x3f0/0x5c0 +[ 1878.226285][T12484] rtnetlink_rcv_msg+0x2fb/0xc10 +[ 1878.226285][T12484] netlink_rcv_skb+0x130/0x360 +[ 1878.226285][T12484] netlink_unicast+0x449/0x710 +[ 1878.226285][T12484] netlink_sendmsg+0x723/0xbe0 +[ 1878.226285][T12484] ____sys_sendmsg+0x800/0xa90 +[ 1878.226285][T12484] ___sys_sendmsg+0xee/0x170 +[ 1878.226285][T12484] __sys_sendmsg+0xc2/0x150 +[ 1878.226285][T12484] do_syscall_64+0xc1/0x1d0 +[ 1878.226285][T12484] entry_SYSCALL_64_after_hwframe+0x77/0x7f +[ 1878.226285][T12484] +[ 1878.228262][T12484] ref_tracker: veth_A-R1@ffff8880060c45e0 has 1/4 users at +[ 1878.228262][T12484] ipv6_add_dev+0x3b9/0x11c0 +[ 1878.228262][T12484] addrconf_notify+0x344/0xd60 +[ 1878.228262][T12484] notifier_call_chain+0xcd/0x150 +[ 1878.228262][T12484] register_netdevice+0x1091/0x1690 +[ 1878.228262][T12484] veth_newlink+0x401/0x830 +[ 1878.228262][T12484] rtnl_newlink_create+0x341/0x850 +[ 1878.228262][T12484] __rtnl_newlink+0xac9/0xd80 +[ 1878.228262][T12484] rtnl_newlink+0x63/0xa0 +[ 1878.228262][T12484] rtnetlink_rcv_msg+0x2fb/0xc10 +[ 1878.228262][T12484] netlink_rcv_skb+0x130/0x360 +[ 1878.228262][T12484] netlink_unicast+0x449/0x710 +[ 1878.228262][T12484] netlink_sendmsg+0x723/0xbe0 +[ 1878.228262][T12484] ____sys_sendmsg+0x800/0xa90 +[ 1878.228262][T12484] ___sys_sendmsg+0xee/0x170 +[ 1878.228262][T12484] __sys_sendmsg+0xc2/0x150 +[ 1878.228262][T12484] do_syscall_64+0xc1/0x1d0 +[ 1878.228262][T12484] +[ 1888.397169][T12484] unregister_netdevice: waiting for veth_A-R1 to become free. Usage count = 5 +[ 1888.397586][T12484] ref_tracker: veth_A-R1@ffff8880060c45e0 has 1/4 users at +[ 1888.397586][T12484] dst_init+0x84/0x4a0 +[ 1888.397586][T12484] dst_alloc+0x97/0x150 +[ 1888.397586][T12484] ip6_dst_alloc+0x23/0x90 +[ 1888.397586][T12484] ip6_rt_pcpu_alloc+0x1e6/0x520 +[ 1888.397586][T12484] ip6_pol_route+0x56f/0x840 +[ 1888.397586][T12484] fib6_rule_lookup+0x334/0x630 +[ 1888.397586][T12484] ip6_route_output_flags+0x259/0x480 +[ 1888.397586][T12484] ip6_dst_lookup_tail.constprop.0+0x700/0xb60 +[ 1888.397586][T12484] ip6_dst_lookup_flow+0x88/0x190 +[ 1888.397586][T12484] udp_tunnel6_dst_lookup+0x2b0/0x4d0 +[ 1888.397586][T12484] vxlan_xmit_one+0xd41/0x4500 [vxlan] +[ 1888.397586][T12484] vxlan_xmit+0x9b6/0xf10 [vxlan] +[ 1888.397586][T12484] dev_hard_start_xmit+0x10e/0x360 +[ 1888.397586][T12484] __dev_queue_xmit+0xe76/0x1740 +[ 1888.397586][T12484] arp_solicit+0x4aa/0xe20 +[ 1888.397586][T12484] neigh_probe+0xaa/0xf0 +[ 1888.397586][T12484] +[ 1888.400065][T12484] ref_tracker: veth_A-R1@ffff8880060c45e0 has 1/4 users at +[ 1888.400065][T12484] dst_init+0x84/0x4a0 +[ 1888.400065][T12484] dst_alloc+0x97/0x150 +[ 1888.400065][T12484] ip6_dst_alloc+0x23/0x90 +[ 1888.400065][T12484] ip6_rt_pcpu_alloc+0x1e6/0x520 +[ 1888.400065][T12484] ip6_pol_route+0x56f/0x840 +[ 1888.400065][T12484] fib6_rule_lookup+0x334/0x630 +[ 1888.400065][T12484] ip6_route_output_flags+0x259/0x480 +[ 1888.400065][T12484] ip6_dst_lookup_tail.constprop.0+0x700/0xb60 +[ 1888.400065][T12484] ip6_dst_lookup_flow+0x88/0x190 +[ 1888.400065][T12484] udp_tunnel6_dst_lookup+0x2b0/0x4d0 +[ 1888.400065][T12484] vxlan_xmit_one+0xd41/0x4500 [vxlan] +[ 1888.400065][T12484] vxlan_xmit+0x9b6/0xf10 [vxlan] +[ 1888.400065][T12484] dev_hard_start_xmit+0x10e/0x360 +[ 1888.400065][T12484] __dev_queue_xmit+0xe76/0x1740 +[ 1888.400065][T12484] ip6_finish_output2+0x59b/0xff0 +[ 1888.400065][T12484] ip6_finish_output+0x553/0xdf0 +[ 1888.400065][T12484] +[ 1888.402504][T12484] ref_tracker: veth_A-R1@ffff8880060c45e0 has 1/4 users at +[ 1888.402504][T12484] netdev_get_by_index+0x5e/0x80 +[ 1888.402504][T12484] fib6_nh_init+0x3dd/0x15c0 +[ 1888.402504][T12484] nh_create_ipv6+0x377/0x600 +[ 1888.402504][T12484] nexthop_create+0x311/0x650 +[ 1888.402504][T12484] rtm_new_nexthop+0x3f0/0x5c0 +[ 1888.402504][T12484] rtnetlink_rcv_msg+0x2fb/0xc10 +[ 1888.402504][T12484] netlink_rcv_skb+0x130/0x360 +[ 1888.402504][T12484] netlink_unicast+0x449/0x710 +[ 1888.402504][T12484] netlink_sendmsg+0x723/0xbe0 +[ 1888.402504][T12484] ____sys_sendmsg+0x800/0xa90 +[ 1888.402504][T12484] ___sys_sendmsg+0xee/0x170 +[ 1888.402504][T12484] __sys_sendmsg+0xc2/0x150 +[ 1888.402504][T12484] do_syscall_64+0xc1/0x1d0 +[ 1888.402504][T12484] entry_SYSCALL_64_after_hwframe+0x77/0x7f +[ 1888.402504][T12484] +[ 1888.404580][T12484] ref_tracker: veth_A-R1@ffff8880060c45e0 has 1/4 users at +[ 1888.404580][T12484] ipv6_add_dev+0x3b9/0x11c0 +[ 1888.404580][T12484] addrconf_notify+0x344/0xd60 +[ 1888.404580][T12484] notifier_call_chain+0xcd/0x150 +[ 1888.404580][T12484] register_netdevice+0x1091/0x1690 +[ 1888.404580][T12484] veth_newlink+0x401/0x830 +[ 1888.404580][T12484] rtnl_newlink_create+0x341/0x850 +[ 1888.404580][T12484] __rtnl_newlink+0xac9/0xd80 +[ 1888.404580][T12484] rtnl_newlink+0x63/0xa0 +[ 1888.404580][T12484] rtnetlink_rcv_msg+0x2fb/0xc10 +[ 1888.404580][T12484] netlink_rcv_skb+0x130/0x360 +[ 1888.404580][T12484] netlink_unicast+0x449/0x710 +[ 1888.404580][T12484] netlink_sendmsg+0x723/0xbe0 +[ 1888.404580][T12484] ____sys_sendmsg+0x800/0xa90 +[ 1888.404580][T12484] ___sys_sendmsg+0xee/0x170 +[ 1888.404580][T12484] __sys_sendmsg+0xc2/0x150 +[ 1888.404580][T12484] do_syscall_64+0xc1/0x1d0 +[ 1888.404580][T12484] +[ 1898.589197][T12484] unregister_netdevice: waiting for veth_A-R1 to become free. Usage count = 5 +[ 1898.589575][T12484] ref_tracker: veth_A-R1@ffff8880060c45e0 has 1/4 users at +[ 1898.589575][T12484] dst_init+0x84/0x4a0 +[ 1898.589575][T12484] dst_alloc+0x97/0x150 +[ 1898.589575][T12484] ip6_dst_alloc+0x23/0x90 +[ 1898.589575][T12484] ip6_rt_pcpu_alloc+0x1e6/0x520 +[ 1898.589575][T12484] ip6_pol_route+0x56f/0x840 +[ 1898.589575][T12484] fib6_rule_lookup+0x334/0x630 +[ 1898.589575][T12484] ip6_route_output_flags+0x259/0x480 +[ 1898.589575][T12484] ip6_dst_lookup_tail.constprop.0+0x700/0xb60 +[ 1898.589575][T12484] ip6_dst_lookup_flow+0x88/0x190 +[ 1898.589575][T12484] udp_tunnel6_dst_lookup+0x2b0/0x4d0 +[ 1898.589575][T12484] vxlan_xmit_one+0xd41/0x4500 [vxlan] +[ 1898.589575][T12484] vxlan_xmit+0x9b6/0xf10 [vxlan] +[ 1898.589575][T12484] dev_hard_start_xmit+0x10e/0x360 +[ 1898.589575][T12484] __dev_queue_xmit+0xe76/0x1740 +[ 1898.589575][T12484] arp_solicit+0x4aa/0xe20 +[ 1898.589575][T12484] neigh_probe+0xaa/0xf0 +[ 1898.589575][T12484] +[ 1898.591877][T12484] ref_tracker: veth_A-R1@ffff8880060c45e0 has 1/4 users at +[ 1898.591877][T12484] dst_init+0x84/0x4a0 +[ 1898.591877][T12484] dst_alloc+0x97/0x150 +[ 1898.591877][T12484] ip6_dst_alloc+0x23/0x90 +[ 1898.591877][T12484] ip6_rt_pcpu_alloc+0x1e6/0x520 +[ 1898.591877][T12484] ip6_pol_route+0x56f/0x840 +[ 1898.591877][T12484] fib6_rule_lookup+0x334/0x630 +[ 1898.591877][T12484] ip6_route_output_flags+0x259/0x480 +[ 1898.591877][T12484] ip6_dst_lookup_tail.constprop.0+0x700/0xb60 +[ 1898.591877][T12484] ip6_dst_lookup_flow+0x88/0x190 +[ 1898.591877][T12484] udp_tunnel6_dst_lookup+0x2b0/0x4d0 +[ 1898.591877][T12484] vxlan_xmit_one+0xd41/0x4500 [vxlan] +[ 1898.591877][T12484] vxlan_xmit+0x9b6/0xf10 [vxlan] +[ 1898.591877][T12484] dev_hard_start_xmit+0x10e/0x360 +[ 1898.591877][T12484] __dev_queue_xmit+0xe76/0x1740 +[ 1898.591877][T12484] ip6_finish_output2+0x59b/0xff0 +[ 1898.591877][T12484] ip6_finish_output+0x553/0xdf0 +[ 1898.591877][T12484] +[ 1898.594146][T12484] ref_tracker: veth_A-R1@ffff8880060c45e0 has 1/4 users at +[ 1898.594146][T12484] netdev_get_by_index+0x5e/0x80 +[ 1898.594146][T12484] fib6_nh_init+0x3dd/0x15c0 +[ 1898.594146][T12484] nh_create_ipv6+0x377/0x600 +[ 1898.594146][T12484] nexthop_create+0x311/0x650 +[ 1898.594146][T12484] rtm_new_nexthop+0x3f0/0x5c0 +[ 1898.594146][T12484] rtnetlink_rcv_msg+0x2fb/0xc10 +[ 1898.594146][T12484] netlink_rcv_skb+0x130/0x360 +[ 1898.594146][T12484] netlink_unicast+0x449/0x710 +[ 1898.594146][T12484] netlink_sendmsg+0x723/0xbe0 +[ 1898.594146][T12484] ____sys_sendmsg+0x800/0xa90 +[ 1898.594146][T12484] ___sys_sendmsg+0xee/0x170 +[ 1898.594146][T12484] __sys_sendmsg+0xc2/0x150 +[ 1898.594146][T12484] do_syscall_64+0xc1/0x1d0 +[ 1898.594146][T12484] entry_SYSCALL_64_after_hwframe+0x77/0x7f +[ 1898.594146][T12484] +[ 1898.596102][T12484] ref_tracker: veth_A-R1@ffff8880060c45e0 has 1/4 users at +[ 1898.596102][T12484] ipv6_add_dev+0x3b9/0x11c0 +[ 1898.596102][T12484] addrconf_notify+0x344/0xd60 +[ 1898.596102][T12484] notifier_call_chain+0xcd/0x150 +[ 1898.596102][T12484] register_netdevice+0x1091/0x1690 +[ 1898.596102][T12484] veth_newlink+0x401/0x830 +[ 1898.596102][T12484] rtnl_newlink_create+0x341/0x850 +[ 1898.596102][T12484] __rtnl_newlink+0xac9/0xd80 +[ 1898.596102][T12484] rtnl_newlink+0x63/0xa0 +[ 1898.596102][T12484] rtnetlink_rcv_msg+0x2fb/0xc10 +[ 1898.596102][T12484] netlink_rcv_skb+0x130/0x360 +[ 1898.596102][T12484] netlink_unicast+0x449/0x710 +[ 1898.596102][T12484] netlink_sendmsg+0x723/0xbe0 +[ 1898.596102][T12484] ____sys_sendmsg+0x800/0xa90 +[ 1898.596102][T12484] ___sys_sendmsg+0xee/0x170 +[ 1898.596102][T12484] __sys_sendmsg+0xc2/0x150 +[ 1898.596102][T12484] do_syscall_64+0xc1/0x1d0 +[ 1898.596102][T12484] +[ 1908.670144][T12484] unregister_netdevice: waiting for veth_A-R1 to become free. Usage count = 5 +[ 1908.670561][T12484] ref_tracker: veth_A-R1@ffff8880060c45e0 has 1/4 users at +[ 1908.670561][T12484] dst_init+0x84/0x4a0 +[ 1908.670561][T12484] dst_alloc+0x97/0x150 +[ 1908.670561][T12484] ip6_dst_alloc+0x23/0x90 +[ 1908.670561][T12484] ip6_rt_pcpu_alloc+0x1e6/0x520 +[ 1908.670561][T12484] ip6_pol_route+0x56f/0x840 +[ 1908.670561][T12484] fib6_rule_lookup+0x334/0x630 +[ 1908.670561][T12484] ip6_route_output_flags+0x259/0x480 +[ 1908.670561][T12484] ip6_dst_lookup_tail.constprop.0+0x700/0xb60 +[ 1908.670561][T12484] ip6_dst_lookup_flow+0x88/0x190 +[ 1908.670561][T12484] udp_tunnel6_dst_lookup+0x2b0/0x4d0 +[ 1908.670561][T12484] vxlan_xmit_one+0xd41/0x4500 [vxlan] +[ 1908.670561][T12484] vxlan_xmit+0x9b6/0xf10 [vxlan] +[ 1908.670561][T12484] dev_hard_start_xmit+0x10e/0x360 +[ 1908.670561][T12484] __dev_queue_xmit+0xe76/0x1740 +[ 1908.670561][T12484] arp_solicit+0x4aa/0xe20 +[ 1908.670561][T12484] neigh_probe+0xaa/0xf0 +[ 1908.670561][T12484] +[ 1908.673046][T12484] ref_tracker: veth_A-R1@ffff8880060c45e0 has 1/4 users at +[ 1908.673046][T12484] dst_init+0x84/0x4a0 +[ 1908.673046][T12484] dst_alloc+0x97/0x150 +[ 1908.673046][T12484] ip6_dst_alloc+0x23/0x90 +[ 1908.673046][T12484] ip6_rt_pcpu_alloc+0x1e6/0x520 +[ 1908.673046][T12484] ip6_pol_route+0x56f/0x840 +[ 1908.673046][T12484] fib6_rule_lookup+0x334/0x630 +[ 1908.673046][T12484] ip6_route_output_flags+0x259/0x480 +[ 1908.673046][T12484] ip6_dst_lookup_tail.constprop.0+0x700/0xb60 +[ 1908.673046][T12484] ip6_dst_lookup_flow+0x88/0x190 +[ 1908.673046][T12484] udp_tunnel6_dst_lookup+0x2b0/0x4d0 +[ 1908.673046][T12484] vxlan_xmit_one+0xd41/0x4500 [vxlan] +[ 1908.673046][T12484] vxlan_xmit+0x9b6/0xf10 [vxlan] +[ 1908.673046][T12484] dev_hard_start_xmit+0x10e/0x360 +[ 1908.673046][T12484] __dev_queue_xmit+0xe76/0x1740 +[ 1908.673046][T12484] ip6_finish_output2+0x59b/0xff0 +[ 1908.673046][T12484] ip6_finish_output+0x553/0xdf0 +[ 1908.673046][T12484] +[ 1908.675506][T12484] ref_tracker: veth_A-R1@ffff8880060c45e0 has 1/4 users at +[ 1908.675506][T12484] netdev_get_by_index+0x5e/0x80 +[ 1908.675506][T12484] fib6_nh_init+0x3dd/0x15c0 +[ 1908.675506][T12484] nh_create_ipv6+0x377/0x600 +[ 1908.675506][T12484] nexthop_create+0x311/0x650 +[ 1908.675506][T12484] rtm_new_nexthop+0x3f0/0x5c0 +[ 1908.675506][T12484] rtnetlink_rcv_msg+0x2fb/0xc10 +[ 1908.675506][T12484] netlink_rcv_skb+0x130/0x360 +[ 1908.675506][T12484] netlink_unicast+0x449/0x710 +[ 1908.675506][T12484] netlink_sendmsg+0x723/0xbe0 +[ 1908.675506][T12484] ____sys_sendmsg+0x800/0xa90 +[ 1908.675506][T12484] ___sys_sendmsg+0xee/0x170 +[ 1908.675506][T12484] __sys_sendmsg+0xc2/0x150 +[ 1908.675506][T12484] do_syscall_64+0xc1/0x1d0 +[ 1908.675506][T12484] entry_SYSCALL_64_after_hwframe+0x77/0x7f +[ 1908.675506][T12484] +[ 1908.677622][T12484] ref_tracker: veth_A-R1@ffff8880060c45e0 has 1/4 users at +[ 1908.677622][T12484] ipv6_add_dev+0x3b9/0x11c0 +[ 1908.677622][T12484] addrconf_notify+0x344/0xd60 +[ 1908.677622][T12484] notifier_call_chain+0xcd/0x150 +[ 1908.677622][T12484] register_netdevice+0x1091/0x1690 +[ 1908.677622][T12484] veth_newlink+0x401/0x830 +[ 1908.677622][T12484] rtnl_newlink_create+0x341/0x850 +[ 1908.677622][T12484] __rtnl_newlink+0xac9/0xd80 +[ 1908.677622][T12484] rtnl_newlink+0x63/0xa0 +[ 1908.677622][T12484] rtnetlink_rcv_msg+0x2fb/0xc10 +[ 1908.677622][T12484] netlink_rcv_skb+0x130/0x360 +[ 1908.677622][T12484] netlink_unicast+0x449/0x710 +[ 1908.677622][T12484] netlink_sendmsg+0x723/0xbe0 +[ 1908.677622][T12484] ____sys_sendmsg+0x800/0xa90 +[ 1908.677622][T12484] ___sys_sendmsg+0xee/0x170 +[ 1908.677622][T12484] __sys_sendmsg+0xc2/0x150 +[ 1908.677622][T12484] do_syscall_64+0xc1/0x1d0 +[ 1908.677622][T12484] +""" + + +if __name__ == "__main__": + unittest.main() diff --git a/contest/remote/lib/vm.py b/contest/remote/lib/vm.py index 18b23d5..da427a1 100644 --- a/contest/remote/lib/vm.py +++ b/contest/remote/lib/vm.py @@ -10,6 +10,7 @@ import psutil import re import signal +from .crash import has_crash, extract_crash """ @@ -51,35 +52,6 @@ def decode_and_filter(buf): return "".join([x for x in buf if (x in ['\n'] or unicodedata.category(x)[0]!="C")]) -def finger_print_skip_pfx_len(filters, needles): - # Filter may contain a list of needles we want to skip - # Assume it's well sorted, so we don't need LPM... - if filters and 'crash-prefix-skip' in filters: - for skip_pfx in filters['crash-prefix-skip']: - if len(needles) < len(skip_pfx): - continue - if needles[:len(skip_pfx)] == skip_pfx: - return len(skip_pfx) - return 0 - - -def crash_finger_print(filters, lines): - needles = [] - need_re = re.compile(r'.*( |0:)([a-z0-9_]+)\+0x[0-9a-f]+/0x[0-9a-f]+.*') - skip = 0 - for line in lines: - m = need_re.match(line) - if not m: - continue - needles.append(m.groups()[1]) - skip = finger_print_skip_pfx_len(filters, needles) - if len(needles) - skip == 5: - break - - needles = needles[skip:] - return ":".join(needles) - - class VM: def __init__(self, config, vm_name=""): self.fail_state = "" @@ -287,10 +259,7 @@ def _read_pipe_nonblock(self, pipe): return read_some, output read_some = True output = decode_and_filter(buf) - if output.find("] RIP: ") != -1 or \ - output.find("] Call Trace:") != -1 or \ - output.find('] ref_tracker: ') != -1 or \ - output.find('unreferenced object 0x') != -1: + if has_crash(output): self.fail_state = "oops" except BlockingIOError: pass @@ -376,42 +345,17 @@ def dump_log(self, dir_path, result=None, info=None): self.log_err = "" def _load_filters(self): - if self.filter_data is not None: - return - url = self.config.get("remote", "filters", fallback=None) - if not url: - return - r = requests.get(url) - self.filter_data = json.loads(r.content.decode('utf-8')) + if self.filter_data is None: + url = self.config.get("remote", "filters", fallback=None) + if not url: + return None + r = requests.get(url) + self.filter_data = json.loads(r.content.decode('utf-8')) + return self.filter_data def extract_crash(self, out_path): - in_crash = False - start = 0 - crash_lines = [] - finger_prints = set() - last5 = [""] * 5 - combined = self.log_out.split('\n') + self.log_err.split('\n') - for line in combined: - if in_crash: - in_crash &= '] ---[ end trace ' not in line - in_crash &= '] ' not in line - in_crash &= line[-2:] != '] ' - if not in_crash: - self._load_filters() - finger_prints.add(crash_finger_print(self.filter_data, - crash_lines[start:])) - else: - in_crash |= '] Hardware name: ' in line - in_crash |= '] ref_tracker: ' in line - if in_crash: - start = len(crash_lines) - crash_lines += last5 - - # Keep last 5 to get some of the stuff before stack trace - last5 = last5[1:] + ["| " + line] - - if in_crash: - crash_lines.append(line) + crash_lines, finger_prints = extract_crash(self.log_out + self.log_err, "xx__-> ", + lambda : self._load_filters()) if not crash_lines: print(f"WARN{self.print_pfx} extract_crash found no crashes") return ["crash-extract-fail"] From 210a97847b7e3361dd1d1ee7245903af9aa063b3 Mon Sep 17 00:00:00 2001 From: Johannes Berg Date: Tue, 18 Jun 2024 11:35:05 +0200 Subject: [PATCH 188/429] ingest_mdir: wait for workers to actually do something Since the removal of barriers, this just queues the work and then stops it, without ever doing it. Fix that. Fixes: fd7d03f64a7d ("stop using barriers") Signed-off-by: Johannes Berg --- ingest_mdir.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/ingest_mdir.py b/ingest_mdir.py index 3b49aa2..e6fbd46 100755 --- a/ingest_mdir.py +++ b/ingest_mdir.py @@ -73,14 +73,11 @@ tester.start() pending.put(series) - - # Shut workers down - tester.should_die = True pending.put(None) -finally: +except: tester.should_die = True - pending.put(None) +finally: tester.join() # Summary hack From d6fc8d4833cfd26d205a24afdd2d39e7568893c8 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 19 Jun 2024 09:18:30 -0700 Subject: [PATCH 189/429] ui: status: show disk use rather than free Since DB size is in kB having disk in "% free" looks inverted. Mixing lines which go up and down on the same graph. Flip the disk stat from free to used. Signed-off-by: Jakub Kicinski --- ui/status.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/status.js b/ui/status.js index 38a3936..08a7f0e 100644 --- a/ui/status.js +++ b/ui/status.js @@ -319,12 +319,12 @@ function load_db_size(data) data: data.map(function(e){return Math.floor(e.size / 1024);}), }, { yAxisID: 'B', - label: 'free disk %', - data: data.map(function(e){return e.disk;}), + label: 'builder disk use %', + data: data.map(function(e){return 100 - e.disk;}), }, { yAxisID: 'B', - label: 'metal free disk %', - data: data.map(function(e){return e.disk_remote;}), + label: 'metal disk use %', + data: data.map(function(e){return 100 - e.disk_remote;}), }] }, options: { From 183e48b36ce03d4a640de4e37f17f003927ee908 Mon Sep 17 00:00:00 2001 From: Breno Leitao Date: Mon, 24 Jun 2024 09:52:39 -0700 Subject: [PATCH 190/429] tests: deprecated_api: avoid redirecting if DESC_FD is not set Running this command in command-line mode returns the following error: # deprecated_api.sh deprecated_api.sh: line 54: $DESC_FD: ambiguous redirect This is because DESC_FD is not. In this case, just echo to stdout for the user and avoid the redirect. Signed-off-by: Breno Leitao --- tests/patch/deprecated_api/deprecated_api.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/patch/deprecated_api/deprecated_api.sh b/tests/patch/deprecated_api/deprecated_api.sh index 2d4e67b..3f2f062 100755 --- a/tests/patch/deprecated_api/deprecated_api.sh +++ b/tests/patch/deprecated_api/deprecated_api.sh @@ -51,5 +51,10 @@ else msg="Found: ${msg:2}" fi -echo -e "$msg" >&$DESC_FD +if [[ -z $DESC_FD ]] +then + echo -e "$msg" +else + echo -e "$msg" >& $DESC_FD +fi exit $res From 0084b7e63907f754dc9f5265b09c6cb4a4d37c66 Mon Sep 17 00:00:00 2001 From: Breno Leitao Date: Mon, 24 Jun 2024 09:56:10 -0700 Subject: [PATCH 191/429] tests: deprecated_api: Deprecate init_dummy_netdev The function init_dummy_netdev() shouldn't be used, but the alloc_netdev_dummy(), avoiding the user embedding netdev into any other structure. Signed-off-by: Breno Leitao --- tests/patch/deprecated_api/deprecated_api.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/patch/deprecated_api/deprecated_api.sh b/tests/patch/deprecated_api/deprecated_api.sh index 3f2f062..31e1562 100755 --- a/tests/patch/deprecated_api/deprecated_api.sh +++ b/tests/patch/deprecated_api/deprecated_api.sh @@ -4,7 +4,7 @@ # Copyright (c) 2020 Facebook errors=( module_param ) -warnings=( "\Wdev_hold(" "\Wdev_put(" "\Wput_net(" "\Wget_net(" ) +warnings=( "\Wdev_hold(" "\Wdev_put(" "\Wput_net(" "\Wget_net(" "\Winit_dummy_netdev(" ) res=0 msg="" From 5b6e5fe008bf73e01919439cb59a94f290c9241f Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 19 Jun 2024 18:46:00 -0700 Subject: [PATCH 192/429] ui: status: scale the disk use from 0 to 100% Now that we display disk use as busy rather than free it makes sense to show the 100% more than showing 0. Signed-off-by: Jakub Kicinski --- ui/status.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/status.js b/ui/status.js index 08a7f0e..5fc988e 100644 --- a/ui/status.js +++ b/ui/status.js @@ -345,7 +345,7 @@ function load_db_size(data) B: { position: 'right', display: true, - beginAtZero: true + suggestedMax: 100 } }, } From 9ac42ab2c8b0511ec8696f0351e0f578e03770da Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 22 Jun 2024 19:35:15 -0700 Subject: [PATCH 193/429] contest: vmksft-p: gather configs for all targets We support multiple targets per runner, and use a single kernel for all. Find all configs which exist, don't just use the first one. Signed-off-by: Jakub Kicinski --- contest/remote/lib/vm.py | 3 ++- contest/remote/vmksft-p.py | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/contest/remote/lib/vm.py b/contest/remote/lib/vm.py index da427a1..102f8cb 100644 --- a/contest/remote/lib/vm.py +++ b/contest/remote/lib/vm.py @@ -60,6 +60,7 @@ def __init__(self, config, vm_name=""): self.config = config self.vm_name = vm_name self.print_pfx = (": " + vm_name) if vm_name else ":" + self.tree_path = config.get('local', 'tree_path') self.cfg_boot_to = int(config.get('vm', 'boot_timeout')) @@ -73,7 +74,7 @@ def tree_popen(self, cmd): if self.config.get('env', 'paths'): env['PATH'] += ':' + self.config.get('env', 'paths') - return subprocess.Popen(cmd, env=env, cwd=self.config.get('local', 'tree_path'), + return subprocess.Popen(cmd, env=env, cwd=self.tree_path, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) def tree_cmd(self, cmd): diff --git a/contest/remote/vmksft-p.py b/contest/remote/vmksft-p.py index 9999b86..4bc93ad 100755 --- a/contest/remote/vmksft-p.py +++ b/contest/remote/vmksft-p.py @@ -244,7 +244,12 @@ def test(binfo, rinfo, cbarg): vm = VM(config) - if vm.build([f"tools/testing/selftests/{targets[0]}/config"]) == False: + kconfs = [] + for target in targets: + conf = f"tools/testing/selftests/{target}/config" + if os.path.exists(os.path.join(vm.tree_path, conf)): + kconfs.append(conf) + if vm.build(kconfs) == False: vm.dump_log(results_path + '/build') return [{ 'test': 'build', From 8c0346205138c1f4f6d718359a525cd3b617eddd Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 22 Jun 2024 19:46:27 -0700 Subject: [PATCH 194/429] contest: backend: log more info about the query To have a better idea why queries take long make sure we include the format and filter info. Signed-off-by: Jakub Kicinski --- contest/backend/query.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/contest/backend/query.py b/contest/backend/query.py index be68d63..baeb967 100644 --- a/contest/backend/query.py +++ b/contest/backend/query.py @@ -74,6 +74,7 @@ def results(): limit = 0 where = [] + log = "" form = request.args.get('format') remote = request.args.get('remote') @@ -106,6 +107,7 @@ def results(): if remote: where.append(f"remote = '{remote}'") + log += ', remote' where = "WHERE " + " AND ".join(where) if where else "" @@ -125,11 +127,12 @@ def results(): else: rows += r[0] rows += ']' + log += ', l2' else: rows = "[]" t3 = datetime.datetime.now() - print(f"Query for {br_cnt} branches, {limit} records took: {str(t3-t1)} ({str(t2-t1)}+{str(t3-t2)})") + print(f"Query for {br_cnt} branches, {limit} records{log} took: {str(t3-t1)} ({str(t2-t1)}+{str(t3-t2)})") return Response(rows, mimetype='application/json') From 367450bf7f294c5fcb7402e0d54c0b1855978b07 Mon Sep 17 00:00:00 2001 From: Johannes Berg Date: Mon, 24 Jun 2024 11:18:15 +0200 Subject: [PATCH 195/429] docker: add lld/llvm These seem to be necessary now for running clang builds, containing lld and llvm-ar respectively. Signed-off-by: Johannes Berg --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index b328f0b..82d1cf7 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -15,7 +15,7 @@ RUN apt-get update && apt-get install -y \ bison \ libssl-dev \ libelf-dev \ - clang \ + clang lld llvm \ sparse \ bc \ cpio \ From c86677530c79f560dcbc19130ff073081a818dab Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Tue, 25 Jun 2024 18:15:38 -0700 Subject: [PATCH 196/429] tests: build: try to fix the relink force once again The previous attempt tried to detect */Makefile files. But bash globs aren't smart enough to descend, apparently. So use git glob explicitly. Change clang, too. There weren't false positives from the previous version in gcc, so it's likely safe. Signed-off-by: Jakub Kicinski --- tests/patch/build_32bit/build_32bit.sh | 2 +- .../build_allmodconfig_warn/build_allmodconfig.sh | 2 +- tests/patch/build_clang/build_clang.sh | 13 ++++++++----- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/tests/patch/build_32bit/build_32bit.sh b/tests/patch/build_32bit/build_32bit.sh index 97333c5..0b6969e 100755 --- a/tests/patch/build_32bit/build_32bit.sh +++ b/tests/patch/build_32bit/build_32bit.sh @@ -49,7 +49,7 @@ fi # the "before", too. touch_relink=/dev/null if ! git log --diff-filter=A HEAD~.. --exit-code >>/dev/null || \ - ! git log HEAD~.. --exit-code -- */Makefile >>/dev/null + ! git log HEAD~.. --exit-code --glob='*/Makefile' >>/dev/null then echo "Trying to force re-linking, new files were added" touch_relink=${output_dir}/include/generated/utsrelease.h diff --git a/tests/patch/build_allmodconfig_warn/build_allmodconfig.sh b/tests/patch/build_allmodconfig_warn/build_allmodconfig.sh index ec20518..a0d5131 100755 --- a/tests/patch/build_allmodconfig_warn/build_allmodconfig.sh +++ b/tests/patch/build_allmodconfig_warn/build_allmodconfig.sh @@ -49,7 +49,7 @@ fi # the "before", too. touch_relink=/dev/null if ! git log --diff-filter=A HEAD~.. --exit-code >>/dev/null || \ - ! git log HEAD~.. --exit-code -- */Makefile >>/dev/null + ! git log HEAD~.. --exit-code --glob='*/Makefile' >>/dev/null then echo "Trying to force re-linking, new files were added" touch_relink=${output_dir}/include/generated/utsrelease.h diff --git a/tests/patch/build_clang/build_clang.sh b/tests/patch/build_clang/build_clang.sh index 3991f63..b1737d8 100755 --- a/tests/patch/build_clang/build_clang.sh +++ b/tests/patch/build_clang/build_clang.sh @@ -40,11 +40,16 @@ fi # so all module and linker related warnings will pop up in the "after" # but not "before". To avoid this we need to force re-linking on # the "before", too. -if ! git log --diff-filter=A HEAD~.. --exit-code >>/dev/null; then +touch_relink=/dev/null +if ! git log --diff-filter=A HEAD~.. --exit-code >>/dev/null || \ + ! git log HEAD~.. --exit-code --glob='*/Makefile' >>/dev/null +then echo "Trying to force re-linking, new files were added" - touch ${output_dir}/include/generated/utsrelease.h + touch_relink=${output_dir}/include/generated/utsrelease.h fi +touch $touch_relink + git checkout -q HEAD~ echo "Building the tree before the patch" @@ -58,9 +63,7 @@ echo "Building the tree with the patch" git checkout -q $HEAD # Also force rebuild "after" in case the file added isn't important. -if ! git log --diff-filter=A HEAD~.. --exit-code >>/dev/null; then - touch ${output_dir}/include/generated/utsrelease.h -fi +touch $touch_relink prep_config make LLVM=1 O=$output_dir $build_flags 2> >(tee $tmpfile_n >&2) || rc=1 From 4612751d0fb0b3102772ee9d046ae34e9e3b71fa Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 26 Jun 2024 18:12:40 -0700 Subject: [PATCH 197/429] tests: build: fix the glob used to decide on relinking The glob doesn't work well. List the files and grep them. Signed-off-by: Jakub Kicinski --- tests/patch/build_32bit/build_32bit.sh | 2 +- tests/patch/build_allmodconfig_warn/build_allmodconfig.sh | 2 +- tests/patch/build_clang/build_clang.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/patch/build_32bit/build_32bit.sh b/tests/patch/build_32bit/build_32bit.sh index 0b6969e..5eb69a3 100755 --- a/tests/patch/build_32bit/build_32bit.sh +++ b/tests/patch/build_32bit/build_32bit.sh @@ -49,7 +49,7 @@ fi # the "before", too. touch_relink=/dev/null if ! git log --diff-filter=A HEAD~.. --exit-code >>/dev/null || \ - ! git log HEAD~.. --exit-code --glob='*/Makefile' >>/dev/null + git diff --name-only HEAD~ | grep -q -E "Makefile$" then echo "Trying to force re-linking, new files were added" touch_relink=${output_dir}/include/generated/utsrelease.h diff --git a/tests/patch/build_allmodconfig_warn/build_allmodconfig.sh b/tests/patch/build_allmodconfig_warn/build_allmodconfig.sh index a0d5131..d0041bb 100755 --- a/tests/patch/build_allmodconfig_warn/build_allmodconfig.sh +++ b/tests/patch/build_allmodconfig_warn/build_allmodconfig.sh @@ -49,7 +49,7 @@ fi # the "before", too. touch_relink=/dev/null if ! git log --diff-filter=A HEAD~.. --exit-code >>/dev/null || \ - ! git log HEAD~.. --exit-code --glob='*/Makefile' >>/dev/null + git diff --name-only HEAD~ | grep -q -E "Makefile$" then echo "Trying to force re-linking, new files were added" touch_relink=${output_dir}/include/generated/utsrelease.h diff --git a/tests/patch/build_clang/build_clang.sh b/tests/patch/build_clang/build_clang.sh index b1737d8..8d741c5 100755 --- a/tests/patch/build_clang/build_clang.sh +++ b/tests/patch/build_clang/build_clang.sh @@ -42,7 +42,7 @@ fi # the "before", too. touch_relink=/dev/null if ! git log --diff-filter=A HEAD~.. --exit-code >>/dev/null || \ - ! git log HEAD~.. --exit-code --glob='*/Makefile' >>/dev/null + git diff --name-only HEAD~ | grep -q -E "Makefile$" then echo "Trying to force re-linking, new files were added" touch_relink=${output_dir}/include/generated/utsrelease.h From 4d9dea28eecbfe873beac85315009244539a0b92 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 26 Jun 2024 18:14:24 -0700 Subject: [PATCH 198/429] tests: build_tools: use a print helper Use a helper to output to both stdout and stderr. This makes reading the results much easier. Signed-off-by: Jakub Kicinski --- tests/patch/build_tools/build_tools.sh | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/patch/build_tools/build_tools.sh b/tests/patch/build_tools/build_tools.sh index afeff5b..5646d85 100755 --- a/tests/patch/build_tools/build_tools.sh +++ b/tests/patch/build_tools/build_tools.sh @@ -8,6 +8,10 @@ tmpfile_o=$(mktemp) tmpfile_n=$(mktemp) rc=0 +pr() { + echo " ====== $@ ======" | tee -a /dev/stderr +} + # If it doesn't touch tools/ or include/, don't bother if ! git diff --name-only HEAD~ | grep -E "^(include)|(tools)/"; then echo "No tools touched, skip" >&$DESC_FD @@ -24,18 +28,20 @@ HEAD=$(git rev-parse HEAD) echo "Tree base:" git log -1 --pretty='%h ("%s")' HEAD~ +echo "Now at:" +git log -1 --pretty='%h ("%s")' HEAD -echo "Cleaning" +pr "Cleaning" make O=$output_dir $build_flags -C tools/testing/selftests/ clean -echo "Baseline building the tree" +pr "Baseline building the tree" make O=$output_dir $build_flags headers for what in net net/forwarding net/tcp_ao; do make O=$output_dir $build_flags -C tools/testing/selftests/ \ TARGETS=$what done -echo "Building the tree before the patch" +pr "Building the tree before the patch" git checkout -q HEAD~ make O=$output_dir $build_flags headers @@ -46,8 +52,7 @@ done incumbent=$(grep -i -c "\(warn\|error\)" $tmpfile_o) -echo "Building the tree with the patch" - +pr "Building the tree with the patch" git checkout -q $HEAD make O=$output_dir $build_flags headers From cece76efd65f5df642e67a3e67d1a383344de979 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 26 Jun 2024 18:16:00 -0700 Subject: [PATCH 199/429] tests: build_tools: hide the names of matching files Don't output the names of matching files to stdout. Signed-off-by: Jakub Kicinski --- tests/patch/build_tools/build_tools.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/patch/build_tools/build_tools.sh b/tests/patch/build_tools/build_tools.sh index 5646d85..e847195 100755 --- a/tests/patch/build_tools/build_tools.sh +++ b/tests/patch/build_tools/build_tools.sh @@ -13,7 +13,7 @@ pr() { } # If it doesn't touch tools/ or include/, don't bother -if ! git diff --name-only HEAD~ | grep -E "^(include)|(tools)/"; then +if ! git diff --name-only HEAD~ | grep -q -E "^(include)|(tools)/"; then echo "No tools touched, skip" >&$DESC_FD exit 0 fi From 602ff559e57a40395bd3685aeb18f4a4be1b5e50 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 26 Jun 2024 18:17:33 -0700 Subject: [PATCH 200/429] tests: build_tools: distclean YNL before testing YNL may have already been built by other tests with full set of protos. make clean in selftests won't wipe it. So make sure we clean it explicitly. Signed-off-by: Jakub Kicinski --- tests/patch/build_tools/build_tools.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/patch/build_tools/build_tools.sh b/tests/patch/build_tools/build_tools.sh index e847195..8cef3d2 100755 --- a/tests/patch/build_tools/build_tools.sh +++ b/tests/patch/build_tools/build_tools.sh @@ -34,6 +34,9 @@ git log -1 --pretty='%h ("%s")' HEAD pr "Cleaning" make O=$output_dir $build_flags -C tools/testing/selftests/ clean +# Hard-clean YNL, too, otherwise YNL-related build problems may be masked +make -C tools/net/ynl/ distclean + pr "Baseline building the tree" make O=$output_dir $build_flags headers for what in net net/forwarding net/tcp_ao; do From b29d37388d578374fca199a163f52127d95381cc Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 26 Jun 2024 18:18:54 -0700 Subject: [PATCH 201/429] tests: build_tools: use SKIP_TARGETS to exclude bad targets Instead of looping over a handful of targets export SKIP_TARGETS. Signed-off-by: Jakub Kicinski --- tests/patch/build_tools/build_tools.sh | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/tests/patch/build_tools/build_tools.sh b/tests/patch/build_tools/build_tools.sh index 8cef3d2..83451b8 100755 --- a/tests/patch/build_tools/build_tools.sh +++ b/tests/patch/build_tools/build_tools.sh @@ -31,6 +31,9 @@ git log -1 --pretty='%h ("%s")' HEAD~ echo "Now at:" git log -1 --pretty='%h ("%s")' HEAD +# These are either very slow or don't build +export SKIP_TARGETS="bpf dt landlock livepatch lsm user_events mm powerpc" + pr "Cleaning" make O=$output_dir $build_flags -C tools/testing/selftests/ clean @@ -38,20 +41,17 @@ make O=$output_dir $build_flags -C tools/testing/selftests/ clean make -C tools/net/ynl/ distclean pr "Baseline building the tree" +git checkout -q HEAD~ make O=$output_dir $build_flags headers -for what in net net/forwarding net/tcp_ao; do - make O=$output_dir $build_flags -C tools/testing/selftests/ \ - TARGETS=$what -done +make O=$output_dir $build_flags -C tools/testing/selftests/ +git checkout -q $HEAD pr "Building the tree before the patch" git checkout -q HEAD~ make O=$output_dir $build_flags headers -for what in net net/forwarding net/tcp_ao; do - make O=$output_dir $build_flags -C tools/testing/selftests/ \ - TARGETS=$what 2> >(tee -a $tmpfile_o >&2) -done +make O=$output_dir $build_flags -C tools/testing/selftests/ \ + 2> >(tee -a $tmpfile_o >&2) incumbent=$(grep -i -c "\(warn\|error\)" $tmpfile_o) @@ -59,10 +59,8 @@ pr "Building the tree with the patch" git checkout -q $HEAD make O=$output_dir $build_flags headers -for what in net net/forwarding net/tcp_ao; do - make O=$output_dir $build_flags -C tools/testing/selftests/ \ - TARGETS=$what 2> >(tee -a $tmpfile_n >&2) -done +make O=$output_dir $build_flags -C tools/testing/selftests/ \ + 2> >(tee -a $tmpfile_n >&2) current=$(grep -i -c "\(warn\|error\)" $tmpfile_n) From 56852fb3dd1d8569cd07f4d1df42884a90b7744d Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 4 Jul 2024 09:30:09 -0700 Subject: [PATCH 202/429] contest: gh: push constructing the result up To construct more complex results we need to be able to build them deeper in the call chain. Signed-off-by: Jakub Kicinski --- contest/remote/gh.py | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/contest/remote/gh.py b/contest/remote/gh.py index c7ab14f..ad1c477 100755 --- a/contest/remote/gh.py +++ b/contest/remote/gh.py @@ -49,6 +49,13 @@ def get(url, token): return requests.get(url, headers=headers) +def link(cbarg, config): + return "https://github.com/" + \ + config.get('ci', 'owner') + "/" + \ + config.get('ci', 'repo') + "/" + \ + "actions/runs/" + str(cbarg.prev_runid) + + def get_results(config, cbarg, prev_run, page=1): token = config.get('gh', 'token') repo_url = f"https://api.github.com/repos/%7Bconfig.get('ci', 'owner')}/{config.get('ci', 'repo')}" @@ -104,7 +111,9 @@ def get_results(config, cbarg, prev_run, page=1): else: print("Unknown result:", c) result = 5 - return encoder[result] + return [{'test': config.get('executor', 'test'), + 'group': config.get('executor', 'group'), + 'result': encoder[result], 'link': link(cbarg, config)}] def test_run(binfo, rinfo, cbarg, config, start): @@ -117,7 +126,10 @@ def test_run(binfo, rinfo, cbarg, config, start): # If rerere fixed it, just commit res = subprocess.run('git diff -s --exit-code', cwd=tree_path, shell=True) if res.returncode != 0: - return 'skip' + return [{'test': config.get('executor', 'test'), + 'group': config.get('executor', 'group'), + 'result': 'skip', 'link': config.get('gh', 'link')}] + subprocess.run('git commit --no-edit', cwd=tree_path, shell=True, check=True) out_remote = config.get('gh', 'out_remote') @@ -142,7 +154,13 @@ def test_run(binfo, rinfo, cbarg, config, start): print("Not completed, waiting") time.sleep(config.getint('gh', 'wait_poll')) - return 'skip' + url = config.get('gh', 'link') + if hasattr(cbarg, "prev_runid") and cbarg.prev_runid != prev_runid: + url = link(cbarg, config) + + return [{'test': config.get('executor', 'test'), + 'group': config.get('executor', 'group'), + 'result': 'skip', 'link': url}] def test(binfo, rinfo, cbarg): @@ -159,16 +177,7 @@ def test(binfo, rinfo, cbarg): res = test_run(binfo, rinfo, cbarg, config, start) - link = config.get('gh', 'link') - if hasattr(cbarg, "prev_runid"): - link = "https://github.com/" + \ - config.get('ci', 'owner') + "/" + \ - config.get('ci', 'repo') + "/" + \ - "actions/runs/" + str(cbarg.prev_runid) - - return [{'test': config.get('executor', 'test'), - 'group': config.get('executor', 'group'), - 'result': res, 'link': link}] + return res def main() -> None: From 08327b275d0241cabdfa11c4e473fb7a4cbbb53b Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 4 Jul 2024 09:38:34 -0700 Subject: [PATCH 203/429] contest: gh: retry on failures Much like locally executed tests - retry when we see failures. Since we can't kick off individual tests kick off a full run and match up the failed tests with retry results. Signed-off-by: Jakub Kicinski --- contest/remote/gh.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/contest/remote/gh.py b/contest/remote/gh.py index ad1c477..1507aa7 100755 --- a/contest/remote/gh.py +++ b/contest/remote/gh.py @@ -177,6 +177,17 @@ def test(binfo, rinfo, cbarg): res = test_run(binfo, rinfo, cbarg, config, start) + retry = [] + for one in res: + if one['result'] == 'fail': + retry = test_run(binfo, rinfo, cbarg, config, start) + break + for one2 in retry: + for one in res: + if one['test'] == one2['test']: + one['retry'] = one2['result'] + break + return res From b0e07277c5cbd4b2b926fd2ab781875f5575b8b0 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 4 Jul 2024 09:58:50 -0700 Subject: [PATCH 204/429] contest: gh: report individual test cases The GH test names are annoyingly long, but bite the bullet, hardcode some shortening rules and report them. Signed-off-by: Jakub Kicinski --- contest/remote/gh.py | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/contest/remote/gh.py b/contest/remote/gh.py index 1507aa7..fb34a12 100755 --- a/contest/remote/gh.py +++ b/contest/remote/gh.py @@ -10,7 +10,7 @@ import time from core import NipaLifetime -from lib import Fetcher, CbArg +from lib import Fetcher, CbArg, namify """ [executor] @@ -56,6 +56,14 @@ def link(cbarg, config): "actions/runs/" + str(cbarg.prev_runid) +def gh_namify(name): + # This may be pretty BPF specific, the test name looks like: + # x86_64-gcc / test (test_progs, false, 360) / test_progs on x86_64 with gcc + name = ' / '.join(name.split(' / ')[:2]) + name = name.replace('test (test', '') + return namify(name) + + def get_results(config, cbarg, prev_run, page=1): token = config.get('gh', 'token') repo_url = f"https://api.github.com/repos/%7Bconfig.get('ci', 'owner')}/{config.get('ci', 'repo')}" @@ -103,17 +111,29 @@ def get_results(config, cbarg, prev_run, page=1): 5: 'fail', } - result = -1 + url = link(cbarg, config) + res = [] for job in jobs["jobs"]: - c = job["conclusion"] + if job["conclusion"] is None: + print("Still running, waiting for job:", job["name"]) + return None + if job["conclusion"] == 'skipped': + continue + if job["conclusion"] in decoder: - result = max(result, decoder[c]) + result = encoder[decoder[job["conclusion"]]] else: - print("Unknown result:", c) - result = 5 - return [{'test': config.get('executor', 'test'), - 'group': config.get('executor', 'group'), - 'result': encoder[result], 'link': link(cbarg, config)}] + print("Unknown result:", job["conclusion"]) + result = 'fail' + + test_link = job.get('html_url', url) + + res.append({'test': gh_namify(job["name"]), + 'group': config.get('executor', 'group'), + 'result': result, 'link': test_link}) + if not res: + print(f"Still waiting, {len(jobs['jobs'])} jobs skipped") + return res def test_run(binfo, rinfo, cbarg, config, start): @@ -151,7 +171,6 @@ def test_run(binfo, rinfo, cbarg, config, start): print("Got result:", res) return res - print("Not completed, waiting") time.sleep(config.getint('gh', 'wait_poll')) url = config.get('gh', 'link') From aa59bb10928d4d049687f20c10ddd750fc3b6990 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 5 Jul 2024 06:35:23 -0700 Subject: [PATCH 205/429] contest: cocci: ignore flex array in mlx5 interface files Signed-off-by: Jakub Kicinski --- contest/tests/cocci-check.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/contest/tests/cocci-check.sh b/contest/tests/cocci-check.sh index eb0f0aa..0de1309 100755 --- a/contest/tests/cocci-check.sh +++ b/contest/tests/cocci-check.sh @@ -27,6 +27,7 @@ clean_up_output() { sed -i '/^EXN: .*No such file or directory/d' $file sed -i '/^EXN: Coccinelle_modules.Common.Timeout /d' $file sed -i '/An error occurred when attempting /d' $file + sed -i '/mlx5_ifc.h:.* WARNING use flexible-array member instead/d' $file } # Figure out the number of physical cores, save 8 or half for other stuff From 620ca670f84ded404f09bed77d24e171b7e841d1 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 5 Jul 2024 06:42:18 -0700 Subject: [PATCH 206/429] contest: cocci: fix generating per-file breakdown cocci outputs absolute paths, replace the .. with $PWD Signed-off-by: Jakub Kicinski --- contest/tests/cocci-check.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contest/tests/cocci-check.sh b/contest/tests/cocci-check.sh index 0de1309..c8d8b53 100755 --- a/contest/tests/cocci-check.sh +++ b/contest/tests/cocci-check.sh @@ -109,9 +109,9 @@ if [ $rc -ne 0 ]; then tmpfile_fo=$(mktemp) tmpfile_fn=$(mktemp) - grep -i "^$PWD" $out_of | sed -n 's@\(^\.\./[/a-zA-Z0-9_.-]*.[ch]\):.*@\1@p' | sort | uniq -c \ + grep -i "^$PWD" $out_of | sed -n 's@^'$PWD'\([/a-zA-Z0-9_.-]*.[ch]\):.*@\1@p' | sort | uniq -c \ > $tmpfile_fo - grep -i "^$PWD" $out_nf | sed -n 's@\(^\.\./[/a-zA-Z0-9_.-]*.[ch]\):.*@\1@p' | sort | uniq -c \ + grep -i "^$PWD" $out_nf | sed -n 's@^'$PWD'\([/a-zA-Z0-9_.-]*.[ch]\):.*@\1@p' | sort | uniq -c \ > $tmpfile_fn diff -U 0 $tmpfile_fo $tmpfile_fn 1>&2 From 6b3cf62d06e49bc0a585be026053f86fa18e08c9 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 5 Jul 2024 08:47:55 -0700 Subject: [PATCH 207/429] docs: update the system diagram Make the separation between NIPA and external services clearer. Signed-off-by: Jakub Kicinski --- docs/ci.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ci.svg b/docs/ci.svg index ca0c094..3516468 100644 --- a/docs/ci.svg +++ b/docs/ci.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file From 5d6352a84730a0b0ccefaad345939c2ddd3d59af Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Tue, 9 Jul 2024 06:43:41 -0700 Subject: [PATCH 208/429] tests: run kdoc on pull requests Brancher now uses kdoc as a gating test (to prevent broken kdoc from getting flagged as warnings for the second time in the documentation build). Make sure we run that test on PRs, otherwise PRs don't get propagated into contest. Signed-off-by: Jakub Kicinski --- tests/patch/kdoc/info.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/patch/kdoc/info.json b/tests/patch/kdoc/info.json index a409c44..e6d6d9a 100644 --- a/tests/patch/kdoc/info.json +++ b/tests/patch/kdoc/info.json @@ -1,3 +1,4 @@ { - "run": ["kdoc.sh"] + "run": ["kdoc.sh"], + "pull-requests": true } From a331727a24e8c43091eb64aed3c5e53222b0cbd9 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 11 Jul 2024 08:37:00 -0700 Subject: [PATCH 209/429] pw_brancher: make more resilient to repeat checks There's no way to delete checks from patchwork, so new results are added as new checks. Patchwork UI just displays the most recent. Use the most recent to determine if patch is good, too. Signed-off-by: Jakub Kicinski --- pw_brancher.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pw_brancher.py b/pw_brancher.py index 18ab1c6..0a461d2 100755 --- a/pw_brancher.py +++ b/pw_brancher.py @@ -60,11 +60,11 @@ def pwe_has_all_checks(pw, entry) -> bool: if "checks" not in entry: return False checks = pw.request(entry["checks"]) - found = 0 + found = dict.fromkeys(gate_checks, 0) for c in checks: - if c["context"] in gate_checks and c["state"] == "success": - found += 1 - return found == len(gate_checks) + if c["context"] in gate_checks: + found[c["context"]] = int(c["state"] == "success") + return sum(found.values()) == len(gate_checks) def pwe_series_id_or_none(entry) -> int: From 12528186f45b8d1e8e72e22a4462f9654dc1efdd Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 11 Jul 2024 10:19:40 -0700 Subject: [PATCH 210/429] pw: retry check posts When patchwork gets slow everything fails. Try 2 times before crashing. Signed-off-by: Jakub Kicinski --- pw/patchwork.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pw/patchwork.py b/pw/patchwork.py index bd01347..1c30f3a 100644 --- a/pw/patchwork.py +++ b/pw/patchwork.py @@ -9,6 +9,7 @@ import requests from requests.adapters import HTTPAdapter from requests.packages.urllib3.util.retry import Retry +import time import urllib import core @@ -125,7 +126,10 @@ def _post(self, req, headers, data, api='1.1'): core.log("Headers", headers) core.log("Data", data) core.log("Response", ret) - core.log("Response data", ret.json()) + try: + core.log("Response data", ret.json()) + except json.decoder.JSONDecodeError: + core.log("Response data", ret.content.decode()) finally: core.log_end_sec() @@ -189,6 +193,10 @@ def post_check(self, patch, name, state, url, desc): } r = self._post(f'patches/{patch}/checks/', headers=headers, data=data) + if r.status_code == 502 or r.status_code == 504: + # Timeout, let's wait 30 sec and retry, POST isn't retried by the lib. + time.sleep(30) + r = self._post(f'patches/{patch}/checks/', headers=headers, data=data) if r.status_code != 201: raise PatchworkPostException(r) From 5bcb890cbfecd3c1727cec2f026360646a4afc62 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 12 Jul 2024 07:34:28 -0700 Subject: [PATCH 211/429] build: relinking workaround for Kconfig When COMPILE_TEST gets added to Kconfig (presumably among other things) the modules will get re-linked. Signed-off-by: Jakub Kicinski --- tests/patch/build_32bit/build_32bit.sh | 3 ++- tests/patch/build_allmodconfig_warn/build_allmodconfig.sh | 3 ++- tests/patch/build_clang/build_clang.sh | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/patch/build_32bit/build_32bit.sh b/tests/patch/build_32bit/build_32bit.sh index 5eb69a3..b8f30de 100755 --- a/tests/patch/build_32bit/build_32bit.sh +++ b/tests/patch/build_32bit/build_32bit.sh @@ -49,7 +49,8 @@ fi # the "before", too. touch_relink=/dev/null if ! git log --diff-filter=A HEAD~.. --exit-code >>/dev/null || \ - git diff --name-only HEAD~ | grep -q -E "Makefile$" + git diff --name-only HEAD~ | grep -q -E "Makefile$" || \ + git diff --name-only HEAD~ | grep -q -E "Kconfig$" then echo "Trying to force re-linking, new files were added" touch_relink=${output_dir}/include/generated/utsrelease.h diff --git a/tests/patch/build_allmodconfig_warn/build_allmodconfig.sh b/tests/patch/build_allmodconfig_warn/build_allmodconfig.sh index d0041bb..f8851f0 100755 --- a/tests/patch/build_allmodconfig_warn/build_allmodconfig.sh +++ b/tests/patch/build_allmodconfig_warn/build_allmodconfig.sh @@ -49,7 +49,8 @@ fi # the "before", too. touch_relink=/dev/null if ! git log --diff-filter=A HEAD~.. --exit-code >>/dev/null || \ - git diff --name-only HEAD~ | grep -q -E "Makefile$" + git diff --name-only HEAD~ | grep -q -E "Makefile$" || \ + git diff --name-only HEAD~ | grep -q -E "Kconfig$" then echo "Trying to force re-linking, new files were added" touch_relink=${output_dir}/include/generated/utsrelease.h diff --git a/tests/patch/build_clang/build_clang.sh b/tests/patch/build_clang/build_clang.sh index 8d741c5..54a85c1 100755 --- a/tests/patch/build_clang/build_clang.sh +++ b/tests/patch/build_clang/build_clang.sh @@ -42,7 +42,8 @@ fi # the "before", too. touch_relink=/dev/null if ! git log --diff-filter=A HEAD~.. --exit-code >>/dev/null || \ - git diff --name-only HEAD~ | grep -q -E "Makefile$" + git diff --name-only HEAD~ | grep -q -E "Makefile$" || \ + git diff --name-only HEAD~ | grep -q -E "Kconfig$" then echo "Trying to force re-linking, new files were added" touch_relink=${output_dir}/include/generated/utsrelease.h From 83ce7f77ec291ddc7ff3530d2ffb38fde1e49702 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 17 Jul 2024 08:47:25 -0700 Subject: [PATCH 212/429] contest: crash: extract crashes for hung tasks Add parsing for hung tasks. We have some after v6.11 merge window. Signed-off-by: Jakub Kicinski --- contest/remote/lib/crash.py | 261 ++++++++++++++++++++++++++++++++++++ 1 file changed, 261 insertions(+) diff --git a/contest/remote/lib/crash.py b/contest/remote/lib/crash.py index e67afaa..b283428 100755 --- a/contest/remote/lib/crash.py +++ b/contest/remote/lib/crash.py @@ -60,6 +60,7 @@ def extract_crash(outputs, prompt, get_filters): else: in_crash |= '] Hardware name: ' in line in_crash |= '] ref_tracker: ' in line + in_crash |= ' blocked for more than ' in line in_crash |= line.startswith('unreferenced object 0x') if in_crash: start = len(crash_lines) @@ -113,6 +114,14 @@ def test_refleak(self): '___sys_sendmsg:__sys_sendmsg:do_syscall_64:ipv6_add_dev:addrconf_notify', 'dev_hard_start_xmit:__dev_queue_xmit:arp_solicit:neigh_probe:dst_init'}) + def test_hung_task(self): + self.assertTrue(has_crash(TestCrashes.hung_task)) + lines, fingers = extract_crash(TestCrashes.hung_task, "xx__->", lambda : None) + self.assertGreater(len(lines), 10) + self.assertEqual(fingers, + {'__schedule:schedule:__wait_on_freeing_inode:find_inode_fast:iget_locked', + '__schedule:schedule:d_alloc_parallel:__lookup_slow:walk_component'}) + ######################################################### ### Sample outputs ######################################################### @@ -561,6 +570,258 @@ def test_refleak(self): [ 1908.677622][T12484] """ + hung_task = """ +[ 1863.157993][ T9043] br0: port 1(vx0) entered forwarding state +[ 2090.392704][ T43] INFO: task tc:9091 blocked for more than 122 seconds. +[ 2090.393146][ T43] Not tainted 6.10.0-virtme #1 +[ 2090.393327][ T43] "echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message. +[ 2090.393598][ T43] task:tc state:D stack:26464 pid:9091 tgid:9091 ppid:9090 flags:0x00000000 +[ 2090.393857][ T43] Call Trace: +[ 2090.393956][ T43] +[ 2090.394033][ T43] __schedule+0x6e0/0x17e0 +[ 2090.394184][ T43] ? __pfx___schedule+0x10/0x10 +[ 2090.394318][ T43] ? schedule+0x1a5/0x210 +[ 2090.394420][ T43] ? __pfx_lock_acquire.part.0+0x10/0x10 +[ 2090.394562][ T43] ? trace_lock_acquire+0x14d/0x1f0 +[ 2090.394701][ T43] ? schedule+0x1a5/0x210 +[ 2090.394800][ T43] schedule+0xdf/0x210 +[ 2090.395240][ T43] d_alloc_parallel+0xaef/0xed0 +[ 2090.395379][ T43] ? __pfx_d_alloc_parallel+0x10/0x10 +[ 2090.395505][ T43] ? __pfx_default_wake_function+0x10/0x10 +[ 2090.395676][ T43] ? lockdep_init_map_type+0x2cb/0x7c0 +[ 2090.395809][ T43] __lookup_slow+0x17f/0x3c0 +[ 2090.395942][ T43] ? __pfx___lookup_slow+0x10/0x10 +[ 2090.396075][ T43] ? walk_component+0x29e/0x4f0 +[ 2090.396219][ T43] walk_component+0x2ab/0x4f0 +[ 2090.396350][ T43] link_path_walk.part.0.constprop.0+0x416/0x940 +[ 2090.396517][ T43] ? __pfx_link_path_walk.part.0.constprop.0+0x10/0x10 +[ 2090.396706][ T43] path_lookupat+0x72/0x660 +[ 2090.396832][ T43] filename_lookup+0x19e/0x420 +[ 2090.396958][ T43] ? __pfx_filename_lookup+0x10/0x10 +[ 2090.397090][ T43] ? find_held_lock+0x2c/0x110 +[ 2090.397213][ T43] ? __lock_release+0x103/0x460 +[ 2090.397335][ T43] ? __pfx___lock_release+0x10/0x10 +[ 2090.397456][ T43] ? trace_lock_acquire+0x14d/0x1f0 +[ 2090.397590][ T43] ? __might_fault+0xc3/0x170 +[ 2090.397720][ T43] ? lock_acquire+0x32/0xc0 +[ 2090.397838][ T43] ? __might_fault+0xc3/0x170 +[ 2090.397966][ T43] vfs_statx+0xbf/0x140 +[ 2090.398060][ T43] ? __pfx_vfs_statx+0x10/0x10 +[ 2090.398183][ T43] ? getname_flags+0xb3/0x410 +[ 2090.398307][ T43] vfs_fstatat+0x80/0xc0 +[ 2090.398400][ T43] __do_sys_newfstatat+0x75/0xd0 +[ 2090.398548][ T43] ? __pfx___do_sys_newfstatat+0x10/0x10 +[ 2090.398669][ T43] ? user_path_at+0x45/0x60 +[ 2090.398802][ T43] ? __x64_sys_openat+0x123/0x1e0 +[ 2090.398929][ T43] ? __pfx___x64_sys_openat+0x10/0x10 +[ 2090.399052][ T43] ? __pfx_do_faccessat+0x10/0x10 +[ 2090.399179][ T43] ? lockdep_hardirqs_on_prepare+0x275/0x410 +[ 2090.399327][ T43] do_syscall_64+0xc1/0x1d0 +[ 2090.399451][ T43] entry_SYSCALL_64_after_hwframe+0x77/0x7f +[ 2090.399612][ T43] RIP: 0033:0x7fef39aaceae +[ 2090.399746][ T43] RSP: 002b:00007ffc38865528 EFLAGS: 00000246 ORIG_RAX: 0000000000000106 +[ 2090.399969][ T43] RAX: ffffffffffffffda RBX: 0000000000000004 RCX: 00007fef39aaceae +[ 2090.400180][ T43] RDX: 00007ffc38865600 RSI: 00007ffc38865530 RDI: 00000000ffffff9c +[ 2090.400371][ T43] RBP: 00007ffc388656c0 R08: 00000000ffffffff R09: 00007ffc38865530 +[ 2090.400563][ T43] R10: 0000000000000000 R11: 0000000000000246 R12: 00007ffc38865537 +[ 2090.400745][ T43] R13: 00007ffc38865530 R14: 00007fef39abc220 R15: 00007fef39a7e000 +[ 2090.400949][ T43] +[ 2090.401044][ T43] INFO: task jq:9092 blocked for more than 122 seconds. +[ 2090.401211][ T43] Not tainted 6.10.0-virtme #1 +[ 2090.401326][ T43] "echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message. +[ 2090.401539][ T43] task:jq state:D stack:26464 pid:9092 tgid:9092 ppid:9090 flags:0x00004000 +[ 2090.401808][ T43] Call Trace: +[ 2090.401901][ T43] +[ 2090.401968][ T43] __schedule+0x6e0/0x17e0 +[ 2090.402124][ T43] ? __pfx___schedule+0x10/0x10 +[ 2090.402243][ T43] ? schedule+0x1a5/0x210 +[ 2090.402338][ T43] ? __pfx_lock_acquire.part.0+0x10/0x10 +[ 2090.402477][ T43] ? trace_lock_acquire+0x14d/0x1f0 +[ 2090.402626][ T43] ? schedule+0x1a5/0x210 +[ 2090.402731][ T43] schedule+0xdf/0x210 +[ 2090.402824][ T43] __wait_on_freeing_inode+0x115/0x280 +[ 2090.402982][ T43] ? __pfx___wait_on_freeing_inode+0x10/0x10 +[ 2090.403171][ T43] ? __pfx_wake_bit_function+0x10/0x10 +[ 2090.403329][ T43] ? lock_acquire+0x32/0xc0 +[ 2090.403450][ T43] ? find_inode_fast+0x158/0x450 +[ 2090.403605][ T43] find_inode_fast+0x18d/0x450 +[ 2090.403742][ T43] iget_locked+0x7d/0x390 +[ 2090.403834][ T43] ? hlock_class+0x4e/0x130 +[ 2090.403972][ T43] v9fs_fid_iget_dotl+0x78/0x2d0 +[ 2090.404117][ T43] v9fs_vfs_lookup.part.0+0x1ed/0x390 +[ 2090.404263][ T43] ? __pfx_v9fs_vfs_lookup.part.0+0x10/0x10 +[ 2090.404417][ T43] ? lockdep_init_map_type+0x2cb/0x7c0 +[ 2090.404589][ T43] __lookup_slow+0x209/0x3c0 +[ 2090.404723][ T43] ? __pfx___lookup_slow+0x10/0x10 +[ 2090.404854][ T43] ? walk_component+0x29e/0x4f0 +[ 2090.405009][ T43] walk_component+0x2ab/0x4f0 +[ 2090.405137][ T43] link_path_walk.part.0.constprop.0+0x416/0x940 +[ 2090.405312][ T43] ? __pfx_link_path_walk.part.0.constprop.0+0x10/0x10 +[ 2090.405474][ T43] path_openat+0x1be/0x440 +[ 2090.405608][ T43] ? __pfx_path_openat+0x10/0x10 +[ 2090.405756][ T43] ? __lock_acquire+0xaf0/0x1570 +[ 2090.405883][ T43] do_filp_open+0x1b3/0x3e0 +[ 2090.406008][ T43] ? __pfx_do_filp_open+0x10/0x10 +[ 2090.406162][ T43] ? find_held_lock+0x2c/0x110 +[ 2090.406294][ T43] ? do_raw_spin_lock+0x131/0x270 +[ 2090.406442][ T43] ? __pfx_do_raw_spin_lock+0x10/0x10 +[ 2090.406589][ T43] ? alloc_fd+0x1f5/0x650 +[ 2090.406693][ T43] ? do_raw_spin_unlock+0x58/0x220 +[ 2090.406815][ T43] ? _raw_spin_unlock+0x23/0x40 +[ 2090.406954][ T43] ? alloc_fd+0x1f5/0x650 +[ 2090.407075][ T43] do_sys_openat2+0x122/0x160 +[ 2090.407208][ T43] ? __pfx_do_sys_openat2+0x10/0x10 +[ 2090.407332][ T43] ? user_path_at+0x45/0x60 +[ 2090.407490][ T43] __x64_sys_openat+0x123/0x1e0 +[ 2090.407635][ T43] ? __pfx___x64_sys_openat+0x10/0x10 +[ 2090.407764][ T43] ? __pfx_do_faccessat+0x10/0x10 +[ 2090.407950][ T43] do_syscall_64+0xc1/0x1d0 +[ 2090.408091][ T43] entry_SYSCALL_64_after_hwframe+0x77/0x7f +[ 2090.408251][ T43] RIP: 0033:0x7f7b9086b0e8 +[ 2090.408406][ T43] RSP: 002b:00007fff468c5918 EFLAGS: 00000287 ORIG_RAX: 0000000000000101 +[ 2090.408637][ T43] RAX: ffffffffffffffda RBX: 00007fff468c5b9f RCX: 00007f7b9086b0e8 +[ 2090.408838][ T43] RDX: 0000000000080000 RSI: 00007fff468c5990 RDI: 00000000ffffff9c +[ 2090.409040][ T43] RBP: 00007fff468c5980 R08: 0000000000080000 R09: 00007fff468c5990 +[ 2090.409220][ T43] R10: 0000000000000000 R11: 0000000000000287 R12: 00007fff468c5997 +[ 2090.409439][ T43] R13: 00007fff468c5bb0 R14: 00007fff468c5990 R15: 00007f7b9083c000 +[ 2090.409652][ T43] +[ 2090.409788][ T43] +[ 2090.409788][ T43] Showing all locks held in the system: +[ 2090.409977][ T43] 1 lock held by khungtaskd/43: +[ 2090.410101][ T43] #0: ffffffffb9368c00 (rcu_read_lock){....}-{1:2}, at: debug_show_all_locks+0x70/0x3a0 +[ 2090.410350][ T43] 1 lock held by tc/9091: +[ 2090.410439][ T43] #0: ffff888001720148 (&type->i_mutex_dir_key#3){++++}-{3:3}, at: walk_component+0x29e/0x4f0 +[ 2090.410714][ T43] 1 lock held by jq/9092: +[ 2090.410814][ T43] #0: ffff888001720148 (&type->i_mutex_dir_key#3){++++}-{3:3}, at: walk_component+0x29e/0x4f0 +[ 2090.411085][ T43] +[ 2090.411150][ T43] ============================================= +[ 2090.411150][ T43] +[ 2213.272644][ T43] INFO: task tc:9091 blocked for more than 245 seconds. +[ 2213.272927][ T43] Not tainted 6.10.0-virtme #1 +[ 2213.273055][ T43] "echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message. +[ 2213.273269][ T43] task:tc state:D stack:26464 pid:9091 tgid:9091 ppid:9090 flags:0x00000000 +[ 2213.273565][ T43] Call Trace: +[ 2213.273667][ T43] +[ 2213.273738][ T43] __schedule+0x6e0/0x17e0 +[ 2213.273875][ T43] ? __pfx___schedule+0x10/0x10 +[ 2213.274005][ T43] ? schedule+0x1a5/0x210 +[ 2213.274109][ T43] ? __pfx_lock_acquire.part.0+0x10/0x10 +[ 2213.274250][ T43] ? trace_lock_acquire+0x14d/0x1f0 +[ 2213.274387][ T43] ? schedule+0x1a5/0x210 +[ 2213.274486][ T43] schedule+0xdf/0x210 +[ 2213.274603][ T43] d_alloc_parallel+0xaef/0xed0 +[ 2213.274757][ T43] ? __pfx_d_alloc_parallel+0x10/0x10 +[ 2213.274882][ T43] ? __pfx_default_wake_function+0x10/0x10 +[ 2213.275038][ T43] ? lockdep_init_map_type+0x2cb/0x7c0 +[ 2213.275175][ T43] __lookup_slow+0x17f/0x3c0 +[ 2213.275305][ T43] ? __pfx___lookup_slow+0x10/0x10 +[ 2213.275438][ T43] ? walk_component+0x29e/0x4f0 +[ 2213.275597][ T43] walk_component+0x2ab/0x4f0 +[ 2213.275749][ T43] link_path_walk.part.0.constprop.0+0x416/0x940 +[ 2213.275908][ T43] ? __pfx_link_path_walk.part.0.constprop.0+0x10/0x10 +[ 2213.276067][ T43] path_lookupat+0x72/0x660 +[ 2213.276193][ T43] filename_lookup+0x19e/0x420 +[ 2213.276317][ T43] ? __pfx_filename_lookup+0x10/0x10 +[ 2213.276454][ T43] ? find_held_lock+0x2c/0x110 +[ 2213.276596][ T43] ? __lock_release+0x103/0x460 +[ 2213.276723][ T43] ? __pfx___lock_release+0x10/0x10 +[ 2213.276850][ T43] ? trace_lock_acquire+0x14d/0x1f0 +[ 2213.276984][ T43] ? __might_fault+0xc3/0x170 +[ 2213.277110][ T43] ? lock_acquire+0x32/0xc0 +[ 2213.277230][ T43] ? __might_fault+0xc3/0x170 +[ 2213.277356][ T43] vfs_statx+0xbf/0x140 +[ 2213.277457][ T43] ? __pfx_vfs_statx+0x10/0x10 +[ 2213.277617][ T43] ? getname_flags+0xb3/0x410 +[ 2213.277748][ T43] vfs_fstatat+0x80/0xc0 +[ 2213.277850][ T43] __do_sys_newfstatat+0x75/0xd0 +[ 2213.277982][ T43] ? __pfx___do_sys_newfstatat+0x10/0x10 +[ 2213.278105][ T43] ? user_path_at+0x45/0x60 +[ 2213.278239][ T43] ? __x64_sys_openat+0x123/0x1e0 +[ 2213.278363][ T43] ? __pfx___x64_sys_openat+0x10/0x10 +[ 2213.278483][ T43] ? __pfx_do_faccessat+0x10/0x10 +[ 2213.278627][ T43] ? lockdep_hardirqs_on_prepare+0x275/0x410 +[ 2213.278779][ T43] do_syscall_64+0xc1/0x1d0 +[ 2213.278921][ T43] entry_SYSCALL_64_after_hwframe+0x77/0x7f +[ 2213.279083][ T43] RIP: 0033:0x7fef39aaceae +[ 2213.279221][ T43] RSP: 002b:00007ffc38865528 EFLAGS: 00000246 ORIG_RAX: 0000000000000106 +[ 2213.279415][ T43] RAX: ffffffffffffffda RBX: 0000000000000004 RCX: 00007fef39aaceae +[ 2213.279615][ T43] RDX: 00007ffc38865600 RSI: 00007ffc38865530 RDI: 00000000ffffff9c +[ 2213.279801][ T43] RBP: 00007ffc388656c0 R08: 00000000ffffffff R09: 00007ffc38865530 +[ 2213.279989][ T43] R10: 0000000000000000 R11: 0000000000000246 R12: 00007ffc38865537 +[ 2213.280184][ T43] R13: 00007ffc38865530 R14: 00007fef39abc220 R15: 00007fef39a7e000 +[ 2213.280377][ T43] +[ 2213.280470][ T43] INFO: task jq:9092 blocked for more than 245 seconds. +[ 2213.280628][ T43] Not tainted 6.10.0-virtme #1 +[ 2213.280743][ T43] "echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message. +[ 2213.280955][ T43] task:jq state:D stack:26464 pid:9092 tgid:9092 ppid:9090 flags:0x00004000 +[ 2213.281199][ T43] Call Trace: +[ 2213.281296][ T43] +[ 2213.281364][ T43] __schedule+0x6e0/0x17e0 +[ 2213.281495][ T43] ? __pfx___schedule+0x10/0x10 +[ 2213.281642][ T43] ? schedule+0x1a5/0x210 +[ 2213.281737][ T43] ? __pfx_lock_acquire.part.0+0x10/0x10 +[ 2213.281868][ T43] ? trace_lock_acquire+0x14d/0x1f0 +[ 2213.282003][ T43] ? schedule+0x1a5/0x210 +[ 2213.282109][ T43] schedule+0xdf/0x210 +[ 2213.282219][ T43] __wait_on_freeing_inode+0x115/0x280 +[ 2213.282350][ T43] ? __pfx___wait_on_freeing_inode+0x10/0x10 +[ 2213.282505][ T43] ? __pfx_wake_bit_function+0x10/0x10 +[ 2213.282650][ T43] ? lock_acquire+0x32/0xc0 +[ 2213.282775][ T43] ? find_inode_fast+0x158/0x450 +[ 2213.282903][ T43] find_inode_fast+0x18d/0x450 +[ 2213.283054][ T43] iget_locked+0x7d/0x390 +[ 2213.283149][ T43] ? hlock_class+0x4e/0x130 +[ 2213.283280][ T43] v9fs_fid_iget_dotl+0x78/0x2d0 +[ 2213.283410][ T43] v9fs_vfs_lookup.part.0+0x1ed/0x390 +[ 2213.283539][ T43] ? __pfx_v9fs_vfs_lookup.part.0+0x10/0x10 +[ 2213.283705][ T43] ? lockdep_init_map_type+0x2cb/0x7c0 +[ 2213.283835][ T43] __lookup_slow+0x209/0x3c0 +[ 2213.283959][ T43] ? __pfx___lookup_slow+0x10/0x10 +[ 2213.284091][ T43] ? walk_component+0x29e/0x4f0 +[ 2213.284232][ T43] walk_component+0x2ab/0x4f0 +[ 2213.284356][ T43] link_path_walk.part.0.constprop.0+0x416/0x940 +[ 2213.284510][ T43] ? __pfx_link_path_walk.part.0.constprop.0+0x10/0x10 +[ 2213.284680][ T43] path_openat+0x1be/0x440 +[ 2213.284804][ T43] ? __pfx_path_openat+0x10/0x10 +[ 2213.284926][ T43] ? __lock_acquire+0xaf0/0x1570 +[ 2213.285051][ T43] do_filp_open+0x1b3/0x3e0 +[ 2213.285180][ T43] ? __pfx_do_filp_open+0x10/0x10 +[ 2213.285321][ T43] ? find_held_lock+0x2c/0x110 +[ 2213.285455][ T43] ? do_raw_spin_lock+0x131/0x270 +[ 2213.285599][ T43] ? __pfx_do_raw_spin_lock+0x10/0x10 +[ 2213.285735][ T43] ? alloc_fd+0x1f5/0x650 +[ 2213.285834][ T43] ? do_raw_spin_unlock+0x58/0x220 +[ 2213.285964][ T43] ? _raw_spin_unlock+0x23/0x40 +[ 2213.286086][ T43] ? alloc_fd+0x1f5/0x650 +[ 2213.286188][ T43] do_sys_openat2+0x122/0x160 +[ 2213.286320][ T43] ? __pfx_do_sys_openat2+0x10/0x10 +[ 2213.286446][ T43] ? user_path_at+0x45/0x60 +[ 2213.286586][ T43] __x64_sys_openat+0x123/0x1e0 +[ 2213.286709][ T43] ? __pfx___x64_sys_openat+0x10/0x10 +[ 2213.286829][ T43] ? __pfx_do_faccessat+0x10/0x10 +[ 2213.286972][ T43] do_syscall_64+0xc1/0x1d0 +[ 2213.287100][ T43] entry_SYSCALL_64_after_hwframe+0x77/0x7f +[ 2213.287251][ T43] RIP: 0033:0x7f7b9086b0e8 +[ 2213.287381][ T43] RSP: 002b:00007fff468c5918 EFLAGS: 00000287 ORIG_RAX: 0000000000000101 +[ 2213.287577][ T43] RAX: ffffffffffffffda RBX: 00007fff468c5b9f RCX: 00007f7b9086b0e8 +[ 2213.287758][ T43] RDX: 0000000000080000 RSI: 00007fff468c5990 RDI: 00000000ffffff9c +[ 2213.287935][ T43] RBP: 00007fff468c5980 R08: 0000000000080000 R09: 00007fff468c5990 +[ 2213.288117][ T43] R10: 0000000000000000 R11: 0000000000000287 R12: 00007fff468c5997 +[ 2213.288318][ T43] R13: 00007fff468c5bb0 R14: 00007fff468c5990 R15: 00007f7b9083c000 +[ 2213.288514][ T43] +[ 2213.288614][ T43] +[ 2213.288614][ T43] Showing all locks held in the system: +[ 2213.288798][ T43] 1 lock held by khungtaskd/43: +[ 2213.288925][ T43] #0: ffffffffb9368c00 (rcu_read_lock){....}-{1:2}, at: debug_show_all_locks+0x70/0x3a0 +[ 2213.289157][ T43] 1 lock held by tc/9091: +[ 2213.289251][ T43] #0: ffff888001720148 (&type->i_mutex_dir_key#3){++++}-{3:3}, at: walk_component+0x29e/0x4f0 +[ 2213.289500][ T43] 1 lock held by jq/9092: +[ 2213.289611][ T43] #0: ffff888001720148 (&type->i_mutex_dir_key#3){++++}-{3:3}, at: walk_component+0x29e/0x4f0 +[ 2213.289855][ T43] +[ 2213.289919][ T43] ============================================= +[ 2213.289919][ T43] +""" + if __name__ == "__main__": unittest.main() From 9ba9d096d52518b352794af5c698a8f6c761f2ee Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Tue, 23 Jul 2024 12:05:40 -0700 Subject: [PATCH 213/429] contest: gh: support pagination in jobs Jobs are split into multiple pages, too, we have to keep fetching until the jobs array is empty (or check link in the headers but that seems like more effort). Signed-off-by: Jakub Kicinski --- contest/remote/gh.py | 72 +++++++++++++++++++++++++------------------- 1 file changed, 41 insertions(+), 31 deletions(-) diff --git a/contest/remote/gh.py b/contest/remote/gh.py index fb34a12..b6d7f3e 100755 --- a/contest/remote/gh.py +++ b/contest/remote/gh.py @@ -49,11 +49,11 @@ def get(url, token): return requests.get(url, headers=headers) -def link(cbarg, config): +def link(runid, config): return "https://github.com/" + \ config.get('ci', 'owner') + "/" + \ config.get('ci', 'repo') + "/" + \ - "actions/runs/" + str(cbarg.prev_runid) + "actions/runs/" + str(runid) def gh_namify(name): @@ -64,36 +64,22 @@ def gh_namify(name): return namify(name) -def get_results(config, cbarg, prev_run, page=1): - token = config.get('gh', 'token') - repo_url = f"https://api.github.com/repos/%7Bconfig.get('ci', 'owner')}/{config.get('ci', 'repo')}" - ref = config.get('ci', 'runs_ref') - - resp = get(repo_url + f'/actions/runs?page={page}', token) - runs = resp.json() - found = None - for run in runs.get('workflow_runs'): - if ref in [r['ref'] for r in run['referenced_workflows']]: - if found is None or found["id"] < run["id"]: - found = run - if found is None: - if page < 10: - return get_results(config, cbarg, prev_run, page=(page + 1)) - print(f"Run not found, tried all {page} pages!") - return None - if prev_run == found["id"]: - print("Found old run:", prev_run) - return None - cbarg.prev_runid = found["id"] - - resp = get(repo_url + f'/actions/runs/{found["id"]}/jobs', token) +def get_jobs_page(config, repo_url, found, token, page=1, res=None): + resp = get(repo_url + f'/actions/runs/{found["id"]}/jobs?page={page}', token) jobs = resp.json() if 'jobs' not in jobs: - print("bad jobs") - print(jobs) + print("bad jobs", jobs) return None + if len(jobs['jobs']) == 0: + if page == 1: + print("short jobs", jobs) + return res + # Must be page 1, init res to empty array + if res is None: + res = [] + decoder = { 'success': 0, 'skipped': 1, @@ -111,8 +97,7 @@ def get_results(config, cbarg, prev_run, page=1): 5: 'fail', } - url = link(cbarg, config) - res = [] + url = link(found["id"], config) for job in jobs["jobs"]: if job["conclusion"] is None: print("Still running, waiting for job:", job["name"]) @@ -133,7 +118,32 @@ def get_results(config, cbarg, prev_run, page=1): 'result': result, 'link': test_link}) if not res: print(f"Still waiting, {len(jobs['jobs'])} jobs skipped") - return res + return get_jobs_page(config, repo_url, found, token, page=(page + 1), res=res) + + +def get_results(config, cbarg, prev_run, page=1): + token = config.get('gh', 'token') + repo_url = f"https://api.github.com/repos/%7Bconfig.get('ci', 'owner')}/{config.get('ci', 'repo')}" + ref = config.get('ci', 'runs_ref') + + resp = get(repo_url + f'/actions/runs?page={page}', token) + runs = resp.json() + found = None + for run in runs.get('workflow_runs'): + if ref in [r['ref'] for r in run['referenced_workflows']]: + if found is None or found["id"] < run["id"]: + found = run + if found is None: + if page < 10: + return get_results(config, cbarg, prev_run, page=(page + 1)) + print(f"Run not found, tried all {page} pages!") + return None + if prev_run == found["id"]: + print("Found old run:", prev_run) + return None + cbarg.prev_runid = found["id"] + + return get_jobs_page(config, repo_url, found, token) def test_run(binfo, rinfo, cbarg, config, start): @@ -175,7 +185,7 @@ def test_run(binfo, rinfo, cbarg, config, start): url = config.get('gh', 'link') if hasattr(cbarg, "prev_runid") and cbarg.prev_runid != prev_runid: - url = link(cbarg, config) + url = link(cbarg.prev_runid, config) return [{'test': config.get('executor', 'test'), 'group': config.get('executor', 'group'), From 6112db7d472660450c69457c98ab37b431063301 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Tue, 23 Jul 2024 17:10:09 -0700 Subject: [PATCH 214/429] contest: gh: protect from empty workflow_runs Signed-off-by: Jakub Kicinski --- contest/remote/gh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contest/remote/gh.py b/contest/remote/gh.py index b6d7f3e..0b3d6af 100755 --- a/contest/remote/gh.py +++ b/contest/remote/gh.py @@ -129,7 +129,7 @@ def get_results(config, cbarg, prev_run, page=1): resp = get(repo_url + f'/actions/runs?page={page}', token) runs = resp.json() found = None - for run in runs.get('workflow_runs'): + for run in runs.get('workflow_runs', []): if ref in [r['ref'] for r in run['referenced_workflows']]: if found is None or found["id"] < run["id"]: found = run From 88a5cda078af458193265090c50f84476132c527 Mon Sep 17 00:00:00 2001 From: Simon Horman Date: Thu, 8 Aug 2024 15:50:17 +0100 Subject: [PATCH 215/429] tests: build: run baseline build for Kconfig updates Some Kconfig updates may result in .config changes which may in turn alter the preprocessor output of header files, which may in turn result in file rebuilds. Due to the incremental way in which these tests may be run over a patchset this can result in inaccurate incumbent warning counts for patches, other than the first patch in a series for which a baseline build already occurs. This can, in turn, lead to false positives about new warnings being introduced. Some discussion of this problem can be found at: - Re: [PATCH v3 08/12] testing: net-drv: add basic shaper test https://lore.kernel.org/netdev/20240808122042.GA3067851@kernel.org/ While possibly heavy handed, the solution in this patch does seem address the problem. And can always be improved upon later. Signed-off-by: Simon Horman --- tests/patch/build_32bit/build_32bit.sh | 6 ++++-- tests/patch/build_allmodconfig_warn/build_allmodconfig.sh | 6 ++++-- tests/patch/build_clang/build_clang.sh | 6 ++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/patch/build_32bit/build_32bit.sh b/tests/patch/build_32bit/build_32bit.sh index b8f30de..6232aab 100755 --- a/tests/patch/build_32bit/build_32bit.sh +++ b/tests/patch/build_32bit/build_32bit.sh @@ -34,8 +34,10 @@ HEAD=$(git rev-parse HEAD) echo "Tree base:" git log -1 --pretty='%h ("%s")' HEAD~ -if [ x$FIRST_IN_SERIES == x0 ]; then - echo "Skip baseline build, not the first patch" +if [ x$FIRST_IN_SERIES == x0 ] && \ + ! git diff --name-only HEAD~ | grep -q -E "Kconfig$" +then + echo "Skip baseline build, not the first patch and no Kconfig updates" else echo "Baseline building the tree" diff --git a/tests/patch/build_allmodconfig_warn/build_allmodconfig.sh b/tests/patch/build_allmodconfig_warn/build_allmodconfig.sh index f8851f0..0ed1a13 100755 --- a/tests/patch/build_allmodconfig_warn/build_allmodconfig.sh +++ b/tests/patch/build_allmodconfig_warn/build_allmodconfig.sh @@ -34,8 +34,10 @@ HEAD=$(git rev-parse HEAD) echo "Tree base:" git log -1 --pretty='%h ("%s")' HEAD~ -if [ x$FIRST_IN_SERIES == x0 ]; then - echo "Skip baseline build, not the first patch" +if [ x$FIRST_IN_SERIES == x0 ] && \ + ! git diff --name-only HEAD~ | grep -q -E "Kconfig$" +then + echo "Skip baseline build, not the first patch and no Kconfig updates" else echo "Baseline building the tree" diff --git a/tests/patch/build_clang/build_clang.sh b/tests/patch/build_clang/build_clang.sh index 54a85c1..ea33497 100755 --- a/tests/patch/build_clang/build_clang.sh +++ b/tests/patch/build_clang/build_clang.sh @@ -27,8 +27,10 @@ HEAD=$(git rev-parse HEAD) echo "Tree base:" git log -1 --pretty='%h ("%s")' HEAD~ -if [ x$FIRST_IN_SERIES == x0 ]; then - echo "Skip baseline build, not the first patch" +if [ x$FIRST_IN_SERIES == x0 ] && \ + ! git diff --name-only HEAD~ | grep -q -E "Kconfig$" +then + echo "Skip baseline build, not the first patch and no Kconfig updates" else echo "Baseline building the tree" From 22f3c316524b1e7c19fab159bbf397bdceb04db8 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 15 Aug 2024 17:02:45 -0700 Subject: [PATCH 216/429] kunit: fix no-name tests Apparently there are no-name tests in kunit. Signed-off-by: Jakub Kicinski --- contest/remote/lib/fetcher.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contest/remote/lib/fetcher.py b/contest/remote/lib/fetcher.py index 370a6f0..e569e46 100644 --- a/contest/remote/lib/fetcher.py +++ b/contest/remote/lib/fetcher.py @@ -158,6 +158,8 @@ def run(self): def namify(what): + if not what: + return "no-name" name = re.sub(r'[^0-9a-zA-Z]+', '-', what) if name[-1] == '-': name = name[:-1] From 092b26eb49bb314633a48469dd1edb7c48677409 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 16 Aug 2024 19:58:33 -0700 Subject: [PATCH 217/429] contest: vmksft-p: report runtime for all test cases Looks like DB size growth is reasonable. Throw in the time. Signed-off-by: Jakub Kicinski --- contest/remote/vmksft-p.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contest/remote/vmksft-p.py b/contest/remote/vmksft-p.py index 4bc93ad..fd41df8 100755 --- a/contest/remote/vmksft-p.py +++ b/contest/remote/vmksft-p.py @@ -309,7 +309,7 @@ def test(binfo, rinfo, cbarg): 'result': r["result"], 'link': link + '/' + r['file_name'] } - for key in ['retry', 'crashes', 'results']: + for key in ['time', 'retry', 'crashes', 'results']: if key in r: outcome[key] = r[key] cases.append(outcome) From afd77863f0bffd68eef3b71557772fbafdb3e6d6 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 16 Aug 2024 20:10:02 -0700 Subject: [PATCH 218/429] ui: status: hide pass results in contest breakdown of fails Hide pass results when we display contest (full result list) for a branch for which some tests have failed. Majority of the time this link is used to see what failed, so showing passes just adds noise. Signed-off-by: Jakub Kicinski --- ui/status.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ui/status.js b/ui/status.js index 5fc988e..817fcd9 100644 --- a/ui/status.js +++ b/ui/status.js @@ -554,6 +554,8 @@ function add_summaries(table, summary, reported) link_to_contest += "&pw-n=0"; else link_to_contest += "&pw-y=0"; + if (summary["fail"] + summary["skip"] > 0) + link_to_contest += "&pass=0"; link_to_contest += "\">" + str_psf.str + ""; cell = row.insertCell(i++); // tests @@ -671,6 +673,8 @@ function load_result_table_one(data_raw, table, reported, avgs) link_to_contest += "&pw-n=0"; else link_to_contest += "&pw-y=0"; + if (fail + skip > 0) + link_to_contest += "&pass=0"; link_to_contest += "\">"; cnt.innerHTML = link_to_contest + str_psf.str + ""; From e14201575a63a02aa7afe84137ecd85a1215b616 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 16 Aug 2024 21:14:19 -0700 Subject: [PATCH 219/429] ui: contest: support displaying and sorting by exec time Support showing exec time. Let's see which tests are slow. Signed-off-by: Jakub Kicinski --- ui/contest.html | 1 + ui/contest.js | 36 +++++++++++++++++++++----- ui/nipa.js | 69 +++++++++++++++++++++++++++++++++++++++++++++++++ ui/status.js | 27 +------------------ 4 files changed, 100 insertions(+), 33 deletions(-) diff --git a/ui/contest.html b/ui/contest.html index 50c649a..f5827b7 100644 --- a/ui/contest.html +++ b/ui/contest.html @@ -114,6 +114,7 @@ Test Result Retry + Time Links diff --git a/ui/contest.js b/ui/contest.js index 7c8706e..e06a692 100644 --- a/ui/contest.js +++ b/ui/contest.js @@ -32,14 +32,14 @@ function load_result_table(data_raw) let warn_box = document.getElementById("fl-warn-box"); warn_box.innerHTML = ""; - let row_count = 0; - let form = ""; if (document.getElementById("ld-cases").checked) form = "&ld-cases=1"; + let rows = []; + $.each(data_raw, function(i, v) { - if (row_count >= 5000) { + if (rows.length >= 5000) { warn_box.innerHTML = "Reached 5000 rows. Set an executor, branch or test filter. Otherwise this page will set your browser on fire..."; return 0; } @@ -65,6 +65,26 @@ function load_result_table(data_raw) if (pw_n == false && nipa_pw_reported(v, r) == false) return 1; + rows.push({"v": v, "r": r}); + }); + }); + + let sort_time = nipa_sort_get('time'); + if (sort_time) + rows.sort(function(a, b) { + if (a.r.time === undefined && b.r.time === undefined) + return 0; + if (a.r.time === undefined) + return 1; + if (b.r.time === undefined) + return -1; + return sort_time * (b.r.time - a.r.time); + }); + + for (const result of rows) { + const r = result.r; + const v = result.v; + var row = table.insertRow(); var date = row.insertCell(0); @@ -76,6 +96,7 @@ function load_result_table(data_raw) var res = row.insertCell(6); let row_id = 7; var retry = row.insertCell(row_id++); + var time = row.insertCell(row_id++); var outputs = row.insertCell(row_id++); var flake = row.insertCell(row_id++); var hist = row.insertCell(row_id++); @@ -88,14 +109,13 @@ function load_result_table(data_raw) test.innerHTML = "" + r.test + ""; if ("retry" in r) retry.innerHTML = colorify_str(r.retry); + if ("time" in r) + time.innerHTML = nipa_msec_to_str(Math.round(r.time) * 1000); res.innerHTML = colorify_str(r.result); outputs.innerHTML = "outputs"; hist.innerHTML = "history"; flake.innerHTML = "matrix"; - - row_count++; - }); - }); + } } function find_branch_urls(loaded_data) @@ -228,6 +248,8 @@ function do_it() document.getElementById("ld_cnt").value = 1; } + nipa_sort_cb = results_update; + /* * Please remember to keep these assets in sync with `scripts/ui_assets.sh` */ diff --git a/ui/nipa.js b/ui/nipa.js index 9ccdd31..9a89b53 100644 --- a/ui/nipa.js +++ b/ui/nipa.js @@ -1,3 +1,32 @@ +function nipa_msec_to_str(msec) { + const convs = [ + [1, "ms"], + [1000, "s"], + [60, "m"], + [60, "h"], + [24, "d"], + [7, "w"] + ]; + + if (msec <= 0) + return msec.toString(); + + for (i = 0; i < convs.length; i++) { + if (msec < convs[i][0]) { + var full = Math.floor(msec) + convs[i - 1][1]; + if (i > 1) { + var frac = Math.round(msec * convs[i - 1][0] % convs[i - 1][0]); + if (frac) + full += " " + frac + convs[i - 2][1]; + } + return full; + } + msec /= convs[i][0]; + } + + return "TLE"; +} + function nipa_test_fullname(v, r) { return v.remote + "/" + v.executor + "/" + r.group + "/" + r.test; @@ -135,3 +164,43 @@ function nipa_load_sitemap() $("#sitemap").load("sitemap.html") }); } + +// ------------------ + +var nipa_sort_cb = null; +let nipa_sort_keys = []; +let nipa_sort_polarity = []; + +function nipa_sort_key_set(what) +{ + const index = nipa_sort_keys.indexOf(what); + let polarity = 1; + + if (index != -1) { + polarity = -1 * nipa_sort_polarity[index]; + // delete it + nipa_sort_keys.splice(index, 1); + nipa_sort_polarity.splice(index, 1); + + // We flipped back to normal polarity, that's a reset + if (polarity == 1) { + nipa_sort_cb(); + return; + } + } + + // add it + nipa_sort_keys.unshift(what); + nipa_sort_polarity.unshift(polarity); + + nipa_sort_cb(); +} + +function nipa_sort_get(what) +{ + const index = nipa_sort_keys.indexOf(what); + + if (index == -1) + return 0; + return nipa_sort_polarity[index]; +} diff --git a/ui/status.js b/ui/status.js index 817fcd9..15d9d8d 100644 --- a/ui/status.js +++ b/ui/status.js @@ -361,32 +361,7 @@ function status_system(data_raw) } function msec_to_str(msec) { - const convs = [ - [1, "ms"], - [1000, "s"], - [60, "m"], - [60, "h"], - [24, "d"], - [7, "w"] - ]; - - if (msec <= 0) - return msec.toString(); - - for (i = 0; i < convs.length; i++) { - if (msec < convs[i][0]) { - var full = Math.floor(msec) + convs[i - 1][1]; - if (i > 1) { - var frac = Math.round(msec * convs[i - 1][0] % convs[i - 1][0]); - if (frac) - full += " " + frac + convs[i - 2][1]; - } - return full; - } - msec /= convs[i][0]; - } - - return "TLE"; + return nipa_msec_to_str(msec); } function colorify_str_psf(str_psf, name, value, color) From 523185b575066c2d5d2a5c889cd91a434f748a49 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 17 Aug 2024 11:52:03 -0700 Subject: [PATCH 220/429] contest: vmksft-p: report real target as group Use target for group name reporting when single runner has multiple targets. team and bonding have tests with identical names (addr_list). Signed-off-by: Jakub Kicinski --- contest/remote/vmksft-p.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contest/remote/vmksft-p.py b/contest/remote/vmksft-p.py index fd41df8..5cca644 100755 --- a/contest/remote/vmksft-p.py +++ b/contest/remote/vmksft-p.py @@ -305,7 +305,7 @@ def test(binfo, rinfo, cbarg): cbarg.prev_runtime[r["prog"]] = r["time"] outcome = { 'test': r['test'], - 'group': grp_name, + 'group': "selftests-" + namify(r['target']), 'result': r["result"], 'link': link + '/' + r['file_name'] } From be557ea19334e78542c6851b17c22d108880a900 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 17 Aug 2024 11:53:19 -0700 Subject: [PATCH 221/429] contest: vmksft-p: maintain prev time by prog and group Since there can be test name collisions we need to include the target in the prev runtime sort. Signed-off-by: Jakub Kicinski --- contest/remote/vmksft-p.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contest/remote/vmksft-p.py b/contest/remote/vmksft-p.py index 5cca644..99534ee 100755 --- a/contest/remote/vmksft-p.py +++ b/contest/remote/vmksft-p.py @@ -266,7 +266,7 @@ def test(binfo, rinfo, cbarg): vm.dump_log(results_path + '/build') progs = get_prog_list(vm, targets) - progs.sort(reverse=True, key=lambda prog : cbarg.prev_runtime.get(prog[1], 0)) + progs.sort(reverse=True, key=lambda prog : cbarg.prev_runtime.get(prog, 0)) dl_min = config.getint('executor', 'deadline_minutes', fallback=999999) hard_stop = datetime.datetime.fromisoformat(binfo["date"]) @@ -279,7 +279,7 @@ def test(binfo, rinfo, cbarg): i = 0 for prog in progs: i += 1 - in_queue.put({'tid': i, 'prog': prog[1], 'target': prog[0]}) + in_queue.put({'tid': i, 'target': prog[0], 'prog': prog[1]}) # In case we have multiple tests kicking off on the same machine, # add optional wait to make sure others have finished building @@ -302,7 +302,7 @@ def test(binfo, rinfo, cbarg): while not out_queue.empty(): r = out_queue.get() if 'time' in r: - cbarg.prev_runtime[r["prog"]] = r["time"] + cbarg.prev_runtime[(r["target"], r["prog"])] = r["time"] outcome = { 'test': r['test'], 'group': "selftests-" + namify(r['target']), From 514381c78c013a9ffd45f60b10c1efc259d4e766 Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Wed, 21 Aug 2024 13:43:24 +0200 Subject: [PATCH 222/429] contest: vmksft-p: restrict comment if starts with '#' Just to follow the specs. Signed-off-by: Matthieu Baerts (NGI0) --- contest/remote/vmksft-p.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contest/remote/vmksft-p.py b/contest/remote/vmksft-p.py index 99534ee..b643c52 100755 --- a/contest/remote/vmksft-p.py +++ b/contest/remote/vmksft-p.py @@ -98,7 +98,7 @@ def _parse_nested_tests(full_run): v = result_re.match(line).groups() name = v[3] - if len(v) > 5 and v[5]: + if len(v) > 5 and v[4] and v[5]: if v[5].lower().startswith('skip') and result == "pass": result = "skip" From 51ae64e7166209929eae409ed4b73f0e39ee98e0 Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Wed, 21 Aug 2024 13:53:15 +0200 Subject: [PATCH 223/429] contest: vmksft-p: parse time from nested tests It is a useful info to display if it is available. For the moment, I think only mptcp_connect.sh adds such info. Signed-off-by: Matthieu Baerts (NGI0) --- contest/remote/vmksft-p.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/contest/remote/vmksft-p.py b/contest/remote/vmksft-p.py index b643c52..710720a 100755 --- a/contest/remote/vmksft-p.py +++ b/contest/remote/vmksft-p.py @@ -74,6 +74,7 @@ def _parse_nested_tests(full_run): nested_tests = False result_re = re.compile(r"(not )?ok (\d+)( -)? ([^#]*[^ ])( # )?([^ ].*)?$") + time_re = re.compile(r"time=(\d+)ms") for line in full_run.split('\n'): # nested subtests support: we parse the comments from 'TAP version' @@ -97,12 +98,19 @@ def _parse_nested_tests(full_run): continue v = result_re.match(line).groups() - name = v[3] + r = {'test': namify(v[3])} + if len(v) > 5 and v[4] and v[5]: if v[5].lower().startswith('skip') and result == "pass": result = "skip" - tests.append({'test': namify(name), 'result': result}) + t = time_re.findall(v[5].lower()) + if t: + r['time'] = round(int(t[-1]) / 1000.) # take the last one + + r['result'] = result + + tests.append(r) return tests From 506cfe8fc6abd621d408624b92b1c19930e28060 Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Wed, 21 Aug 2024 19:02:46 +0200 Subject: [PATCH 224/429] contest: vmksft-p: skip even if not ok KTAP is clear about the 'SKIP' directive and the result [1]: > note the result of the test case result line can be either "ok" or > "not ok" if the SKIP directive is used Nothing seems to be imposed on TAP 13/14 side [2], so I suppose it is fine. We can then drop this restriction, and mark the test as 'skipped' if the directive is starting with 'skip'. Link: https://docs.kernel.org/dev-tools/ktap.html#test-case-result-lines [1] Link: https://testanything.org/tap-version-14-specification.html [2] Signed-off-by: Matthieu Baerts (NGI0) --- contest/remote/vmksft-p.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contest/remote/vmksft-p.py b/contest/remote/vmksft-p.py index 710720a..fe9e87a 100755 --- a/contest/remote/vmksft-p.py +++ b/contest/remote/vmksft-p.py @@ -101,7 +101,7 @@ def _parse_nested_tests(full_run): r = {'test': namify(v[3])} if len(v) > 5 and v[4] and v[5]: - if v[5].lower().startswith('skip') and result == "pass": + if v[5].lower().startswith('skip'): result = "skip" t = time_re.findall(v[5].lower()) From 471e875896baf12f5fb4de43195b84936dc10e93 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 23 Aug 2024 10:29:18 -0700 Subject: [PATCH 225/429] ui: contest: support multi-column sort and add markers Make the sorting more generic (as in make it work on all columns). When more than one column is selected we sort in the reverse order of selection, JavaScript sort is stable. Highlight the sorting column and add a direction marker. Signed-off-by: Jakub Kicinski --- ui/contest.html | 2 +- ui/contest.js | 51 +++++++++++++++++++++++++++++++++++++------------ ui/nipa.css | 7 +++++++ ui/nipa.js | 29 +++++++++++++++++++++++----- 4 files changed, 71 insertions(+), 18 deletions(-) diff --git a/ui/contest.html b/ui/contest.html index f5827b7..184d9af 100644 --- a/ui/contest.html +++ b/ui/contest.html @@ -114,7 +114,7 @@ Test Result Retry - Time + Time Links diff --git a/ui/contest.js b/ui/contest.js index e06a692..d957230 100644 --- a/ui/contest.js +++ b/ui/contest.js @@ -10,6 +10,33 @@ function colorify_str(value) return ret + value + ''; } +function sort_results(rows) +{ + for (const sort_key of nipa_sort_keys) { + let sort_ord = nipa_sort_get(sort_key); + + if (sort_key === "date") { + rows.sort(function(a, b) { + return sort_ord * (b.v.end - a.v.end); + }); + } else if (sort_key === "time") { + rows.sort(function(a, b) { + if (a.r[sort_key] === undefined && b.r[sort_key] === undefined) + return 0; + if (a.r[sort_key] === undefined) + return 1; + if (b.r[sort_key] === undefined) + return -1; + return sort_ord * (b.r[sort_key] - a.r[sort_key]); + }); + } else { + rows.sort(function(a, b) { + return sort_ord * (b.r[sort_key] < a.r[sort_key] ? 1 : -1); + }); + } + } +} + function load_result_table(data_raw) { var table = document.getElementById("results"); @@ -69,17 +96,13 @@ function load_result_table(data_raw) }); }); - let sort_time = nipa_sort_get('time'); - if (sort_time) - rows.sort(function(a, b) { - if (a.r.time === undefined && b.r.time === undefined) - return 0; - if (a.r.time === undefined) - return 1; - if (b.r.time === undefined) - return -1; - return sort_time * (b.r.time - a.r.time); - }); + // Trim the time, so that sort behavior matches what user sees + for (const result of rows) { + if (result.r.time) + result.r.time = Math.round(result.r.time); + } + + sort_results(rows); for (const result of rows) { const r = result.r; @@ -110,7 +133,7 @@ function load_result_table(data_raw) if ("retry" in r) retry.innerHTML = colorify_str(r.retry); if ("time" in r) - time.innerHTML = nipa_msec_to_str(Math.round(r.time) * 1000); + time.innerHTML = nipa_msec_to_str(r.time * 1000); res.innerHTML = colorify_str(r.result); outputs.innerHTML = "outputs"; hist.innerHTML = "history"; @@ -172,6 +195,10 @@ function loaded_one() if (--xfr_todo) return; + let headers = document.getElementsByTagName("th"); + for (const th of headers) { + th.addEventListener("click", nipa_sort_key_set); + } reload_select_filters(true); nipa_filters_enable(reload_data, "ld-pw"); nipa_filters_enable(results_update, "fl-pw"); diff --git a/ui/nipa.css b/ui/nipa.css index 81eea5e..5033da3 100644 --- a/ui/nipa.css +++ b/ui/nipa.css @@ -27,6 +27,10 @@ tr:nth-child(even) { font-style: italic; } +.column-sorted { + background-color: #d0d0d0; +} + .box-pass { background-color: green; } .box-skip { background-color: royalblue; } .box-flake { background-color: red; } @@ -92,4 +96,7 @@ tr:nth-child(even) { .end-row td { border-color: #202020; } + .column-sorted { + background-color: #484848; + } } diff --git a/ui/nipa.js b/ui/nipa.js index 9a89b53..6ec15e4 100644 --- a/ui/nipa.js +++ b/ui/nipa.js @@ -171,27 +171,46 @@ var nipa_sort_cb = null; let nipa_sort_keys = []; let nipa_sort_polarity = []; -function nipa_sort_key_set(what) +function nipa_sort_key_set(event) { + let elem = event.target; + let what = elem.innerText.toLowerCase().replace(/[^a-z0-9]/g, ''); const index = nipa_sort_keys.indexOf(what); let polarity = 1; if (index != -1) { - polarity = -1 * nipa_sort_polarity[index]; + polarity = nipa_sort_polarity[index]; + + // if it's the main sort key invert direction, otherwise we're changing + // order of keys but not their direction + let main_key = index == nipa_sort_keys.length - 1; + if (main_key) + polarity *= -1; + // delete it nipa_sort_keys.splice(index, 1); nipa_sort_polarity.splice(index, 1); + elem.innerText = elem.innerText.slice(0, -2); // We flipped back to normal polarity, that's a reset - if (polarity == 1) { + if (main_key && polarity == 1) { + elem.classList.remove('column-sorted'); nipa_sort_cb(); return; } + } else { + elem.classList.add('column-sorted'); + } + + if (polarity == 1) { + elem.innerHTML = elem.innerText + " ⯆"; + } else { + elem.innerHTML = elem.innerText + " ⯅"; } // add it - nipa_sort_keys.unshift(what); - nipa_sort_polarity.unshift(polarity); + nipa_sort_keys.push(what); + nipa_sort_polarity.push(polarity); nipa_sort_cb(); } From 8bc607d8cffaf8acfbe4d39fc7fe497b7faede26 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 24 Aug 2024 14:22:04 -0700 Subject: [PATCH 226/429] contest: add cli tool for listing which tests run in the same VM --- contest/cithreadmap | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100755 contest/cithreadmap diff --git a/contest/cithreadmap b/contest/cithreadmap new file mode 100755 index 0000000..dcc2042 --- /dev/null +++ b/contest/cithreadmap @@ -0,0 +1,42 @@ +#!/bin/bash + +# expect URL to to the base dir as the only agrumnet +# base dir is the one below test outputs, where "config" is +[ -z "$1" ] && echo "Usage: $0 DIR_URL" && exit 1 +URL="$1" + +index=$(mktemp) +info=$(mktemp) + +declare -A worker_to_test + +clr() { + echo -ne " \r" +} + +curl -s $URL > $index + +i=0 +for subtest in $(cat $index | sed -n 's@ $info + + thr=$(cat $info | awk '/thr-id/ { print $2; }') + vm=$(cat $info | awk '/vm-id/ { print $2; }') + + worker_to_test["Thread$thr-VM$vm"]=${worker_to_test["Thread$thr-VM$vm"]}" "$subtest +done + +clr +echo "Fetched $i subtests" + +for key in ${!worker_to_test[@]}; do + echo $key + for value in ${worker_to_test[$key]}; do + echo -e '\t' $value + done +done + +rm $index $info From 4c83e5f3468e0528796b58c9766c340058ff6957 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 28 Aug 2024 08:07:53 -0700 Subject: [PATCH 227/429] brancher: pre-generate deltas when creating a branch Since we batch patches for testing when something fails we need to figure out what went wrong. Instead of having to fetch the branches locally and manually run cidiff just generate the output after creating each branch. Link to it from the status page. Hardcoding part of the URL isn't nice but not sure what to do about that. We could put the link to the cidiff in the branch manifest. But it feels a little auxiliary. Signed-off-by: Jakub Kicinski --- contest/cidiff | 9 +++++++++ pw_brancher.py | 18 ++++++++++++++++++ ui/status.js | 2 +- 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/contest/cidiff b/contest/cidiff index 2df5232..fd5e9af 100755 --- a/contest/cidiff +++ b/contest/cidiff @@ -15,6 +15,15 @@ if [ x$BRANCH1$BRANCH2 == x ]; then BRANCH1=${branches[0]} BRANCH2=${branches[1]} + echo " " $BRANCH1 + echo " " $BRANCH2 + echo +elif [ x$BRANCH2 == x ]; then + echo "Single branch specified, using that and the previous one:" + branches=( $(git branch -a | grep -B1 "$1") ) + BRANCH1=${branches[0]} + BRANCH2=${branches[1]} + echo " " $BRANCH1 echo " " $BRANCH2 echo diff --git a/pw_brancher.py b/pw_brancher.py index 0a461d2..06e3826 100755 --- a/pw_brancher.py +++ b/pw_brancher.py @@ -6,6 +6,7 @@ import json import os import psycopg2 +import subprocess import time from typing import List, Tuple import uuid @@ -32,6 +33,7 @@ [output] branches=branches.json info=branches-info.json +deltas=/path/to/dir/ [db] db=db-name """ @@ -190,6 +192,18 @@ def db_insert(config, state, name): cur.execute("INSERT INTO branches VALUES " + arg.decode('utf-8')) +def generate_deltas(config, name): + outdir = config.get("output", "deltas", fallback=None) + if not outdir: + return + + outfile = os.path.join(outdir, name) + cidiff = os.path.join(os.path.dirname(__file__), "contest", "cidiff") + + with open(outfile, 'w') as fp: + subprocess.run([cidiff, name], stdout=fp, check=True) + + def create_new(pw, config, state, tree, tgt_remote) -> None: now = datetime.datetime.now(datetime.UTC) pfx = config.get("target", "branch_pfx") @@ -235,6 +249,10 @@ def create_new(pw, config, state, tree, tgt_remote) -> None: tree.git_push(tgt_remote, "HEAD:" + branch_name) log_end_sec() + log_open_sec("Generate deltas") + generate_deltas(config, branch_name) + log_end_sec() + def state_delete_branch(state, br): del state["branches"][br] diff --git a/ui/status.js b/ui/status.js index 15d9d8d..18999df 100644 --- a/ui/status.js +++ b/ui/status.js @@ -693,7 +693,7 @@ function load_result_table_one(data_raw, table, reported, avgs) br_pull = " (pull: " + v.pull_status + ")"; branch.innerHTML = a + v.branch + "" + br_pull; branch.setAttribute("colspan", "2"); - res.innerHTML = ""; + res.innerHTML = "cidiff"; row.setAttribute("class", "end-row"); } }); From dde0f093774a4a4c85e595b5cf44bd3e4bd434d1 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 28 Aug 2024 11:20:25 -0700 Subject: [PATCH 228/429] contest: add bash script with manually built tools Add an embarrassingly un-automated script, which lists how things got built on the worker nodes. This is really more notes than a script that can be run. Signed-off-by: Jakub Kicinski --- deploy/contest/remote/worker-setup.sh | 246 ++++++++++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 deploy/contest/remote/worker-setup.sh diff --git a/deploy/contest/remote/worker-setup.sh b/deploy/contest/remote/worker-setup.sh new file mode 100644 index 0000000..c7938c4 --- /dev/null +++ b/deploy/contest/remote/worker-setup.sh @@ -0,0 +1,246 @@ +#!/bin/bash -xe + +# Cocci +# also install ocaml itself +sudo dnf install ocaml-findlib ocaml-findlib-devel +./configure --enable-ocaml --enable-pcre-syntax +make +make install +# explore local installation, ./configure output suggests how + +# Let runners use git on NIPA +git config --global --add safe.directory /opt/nipa + +sudo dnf install pip meson + +sudo dnf install perf +sudo dnf install nftables.x86_64 +sudo dnf install pixman-devel.x86_64 pixman.x86_64 libgudev.x86_64 +sudo dnf install libpcap-devel libpcap cmake +sudo dnf install clang numactl-devel.x86_64 +sudo dnf install socat wireshark nmap-ncat.x86_64 +sudo dnf install libdaemon-devel libdaemon +sudo dnf install libtool patch +sudo dnf install ninja-build.x86_64 texinfo +sudo dnf install bison flex openssl-devel +sudo dnf install capstone bzip2-devel libssh-devel +sudo dnf install git libmnl-devel +sudo dnf install elfutils-devel elfutils-libs elfutils-libelf elfutils-libelf-devel +sudo dnf install iptables + +# NIPA setup +git clone https://github.com/kuba-moo/nipa.git +sudo mv nipa/ /opt/ +sudo useradd virtme + +# nginx setup +sudo dnf -y install nginx +sudo systemctl enable nginx +sudo systemctl start nginx +# do basic config, then +sudo dnf -y install certbot certbot-nginx + +# virtme +git clone https://github.com/arighi/virtme-ng.git + +# as admin: +sudo dnf install python3.11.x86_64 python3.11-devel.x86_64 python3.11-pip.noarch python3.11-libs.x86_64 +# as virtme: +pip-3.11 install requests +pip-3.11 install psutil + +# prep for outside (system wide) +# QEMU +download QEMU +cd qemu-* +pip install sphinx +sudo dnf install glib2 glib2-devel +./configure --target-list=x86_64-softmmu,x86_64-linux-user +udo make install prefix=/usr + +# libcli +git clone https://github.com/dparrish/libcli.git +cd libcli +make -j +sudo make install PREFIX=/usr + +### Local + +mkdir tools +cd tools + +# netperf +git clone https://github.com/HewlettPackard/netperf.git +cd netperf +./autogen.sh +./configure --disable-omni # fails build otherwise +make install DESTDIR=/home/virtme/tools/fs prefix=/usr + +exit 0 + +# Install libbpf +cd $kernel +cd tools/lib/bpf +make -j 40 +sudo make install prefix=/usr + +# bpftool +cd $kernel +make -C tools/bpf/bpftool +cp tools/bpf/bpftool/bpftool ../tools/fs/ + +# Tests need +sudo dnf install socat libcap-devel + +# Build locally +sudo dnf install libnl3.x86_64 libnl3-cli.x86_64 libnl3-devel.x86_64 libnl3-doc.x86_64 +git clone https://github.com/jpirko/libteam.git +cd libteam +./autogen.sh +./configure +make -j 40 +# needs manual install +cp ./utils/teamdctl ../fs/usr/bin/ +cp ./utils/teamnl ../fs/usr/bin/ +cp -v ./libteam/.libs/libteam.so* ../fs/usr/lib/ +cp -v ./libteamdctl/.libs/libteamdctl.so* ../fs/usr/lib/ + +# refresh iproute2 +git clone https://git.kernel.org/pub/scm/network/iproute2/iproute2-next.git +cd iproute2-next +git remote add current https://git.kernel.org/pub/scm/network/iproute2/iproute2.git +git fetch --all +git reset --hard origin/main +git merge current/main -m "merge in current" + +./configure +make -j 40 +make install DESTDIR=/home/virtme/tools/fs prefix=/usr PREFIX=/usr + +# msend / mreceive +git clone https://github.com/troglobit/mtools.git +cd mtools +make +make install DESTDIR=/home/virtme/tools/fs prefix=/usr PREFIX=/usr + +# smcrouted +git clone https://github.com/troglobit/smcroute.git +cd smcroute +./autogen.sh +./configure +make install DESTDIR=/home/virtme/tools/fs prefix=/usr PREFIX=/usr +# it looks for a socket in /usr/local/var/run +sudo su +mkdir -p /usr/local/var/ +ln -sv /run /usr/local/var/ + +# ndisc6 (ndisc6 package on Fedora) +dnf -y install gettext-devel +git clone https://git.remlab.net/git/ndisc6.git +cd ndisc6/ +./autogen.sh +./configure +make -j +make install DESTDIR=/home/virtme/tools/fs prefix=/usr PREFIX=/usr +# make sure the SUID bits don't stick +find tools/fs/ -perm -4000 +fs=$(find tools/fs/ -perm -4000) +chmod -s $fs +ls -l $fs + +# dropwatch (DNF on fedora) +dnf -y install readline-devel binutils-devel +git clone https://github.com/nhorman/dropwatch +cd dropwatch/ +./autogen.sh +./configure +make -j +make install DESTDIR=/home/virtme/tools/fs prefix=/usr PREFIX=/usr + +# ethtool +git clone https://git.kernel.org/pub/scm/network/ethtool/ethtool.git +cd ethtool +./autogen.sh +./configure +make -j +make install DESTDIR=/home/virtme/tools/fs prefix=/usr PREFIX=/usr + +# psample +git clone https://github.com/Mellanox/libpsample +cd libpsample +cmake -DCMAKE_INSTALL_PREFIX:PATH=/home/virtme/tools/fs/usr . +make -j +make install + +# netsniff-ng +sudo dnf install libnetfilter_conntrack.x86_64 libnetfilter_conntrack-devel.x86_64 +sudo dnf install libsodium-devel.x86_64 libsodium.x86_64 +sudo dnf install libnet libnet-devel +git clone https://github.com/netsniff-ng/netsniff-ng.git +cd netsniff-ng +./configure +make -j + + +# AWS iputils are buggy +dnf -y install libxslt-devel libidn2-devel +git clone https://github.com/iputils/iputils.git +cd iputils +./configure +make -j +make install DESTDIR=/tmp +cp -v /tmp/usr/local/bin/* ../fs/usr/bin/ +cd ../fs/usr/bin/ +ln -s ping ping6 + +# ipv6toolkit (ra6 for fib_tests.sh) +git clone https://github.com/fgont/ipv6toolkit +cd ipv6toolkit/ +make +make install DESTDIR=/home/virtme/tools/fs PREFIX=/usr + +# for nf tests +sudo dnf install conntrack iperf3 ipvsadm + +git clone git://git.netfilter.org/libnftnl +./autogen.sh +./configure +make -j 30 +make install DESTDIR=/home/virtme/tools/fs prefix=/usr PREFIX=/usr + +libtool --finish /home/virtme/tools/fs/usr/lib +sudo dnf install gmp gmp-devel + +git clone git://git.netfilter.org/nftables +export PKG_CONFIG_PATH=/home/virtme/tools/fs:/home/virtme/tools/fs/usr:/home/virtme/tools/fs/usr/lib/pkgconfig/ +./configure --with-json --with-xtables + +# Edit paths into the makefile +# LIBNFTNL_CFLAGS = -I/usr/local/include -I/home/virtme/tools/fs/usr/include +# LIBNFTNL_LIBS = -L/usr/local/lib -L/home/virtme/tools/fs/usr/lib -lnftnl + +make install DESTDIR=/home/virtme/tools/fs prefix=/usr PREFIX=/usr +# note that library LD_LIBRARY_PATH must have local libs before /lib64 ! + +git clone git://git.netfilter.org/ebtables +./autogen.sh +./configure --prefix=/ --exec-prefix=/home/virtme/tools/fs +make -j 8 +make install DESTDIR=/home/virtme/tools/fs prefix=/usr PREFIX=/usr +cd /home/virtme/tools/fs/usr/sbin/ +ln -v ebtables-legacy ebtables + +sudo cp /etc/ethertypes /usr/local/etc/ + +# packetdrill +sudo dnf install glibc-static.x86_64 + +git clone https://github.com/google/packetdrill.git +cd packetdrill/gtests/net/packetdrill +./configure +make + +cp packetdrill ~/tools/fs/usr/bin/ + +# Net tests need pyroute2 (for OvS tests) +sudo dnf install python3-pyroute2.noarch From 810a2dd948ce7b2ede9086d51d4443882dba6b32 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 28 Aug 2024 16:50:01 -0700 Subject: [PATCH 229/429] brancher: cwd to the git tree for cidiff Fix running cidiff, we need to be in the git tree. Signed-off-by: Jakub Kicinski --- pw_brancher.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pw_brancher.py b/pw_brancher.py index 06e3826..bd84b67 100755 --- a/pw_brancher.py +++ b/pw_brancher.py @@ -192,7 +192,7 @@ def db_insert(config, state, name): cur.execute("INSERT INTO branches VALUES " + arg.decode('utf-8')) -def generate_deltas(config, name): +def generate_deltas(config, tree, name): outdir = config.get("output", "deltas", fallback=None) if not outdir: return @@ -201,7 +201,7 @@ def generate_deltas(config, name): cidiff = os.path.join(os.path.dirname(__file__), "contest", "cidiff") with open(outfile, 'w') as fp: - subprocess.run([cidiff, name], stdout=fp, check=True) + subprocess.run([cidiff, name], cwd=tree.path, stdout=fp, check=True) def create_new(pw, config, state, tree, tgt_remote) -> None: @@ -250,7 +250,7 @@ def create_new(pw, config, state, tree, tgt_remote) -> None: log_end_sec() log_open_sec("Generate deltas") - generate_deltas(config, branch_name) + generate_deltas(config, tree, branch_name) log_end_sec() From a384915ba58df8ec80b8c8c8f20952958d6cdd50 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 28 Aug 2024 21:45:02 -0700 Subject: [PATCH 230/429] contest: cli: cidiff make sure we exit with 0 git diff sets an non-zero return code, which makes running this script as part of services impossible. Signed-off-by: Jakub Kicinski --- contest/cidiff | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contest/cidiff b/contest/cidiff index fd5e9af..88353a5 100755 --- a/contest/cidiff +++ b/contest/cidiff @@ -54,3 +54,5 @@ git log --format="%s" $base2..$BRANCH2 > $tmp2 echo "==== COMMIT DIFF ====" git --no-pager diff --no-index $tmp1 $tmp2 + +exit 0 From 541dec151e9b8a6b797c3e3e3dd17c5f23fecb97 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 29 Aug 2024 11:58:01 -0700 Subject: [PATCH 231/429] contest: cli: log branch description Show branch description, this will reveal most recent tag, making it more obvious if we pulled from Linus between the branches. Signed-off-by: Jakub Kicinski --- contest/cidiff | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/contest/cidiff b/contest/cidiff index 88353a5..1f18103 100755 --- a/contest/cidiff +++ b/contest/cidiff @@ -14,21 +14,17 @@ if [ x$BRANCH1$BRANCH2 == x ]; then branches=( $(git branch -a | tail -2) ) BRANCH1=${branches[0]} BRANCH2=${branches[1]} - - echo " " $BRANCH1 - echo " " $BRANCH2 - echo elif [ x$BRANCH2 == x ]; then echo "Single branch specified, using that and the previous one:" branches=( $(git branch -a | grep -B1 "$1") ) BRANCH1=${branches[0]} BRANCH2=${branches[1]} - - echo " " $BRANCH1 - echo " " $BRANCH2 - echo fi +echo " " $BRANCH1 "("$(git describe $BRANCH1)")" +echo " " $BRANCH2 "("$(git describe $BRANCH2)")" +echo + get_base() { git log -1 --oneline \ --grep="Merge git://git.kernel.org/pub/scm/linux/kernel/git/netdev/net" $1 | cut -d' ' -f1 From a173c4f967dad9bb831c79cee995f8b5ad58aa3b Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 29 Aug 2024 20:29:57 -0700 Subject: [PATCH 232/429] ui: status: show branch row before results We used to show the row with the branch name and date as the last row, after all results. This was because the branch row was serving as a summary line, with total number of tests. For a while now we've had a separate, dedicated summary row. Branch row can be first, branch row and summary line "sandwich" the results. This seems more readable. Since the summary row now inserts the spacing we can remove the CSS handling for the branch row. Signed-off-by: Jakub Kicinski --- ui/nipa.css | 7 ++----- ui/status.js | 30 +++++++++++++++++++++--------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/ui/nipa.css b/ui/nipa.css index 5033da3..37b67a1 100644 --- a/ui/nipa.css +++ b/ui/nipa.css @@ -17,12 +17,9 @@ tr:nth-child(even) { background-color: #eeeeee; } -.end-row td { +.summary-row td { border-width: 1px 1px 6px 1px; border-color: white; -} - -.summary-row td { text-align: right; font-style: italic; } @@ -93,7 +90,7 @@ tr:nth-child(even) { tr:nth-child(odd) { background-color: #303030; } - .end-row td { + .summary-row td { border-color: #202020; } .column-sorted { diff --git a/ui/status.js b/ui/status.js index 18999df..4cab5e7 100644 --- a/ui/status.js +++ b/ui/status.js @@ -543,9 +543,9 @@ function add_summaries(table, summary, reported) row.setAttribute("class", "summary-row"); } -function reset_summary(summary) +function reset_summary(summary, branch) { - summary["branch"] = null; + summary["branch"] = branch; summary["remote-cnt"] = 0; summary["time-pass"] = 0; summary["total"] = 0; @@ -559,11 +559,17 @@ function load_result_table_one(data_raw, table, reported, avgs) const summarize = document.getElementById("contest-summary").checked; let summary = {}; - reset_summary(summary); + reset_summary(summary, data_raw[0].branch); $.each(data_raw, function(i, v) { var pass = 0, skip = 0, fail = 0, total = 0, ignored = 0; var link = v.link; + + if (summary["branch"] != v.branch) { + add_summaries(table, summary, reported); + reset_summary(summary, v.branch); + } + $.each(v.results, function(i, r) { if (nipa_pw_reported(v, r) != reported) { ignored++; @@ -589,11 +595,7 @@ function load_result_table_one(data_raw, table, reported, avgs) var t_start = new Date(v.start); var t_end = new Date(v.end); - if (v.remote == "brancher") { - summary["branch"] = v.branch; - add_summaries(table, summary, reported); - reset_summary(summary); - } else { + if (v.executor != "brancher") { summary["total"] += total; if (total) { summary["remote-cnt"] += 1; @@ -694,9 +696,11 @@ function load_result_table_one(data_raw, table, reported, avgs) branch.innerHTML = a + v.branch + "" + br_pull; branch.setAttribute("colspan", "2"); res.innerHTML = "cidiff"; - row.setAttribute("class", "end-row"); + res.setAttribute("style", "text-align: right;"); } }); + + add_summaries(table, summary, reported); } function rem_exe(v) @@ -805,6 +809,14 @@ function load_result_table(data_raw, reload) if (b.branch != a.branch) return b.branch > a.branch ? 1 : -1; + // Keep brancher first + if (a.executor == b.executor) + /* use other keys */; + else if (b.executor == "brancher") + return 1; + else if (a.executor == "brancher") + return -1; + // fake entry for "no result" always up top if (b.end === 0) return 1; From aab6face43747bcb29bcc6e936927e60c80eb418 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Tue, 3 Sep 2024 16:23:28 -0700 Subject: [PATCH 233/429] contest: backend: don't propagate total time to l2 L2 results start out as a copy to the top level result. This helps things like URL to get copied over, since sub-cases don't carry them. Propagating other keys, like 'time', make less sense. If we don't have per-case time we should show nothing. We had a case with MPTCP when only some cases failed to parse, and we'd show a mix of L2 times, and total times, which was super confusing. Signed-off-by: Jakub Kicinski --- contest/backend/query.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contest/backend/query.py b/contest/backend/query.py index baeb967..f3d8ded 100644 --- a/contest/backend/query.py +++ b/contest/backend/query.py @@ -61,6 +61,8 @@ def result_as_l2(raw): for case in l1["results"]: data = l1.copy() del data["results"] + if "time" in data: + del data["time"] data |= case data["test"] = l1["test"] + '.' + case["test"] flat.append(data) From d334e523256a374494284d7b3ba68b63ee986e2a Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Wed, 4 Sep 2024 18:22:41 +0200 Subject: [PATCH 234/429] contest: vmksft-p: allow dup spaces in nested tests Jakub recently reported that MPTCP nested tests were not properly parsed due to the presence of multiple whitespaces before the directive delimiter (#), e.g. ok 46 - mptcp_connect: peek mode: saveWithPeek: ns1 MPTCP -> ns1 (10.0.1.1:10043 ) TCP # time=5693ms TAP 14 explicitly allow having multiple whitespaces around the directive delimiter, but TAP 13 and KTAP doesn't mention anything about them. Anyway, it is easy to supported them by tweaking the regex to allow multiple whitespaces around the '#' character. Suggested-by: Jakub Kicinski Signed-off-by: Matthieu Baerts (NGI0) --- contest/remote/vmksft-p.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contest/remote/vmksft-p.py b/contest/remote/vmksft-p.py index fe9e87a..33033f6 100755 --- a/contest/remote/vmksft-p.py +++ b/contest/remote/vmksft-p.py @@ -73,7 +73,7 @@ def _parse_nested_tests(full_run): tests = [] nested_tests = False - result_re = re.compile(r"(not )?ok (\d+)( -)? ([^#]*[^ ])( # )?([^ ].*)?$") + result_re = re.compile(r"(not )?ok (\d+)( -)? ([^#]*[^ ])( +# +)?([^ ].*)?$") time_re = re.compile(r"time=(\d+)ms") for line in full_run.split('\n'): From 48fb3477c056113ee9320654bfcfe18a918fc0e8 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 5 Sep 2024 06:50:55 -0700 Subject: [PATCH 235/429] contest: kunit: copy the log file to results KUnit puts kernel logs in a separate file. Make a copy of that file in the results directory, so that it can be viewed. Signed-off-by: Jakub Kicinski --- contest/remote/kunit.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/contest/remote/kunit.py b/contest/remote/kunit.py index a70e13e..4de752b 100755 --- a/contest/remote/kunit.py +++ b/contest/remote/kunit.py @@ -5,6 +5,7 @@ import datetime import json import os +import shutil import subprocess from core import NipaLifetime @@ -138,9 +139,11 @@ def summary_result(expected, got, link, sub_path=""): def test(binfo, rinfo, config): print("Run at", datetime.datetime.now()) + tree_path = config.get('local', 'tree_path') + process = subprocess.Popen(['./tools/testing/kunit/kunit.py', 'run', '--alltests', '--json', '--arch=x86_64'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, - cwd=config.get('local', 'tree_path')) + cwd=tree_path) stdout, stderr = process.communicate() stdout = stdout.decode("utf-8", "ignore") stderr = stderr.decode("utf-8", "ignore") @@ -160,6 +163,8 @@ def test(binfo, rinfo, config): fp.write(stdout) with open(os.path.join(results_path, 'stderr'), 'w') as fp: fp.write(stderr) + shutil.copyfile(os.path.join(tree_path, '.kunit', 'test.log'), + os.path.join(results_path, 'kunit-test.log')) try: results_json = stdout_get_json(stdout) From 5ac1b1eeebd0a7e5e9e6c0eb6fe90d0cda59c695 Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Fri, 27 Sep 2024 18:09:45 +0200 Subject: [PATCH 236/429] contest: vm: get 'tree_path' config only once 'tree_path' config is fetched at the init phase, no need to get it again later, simply use self.tree_path. Signed-off-by: Matthieu Baerts (NGI0) --- contest/remote/lib/vm.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/contest/remote/lib/vm.py b/contest/remote/lib/vm.py index 102f8cb..b3e011c 100644 --- a/contest/remote/lib/vm.py +++ b/contest/remote/lib/vm.py @@ -116,12 +116,11 @@ def _get_ksft_timeout(self): default_timeout = 45 # from tools/testing/selftests/kselftest/runner.sh targets = self.config.get('ksft', 'target', fallback=None) - tree_path = self.config.get('local', 'tree_path', fallback=None) - if not targets or not tree_path: + if not targets: return default_timeout target = targets.split()[0] - settings_path = f'{tree_path}/tools/testing/selftests/{target}/settings' + settings_path = f'{self.tree_path}/tools/testing/selftests/{target}/settings' if not os.path.isfile(settings_path): return default_timeout From a602171eb4e06f15e424a5edfa029381ddde3dfa Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 4 Oct 2024 07:51:27 -0700 Subject: [PATCH 237/429] tests: build: tools: check for untracked files We seem to often miss adding outputs to gitignore. Try to catch those. Signed-off-by: Jakub Kicinski --- tests/patch/build_tools/build_tools.sh | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/patch/build_tools/build_tools.sh b/tests/patch/build_tools/build_tools.sh index 83451b8..bfad3b1 100755 --- a/tests/patch/build_tools/build_tools.sh +++ b/tests/patch/build_tools/build_tools.sh @@ -55,6 +55,10 @@ make O=$output_dir $build_flags -C tools/testing/selftests/ \ incumbent=$(grep -i -c "\(warn\|error\)" $tmpfile_o) +pr "Checking if tree is clean" +git status -s 1>&2 +incumbent_dirt=$(git status -s | grep -c '^??') + pr "Building the tree with the patch" git checkout -q $HEAD @@ -64,7 +68,11 @@ make O=$output_dir $build_flags -C tools/testing/selftests/ \ current=$(grep -i -c "\(warn\|error\)" $tmpfile_n) -echo "Errors and warnings before: $incumbent this patch: $current" >&$DESC_FD +pr "Checking if tree is clean" +git status -s 1>&2 +current_dirt=$(git status -s | grep -c '^??') + +echo "Errors and warnings before: $incumbent (+$incumbent_dirt) this patch: $current (+$current_dirt)" >&$DESC_FD if [ $current -gt $incumbent ]; then echo "New errors added" 1>&2 @@ -85,6 +93,12 @@ if [ $current -gt $incumbent ]; then rc=1 fi +if [ $current_dirt -gt $incumbent_dirt ]; then + echo "New untracked files added" 1>&2 + + rc=1 +fi + rm $tmpfile_o $tmpfile_n exit $rc From 0a357712bd79533b5c7a93db6adfef7645d9362f Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 4 Oct 2024 12:55:17 -0700 Subject: [PATCH 238/429] ui: status: add notes to filters Add ability to create short notes and link to things from filters. Most filters are temporary now, it's useful to track where things came from. Signed-off-by: Jakub Kicinski --- ui/status.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/ui/status.js b/ui/status.js index 4cab5e7..bdb211a 100644 --- a/ui/status.js +++ b/ui/status.js @@ -882,11 +882,12 @@ function branch_res_doit(data_raw) function add_one_test_filter_hdr(keys_present, key, hdr, row) { if (!keys_present.has(key)) - return ; + return false; let th = document.createElement("th"); th.innerHTML = hdr; row.appendChild(th); + return true; } function add_one_test_filter(keys_present, key, v, i, row) @@ -921,12 +922,17 @@ function filters_doit(data_raw) }); let cf_tests_hdr = document.getElementById("cf-tests-hdr"); + let has_notes = false; add_one_test_filter_hdr(keys_present, "remote", "Remote", cf_tests_hdr); add_one_test_filter_hdr(keys_present, "executor", "Executor", cf_tests_hdr); add_one_test_filter_hdr(keys_present, "branch", "Branch", cf_tests_hdr); add_one_test_filter_hdr(keys_present, "group", "Group", cf_tests_hdr); add_one_test_filter_hdr(keys_present, "test", "Test", cf_tests_hdr); + if (add_one_test_filter_hdr(keys_present, "link", "Notes", cf_tests_hdr) || + add_one_test_filter_hdr(keys_present, "comment", "Notes", cf_tests_hdr)) + has_notes = true; + $.each(data_raw["ignore-results"], function(_i, v) { let row = cf_tests.insertRow(); let i = 0; @@ -936,6 +942,15 @@ function filters_doit(data_raw) i += add_one_test_filter(keys_present, "branch", v, i, row); i += add_one_test_filter(keys_present, "group", v, i, row); i += add_one_test_filter(keys_present, "test", v, i, row); + + // Must be last, we don't handle counting columns properly here. + if (has_notes) + cell = row.insertCell(i); + if (v["comment"] || v["link"]) { + let comment = v["comment"] || "link"; + comment = wrap_link(v, v, comment); + cell.innerHTML = comment; + } }); output = "Crashes ignored:
"; From b273092fcd5ee6bcd808abe66c6e32135e410b7a Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 9 Oct 2024 10:11:36 -0700 Subject: [PATCH 239/429] contest: vm: add ' FAIL:' as a failure indicator Looks like we're missing the failures in rtnetlink.sh right now, since they incorrectly set exit code and only print a "FAIL: ..." message. Signed-off-by: Jakub Kicinski --- contest/remote/lib/vm.py | 1 + 1 file changed, 1 insertion(+) diff --git a/contest/remote/lib/vm.py b/contest/remote/lib/vm.py index b3e011c..c77ab06 100644 --- a/contest/remote/lib/vm.py +++ b/contest/remote/lib/vm.py @@ -417,6 +417,7 @@ def new_vm(results_path, vm_id, thr=None, vm=None, config=None, cwd=None): def guess_indicators(output): return { "fail": output.find("[FAIL]") != -1 or output.find("[fail]") != -1 or \ + output.find(" FAIL:") != -1 or \ output.find("\nnot ok 1 selftests: ") != -1 or \ output.find("\n# not ok 1") != -1, "skip": output.find("[SKIP]") != -1 or output.find("[skip]") != -1 or \ From 3855a91ed2b03e60f15d5c11ada9089dee85910c Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 9 Oct 2024 12:28:59 -0700 Subject: [PATCH 240/429] contest: track if branch has changes since the previous one Add a tiny bit of metadata to tell us the branch is identical. This can be technically computed offline but the branches are garbage collected quickly so for a historical view metadata is better. Signed-off-by: Jakub Kicinski --- pw_brancher.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pw_brancher.py b/pw_brancher.py index bd84b67..071ea34 100755 --- a/pw_brancher.py +++ b/pw_brancher.py @@ -204,6 +204,18 @@ def generate_deltas(config, tree, name): subprocess.run([cidiff, name], cwd=tree.path, stdout=fp, check=True) +def get_change_from_last(tree, branch_list) -> bool: + branch_list = list(sorted(branch_list)) + if len(branch_list) < 2: + return True + + try: + tree.git(['diff', '--quiet', branch_list[-1], branch_list[-2]]) + return False + except: + return True + + def create_new(pw, config, state, tree, tgt_remote) -> None: now = datetime.datetime.now(datetime.UTC) pfx = config.get("target", "branch_pfx") @@ -241,6 +253,8 @@ def create_new(pw, config, state, tree, tgt_remote) -> None: extras = apply_local_patches(config, tree) state["info"][branch_name]["extras"] = extras + state["info"][branch_name]["new-changes"] = get_change_from_last(tree, state["info"].keys()) + state["branches"][branch_name] = now.isoformat() db_insert(config, state, branch_name) From 012404317acb6ac770b07c047753de08afcec6dd Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Fri, 27 Sep 2024 18:38:10 +0200 Subject: [PATCH 241/429] contest: vm: capture code coverage Code coverage is a valuable info to get to know how much we can trust a test suite, and easily find out what needs to be improved. It is quite easy to get such info with the kernel: - The kernel needs to be compiled with GCOV_KERNEL=y, and have either GCOV_PROFILE_ALL=y, or GCOV_PROFILE := y set in the Makefiles. The recommended way is to add 'GCOV_PROFILE' in net/Makefile and drivers/net/Makefile. - Before stopping the VM, the LCOV file can be captured using the 'lcov' tool, version >= 2.0 is recommended. - 'genhtml' from the LCOV project can be used later to generate an HTML version using all the .lcov files. It could be done per LCOV file, but that will then only show the coverage per VM, not the global one. This GCOV support is disabled by default. It can be enabled via 'vm.gcov=on'. I suggest to keep it off by default, and switch it to on later when everything is in place. Signed-off-by: Matthieu Baerts (NGI0) --- contest/remote/lib/vm.py | 20 +++++++++++++++++++- contest/remote/vmksft-p.py | 1 + 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/contest/remote/lib/vm.py b/contest/remote/lib/vm.py index c77ab06..a7e8a64 100644 --- a/contest/remote/lib/vm.py +++ b/contest/remote/lib/vm.py @@ -9,6 +9,7 @@ import os import psutil import re +import shutil import signal from .crash import has_crash, extract_crash @@ -38,6 +39,7 @@ default_timeout=15 boot_timeout=45 slowdown=2.5 # mark the machine as slow and multiply the ksft timeout by 2.5 +gcov=off / on """ @@ -66,6 +68,7 @@ def __init__(self, config, vm_name=""): self.filter_data = None self.has_kmemleak = None + self.has_gcov = self.config.getboolean('vm', 'gcov', fallback=False) self.log_out = "" self.log_err = "" @@ -101,11 +104,13 @@ def build(self, extra_configs, override_configs=None): if extra_configs: configs += extra_configs + gcov = " --configitem GCOV_KERNEL=y" if self.has_gcov else "" + print(f"INFO{self.print_pfx} building kernel") # Make sure we rebuild, config and module deps can be stale otherwise self.tree_cmd("make mrproper") - rc = self.tree_cmd("vng -v -b" + " -f ".join([""] + configs)) + rc = self.tree_cmd("vng -v -b" + " -f ".join([""] + configs) + gcov) if rc != 0: print(f"INFO{self.print_pfx} kernel build failed") return False @@ -203,6 +208,7 @@ def start(self, cwd=None): self.cmd("ls /sys/kernel/debug/") self.drain_to_prompt() self.has_kmemleak = "kmemleak" in self.log_out[off:] + self.has_gcov = self.has_gcov and "gcov" in self.log_out[off:] self._set_env() @@ -387,6 +393,18 @@ def check_health(self): self.cmd("echo scan > /sys/kernel/debug/kmemleak && cat /sys/kernel/debug/kmemleak") self.drain_to_prompt() + def capture_gcov(self, dest): + if not self.has_gcov: + return + + lcov = "kernel.lcov" + self.cmd(f"lcov --capture --keep-going --rc geninfo_unexecuted_blocks=1 --function-coverage --branch-coverage -j $(nproc) -o {lcov}") + self.drain_to_prompt() + + lcov = os.path.join(self.tree_path, lcov) + if os.path.isfile(lcov): + shutil.copy(lcov, dest) + def bash_prev_retcode(self): self.cmd("echo $?") stdout, stderr = self.drain_to_prompt() diff --git a/contest/remote/vmksft-p.py b/contest/remote/vmksft-p.py index 33033f6..ff73e33 100755 --- a/contest/remote/vmksft-p.py +++ b/contest/remote/vmksft-p.py @@ -218,6 +218,7 @@ def _vm_thread(config, results_path, thr_id, hard_stop, in_queue, out_queue): vm = None if vm is not None: + vm.capture_gcov(results_path + f'/kernel-thr{thr_id}-{vm_id}.lcov') vm.stop() vm.dump_log(results_path + f'/vm-stop-thr{thr_id}-{vm_id}') return From 88c3ba7bb10770049535579e055e43682ff7c414 Mon Sep 17 00:00:00 2001 From: Johannes Berg Date: Mon, 28 Oct 2024 08:51:35 +0100 Subject: [PATCH 242/429] docker: install gawk It's explicitly needed by the kernel, but apparently not always pulled in. Signed-off-by: Johannes Berg --- docker/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/Dockerfile b/docker/Dockerfile index 82d1cf7..9a59a5e 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -13,6 +13,7 @@ RUN apt-get update && apt-get install -y \ ccache \ flex \ bison \ + gawk \ libssl-dev \ libelf-dev \ clang lld llvm \ From efaf9c969fa0a086d00db84035c0d9c5a61f1e59 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Mon, 28 Oct 2024 14:40:38 -0700 Subject: [PATCH 243/429] poller: save state before waiting for workers If workers hang stopping them will fail and we'll never save the processing state. Looks like this has be broken for a while. Signed-off-by: Jakub Kicinski --- pw_poller.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pw_poller.py b/pw_poller.py index fb034a5..2e403c0 100755 --- a/pw_poller.py +++ b/pw_poller.py @@ -249,6 +249,12 @@ def run(self, life) -> None: except KeyboardInterrupt: pass # finally will still run, but don't splat finally: + # Dump state before trying to stop workers, in case they hang + self._state['last_poll'] = prev_big_scan.timestamp() + self._state['done_series'] = list(self.seen_series) + with open('poller.state', 'w') as f: + json.dump(self._state, f) + log_open_sec(f"Stopping threads") for worker in self._workers: worker.should_die = True @@ -258,12 +264,6 @@ def run(self, life) -> None: worker.join() log_end_sec() - self._state['last_poll'] = prev_big_scan.timestamp() - self._state['done_series'] = list(self.seen_series) - # Dump state - with open('poller.state', 'w') as f: - json.dump(self._state, f) - if __name__ == "__main__": os.umask(0o002) From f08356bcbd09e1b31fd88467829896b07bf590a0 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Tue, 29 Oct 2024 16:57:54 -0700 Subject: [PATCH 244/429] tests: build: tools: skip building sched_ext It's BPF it required living in an alternate universe to build. Signed-off-by: Jakub Kicinski --- tests/patch/build_tools/build_tools.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/patch/build_tools/build_tools.sh b/tests/patch/build_tools/build_tools.sh index bfad3b1..9198cc0 100755 --- a/tests/patch/build_tools/build_tools.sh +++ b/tests/patch/build_tools/build_tools.sh @@ -32,7 +32,7 @@ echo "Now at:" git log -1 --pretty='%h ("%s")' HEAD # These are either very slow or don't build -export SKIP_TARGETS="bpf dt landlock livepatch lsm user_events mm powerpc" +export SKIP_TARGETS="bpf dt landlock livepatch lsm sched_ext user_events mm powerpc" pr "Cleaning" make O=$output_dir $build_flags -C tools/testing/selftests/ clean From c16fc8e2795f8241d0857f7e82e2d169e6f524e3 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 30 Oct 2024 06:13:28 -0700 Subject: [PATCH 245/429] contest: cocci: ignore str_*() helpers We don't care about the use of str_on_off() and such in networking. Signed-off-by: Jakub Kicinski --- contest/tests/cocci-check.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contest/tests/cocci-check.sh b/contest/tests/cocci-check.sh index c8d8b53..1e96adf 100755 --- a/contest/tests/cocci-check.sh +++ b/contest/tests/cocci-check.sh @@ -23,6 +23,9 @@ clean_up_output() { # remove the command lines sed -i '/^\/usr\/local\/bin\/spatch -D report /d' $file + # ignore the str helpers like str_on_off(), we don't care + sed -i '/: opportunity for str_/d' $file + # if files are removed or added cocci will fail in pre- or post- run sed -i '/^EXN: .*No such file or directory/d' $file sed -i '/^EXN: Coccinelle_modules.Common.Timeout /d' $file From c5eb5d6913b14ca5dbfe42bf9279baff88403c77 Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Wed, 30 Oct 2024 17:58:46 +0100 Subject: [PATCH 246/429] contest: subcases: handle retry cases In case of failure, some tests are restarted. That's great, but when parsing the subcases (nested tests), the previous results were overridden. Also, when displaying the results, the 'retry' field from the main test was re-used in each subcases: quite confusing for the reader. Now, in case of retry, the previous 'results' list will be modified to add a new 'retry' entry for each test that has been re-validated. If the previous test with the same name cannot be found, that could be because there was major issue before and some subcases have not been executed (or the names are not fixed). In this case, a new entry with a skipped first attempt will be added to the list. When querying the subcases results, the new 'retry' entry will be used, or none if it is not there. Signed-off-by: Matthieu Baerts (NGI0) --- contest/backend/query.py | 3 +++ contest/remote/vmksft-p.py | 23 ++++++++++++++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/contest/backend/query.py b/contest/backend/query.py index f3d8ded..274b704 100644 --- a/contest/backend/query.py +++ b/contest/backend/query.py @@ -63,6 +63,9 @@ def result_as_l2(raw): del data["results"] if "time" in data: del data["time"] + # in case of retry, the subtest might not have been re-executed + if "retry" in data: + del data["retry"] data |= case data["test"] = l1["test"] + '.' + case["test"] flat.append(data) diff --git a/contest/remote/vmksft-p.py b/contest/remote/vmksft-p.py index ff73e33..f434740 100755 --- a/contest/remote/vmksft-p.py +++ b/contest/remote/vmksft-p.py @@ -69,7 +69,7 @@ def get_prog_list(vm, targets): return [(e.split(":")[0].strip(), e.split(":")[1].strip()) for e in targets] -def _parse_nested_tests(full_run): +def _parse_nested_tests(full_run, prev_results): tests = [] nested_tests = False @@ -110,8 +110,20 @@ def _parse_nested_tests(full_run): r['result'] = result - tests.append(r) + if prev_results is not None: + for entry in prev_results: + if entry['test'] == r['test']: + entry['retry'] = result + break + else: + # the first run didn't validate this test: add it to the list + r['result'] = 'skip' + r['retry'] = result + prev_results.append(r) + else: + tests.append(r) + # return an empty list when there are prev results: no replacement needed return tests def _vm_thread(config, results_path, thr_id, hard_stop, in_queue, out_queue): @@ -195,8 +207,13 @@ def _vm_thread(config, results_path, thr_id, hard_stop, in_queue, out_queue): outcome['crashes'] = crashes if config.getboolean('ksft', 'nested_tests', fallback=False): + if is_retry: + prev_results = outcome['results'] if 'results' in outcome else [] + else: + prev_results = None + # this will only parse nested tests inside the TAP comments - nested_tests = _parse_nested_tests(vm.log_out) + nested_tests = _parse_nested_tests(vm.log_out, prev_results) if nested_tests: outcome['results'] = nested_tests From b0fb656f8dbba0e49e447cb563435939f4f52bb2 Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Mon, 4 Nov 2024 11:31:58 +0100 Subject: [PATCH 247/429] tree_match: handle bpf-next/net For a few months now [1], BPF patches that are "Networking related" should have the "bpf-next/net" prefix. With the previous order, the PW poller was considering this prefix as "for net", leading to these results on Patchwork [2]: Context | Check | Description ----------------------|---------|------------------------------ netdev/tree_selection | success | Clearly marked for net netdev/apply | fail | Patch does not apply to net-0 By changing the order of the list, such patches with "[bpf-next/net]" will be marked for "bpf-next", and they will not be applied locally like it was the case before with "bpf-next" patches. Link: https://lore.kernel.org/netdev/CAADnVQJgwGh+Jf=DUFuX28R2bpWVezigQYObNoKJT8UbqekOHA@mail.gmail.com/ [1] Link: https://patchwork.kernel.org/project/netdevbpf/patch/c02fda3177b34f9e74a044833fda9761627f4d07.1730338692.git.tanggeliang@kylinos.cn/ [2] Signed-off-by: Matthieu Baerts (NGI0) --- netdev/tree_match.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netdev/tree_match.py b/netdev/tree_match.py index dfccc3d..8b8cc50 100644 --- a/netdev/tree_match.py +++ b/netdev/tree_match.py @@ -10,7 +10,7 @@ def series_tree_name_direct(series): - for t in ['net-next', 'net', 'bpf-next', 'bpf']: + for t in ['net-next', 'bpf-next', 'net', 'bpf']: if re.match(r'\[.*{pfx}.*\]'.format(pfx=t), series.subject): return t From a0992af0b17dfab3e835d96ea7cef596e4dea34a Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sun, 3 Nov 2024 12:26:07 -0800 Subject: [PATCH 248/429] netdev: tree_match: include lib/ as local There are various bits of lib/ we want to test: dim, nla, packing, objagg, parman, etc. Include all of it, there aren't very many lib/ patches. Signed-off-by: Jakub Kicinski --- netdev/tree_match.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netdev/tree_match.py b/netdev/tree_match.py index 8b8cc50..68bbd35 100644 --- a/netdev/tree_match.py +++ b/netdev/tree_match.py @@ -53,6 +53,8 @@ def _tree_name_should_be_local_files(raw_email): 'include/linux/skbuff.h', 'include/net/', 'include/phy/', + # lib/ is pretty broad but patch volume is low + 'lib/', 'net/', 'drivers/atm/', 'drivers/dpll/', From 4d92db1c37bc9ef60135bf9b6e04e0b5c02b8eb6 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sun, 3 Nov 2024 16:16:25 -0800 Subject: [PATCH 249/429] contest: vm: support setting memory size Add first class citizen setting for memory size, since we have one for CPU count. Like QEMU and vng the memory size is in MB. Signed-off-by: Jakub Kicinski --- contest/remote/lib/vm.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contest/remote/lib/vm.py b/contest/remote/lib/vm.py index a7e8a64..ed5ef4b 100644 --- a/contest/remote/lib/vm.py +++ b/contest/remote/lib/vm.py @@ -179,6 +179,9 @@ def start(self, cwd=None): cpus = self.config.get('vm', 'cpus', fallback="") if cpus: cmd += ["--cpus", cpus] + mem = self.config.get('vm', 'mem', fallback="") + if mem: + cmd += ["--memory", mem] print(f"INFO{self.print_pfx} VM starting:", " ".join(cmd)) self.log_out += "# " + " ".join(cmd) + "\n" From e4397f9c223ea3ea6cdfb972b5ead6927084da98 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 9 Nov 2024 09:44:38 -0800 Subject: [PATCH 250/429] netdev: tree_match: include Documentation/netlink as local Signed-off-by: Jakub Kicinski --- netdev/tree_match.py | 1 + 1 file changed, 1 insertion(+) diff --git a/netdev/tree_match.py b/netdev/tree_match.py index 68bbd35..9212f96 100644 --- a/netdev/tree_match.py +++ b/netdev/tree_match.py @@ -48,6 +48,7 @@ def _tree_name_should_be_local_files(raw_email): 'drivers/vhost/', } required_files = { + 'Documentation/netlink/', 'Documentation/networking/', 'include/linux/netdevice.h', 'include/linux/skbuff.h', From 9ad00d3caa3e609560ef8ef6e84b47dc60d4a47d Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 9 Nov 2024 10:43:41 -0800 Subject: [PATCH 251/429] tests: build: tools: skip building kvm The define VMX_BASIC_MEM_TYPE_WB does not seem to exist anymore. This will like get fixed during the merge window, but we don't care about KVM in the first place. Signed-off-by: Jakub Kicinski --- tests/patch/build_tools/build_tools.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/patch/build_tools/build_tools.sh b/tests/patch/build_tools/build_tools.sh index 9198cc0..5c08db3 100755 --- a/tests/patch/build_tools/build_tools.sh +++ b/tests/patch/build_tools/build_tools.sh @@ -32,7 +32,7 @@ echo "Now at:" git log -1 --pretty='%h ("%s")' HEAD # These are either very slow or don't build -export SKIP_TARGETS="bpf dt landlock livepatch lsm sched_ext user_events mm powerpc" +export SKIP_TARGETS="bpf dt kvm landlock livepatch lsm sched_ext user_events mm powerpc" pr "Cleaning" make O=$output_dir $build_flags -C tools/testing/selftests/ clean From 50048c0732135e60ec0ee7ad29426f1f642b2a4f Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Mon, 11 Nov 2024 10:49:33 -0800 Subject: [PATCH 252/429] docs: skip non-files rather than exiting Don't terminate all processing when we find a directory. Signed-off-by: Jakub Kicinski --- docs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs.py b/docs.py index f5a3691..eefcf23 100755 --- a/docs.py +++ b/docs.py @@ -247,7 +247,7 @@ def main(): dr = DocRefs() for file in os.listdir(os.path.join(sys.argv[1], 'Documentation', 'process')): if not os.path.isfile(os.path.join(sys.argv[1], 'Documentation', 'process', file)): - return + continue name = file[:-4] dr.load_section('process/' + name, name) if len(sys.argv) > 2: From 872c419cdda71bd79fa70067a4d7111d8b61ea96 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Mon, 11 Nov 2024 11:58:12 -0800 Subject: [PATCH 253/429] docs: add images for executors --- docs/ci.svg | 2 +- docs/exe-exec.svg | 1 + docs/exe-gh.svg | 1 + docs/exe-vmksft.svg | 1 + docs/execs.svg | 1 + 5 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 docs/exe-exec.svg create mode 100644 docs/exe-gh.svg create mode 100644 docs/exe-vmksft.svg create mode 100644 docs/execs.svg diff --git a/docs/ci.svg b/docs/ci.svg index 3516468..32b8317 100644 --- a/docs/ci.svg +++ b/docs/ci.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/exe-exec.svg b/docs/exe-exec.svg new file mode 100644 index 0000000..b38ad4a --- /dev/null +++ b/docs/exe-exec.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/exe-gh.svg b/docs/exe-gh.svg new file mode 100644 index 0000000..7436c26 --- /dev/null +++ b/docs/exe-gh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/exe-vmksft.svg b/docs/exe-vmksft.svg new file mode 100644 index 0000000..c5c2b6f --- /dev/null +++ b/docs/exe-vmksft.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/execs.svg b/docs/execs.svg new file mode 100644 index 0000000..07e80bf --- /dev/null +++ b/docs/execs.svg @@ -0,0 +1 @@ + \ No newline at end of file From fa662beaa6f204f8b03140da2a430b8ef9ead882 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sun, 15 Dec 2024 14:23:15 -0800 Subject: [PATCH 254/429] tests: kdoc: fix per-file breakdown The paths in kdoc output do not start with ../ Signed-off-by: Jakub Kicinski --- tests/patch/kdoc/kdoc.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/patch/kdoc/kdoc.sh b/tests/patch/kdoc/kdoc.sh index 0d75e96..a4f96ad 100755 --- a/tests/patch/kdoc/kdoc.sh +++ b/tests/patch/kdoc/kdoc.sh @@ -35,9 +35,9 @@ if [ $current -gt $incumbent ]; then tmpfile_fo=$(mktemp) tmpfile_fn=$(mktemp) - grep -i "\(warn\|error\)" $tmpfile_o | sed -n 's@\(^\.\./[/a-zA-Z0-9_.-]*.[ch]\):.*@\1@p' | sort | uniq -c \ + grep -i "\(warn\|error\)" $tmpfile_o | sed -n 's@\(^[/a-zA-Z0-9_.-]*.[ch]\):.*@\1@p' | sort | uniq -c \ > $tmpfile_fo - grep -i "\(warn\|error\)" $tmpfile_n | sed -n 's@\(^\.\./[/a-zA-Z0-9_.-]*.[ch]\):.*@\1@p' | sort | uniq -c \ + grep -i "\(warn\|error\)" $tmpfile_n | sed -n 's@\(^[/a-zA-Z0-9_.-]*.[ch]\):.*@\1@p' | sort | uniq -c \ > $tmpfile_fn diff $tmpfile_fo $tmpfile_fn 1>&2 From 07ebcd3cf666d0c8ce447a56e89123ed324d71be Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 18 Dec 2024 11:43:15 -0800 Subject: [PATCH 255/429] tests: build: rust: pass make flags to all invocations Passing build_flags should not matter all that much for config targets, but let's pass them just to be safe. Here especially since debugging why CONFIG_RUST doesn't get enabled can be a bit tricky. Signed-off-by: Jakub Kicinski --- tests/patch/build_clang_rust/build_clang_rust.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/patch/build_clang_rust/build_clang_rust.sh b/tests/patch/build_clang_rust/build_clang_rust.sh index d715bbc..51f0e4a 100755 --- a/tests/patch/build_clang_rust/build_clang_rust.sh +++ b/tests/patch/build_clang_rust/build_clang_rust.sh @@ -12,7 +12,7 @@ tmpfile_n=$(mktemp) rc=0 prep_config() { - make LLVM=1 O=$output_dir allmodconfig + make LLVM=1 O=$output_dir allmodconfig $build_flags # Disable -Werror so we get to see all the errors ./scripts/config --file $output_dir/.config -d werror @@ -49,7 +49,7 @@ prep_config() { # Setting options above enabled some new options. Set them to their # defaults - make LLVM=1 O=$output_dir olddefconfig + make LLVM=1 O=$output_dir olddefconfig $build_flags # And verify rust is now actually enabled in the configuration. config_rust=$(./scripts/config --file $output_dir/.config --state CONFIG_RUST) From b74b66f6adb9ef288334d21d6e47b1fd489ff098 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 18 Dec 2024 11:50:46 -0800 Subject: [PATCH 256/429] tests: build: rust: update the config selection Update the build features which Rust 1.83 (Fedora 41) is not compatible with. Signed-off-by: Jakub Kicinski --- tests/patch/build_clang_rust/build_clang_rust.sh | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/patch/build_clang_rust/build_clang_rust.sh b/tests/patch/build_clang_rust/build_clang_rust.sh index 51f0e4a..cc1bc4c 100755 --- a/tests/patch/build_clang_rust/build_clang_rust.sh +++ b/tests/patch/build_clang_rust/build_clang_rust.sh @@ -28,15 +28,12 @@ prep_config() { ./scripts/config --file $output_dir/.config -d randstruct_full ./scripts/config --file $output_dir/.config -e randstruct_none ./scripts/config --file $output_dir/.config -d modversions + # Rust also seems currently incompatible with CFI (Rust 1.83) + ./scripts/config --file $output_dir/.config -d cfi_clang # Now Rust can be enabled ./scripts/config --file $output_dir/.config -e rust - # The Rust compiler does not play nicely with the kernel workarounds - # for speculation attacks. So turn off RETHUNK and X86_KERNEL_IBT - ./scripts/config --file $output_dir/.config -d rethunk - ./scripts/config --file $output_dir/.config -d x86_kernel_ibt - # Rust currently requires all dependencies are built in, so make # phylib built in. ./scripts/config --file $output_dir/.config -e phylib From 1d508e54c629c9f55e109a46255cc2b707a84824 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 18 Dec 2024 11:51:36 -0800 Subject: [PATCH 257/429] tests: build: rust: switch from SHA1 to SHA256 for module signing Root cause is unclear but it seems that attempts to build with SHA1 on Fedora 41 are failing. We need to keep an eye on this for other build scripts. Signed-off-by: Jakub Kicinski --- tests/patch/build_clang_rust/build_clang_rust.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/patch/build_clang_rust/build_clang_rust.sh b/tests/patch/build_clang_rust/build_clang_rust.sh index cc1bc4c..8d3421a 100755 --- a/tests/patch/build_clang_rust/build_clang_rust.sh +++ b/tests/patch/build_clang_rust/build_clang_rust.sh @@ -20,6 +20,12 @@ prep_config() { # KVM has its own WERROR control, and it currently does generate errors! ./scripts/config --file $output_dir/.config -d kvm_werror + # Unclear if this is related to Rust but we seem to get key generation + # issues with SHA1 on Fedora 41. Switch to SHA256. + ./scripts/config --file $output_dir/.config -d module_sig_sha1 + ./scripts/config --file $output_dir/.config -e module_sig_sha256 + ./scripts/config --file $output_dir/.config --set-str module_sig_hash sha256 + # allmodconfig is not sufficient to get Rust support enabled. So # flip some options. From 471071a89f00c07a54f09190f503d2be4850c00a Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 18 Dec 2024 11:53:23 -0800 Subject: [PATCH 258/429] tests: build: rust: output CONFIG_RUST failure to DESC Make sure the problems with getting CONFIG_RUST to be set are displayed in short summary / patchwork. Previously we would get a failure and a bare "Link". Signed-off-by: Jakub Kicinski --- tests/patch/build_clang_rust/build_clang_rust.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/patch/build_clang_rust/build_clang_rust.sh b/tests/patch/build_clang_rust/build_clang_rust.sh index 8d3421a..a69b0d9 100755 --- a/tests/patch/build_clang_rust/build_clang_rust.sh +++ b/tests/patch/build_clang_rust/build_clang_rust.sh @@ -58,7 +58,7 @@ prep_config() { config_rust=$(./scripts/config --file $output_dir/.config --state CONFIG_RUST) if [ $config_rust != "y" ]; then - echo Unable to enable CONFIG_RUST + echo "CONFIG_RUST not set in generated config" >& $DESC_FD exit 1 fi } From 90af0b1260f170bd4800c6c4e75beae3271bb029 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Mon, 6 Jan 2025 09:28:17 -0800 Subject: [PATCH 259/429] cc_maintainers: convert all addresses to lower case Heiner reports false positives when CC list and MAINTAINERS differ on case. Signed-off-by: Jakub Kicinski --- form-letters/net-next-closed | 1 + tests/patch/cc_maintainers/test.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/form-letters/net-next-closed b/form-letters/net-next-closed index 187ad5b..f46841e 100644 --- a/form-letters/net-next-closed +++ b/form-letters/net-next-closed @@ -9,3 +9,4 @@ RFC patches sent for review only are obviously welcome at any time. See: https://www.kernel.org/doc/html/next/process/maintainer-netdev.html#development-cycle -- pw-bot: defer +pv-bot: closed diff --git a/tests/patch/cc_maintainers/test.py b/tests/patch/cc_maintainers/test.py index 1c9dbb7..8d7dd8f 100644 --- a/tests/patch/cc_maintainers/test.py +++ b/tests/patch/cc_maintainers/test.py @@ -148,7 +148,7 @@ def cc_maintainers(tree, thing, result_dir) -> Tuple[int, str, str]: addrs += msg.get_all('cc', []) addrs += msg.get_all('from', []) addrs += msg.get_all('sender', []) - included = set([e for n, e in email.utils.getaddresses(addrs)]) + included = set([e.lower() for n, e in email.utils.getaddresses(addrs)]) out += ["=== Email ===", f"From: {msg.get_all('from')}", f"Included: {included}", ""] @@ -174,7 +174,7 @@ def cc_maintainers(tree, thing, result_dir) -> Tuple[int, str, str]: raw_gm.append(line.strip()) match = emailpat.search(line) if match: - addr = match.group(1) + addr = match.group(1).lower() expected.add(addr) if 'blamed_fixes' in line: blamed.add(addr) @@ -226,7 +226,7 @@ def cc_maintainers(tree, thing, result_dir) -> Tuple[int, str, str]: continue for have in included: if have in mmap_emails: - mapped.add(m) + mapped.add(m.lower()) found.update(mapped) missing.difference_update(mapped) From a3ef018671ef228f3f705b2872cef8b010af9b8a Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Mon, 27 Jan 2025 18:26:21 -0800 Subject: [PATCH 260/429] contest: vm: scan with kmemleak twice During 6.14 merge window we noticed that IOAM6 tests would cause a leak and it was only detected after the next test. Manual testing confirms the same behavior. Just to me sure scan for leaks twice. Signed-off-by: Jakub Kicinski --- contest/remote/lib/vm.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contest/remote/lib/vm.py b/contest/remote/lib/vm.py index ed5ef4b..f3c5515 100644 --- a/contest/remote/lib/vm.py +++ b/contest/remote/lib/vm.py @@ -395,6 +395,10 @@ def check_health(self): if self.has_kmemleak: self.cmd("echo scan > /sys/kernel/debug/kmemleak && cat /sys/kernel/debug/kmemleak") self.drain_to_prompt() + # Do it twice, kmemleak likes to hide the leak on the first attempt + self.cmd("echo scan > /sys/kernel/debug/kmemleak && cat /sys/kernel/debug/kmemleak") + self.drain_to_prompt() + def capture_gcov(self, dest): if not self.has_gcov: From 6dab047b9710fb2ea82286f900d089bb67f983da Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Mon, 3 Feb 2025 07:21:38 -0800 Subject: [PATCH 261/429] tests: cocci: reset the tree at the start The ignored tests may be left over, make sure we reset at the start. Signed-off-by: Jakub Kicinski --- contest/tests/cocci-check.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contest/tests/cocci-check.sh b/contest/tests/cocci-check.sh index 1e96adf..869a3b0 100755 --- a/contest/tests/cocci-check.sh +++ b/contest/tests/cocci-check.sh @@ -63,6 +63,8 @@ echo "Starting at $(date)" echo IGNORED=( scripts/coccinelle/misc/minmax.cocci ) + +git reset --hard for ign_file in ${IGNORED[@]}; do echo "Ignoring " $ign_file mv $ign_file $ign_file.ignore From ca47296a6c26d2596613ae52d207f12e20f48e7e Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 13 Feb 2025 10:49:13 -0800 Subject: [PATCH 262/429] contest: kunit: support waiting for loadavg Signed-off-by: Jakub Kicinski --- contest/remote/kunit.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/contest/remote/kunit.py b/contest/remote/kunit.py index 4de752b..e1a70b3 100755 --- a/contest/remote/kunit.py +++ b/contest/remote/kunit.py @@ -10,6 +10,7 @@ from core import NipaLifetime from lib import Fetcher, namify +from lib import wait_loadavg """ @@ -30,6 +31,8 @@ patches_path=/root-path/to/patches/dir [www] url=https://url-to-reach-base-path +[cfg] +wait_loadavg= Expected: @@ -141,6 +144,10 @@ def test(binfo, rinfo, config): tree_path = config.get('local', 'tree_path') + load_tgt = config.getfloat("cfg", "wait_loadavg", fallback=None) + if load_tgt: + wait_loadavg(load_tgt) + process = subprocess.Popen(['./tools/testing/kunit/kunit.py', 'run', '--alltests', '--json', '--arch=x86_64'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=tree_path) From 82ce0404ffe075751040005e809199e0ecec6c9a Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 13 Feb 2025 10:49:35 -0800 Subject: [PATCH 263/429] docs: include maintainer/ in the loaded docs Signed-off-by: Jakub Kicinski --- docs.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs.py b/docs.py index eefcf23..b24123c 100755 --- a/docs.py +++ b/docs.py @@ -250,6 +250,11 @@ def main(): continue name = file[:-4] dr.load_section('process/' + name, name) + for file in os.listdir(os.path.join(sys.argv[1], 'Documentation', 'maintainer')): + if not os.path.isfile(os.path.join(sys.argv[1], 'Documentation', 'maintainer', file)): + continue + name = file[:-4] + dr.load_section('maintainer/' + name, name) if len(sys.argv) > 2: form_letters = sys.argv[2] else: From 1a3550748bbea7b1d0d71ba492c58eccf19c9caa Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 14 Feb 2025 11:46:48 -0800 Subject: [PATCH 264/429] contest: kunit: remove PYTHONUNBUFFERED when running kunit Looks like kunit has output buffering problems. Try to unset PYTHONUNBUFFERED for it. We set it in the process to make sure we get logs without delays. Signed-off-by: Jakub Kicinski --- contest/remote/kunit.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/contest/remote/kunit.py b/contest/remote/kunit.py index e1a70b3..bdfa2fd 100755 --- a/contest/remote/kunit.py +++ b/contest/remote/kunit.py @@ -148,9 +148,13 @@ def test(binfo, rinfo, config): if load_tgt: wait_loadavg(load_tgt) + penv = os.environ.copy() + if 'PYTHONUNBUFFERED' in penv: + del penv['PYTHONUNBUFFERED'] + process = subprocess.Popen(['./tools/testing/kunit/kunit.py', 'run', '--alltests', '--json', '--arch=x86_64'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, - cwd=tree_path) + cwd=tree_path, env=penv) stdout, stderr = process.communicate() stdout = stdout.decode("utf-8", "ignore") stderr = stderr.decode("utf-8", "ignore") From b84969aa45daf59bf08eb7564a64a880b2465b1e Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 14 Feb 2025 11:48:16 -0800 Subject: [PATCH 265/429] tests: cocci: ignore secs_to_jiffies Signed-off-by: Jakub Kicinski --- contest/tests/cocci-check.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/contest/tests/cocci-check.sh b/contest/tests/cocci-check.sh index 869a3b0..86953b6 100755 --- a/contest/tests/cocci-check.sh +++ b/contest/tests/cocci-check.sh @@ -62,7 +62,11 @@ done echo "Starting at $(date)" echo -IGNORED=( scripts/coccinelle/misc/minmax.cocci ) +IGNORED=( + scripts/coccinelle/misc/minmax.cocci + # secs_to_jiffies is broken in report mode + scripts/coccinelle/misc/secs_to_jiffies.cocci +) git reset --hard for ign_file in ${IGNORED[@]}; do From cc85723b50b5f68305d07aa7daa1d3560494ad48 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 15 Feb 2025 15:57:40 -0800 Subject: [PATCH 266/429] brancher: make brancher cleanup safe against other branchers If another brancher uses the same push repo we need to make sure we don't overlap on prefixes. Be more careful when deciding if we own a branch. Signed-off-by: Jakub Kicinski --- pw_brancher.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pw_brancher.py b/pw_brancher.py index 071ea34..7c59ec3 100755 --- a/pw_brancher.py +++ b/pw_brancher.py @@ -290,6 +290,9 @@ def reap_old(config, state, tree, tgt_remote) -> None: br = br.strip() if not br.startswith(r_tgt_pfx + pfx): continue + # In case our prefix is a prefix of another brancher + if len(br) != len(r_tgt_pfx + pfx + "2000-01-01--00-00"): + continue br = br[len(r_tgt_pfx):] found.add(br) if br not in state["branches"]: From 04b9271a9d108cc5b641f886f33ad96dfb5b860d Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 15 Feb 2025 16:15:54 -0800 Subject: [PATCH 267/429] brancher: insert stream name into the table In preparation for more branchers insert stream name into the database. Signed-off-by: Jakub Kicinski --- deploy/contest/db | 1 + pw_brancher.py | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/deploy/contest/db b/deploy/contest/db index 9dc3aee..e1c14d0 100644 --- a/deploy/contest/db +++ b/deploy/contest/db @@ -49,6 +49,7 @@ CREATE INDEX ON branches (branch DESC); CREATE TABLE branches ( branch varchar(80), + stream varchar(60), t_date timestamp, base varchar(80), url varchar(200), diff --git a/pw_brancher.py b/pw_brancher.py index 7c59ec3..1d511bc 100755 --- a/pw_brancher.py +++ b/pw_brancher.py @@ -179,6 +179,8 @@ def apply_local_patches(config, tree) -> List: def db_insert(config, state, name): global psql_conn + # Branches usually have a trailing separator + pfx = config.get("target", "branch_pfx")[:-1] pub_url = config.get('target', 'public_url') row = {"branch": name, "date": state["branches"][name], @@ -187,9 +189,11 @@ def db_insert(config, state, name): row |= state["info"][name] with psql_conn.cursor() as cur: - arg = cur.mogrify("(%s,%s,%s,%s,%s)", (row["branch"], row["date"], row["base"], row["url"], - json.dumps(row))) - cur.execute("INSERT INTO branches VALUES " + arg.decode('utf-8')) + cols = "(branch, stream, t_date, base, url, info)" + arg = cur.mogrify("(%s,%s,%s,%s,%s,%s)", + (row["branch"], pfx, row["date"], row["base"], + row["url"], json.dumps(row))) + cur.execute(f"INSERT INTO branches {cols} VALUES " + arg.decode('utf-8')) def generate_deltas(config, tree, name): From fffbdaff60a31c4223fd4844bc1cbb554db287f0 Mon Sep 17 00:00:00 2001 From: Johannes Berg Date: Mon, 24 Feb 2025 11:03:26 +0100 Subject: [PATCH 268/429] pw_poller: avoid crash on unknown tree Seems at least with certain misconfigurations we can get a tree here that doesn't actually exist in the work queues, skip that instead of crashing. Signed-off-by: Johannes Berg --- pw_poller.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pw_poller.py b/pw_poller.py index 2e403c0..20a5a08 100755 --- a/pw_poller.py +++ b/pw_poller.py @@ -172,6 +172,9 @@ def _process_series(self, pw_series) -> None: if hasattr(s, 'tree_name') and s.tree_name: s.tree_selection_comment = comment + if not s.tree_name in self._work_queues: + log(f"skip {pw_series['id']} for unknown tree {s.tree_name}", "") + return self._work_queues[s.tree_name].put(s) else: core.write_tree_selection_result(self.result_dir, s, comment) From e0cf43753a1a23f7bc03167851b5e84eda758e13 Mon Sep 17 00:00:00 2001 From: Johannes Berg Date: Mon, 24 Feb 2025 11:15:05 +0100 Subject: [PATCH 269/429] pw_poller: make list module configurable There only is a 'netdev' module now, but make that configuration so we can add other modules (i.e. wireless) later. Put the names of trees into the module instead of hard-coding. Signed-off-by: Johannes Berg --- netdev/__init__.py | 3 +++ pw_poller.py | 28 ++++++++++++++++------------ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/netdev/__init__.py b/netdev/__init__.py index d20d9f1..e081618 100644 --- a/netdev/__init__.py +++ b/netdev/__init__.py @@ -12,3 +12,6 @@ series_tree_name_should_be_local, \ series_is_a_fix_for, \ series_needs_async + +current_tree = 'net' +next_tree = 'net-next' diff --git a/pw_poller.py b/pw_poller.py index 20a5a08..0176e97 100755 --- a/pw_poller.py +++ b/pw_poller.py @@ -12,6 +12,7 @@ import time import queue from typing import Dict +from importlib import import_module from core import NIPA_DIR from core import NipaLifetime @@ -21,7 +22,6 @@ from pw import Patchwork from pw import PwSeries import core -import netdev class IncompleteSeries(Exception): @@ -80,6 +80,9 @@ def __init__(self, config) -> None: self._recheck_period = config.getint('poller', 'recheck_period', fallback=3) self._recheck_lookback = config.getint('poller', 'recheck_lookback', fallback=9) + listmodname = config.get('list', 'module', fallback='netdev') + self.list_module = import_module(listmodname) + def init_state_from_disk(self) -> None: try: with open('poller.state', 'r') as f: @@ -91,15 +94,15 @@ def init_state_from_disk(self) -> None: pass def _series_determine_tree(self, s: PwSeries) -> str: - s.tree_name = netdev.series_tree_name_direct(s) + s.tree_name = self.list_module.series_tree_name_direct(s) s.tree_mark_expected = True s.tree_marked = bool(s.tree_name) if s.is_pure_pull(): if s.title.find('-next') >= 0: - s.tree_name = 'net-next' + s.tree_name = self.list_module.next_tree else: - s.tree_name = 'net' + s.tree_name = self.list_module.current_tree s.tree_mark_expected = None return f"Pull request for {s.tree_name}" @@ -107,12 +110,12 @@ def _series_determine_tree(self, s: PwSeries) -> str: log(f'Series is clearly designated for: {s.tree_name}', "") return f"Clearly marked for {s.tree_name}" - s.tree_mark_expected, should_test = netdev.series_tree_name_should_be_local(s) + s.tree_mark_expected, should_test = self.list_module.series_tree_name_should_be_local(s) if not should_test: log("No tree designation found or guessed", "") return "Not a local patch" - if netdev.series_ignore_missing_tree_name(s): + if self.list_module.series_ignore_missing_tree_name(s): s.tree_mark_expected = None log('Okay to ignore lack of tree in subject, ignoring series', "") return "Series ignored based on subject" @@ -122,11 +125,12 @@ def _series_determine_tree(self, s: PwSeries) -> str: else: log_open_sec('Series okay without a tree designation') - # TODO: make this configurable - if "net" in self._trees and netdev.series_is_a_fix_for(s, self._trees["net"]): - s.tree_name = "net" - elif "net-next" in self._trees and self._trees["net-next"].check_applies(s): - s.tree_name = "net-next" + if self.list_module.current_tree in self._trees and \ + self.list_module.series_is_a_fix_for(s, self._trees[self.list_module.current_tree]): + s.tree_name = self.list_module.current_tree + elif self.list_module.next_tree in self._trees and \ + self._trees[self.list_module.next_tree].check_applies(s): + s.tree_name = self.list_module.next_tree if s.tree_name: log(f"Target tree - {s.tree_name}", "") @@ -166,7 +170,7 @@ def _process_series(self, pw_series) -> None: raise IncompleteSeries comment = self.series_determine_tree(s) - s.need_async = netdev.series_needs_async(s) + s.need_async = self.list_module.series_needs_async(s) if s.need_async: comment += ', async' From 6684b79e47992102b46352644a7848516581ec8e Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 22 Feb 2025 10:03:59 -0800 Subject: [PATCH 270/429] ui: highlight stuck workers Capture last modification time of logs and highlight their row as red if they have a job but no output for more than 90 minutes. Signed-off-by: Jakub Kicinski --- system-status.py | 8 ++++++-- ui/status.html | 1 + ui/status.js | 7 +++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/system-status.py b/system-status.py index d553aaf..a1ad8cc 100755 --- a/system-status.py +++ b/system-status.py @@ -53,7 +53,10 @@ def pre_strip(line, needle): def add_one_tree(result, pfx, name): global char_filter - with open(os.path.join(pfx, name), 'r') as fp: + log_file = os.path.join(pfx, name) + stat = os.stat(log_file) + + with open(log_file, 'r') as fp: lines = fp.readlines() last = None test = '' @@ -93,7 +96,8 @@ def add_one_tree(result, pfx, name): "progress": progress, "test": test, "test-progress": test_prog, - "backlog": blog} + "backlog": blog, + "mtime": stat.st_mtime} def add_one_runtime(fname, total, res): diff --git a/ui/status.html b/ui/status.html index f35188c..7d5b77d 100644 --- a/ui/status.html +++ b/ui/status.html @@ -35,6 +35,7 @@

Build processing

Tree Qlen + Last Tid Test Pid diff --git a/ui/status.js b/ui/status.js index bdb211a..19be68d 100644 --- a/ui/status.js +++ b/ui/status.js @@ -236,6 +236,7 @@ function load_runners(data_raw) let cell_id = 0; var name = row.insertCell(cell_id++); var qlen = row.insertCell(cell_id++); + var modify = row.insertCell(cell_id++); var tid = row.insertCell(cell_id++); var test = row.insertCell(cell_id++); var pid = row.insertCell(cell_id++); @@ -247,6 +248,12 @@ function load_runners(data_raw) tid.innerHTML = v["test-progress"]; test.innerHTML = v.test; qlen.innerHTML = v.backlog; + + let since = Date.now() - (new Date(v.mtime * 1000)); + modify.innerHTML = nipa_msec_to_str(since); + if (v.patch && since > 90 * 60 * 1000) { // 1.5 hours + row.setAttribute("style", "color: red"); + } }); } From c5aa5c1f4bda6053c96e76119a0c0f6617f33300 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Mon, 24 Feb 2025 15:25:25 -0800 Subject: [PATCH 271/429] contest: vm: support running a setup script Add a hook for running a setup script. We will use a setup script to prep virtio loop for running HW tests. Signed-off-by: Jakub Kicinski --- contest/remote/lib/vm.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contest/remote/lib/vm.py b/contest/remote/lib/vm.py index f3c5515..7c6ba98 100644 --- a/contest/remote/lib/vm.py +++ b/contest/remote/lib/vm.py @@ -147,6 +147,10 @@ def _set_env(self): self.cmd("export LD_LIBRARY_PATH=" + self.config.get('vm', 'ld_paths') + ':$LD_LIBRARY_PATH') self.drain_to_prompt() + if self.config.get('vm', 'setup', fallback=None): + self.cmd(self.config.get('vm', 'setup')) + self.drain_to_prompt() + exports = self.config.get('vm', 'exports', fallback=None) if exports: for export in exports.split(','): From 96aef0f5d3609520c3acda81623f3b1ef50ae126 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Mon, 24 Feb 2025 15:35:18 -0800 Subject: [PATCH 272/429] contest: scripts: add a virtio setup script Add a script to run tests over virtio net loop. Signed-off-by: Jakub Kicinski --- contest/scripts/vm-virtio-loop.sh | 32 +++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100755 contest/scripts/vm-virtio-loop.sh diff --git a/contest/scripts/vm-virtio-loop.sh b/contest/scripts/vm-virtio-loop.sh new file mode 100755 index 0000000..8a28c41 --- /dev/null +++ b/contest/scripts/vm-virtio-loop.sh @@ -0,0 +1,32 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0 + +# Expect we were booted into a virtme-ng VM with "--net loop" + +for ifc in eth0 eth1; do + if ! ethtool -i "$ifc" | grep -q virtio; then + echo "Error: $ifc is not virtio" + exit 1 + fi +done + +ip netns add ns-remote +ip link set dev eth1 netns ns-remote +export REMOTE_TYPE=netns +export REMOTE_ARGS=ns-remote + +ip link set dev eth0 up +ip -netns ns-remote link set dev eth1 up +export NETIF=eth0 + +ip addr add dev eth0 192.0.2.1/24 +ip -netns ns-remote addr add dev eth1 192.0.2.2/24 +export LOCAL_V4=192.0.2.1 +export REMOTE_V4=192.0.2.2 + +ip addr add dev eth0 2001:db8::1/64 +ip -netns ns-remote addr add dev eth1 2001:db8::2/64 +export LOCAL_V6=2001:db8::1 +export REMOTE_V6=2001:db8::2 + +sleep 1 From 8e7fece260f59fcd33a8ff874147584b2feddabd Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Tue, 25 Feb 2025 19:42:25 -0800 Subject: [PATCH 273/429] contest: fetcher: try to work around a git lock issue After updating git to 2.47 we see random crashes like this: ``` From https://github.com/linux-netdev/testing - [deleted] (none) -> origin/net-next-2025-02-21--00-00 * [new branch] net-next-2025-02-26--03-00 -> origin/net-next-2025-02-26--03-00 error: cannot lock ref 'HEAD': Unable to create '/home/virtme/testing-13/.git/HEAD.lock': File exists. Another git process seems to be running in this repository, e.g. an editor opened by 'git commit'. Please make sure all processes are terminated then try again. If it still fails, a git process may have crashed in this repository earlier: remove the file manually to continue. fatal: unable to update HEAD branch 'net-next-2025-02-26--03-00' set up to track 'origin/net-next-2025-02-26--03-00'. Traceback (most recent call last): File "/opt/nipa/contest/remote/vmksft-p.py", line 376, in main() File "/opt/nipa/contest/remote/vmksft-p.py", line 371, in main f.run() File "/opt/nipa/contest/remote/lib/fetcher.py", line 157, in run self._run_once() File "/opt/nipa/contest/remote/lib/fetcher.py", line 143, in _run_once subprocess.run('git checkout ' + to_test["branch"], File "/usr/lib64/python3.11/subprocess.py", line 571, in run raise CalledProcessError(retcode, process.args, subprocess.CalledProcessError: Command 'git checkout net-next-2025-02-26--03-00' returned non-zero exit status 128. ``` Note that the initial sleep seems necessary. Signed-off-by: Jakub Kicinski --- contest/remote/lib/fetcher.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/contest/remote/lib/fetcher.py b/contest/remote/lib/fetcher.py index e569e46..b0d2294 100644 --- a/contest/remote/lib/fetcher.py +++ b/contest/remote/lib/fetcher.py @@ -139,7 +139,17 @@ def _run_once(self): # For now assume URL is in one of the remotes subprocess.run('git fetch --all --prune', cwd=self._tree_path, - shell=True) + shell=True, check=True) + + # After upgrading git 2.40.1 -> 2.47.1 CI hits a race in git, + # where tree is locked, even though previous command has finished. + # We need to sleep a bit and then wait for the lock to go away. + time.sleep(0.2) + lock_path = os.path.join(self._tree_path, '.git/HEAD.lock') + while os.path.exists(lock_path): + print("HEAD is still locked! Sleeping..") + time.sleep(0.2) + subprocess.run('git checkout ' + to_test["branch"], cwd=self._tree_path, shell=True, check=True) From 8261ab4816ce36b364f0029e2b65b7ee31e16742 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Tue, 25 Feb 2025 20:05:17 -0800 Subject: [PATCH 274/429] contest: fetcher: fix old branch cleanup We are accumulating branches on all runners, the cleanup must not be working. It's most likely because we forgot to set the cwd. Make sure we check if the commands failed. Signed-off-by: Jakub Kicinski --- contest/remote/lib/fetcher.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/contest/remote/lib/fetcher.py b/contest/remote/lib/fetcher.py index b0d2294..1f81eab 100644 --- a/contest/remote/lib/fetcher.py +++ b/contest/remote/lib/fetcher.py @@ -105,13 +105,18 @@ def _run_test(self, binfo): self._result_set(binfo['branch'], url) def _clean_old_branches(self, remote, current): - ret = subprocess.run('git branch', shell=True, capture_output=True) + ret = subprocess.run('git branch', + cwd=self._tree_path, shell=True, + capture_output=True, check=True) + existing = set([x.strip() for x in ret.stdout.decode('utf-8').split('\n')]) for b in remote: if b["branch"] in existing and b["branch"] != current: - subprocess.run('git branch -d ' + b["branch"], - cwd=self._tree_path, shell=True) + print("Clean up old branch", b["branch"]) + subprocess.run('git branch -D ' + b["branch"], + cwd=self._tree_path, shell=True, + check=True) def _run_once(self): r = requests.get(self._branches_url) From 92625efe5b8349059a9fdc94a3ebd59460c8900b Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 26 Feb 2025 13:34:53 -0800 Subject: [PATCH 275/429] contest: fetcher: always use url arg when adding result We call _result_set() twice, first to flag that we're working, second time to communicate that we finished. In steady state we don't find the entry on the first run, when url = None. Don't depend on this, just always use url caller passed in. Signed-off-by: Jakub Kicinski --- contest/remote/lib/fetcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contest/remote/lib/fetcher.py b/contest/remote/lib/fetcher.py index 1f81eab..4899f5e 100644 --- a/contest/remote/lib/fetcher.py +++ b/contest/remote/lib/fetcher.py @@ -69,7 +69,7 @@ def _result_set(self, branch_name, url): found = True break if not found: - old_db.append({'url': None, 'branch': branch_name, 'executor': self.name}) + old_db.append({'url': url, 'branch': branch_name, 'executor': self.name}) with open(self._results_manifest, "w") as fp: json.dump(old_db, fp) From 19e035e3fc088d856ed0ac55d00c7b5bf9053a17 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 26 Feb 2025 13:44:21 -0800 Subject: [PATCH 276/429] contest: fetcher: trim the result manifest at 500 entries NIPA CI has now run for a year, we have generated ~3150 branches. The size of the each is around 0.5MB. It's not a problem, yet, but keep the growth in check, trip the manifest at 500 entries. The result-fetcher does not care about disappearing entries, all-results.json will shrink accordingly but that's only for the best. Signed-off-by: Jakub Kicinski --- contest/remote/lib/fetcher.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contest/remote/lib/fetcher.py b/contest/remote/lib/fetcher.py index 4899f5e..d3be2ea 100644 --- a/contest/remote/lib/fetcher.py +++ b/contest/remote/lib/fetcher.py @@ -71,6 +71,9 @@ def _result_set(self, branch_name, url): if not found: old_db.append({'url': url, 'branch': branch_name, 'executor': self.name}) + # Maintain only the last 500 entries + old_db = old_db[-500:] + with open(self._results_manifest, "w") as fp: json.dump(old_db, fp) From 34a6821249513de0391f14a497cbc78e37fbf5a5 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 26 Feb 2025 15:58:31 -0800 Subject: [PATCH 277/429] contest: fetcher: compute test stability As the first step for HW test ingest we need to auto-judge if given test is stable and passing or not supported by HW. Create a DB of all test cases we have seen and compute pass/fail statistics. We will judge a test as stable / passing if it had 15 clean runs. "undoing" the passing status will be manual for now. Signed-off-by: Jakub Kicinski --- contest/results-fetcher.py | 93 ++++++++++++++++++++++++++++++++++++++ deploy/contest/db | 16 +++++++ 2 files changed, 109 insertions(+) diff --git a/contest/results-fetcher.py b/contest/results-fetcher.py index f54fadc..d78f82e 100755 --- a/contest/results-fetcher.py +++ b/contest/results-fetcher.py @@ -26,10 +26,36 @@ combined=name-of-manifest.json [db] db=db-name +stability-name=table-name results-name=table-name branches-name=table-name """ +def result_flatten(full): + """ + Take in a full result dict (for one run, with subtests). + Return a list of dicts: + [ + { "group": str, "test": str, "subtest": str/None, "result": bool }, + ] + """ + flat = [] + + for test in full["results"]: + l1 = { "group": test["group"], + "test": test["test"], + "subtest": None, + "result": test["result"].lower() == "pass" + } + flat.append(l1) + for case in test.get("results", []): + data = l1.copy() + data["subtest"] = case["test"] + data["result"] = case["result"].lower() == "pass" + flat.append(data) + + return flat + class FetcherState: def __init__(self): @@ -39,6 +65,7 @@ def __init__(self): # "fetched" is more of a "need state rebuild" self.fetched = True + self.tbl_stb = self.config.get("db", "stability-name", fallback="stability") self.tbl_res = self.config.get("db", "results-name", fallback="results") self.tbl_brn = self.config.get("db", "branches-name", fallback="branches") @@ -104,6 +131,70 @@ def psql_json_split(self, data): full = json.dumps(data) return json.dumps(normal), full + def psql_stability_selector(self, cur, data, row): + base = cur.mogrify("WHERE remote = %s AND executor = %s AND grp = %s AND test = %s", + (data["remote"], data["executor"], row["group"], row["test"],)).decode('utf-8') + + if row["subtest"] is None: + return base + " AND subtest is NULL" + return base + cur.mogrify(" AND subtest = %s", (row["subtest"],)).decode('utf-8') + + def psql_get_stability(self, data, row): + with self.psql_conn.cursor() as cur: + cur.execute(f"SELECT pass_cnt, fail_cnt, pass_srk, fail_srk, pass_cur, fail_cur, passing FROM {self.tbl_stb} " + + self.psql_stability_selector(cur, data, row)) + rows = cur.fetchall() + if rows and len(rows) > 0: + res = rows[0] + else: + res = [0] * 10 + res[6] = None # passing + return { + "pass_cnt": res[0], + "fail_cnt": res[1], + "pass_srk": res[2], + "fail_srk": res[3], + "pass_cur": res[4], + "fail_cur": res[5], + "passing": res[6], + "exists": bool(rows), + } + + def psql_insert_stability(self, data): + flat = result_flatten(data) + + for row in flat: + # Fetch current state + stability = self.psql_get_stability(data, row) + if not stability["exists"]: + with self.psql_conn.cursor() as cur: + cur.execute(f"INSERT INTO {self.tbl_stb} (remote, executor, grp, test, subtest) " + + cur.mogrify("VALUES (%s, %s, %s, %s, %s)", + (data["remote"], data["executor"], row["group"], row["test"], row["subtest"],)).decode('utf-8')) + # Update state + if row["result"]: + key_pfx = "pass" + stability["fail_cur"] = 0 + else: + key_pfx = "fail" + stability["pass_cur"] = 0 + + stability[key_pfx + "_cnt"] += 1 + stability[key_pfx + "_cur"] += 1 + stability[key_pfx + "_srk"] = max(stability[key_pfx + "_cur"], stability[key_pfx + "_srk"]) + + now = datetime.datetime.now().isoformat() + "+00:00" + if stability[key_pfx + "_srk"] > 15 and not stability["passing"]: # 5 clean days for HW + print("Test reached stability", row["remote"], row["test"], row["subtest"]) + stability["passing"] = now + + with self.psql_conn.cursor() as cur: + cur.execute(f"UPDATE {self.tbl_stb} SET " + + cur.mogrify("pass_cnt = %s, fail_cnt = %s, pass_srk = %s, fail_srk = %s, pass_cur = %s, fail_cur = %s, passing = %s, last_update = %s", + (stability["pass_cnt"], stability["fail_cnt"], stability["pass_srk"], stability["fail_srk"], + stability["pass_cur"], stability["fail_cur"], stability["passing"], now)).decode('utf-8') + + self.psql_stability_selector(cur, data, row)) + def insert_real(self, remote, run): data = run.copy() data["remote"] = remote["name"] @@ -119,6 +210,8 @@ def insert_real(self, remote, run): q = f"UPDATE {self.tbl_res} " + vals + ' ' + selector cur.execute(q) + self.psql_insert_stability(data) + def write_json_atomic(path, data): tmp = path + '.new' diff --git a/deploy/contest/db b/deploy/contest/db index e1c14d0..1e0b32a 100644 --- a/deploy/contest/db +++ b/deploy/contest/db @@ -65,3 +65,19 @@ CREATE TABLE db_monitor ( disk_pct REAL, disk_pct_metal REAL ); + +CREATE TABLE stability ( + remote varchar(80), + executor varchar(80), + grp varchar(80), + test varchar(128), + subtest varchar(256), + pass_cnt integer NOT NULL DEFAULT 0, + fail_cnt integer NOT NULL DEFAULT 0, + pass_srk integer NOT NULL DEFAULT 0, + fail_srk integer NOT NULL DEFAULT 0, + pass_cur integer NOT NULL DEFAULT 0, + fail_cur integer NOT NULL DEFAULT 0, + last_update timestamp, + passing timestamp +); From b722b1059f99dfe028aa571c33d9112b09794a25 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 27 Feb 2025 10:34:24 -0800 Subject: [PATCH 278/429] contest: fetcher: correct the dict for remote in logs We try to get remote from the test results, the remote is in remote metadata.. Signed-off-by: Jakub Kicinski --- contest/results-fetcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contest/results-fetcher.py b/contest/results-fetcher.py index d78f82e..ba7a2c5 100755 --- a/contest/results-fetcher.py +++ b/contest/results-fetcher.py @@ -185,7 +185,7 @@ def psql_insert_stability(self, data): now = datetime.datetime.now().isoformat() + "+00:00" if stability[key_pfx + "_srk"] > 15 and not stability["passing"]: # 5 clean days for HW - print("Test reached stability", row["remote"], row["test"], row["subtest"]) + print("Test reached stability", data["remote"], row["test"], row["subtest"]) stability["passing"] = now with self.psql_conn.cursor() as cur: From ef6050e638a701a0d0fd082a7792d080d759e323 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 27 Feb 2025 11:27:02 -0800 Subject: [PATCH 279/429] contest: fetcher: add autoignore based on device presence Device driver may simply not support certain features, so tests will be permanently failing. We want to differentiate the tests which are broken for those not supported by a device / platform for ease of tracking which tests can / should be fixed. Add a "autoignore" column (not a great name) which will be false for SW tests but we can set to true for certain runner + test combinations. We expect HW results to come with a new "device" key, use its presence to set the autoignore. Signed-off-by: Jakub Kicinski --- contest/results-fetcher.py | 8 +++++--- deploy/contest/db | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/contest/results-fetcher.py b/contest/results-fetcher.py index ba7a2c5..738f074 100755 --- a/contest/results-fetcher.py +++ b/contest/results-fetcher.py @@ -168,9 +168,11 @@ def psql_insert_stability(self, data): stability = self.psql_get_stability(data, row) if not stability["exists"]: with self.psql_conn.cursor() as cur: - cur.execute(f"INSERT INTO {self.tbl_stb} (remote, executor, grp, test, subtest) " + - cur.mogrify("VALUES (%s, %s, %s, %s, %s)", - (data["remote"], data["executor"], row["group"], row["test"], row["subtest"],)).decode('utf-8')) + cur.execute(f"INSERT INTO {self.tbl_stb} (remote, executor, grp, test, subtest, autoignore) " + + cur.mogrify("VALUES (%s, %s, %s, %s, %s, %s)", + (data["remote"], data["executor"], row["group"], + row["test"], row["subtest"], int("device" in data)) + ).decode('utf-8')) # Update state if row["result"]: key_pfx = "pass" diff --git a/deploy/contest/db b/deploy/contest/db index 1e0b32a..3d2f2b2 100644 --- a/deploy/contest/db +++ b/deploy/contest/db @@ -72,6 +72,7 @@ CREATE TABLE stability ( grp varchar(80), test varchar(128), subtest varchar(256), + autoignore boolean DEFAULT false, pass_cnt integer NOT NULL DEFAULT 0, fail_cnt integer NOT NULL DEFAULT 0, pass_srk integer NOT NULL DEFAULT 0, From 56fa242b5f37f6649acf781fec469f46b2f6135e Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 27 Feb 2025 12:14:30 -0800 Subject: [PATCH 280/429] contest: support reporting device info and add a script for virtio For HW testing we need the results to have a "device" object, identifying what device the test has been run against. Support reporting that from the VM test, for now add a script which can fake this data for virtio. Signed-off-by: Jakub Kicinski --- contest/remote/lib/fetcher.py | 2 ++ contest/remote/lib/vm.py | 1 + contest/remote/vmksft-p.py | 8 ++++++++ contest/scripts/vm-virtio-dev-info.sh | 6 ++++++ 4 files changed, 17 insertions(+) create mode 100755 contest/scripts/vm-virtio-dev-info.sh diff --git a/contest/remote/lib/fetcher.py b/contest/remote/lib/fetcher.py index d3be2ea..3b1ec09 100644 --- a/contest/remote/lib/fetcher.py +++ b/contest/remote/lib/fetcher.py @@ -103,6 +103,8 @@ def _run_test(self, binfo): } if 'link' in rinfo: entry['link'] = rinfo['link'] + if 'device' in rinfo: + entry['device'] = rinfo['device'] url = self._write_result(entry, run_id_cookie) self._result_set(binfo['branch'], url) diff --git a/contest/remote/lib/vm.py b/contest/remote/lib/vm.py index 7c6ba98..fae58bd 100644 --- a/contest/remote/lib/vm.py +++ b/contest/remote/lib/vm.py @@ -33,6 +33,7 @@ paths=/extra/exec/PATH:/another/bin ld_paths=/extra/lib/PATH:/another/lib exports=VAR1=val1,VAR2=val2 +setup=path_to_script.sh configs=relative/path/config,another/config init_prompt=expected_on-boot# virtme_opt=--opt,--another one diff --git a/contest/remote/vmksft-p.py b/contest/remote/vmksft-p.py index f434740..e34acf5 100755 --- a/contest/remote/vmksft-p.py +++ b/contest/remote/vmksft-p.py @@ -7,6 +7,7 @@ import os import re import queue +import subprocess import sys import tempfile import threading @@ -49,6 +50,8 @@ [ksft] target=net nested_tests=off / on +[device] +info_script=cmd_printing_json Expected: @@ -268,6 +271,11 @@ def test(binfo, rinfo, cbarg): targets = config.get('ksft', 'target').split() grp_name = "selftests-" + namify(targets[0]) + if config.get('device', 'info_script', fallback=None): + dev_info = subprocess.run(config.get('device', 'info_script'), + shell=True, stdout=subprocess.PIPE, check=True) + rinfo['device'] = dev_info.stdout.decode('utf-8').strip() + vm = VM(config) kconfs = [] diff --git a/contest/scripts/vm-virtio-dev-info.sh b/contest/scripts/vm-virtio-dev-info.sh new file mode 100755 index 0000000..dc38523 --- /dev/null +++ b/contest/scripts/vm-virtio-dev-info.sh @@ -0,0 +1,6 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0 + +qver=$(qemu-system-x86_64 --version | head -1) + +echo '{"driver":"virtio_net","versions":{"fixed":{},"stored":{},"running":{"fw":"'"$qver"'"}}}' From 820397093de832fa9e882ad4ae5f6677f95e1d4a Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 27 Feb 2025 13:01:25 -0800 Subject: [PATCH 281/429] contest: record branch data in the fetcher For fast fetch of latest branches we need to be able to order just by the date component. Now that we have both net-next and net-next-hw prefixes the hw ones always come before non-HW in naive sort. If only the date was first :( Signed-off-by: Jakub Kicinski --- contest/results-fetcher.py | 8 +++++--- deploy/contest/db | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/contest/results-fetcher.py b/contest/results-fetcher.py index 738f074..6d16e94 100755 --- a/contest/results-fetcher.py +++ b/contest/results-fetcher.py @@ -90,10 +90,12 @@ def psql_has_wip(self, remote, run): return rows and len(rows) > 0 def insert_result_psql(self, cur, data): + fields = "(branch, branch_date, remote, executor, t_start, t_end, json_normal, json_full)" normal, full = self.psql_json_split(data) - arg = cur.mogrify("(%s,%s,%s,%s,%s,%s,%s)", (data["branch"], data["remote"], data["executor"], - data["start"], data["end"], normal, full)) - cur.execute(f"INSERT INTO {self.tbl_res} VALUES " + arg.decode('utf-8')) + arg = cur.mogrify("(%s,%s,%s,%s,%s,%s,%s,%s)", + (data["branch"], data["branch"][-17:], data["remote"], data["executor"], + data["start"], data["end"], normal, full)) + cur.execute(f"INSERT INTO {self.tbl_res} {fields} VALUES " + arg.decode('utf-8')) def insert_wip(self, remote, run): if self.psql_has_wip(remote, run): diff --git a/deploy/contest/db b/deploy/contest/db index 3d2f2b2..0ea89a7 100644 --- a/deploy/contest/db +++ b/deploy/contest/db @@ -39,6 +39,7 @@ CREATE TABLE results ( branch varchar(80), remote varchar(80), executor varchar(80), + branch_date varchar(17), t_start timestamp, t_end timestamp, json_normal text, From 65fb2eb2b8dfa259d745e0edf0460268eb94e321 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 27 Feb 2025 13:18:47 -0800 Subject: [PATCH 282/429] contest: backend: use branch date in queries Now that we have branches with multiple prefixes we must order by branch date. Otherwise prefix dominates. Signed-off-by: Jakub Kicinski --- contest/backend/query.py | 12 +++++++----- deploy/contest/db | 2 ++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/contest/backend/query.py b/contest/backend/query.py index 274b704..abec250 100644 --- a/contest/backend/query.py +++ b/contest/backend/query.py @@ -29,21 +29,23 @@ def branches(): global psql with psql.cursor() as cur: - cur.execute(f"SELECT branch, t_date, base, url FROM branches ORDER BY branch DESC LIMIT 40") + cur.execute(f"SELECT branch, t_date, base, url FROM branches ORDER BY t_date DESC LIMIT 40") rows = [{"branch": r[0], "date": r[1].isoformat() + "+00:00", "base": r[2], "url": r[3]} for r in cur.fetchall()] rows.reverse() return rows +#SELECT branch,branch_date,count(*),remote FROM results GROUP BY branch,branch_date,remote ORDER BY branch_date DESC LIMIT 510; + def branches_to_rows(br_cnt, remote): global psql cnt = 0 with psql.cursor() as cur: if remote: - q = f"SELECT branch,count(*),remote FROM results GROUP BY branch, remote ORDER BY branch DESC LIMIT {br_cnt}" + q = f"SELECT branch,count(*),branch_date,remote FROM results GROUP BY branch,branch_date,remote ORDER BY branch_date DESC LIMIT {br_cnt}" else: - q = f"SELECT branch,count(*) FROM results GROUP BY branch ORDER BY branch DESC LIMIT {br_cnt}" + q = f"SELECT branch,count(*),branch_date FROM results GROUP BY branch,branch_date ORDER BY branch_date DESC LIMIT {br_cnt}" cur.execute(q) for r in cur.fetchall(): cnt += r[1] @@ -118,11 +120,11 @@ def results(): if not form or form == "normal": with psql.cursor() as cur: - cur.execute(f"SELECT json_normal FROM results {where} ORDER BY branch DESC LIMIT {limit}") + cur.execute(f"SELECT json_normal FROM results {where} ORDER BY branch_date DESC LIMIT {limit}") rows = "[" + ",".join([r[0] for r in cur.fetchall()]) + "]" elif form == "l2": with psql.cursor() as cur: - cur.execute(f"SELECT json_normal, json_full FROM results {where} ORDER BY branch DESC LIMIT {limit}") + cur.execute(f"SELECT json_normal, json_full FROM results {where} ORDER BY branch_date DESC LIMIT {limit}") rows = "[" for r in cur.fetchall(): if rows[-1] != '[': diff --git a/deploy/contest/db b/deploy/contest/db index 0ea89a7..eddae3f 100644 --- a/deploy/contest/db +++ b/deploy/contest/db @@ -47,6 +47,7 @@ CREATE TABLE results ( ); CREATE INDEX ON branches (branch DESC); +CREATE INDEX ON branches (t_date DESC); CREATE TABLE branches ( branch varchar(80), @@ -58,6 +59,7 @@ CREATE TABLE branches ( ); CREATE INDEX by_branch ON results (branch DESC); +CREATE INDEX by_branch_date ON results (branch_date DESC); CREATE TABLE db_monitor ( id serial primary key, From 09770f91875260869a1c1c7c5055077282774f01 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 27 Feb 2025 13:29:34 -0800 Subject: [PATCH 283/429] ui: status: bump max DPI for two column Chrome on my laptop thinks it's >200DPI now. Phones should be in the 400+ range, so 250 seems safe. Obviously this is a hack and not a great way of telling screen size :S Signed-off-by: Jakub Kicinski --- ui/nipa.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/nipa.css b/ui/nipa.css index 37b67a1..e013ee1 100644 --- a/ui/nipa.css +++ b/ui/nipa.css @@ -33,7 +33,7 @@ tr:nth-child(even) { .box-flake { background-color: red; } .box-fail { background-color: #d06060; } -@media screen and (max-resolution: 116dpi) { +@media screen and (max-resolution: 250dpi) { .row { display: flex; } From 6af8804af0800cc42e0428a440dfb1e92bb4904d Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 27 Feb 2025 13:37:05 -0800 Subject: [PATCH 284/429] ui: status: correct sorting for multi-branch Correct branch sort order, now that we have branches with different name prefixes. Signed-off-by: Jakub Kicinski --- ui/status.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/ui/status.js b/ui/status.js index 19be68d..ae2123d 100644 --- a/ui/status.js +++ b/ui/status.js @@ -715,6 +715,11 @@ function rem_exe(v) return v.remote + "/" + v.executor; } +function br_pfx_get(name) +{ + return name.substring(0, name.length - 18); +} + var awol_executors; function load_result_table(data_raw, reload) @@ -744,6 +749,9 @@ function load_result_table(data_raw, reload) v.start = new Date(v.start); v.end = new Date(v.end); + v.br_pfx = br_pfx_get(v.branch); + v.br_date = v.branch.substring(v.branch.length - 17); + branches.add(v.branch); if (v.remote == "brancher") { @@ -804,6 +812,8 @@ function load_result_table(data_raw, reload) "executor" : known_execs[re].executor, "remote" : known_execs[re].remote, "branch" : br, + "br_pfx" : br.substring(0, br.length - 18), + "br_date": br.substring(br.length - 17), "start" : branch_start[br], "end" : 0, }); @@ -813,8 +823,11 @@ function load_result_table(data_raw, reload) // Sort & display data_raw.sort(function(a, b){ - if (b.branch != a.branch) - return b.branch > a.branch ? 1 : -1; + if (b.branch !== a.branch) { + if (b.br_date !== a.br_date) + return b.br_date.localeCompare(a.br_date); + return b.br_pfx.localeCompare(a.br_pfx); + } // Keep brancher first if (a.executor == b.executor) @@ -837,7 +850,9 @@ function load_result_table(data_raw, reload) if (a.results == null) return -1; - return b.end - a.end; + if (b.end != a.end) + return b.end - a.end > 0 ? 1 : -1; + return 0; }); $("#contest tr").slice(1).remove(); From 30e2a9d4af3398be4c0d8ff5312fc9f2621479f5 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 27 Feb 2025 14:29:54 -0800 Subject: [PATCH 285/429] ui: status: fix AWoL reporting with multiple branch streams Signed-off-by: Jakub Kicinski --- ui/status.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ui/status.js b/ui/status.js index ae2123d..9045f6b 100644 --- a/ui/status.js +++ b/ui/status.js @@ -766,7 +766,7 @@ function load_result_table(data_raw, reload) if (!v.results) return 1; - const ent_name = v.remote + '/' + v.executor; + const ent_name = rem_exe(v); if (!(ent_name in avgs)) avgs[ent_name] = {"cnt": 0, "sum": 0, "min-dly": 0}; @@ -796,8 +796,10 @@ function load_result_table(data_raw, reload) known_execs[re] = { "executor": v.executor, "remote" : v.remote, + "br_pfx" : new Set(), "branches" : new Set() }; + known_execs[re].br_pfx.add(v.br_pfx); known_execs[re].branches.add(v.branch); } @@ -807,6 +809,9 @@ function load_result_table(data_raw, reload) for (re of known_exec_set) { if (branch_execs[br].has(re)) continue; + // Exec works on different branch stream + if (!known_execs[re].br_pfx.has(br_pfx_get(br))) + continue; data_raw.push({ "executor" : known_execs[re].executor, From 4b04edae6943d8141773fed04c9a3340fa702c21 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 27 Feb 2025 14:44:50 -0800 Subject: [PATCH 286/429] contest: faker: support multiple branch lists Support reading multiple branch streams. Signed-off-by: Jakub Kicinski --- contest/results-faker.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/contest/results-faker.py b/contest/results-faker.py index 043840c..1af291c 100755 --- a/contest/results-faker.py +++ b/contest/results-faker.py @@ -15,7 +15,7 @@ Config: [input] -branches=/path/to/branches.json +branches=/path/to/branches.json,/path/to/branches2.json [output] dir=/path/to/output url_pfx=relative/within/server @@ -25,8 +25,13 @@ def main() -> None: config = configparser.ConfigParser() config.read(['faker.config']) - with open(config.get("input", "branches"), "r") as fp: - branches = json.load(fp) + branches = [] + paths = config.get("input", "branches") + for path in paths.split(','): + with open(path, "r") as fp: + branches += json.load(fp) + + branches = sorted(branches, key=lambda x: x["date"]) url = config.get("output", "url_pfx") if url[-1] != '/': From 3e4a12677e1cf6aa5cae6df18dbab8005a060c8e Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 27 Feb 2025 14:53:31 -0800 Subject: [PATCH 287/429] contest: faker: support combining infos Faker is a convenient places to combine the branch infos. Signed-off-by: Jakub Kicinski --- contest/results-faker.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/contest/results-faker.py b/contest/results-faker.py index 1af291c..690da70 100755 --- a/contest/results-faker.py +++ b/contest/results-faker.py @@ -16,15 +16,33 @@ [input] branches=/path/to/branches.json,/path/to/branches2.json +infos=/path/to/infos.json,/path/to/infos2.json [output] dir=/path/to/output url_pfx=relative/within/server +info=/path/to/info.json """ +def combine_infos(config): + paths = config.get("input", "infos", fallback="").split(',') + if not paths: + return + + infos = {} + for path in paths: + with open(path, "r") as fp: + infos.update(json.load(fp)) + + with open(config.get("output", "info"), 'w') as fp: + json.dump(infos, fp) + + def main() -> None: config = configparser.ConfigParser() config.read(['faker.config']) + combine_infos(config) + branches = [] paths = config.get("input", "branches") for path in paths.split(','): From 6e1044c5db63b01a781b1dc0940f5136435624b3 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 28 Feb 2025 08:27:26 -0800 Subject: [PATCH 288/429] ui: move br_pfx_get() helper to the lib Signed-off-by: Jakub Kicinski --- ui/nipa.js | 5 +++++ ui/status.js | 9 ++------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ui/nipa.js b/ui/nipa.js index 6ec15e4..45835ce 100644 --- a/ui/nipa.js +++ b/ui/nipa.js @@ -27,6 +27,11 @@ function nipa_msec_to_str(msec) { return "TLE"; } +function nipa_br_pfx_get(name) +{ + return name.substring(0, name.length - 18); +} + function nipa_test_fullname(v, r) { return v.remote + "/" + v.executor + "/" + r.group + "/" + r.test; diff --git a/ui/status.js b/ui/status.js index 9045f6b..65a6829 100644 --- a/ui/status.js +++ b/ui/status.js @@ -715,11 +715,6 @@ function rem_exe(v) return v.remote + "/" + v.executor; } -function br_pfx_get(name) -{ - return name.substring(0, name.length - 18); -} - var awol_executors; function load_result_table(data_raw, reload) @@ -749,7 +744,7 @@ function load_result_table(data_raw, reload) v.start = new Date(v.start); v.end = new Date(v.end); - v.br_pfx = br_pfx_get(v.branch); + v.br_pfx = nipa_br_pfx_get(v.branch); v.br_date = v.branch.substring(v.branch.length - 17); branches.add(v.branch); @@ -810,7 +805,7 @@ function load_result_table(data_raw, reload) if (branch_execs[br].has(re)) continue; // Exec works on different branch stream - if (!known_execs[re].br_pfx.has(br_pfx_get(br))) + if (!known_execs[re].br_pfx.has(nipa_br_pfx_get(br))) continue; data_raw.push({ From a88484e2ca51bc05383d0ff953a48535a46bd2ff Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 28 Feb 2025 08:52:55 -0800 Subject: [PATCH 289/429] ui: factor out nipa_select_add_option() Signed-off-by: Jakub Kicinski --- ui/nipa.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/ui/nipa.js b/ui/nipa.js index 45835ce..bf32824 100644 --- a/ui/nipa.js +++ b/ui/nipa.js @@ -108,16 +108,21 @@ function nipa_filters_set_from_url() nipa_input_set_from_url("https://github.com/fl-pw"); } +function nipa_select_add_option(select_elem, show_str, value) +{ + const opt = document.createElement('option'); + opt.value = value; + opt.innerHTML = show_str; + select_elem.appendChild(opt); +} + function nipa_filter_add_options(data_raw, elem_id, field) { var elem = document.getElementById(elem_id); var values = new Set(); // Re-create "all" - const opt = document.createElement('option'); - opt.value = ""; - opt.innerHTML = "-- all --"; - elem.appendChild(opt); + nipa_select_add_option(elem, "-- all --", ""); // Create the dynamic entries $.each(data_raw, function(i, v) { @@ -127,10 +132,7 @@ function nipa_filter_add_options(data_raw, elem_id, field) values.add(v); }); for (const value of values) { - const opt = document.createElement('option'); - opt.value = value; - opt.innerHTML = value; - elem.appendChild(opt); + nipa_select_add_option(elem, value, value); } } From f0aa09608ccfb06c6899e0dd0699767fce61b032 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 28 Feb 2025 09:03:21 -0800 Subject: [PATCH 290/429] ui: flakes: support filtering by branch prefix Signed-off-by: Jakub Kicinski --- contest/backend/query.py | 21 +++++++++++++-------- ui/contest.js | 2 +- ui/flakes.html | 3 +++ ui/flakes.js | 18 ++++++++++++++++++ 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/contest/backend/query.py b/contest/backend/query.py index abec250..32340a0 100644 --- a/contest/backend/query.py +++ b/contest/backend/query.py @@ -35,17 +35,17 @@ def branches(): return rows -#SELECT branch,branch_date,count(*),remote FROM results GROUP BY branch,branch_date,remote ORDER BY branch_date DESC LIMIT 510; - -def branches_to_rows(br_cnt, remote): +def branches_to_rows(br_cnt, remote, br_pfx=None): global psql cnt = 0 with psql.cursor() as cur: - if remote: - q = f"SELECT branch,count(*),branch_date,remote FROM results GROUP BY branch,branch_date,remote ORDER BY branch_date DESC LIMIT {br_cnt}" - else: - q = f"SELECT branch,count(*),branch_date FROM results GROUP BY branch,branch_date ORDER BY branch_date DESC LIMIT {br_cnt}" + remote_k = ",remote" if remote else "" + # Slap the -2 in here as the first letter of the date, to avoid prefix of prefix matches + pfx_flt = f"WHERE branch LIKE '{br_pfx}-2%' " if br_pfx else "" + + q = f"SELECT branch,count(*),branch_date{remote_k} FROM results {pfx_flt} GROUP BY branch,branch_date{remote_k} ORDER BY branch_date DESC LIMIT {br_cnt}" + cur.execute(q) for r in cur.fetchall(): cnt += r[1] @@ -108,7 +108,12 @@ def results(): if not br_cnt: br_cnt = 10 - limit = branches_to_rows(br_cnt, remote) + br_pfx = request.args.get('br-pfx') + if br_pfx: + # Slap the -2 in here as the first letter of the date, to avoid prefix of prefix matches + where.append(f"branch LIKE '{br_pfx}-2%'") + + limit = branches_to_rows(br_cnt, remote, br_pfx) t2 = datetime.datetime.now() diff --git a/ui/contest.js b/ui/contest.js index d957230..3ae5d67 100644 --- a/ui/contest.js +++ b/ui/contest.js @@ -168,7 +168,7 @@ function reload_select_filters(first_load) old_values[elem_id] = elem.value; } - // Keep the "all" option, remove the rest + // Wipe the options and re-add $("select option").remove(); // We have all JSONs now, do processing. diff --git a/ui/flakes.html b/ui/flakes.html index aa81608..9ec16b1 100644 --- a/ui/flakes.html +++ b/ui/flakes.html @@ -29,6 +29,9 @@
 
+ + +
 
 
diff --git a/ui/flakes.js b/ui/flakes.js index 2938c60..b862153 100644 --- a/ui/flakes.js +++ b/ui/flakes.js @@ -12,6 +12,8 @@ function get_sort_key() return "cnt"; } +var branch_pfx_set = new Set(); + function load_result_table(data_raw) { // Get all branch names @@ -22,6 +24,19 @@ function load_result_table(data_raw) let br_cnt = document.getElementById("br-cnt").value; const branches = Array.from(branch_set).slice(0, br_cnt); + // Populate the load filters with prefixes + let select_br_pfx = document.getElementById("br-pfx"); + for (const br of branches) { + const br_pfx = nipa_br_pfx_get(br); + + if (select_br_pfx.length == 0) + nipa_select_add_option(select_br_pfx, "-- all --", ""); + if (branch_pfx_set.has(br_pfx)) + continue; + nipa_select_add_option(select_br_pfx, br_pfx, br_pfx); + branch_pfx_set.add(br_pfx); + } + // Build the result map var pw_n = document.getElementById("pw-n").checked; var pw_y = document.getElementById("pw-y").checked; @@ -175,6 +190,7 @@ function reload_data() { const format_l2 = document.getElementById("ld-cases"); const br_cnt = document.getElementById("br-cnt"); + const br_pfx = document.getElementById("br-pfx"); const remote = document.getElementById("ld-remote"); let req_url = "query/results"; @@ -184,6 +200,8 @@ function reload_data() req_url += "&format=l2"; if (remote.value) req_url += "&remote=" + remote.value; + if (br_pfx.value) + req_url += "&br-pfx=" + br_pfx.value; nipa_filters_disable(["ld-pw", "fl-pw"]); $(document).ready(function() { From 32fe9e23b1ffd626a2bf10f6355899a88df2d8a9 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 28 Feb 2025 18:32:54 -0800 Subject: [PATCH 291/429] contest: fetcher: fix format for boolean Signed-off-by: Jakub Kicinski --- contest/results-fetcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contest/results-fetcher.py b/contest/results-fetcher.py index 6d16e94..acd14df 100755 --- a/contest/results-fetcher.py +++ b/contest/results-fetcher.py @@ -173,7 +173,7 @@ def psql_insert_stability(self, data): cur.execute(f"INSERT INTO {self.tbl_stb} (remote, executor, grp, test, subtest, autoignore) " + cur.mogrify("VALUES (%s, %s, %s, %s, %s, %s)", (data["remote"], data["executor"], row["group"], - row["test"], row["subtest"], int("device" in data)) + row["test"], row["subtest"], "device" in data) ).decode('utf-8')) # Update state if row["result"]: From d4a180ef2374dd1c3941869dcc9199dbad367869 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sun, 2 Mar 2025 14:02:21 -0800 Subject: [PATCH 292/429] ui: status: correct the missing status with multi-pfx Filter missing results by branch prefix. Signed-off-by: Jakub Kicinski --- ui/status.js | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/ui/status.js b/ui/status.js index 65a6829..259059f 100644 --- a/ui/status.js +++ b/ui/status.js @@ -449,7 +449,8 @@ function load_partial_tests(data) let table = document.getElementById("test-presence"); let pending_executors = {}; let count_map = {}; - let total = 0; + let br_map = {}; + let total = {}; $.each(data, function(i, v) { // Ignore tests from AWOL executors, that should be rare @@ -457,7 +458,10 @@ function load_partial_tests(data) return 1; if (v.executor == "brancher") { - total++; + if (v.br_pfx in total) + total[v.br_pfx]++; + else + total[v.br_pfx] = 1; return 1; } @@ -474,15 +478,24 @@ function load_partial_tests(data) $.each(v.results, function(i, r) { let name = nipa_test_fullname(v, r); - if (name in count_map) + if (name in count_map) { count_map[name]++; - else + } else { count_map[name] = 1; + br_map[name] = new Set(); + } + + br_map[name].add(v.br_pfx); }); }); for (const name of Object.keys(count_map)) { - let missing = total - count_map[name]; + let expect = 0; + + for (const br_pfx of br_map[name]) + expect += total[br_pfx]; + + let missing = expect - count_map[name]; if (!missing) continue; From 7906310440c7ab13e768d9845d173ecbb5bbe6c4 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sun, 2 Mar 2025 14:12:23 -0800 Subject: [PATCH 293/429] ui: status: make summary counts less confusing We show "total" test count in green in summary, but in specific result lines we show "pass" as green. This is confusing. Make green result be pass in all cases. Signed-off-by: Jakub Kicinski --- ui/status.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/status.js b/ui/status.js index 259059f..6c510c7 100644 --- a/ui/status.js +++ b/ui/status.js @@ -541,7 +541,7 @@ function add_summaries(table, summary, reported) colorify_str_psf(str_psf, "fail", summary["fail"], "red"); colorify_str_psf(str_psf, "skip", summary["skip"], "#809fff"); - colorify_str_psf(str_psf, "pass", summary["total"], "green"); + colorify_str_psf(str_psf, "pass", summary["pass"], "green"); var link_to_contest = " Date: Sun, 2 Mar 2025 14:15:19 -0800 Subject: [PATCH 294/429] contest: fetcher: correct conditions for passing We want a streak of 15 passes, not just any streak of 15 same results. Signed-off-by: Jakub Kicinski --- contest/results-fetcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contest/results-fetcher.py b/contest/results-fetcher.py index acd14df..83a31c5 100755 --- a/contest/results-fetcher.py +++ b/contest/results-fetcher.py @@ -188,7 +188,7 @@ def psql_insert_stability(self, data): stability[key_pfx + "_srk"] = max(stability[key_pfx + "_cur"], stability[key_pfx + "_srk"]) now = datetime.datetime.now().isoformat() + "+00:00" - if stability[key_pfx + "_srk"] > 15 and not stability["passing"]: # 5 clean days for HW + if stability["pass_srk"] > 15 and not stability["passing"]: # 5 clean days for HW print("Test reached stability", data["remote"], row["test"], row["subtest"]) stability["passing"] = now From 5be2d421000b8567b4624108ba439579257b0998 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Tue, 4 Mar 2025 08:40:01 -0800 Subject: [PATCH 295/429] contest: fetcher: wait a full second for git to stabilize We still see lock failures, although less frequent. Increase the initial wait to 1s. Signed-off-by: Jakub Kicinski --- contest/remote/lib/fetcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contest/remote/lib/fetcher.py b/contest/remote/lib/fetcher.py index 3b1ec09..29c8786 100644 --- a/contest/remote/lib/fetcher.py +++ b/contest/remote/lib/fetcher.py @@ -154,7 +154,7 @@ def _run_once(self): # After upgrading git 2.40.1 -> 2.47.1 CI hits a race in git, # where tree is locked, even though previous command has finished. # We need to sleep a bit and then wait for the lock to go away. - time.sleep(0.2) + time.sleep(1) lock_path = os.path.join(self._tree_path, '.git/HEAD.lock') while os.path.exists(lock_path): print("HEAD is still locked! Sleeping..") From e68b25e06092adf399583aecc9b3a438bc58b132 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Tue, 4 Mar 2025 11:12:20 -0800 Subject: [PATCH 296/429] contest: collector: rename psql_get_stability() Signed-off-by: Jakub Kicinski --- contest/results-fetcher.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contest/results-fetcher.py b/contest/results-fetcher.py index 83a31c5..715b000 100755 --- a/contest/results-fetcher.py +++ b/contest/results-fetcher.py @@ -141,7 +141,7 @@ def psql_stability_selector(self, cur, data, row): return base + " AND subtest is NULL" return base + cur.mogrify(" AND subtest = %s", (row["subtest"],)).decode('utf-8') - def psql_get_stability(self, data, row): + def psql_get_test_stability(self, data, row): with self.psql_conn.cursor() as cur: cur.execute(f"SELECT pass_cnt, fail_cnt, pass_srk, fail_srk, pass_cur, fail_cur, passing FROM {self.tbl_stb} " + self.psql_stability_selector(cur, data, row)) @@ -167,7 +167,7 @@ def psql_insert_stability(self, data): for row in flat: # Fetch current state - stability = self.psql_get_stability(data, row) + stability = self.psql_get_test_stability(data, row) if not stability["exists"]: with self.psql_conn.cursor() as cur: cur.execute(f"INSERT INTO {self.tbl_stb} (remote, executor, grp, test, subtest, autoignore) " + From eec7149a2a96f2d0395d699e8fe442691b9a902a Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Tue, 4 Mar 2025 11:12:47 -0800 Subject: [PATCH 297/429] contest: rename results-fetcher.py -> results-collector.py We also have a fetcher.py for fetching the branch and running tests. Rename results fetcher to collector. Signed-off-by: Jakub Kicinski --- contest/{results-fetcher.py => results-collector.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename contest/{results-fetcher.py => results-collector.py} (100%) diff --git a/contest/results-fetcher.py b/contest/results-collector.py similarity index 100% rename from contest/results-fetcher.py rename to contest/results-collector.py From a027448b1e0f8f95d2701e7c67debf6ef2a5b880 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Tue, 4 Mar 2025 11:22:43 -0800 Subject: [PATCH 298/429] contest: collector: insert stability first We want to make sure that new (HW) tests are reported as unstable until we know they are stable. Insert stability before we break down the data for main result insert. It was initially late just to make sure if we crash we already inserted the results. We have been populating the stability data for about a week now, the code should be solid enough. Signed-off-by: Jakub Kicinski --- contest/results-collector.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contest/results-collector.py b/contest/results-collector.py index 715b000..73699a7 100755 --- a/contest/results-collector.py +++ b/contest/results-collector.py @@ -203,6 +203,8 @@ def insert_real(self, remote, run): data = run.copy() data["remote"] = remote["name"] + self.psql_insert_stability(data) + with self.psql_conn.cursor() as cur: if not self.psql_has_wip(remote, run): self.insert_result_psql(cur, data) @@ -214,8 +216,6 @@ def insert_real(self, remote, run): q = f"UPDATE {self.tbl_res} " + vals + ' ' + selector cur.execute(q) - self.psql_insert_stability(data) - def write_json_atomic(path, data): tmp = path + '.new' From f9c7e87f448bf9e65b9f2028e4c7d30e6f335982 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Tue, 4 Mar 2025 11:55:25 -0800 Subject: [PATCH 299/429] contest: collector: filter down combined results Filter combined results by stability. Only for the "combined" JSON file for now. Signed-off-by: Jakub Kicinski --- contest/results-collector.py | 64 ++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/contest/results-collector.py b/contest/results-collector.py index 73699a7..7722422 100755 --- a/contest/results-collector.py +++ b/contest/results-collector.py @@ -4,6 +4,7 @@ import configparser import copy import datetime +import functools import json import os import psycopg2 @@ -141,6 +142,24 @@ def psql_stability_selector(self, cur, data, row): return base + " AND subtest is NULL" return base + cur.mogrify(" AND subtest = %s", (row["subtest"],)).decode('utf-8') + def psql_get_unstable(self, data): + with self.psql_conn.cursor() as cur: + rem_exe = cur.mogrify("remote = %s AND executor = %s", + (data["remote"], data["executor"],)).decode('utf-8') + cur.execute(f"SELECT grp, test, subtest FROM {self.tbl_stb} " + + "WHERE autoignore = True AND passing IS NULL AND " + rem_exe) + rows = cur.fetchall() + res = {} + for row in rows: + res[(row[0], row[1], row[2])] = { + "group": row[0], + "test": row[1], + "subtest": row[2] + } + if res: + print(f"Unstable for {data['remote']}/{data['executor']} got", len(res)) + return res + def psql_get_test_stability(self, data, row): with self.psql_conn.cursor() as cur: cur.execute(f"SELECT pass_cnt, fail_cnt, pass_srk, fail_srk, pass_cur, fail_cur, passing FROM {self.tbl_stb} " + @@ -262,6 +281,46 @@ def fetch_remote(fetcher, remote, seen): json.dump(manifest, fp) +def apply_stability(fetcher, data, unstable): + u_key = (data['remote'], data['executor']) + if u_key not in unstable: + unstable[u_key] = fetcher.psql_get_unstable(data) + + # Non-HW runners have full stability, usually + if not unstable[u_key]: + return + + def filter_l1(test): + # Defer filtering to L2 + if test.get("results"): + return True + return (test['group'], test['test'], None) not in unstable[u_key] + + def trim_l2(test): + # Skip over pure L1s + if "results" not in test: + return test + + def filter_l1_l2(case): + return (test['group'], test['test'], case['test']) not in unstable[u_key] + + test["results"] = list(filter(filter_l1_l2, test["results"])) + if not test["results"]: + return None + + # See if we removed all failing subtests + all_pass = True + all_pass &= not test.get("crashes") + if test["result"].lower() != "pass": + all_pass = functools.reduce(lambda x, y: x and y["result"].lower() == "pass", test["results"], all_pass) + if all_pass: + test["result"] = "pass" + return test + + data["results"] = list(filter(filter_l1, data["results"])) + data["results"] = list(map(trim_l2, data["results"])) + data["results"] = list(filter(lambda x: x is not None, data["results"])) + def build_combined(fetcher, remote_db): r = requests.get(fetcher.config.get('input', 'branch_url')) @@ -303,6 +362,11 @@ def build_combined(fetcher, remote_db): data['remote'] = name combined.append(data) + + unstable = {} + for run in combined: + apply_stability(fetcher, run, unstable) + return combined From 3819987f15f2f8b0fbce9cd36be995b1a84022b8 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Tue, 4 Mar 2025 12:43:00 -0800 Subject: [PATCH 300/429] contest: collector: filter down by stability when inserting into DB Use the "normal" tests as tests filtered down by stability. They also have l2 removed. Signed-off-by: Jakub Kicinski --- contest/results-collector.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/contest/results-collector.py b/contest/results-collector.py index 7722422..8e3144c 100755 --- a/contest/results-collector.py +++ b/contest/results-collector.py @@ -119,20 +119,22 @@ def insert_wip(self, remote, run): def psql_json_split(self, data): # return "normal" and "full" as json string or None # "full" will be None if they are the same to save storage - if data.get("results") is None: - return json.dumps(data), None + data = copy.deepcopy(data) + full_s = json.dumps(data) - normal = copy.deepcopy(data) - full = None + # Filter down the results + apply_stability(self, data, {}) - for row in normal["results"]: - if "results" in row: - full = True - del row["results"] + if data.get("results"): + for row in data["results"]: + if "results" in row: + del row["results"] - if full: - full = json.dumps(data) - return json.dumps(normal), full + norm_s = json.dumps(data) + + if norm_s != full_s: + return norm_s, full_s + return full_s, None def psql_stability_selector(self, cur, data, row): base = cur.mogrify("WHERE remote = %s AND executor = %s AND grp = %s AND test = %s", From fc47541f5a955a1fdb37d9943e08a71160d8a2ac Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Tue, 4 Mar 2025 13:40:14 -0800 Subject: [PATCH 301/429] contest: virtio: increase setup wait Looks like we're not giving ourselves enough time for DAD to reliably finish. Signed-off-by: Jakub Kicinski --- contest/scripts/vm-virtio-loop.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contest/scripts/vm-virtio-loop.sh b/contest/scripts/vm-virtio-loop.sh index 8a28c41..5cec837 100755 --- a/contest/scripts/vm-virtio-loop.sh +++ b/contest/scripts/vm-virtio-loop.sh @@ -29,4 +29,4 @@ ip -netns ns-remote addr add dev eth1 2001:db8::2/64 export LOCAL_V6=2001:db8::1 export REMOTE_V6=2001:db8::2 -sleep 1 +sleep 2 From 4ec58f60fcbe81eb418c230102bdde11133c7b4f Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Tue, 4 Mar 2025 18:06:10 -0800 Subject: [PATCH 302/429] contest: collector: skip stability on WIP results Signed-off-by: Jakub Kicinski --- contest/results-collector.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/contest/results-collector.py b/contest/results-collector.py index 8e3144c..0be43ba 100755 --- a/contest/results-collector.py +++ b/contest/results-collector.py @@ -119,16 +119,17 @@ def insert_wip(self, remote, run): def psql_json_split(self, data): # return "normal" and "full" as json string or None # "full" will be None if they are the same to save storage - data = copy.deepcopy(data) full_s = json.dumps(data) + if data.get("results") is None: # WIP result + return full_s, None + data = copy.deepcopy(data) # Filter down the results apply_stability(self, data, {}) - if data.get("results"): - for row in data["results"]: - if "results" in row: - del row["results"] + for row in data.get("results", []): + if "results" in row: + del row["results"] norm_s = json.dumps(data) @@ -284,6 +285,9 @@ def fetch_remote(fetcher, remote, seen): def apply_stability(fetcher, data, unstable): + if data.get("results") is None: # WIP result + return + u_key = (data['remote'], data['executor']) if u_key not in unstable: unstable[u_key] = fetcher.psql_get_unstable(data) From de60422d30edaf4ba72702abf2b2ff54e5de3134 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Tue, 4 Mar 2025 18:10:07 -0800 Subject: [PATCH 303/429] contest: virtio: improve v6 address config nodad to reliably avoid waits, and don't flush on down. Otherwise destructive tests flush the addresses for everyone. Signed-off-by: Jakub Kicinski --- contest/scripts/vm-virtio-loop.sh | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/contest/scripts/vm-virtio-loop.sh b/contest/scripts/vm-virtio-loop.sh index 5cec837..d15f8b0 100755 --- a/contest/scripts/vm-virtio-loop.sh +++ b/contest/scripts/vm-virtio-loop.sh @@ -24,9 +24,12 @@ ip -netns ns-remote addr add dev eth1 192.0.2.2/24 export LOCAL_V4=192.0.2.1 export REMOTE_V4=192.0.2.2 -ip addr add dev eth0 2001:db8::1/64 -ip -netns ns-remote addr add dev eth1 2001:db8::2/64 +ip addr add dev eth0 2001:db8::1/64 nodad +ip -netns ns-remote addr add dev eth1 2001:db8::2/64 nodad export LOCAL_V6=2001:db8::1 export REMOTE_V6=2001:db8::2 -sleep 2 +sysctl -w net.ipv6.conf.eth0.keep_addr_on_down=1 +# We don't bring remote down, it'd break remote via SSH + +sleep 1 From 6d1766cc5db76f13ba8ce9f9043546240058e4ac Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 6 Mar 2025 11:14:50 -0800 Subject: [PATCH 304/429] mailbot: retry fetching the lore archive Various AI data scraping bots are overloading kernel.org Add retries for fetching emails. Signed-off-by: Jakub Kicinski --- mailbot.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/mailbot.py b/mailbot.py index 33f8058..5782c37 100755 --- a/mailbot.py +++ b/mailbot.py @@ -675,8 +675,18 @@ def do_mail_delayed(msg, pw, dr): print("ERROR: message delayed for the second time", str(e)) +def fetch_tree(tree): + for _ in range(3): + try: + tree.git_fetch(tree.remote) + return + except: + print('WARNING: git fetch failed, retrying') + time.sleep(300) + + def check_new(tree, pw, dr): - tree.git_fetch(tree.remote) + fetch_tree(tree) hashes = tree.git(['log', "--format=%h", f'..{tree.remote}/{tree.branch}', '--reverse']) hashes = hashes.split() for h in hashes: From 7be90b2d60a73f2b720381ca72165cb7813ddbc4 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 8 Mar 2025 12:54:09 -0800 Subject: [PATCH 305/429] contest: faker: make sure run_ids are unique Looks like contest doesn't see a "brancher result" for branches which are cut on multiple branch streams (net-next and net-next-hw). It's probably due to the fact that we use branch date as the run id. Signed-off-by: Jakub Kicinski --- contest/results-faker.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/contest/results-faker.py b/contest/results-faker.py index 690da70..c98167e 100755 --- a/contest/results-faker.py +++ b/contest/results-faker.py @@ -56,10 +56,14 @@ def main() -> None: url += '/' directory = config.get("output", "dir") + used_cookies = set() results = [] for br in branches: br_dt = datetime.datetime.fromisoformat(br["date"]) - run_id_cookie = str(int(br_dt.timestamp() / 60) % 1000000) + run_id_cookie = int(br_dt.timestamp() / 60) % 1000000 + while run_id_cookie in used_cookies: + run_id_cookie += 1 + used_cookies.add(run_id_cookie) fname = f"results-{run_id_cookie}.json" data = {'url': url + fname, From 583cfa8019107660f2f8bbfa94020cdaca85110d Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 10 Apr 2025 18:11:25 -0700 Subject: [PATCH 306/429] patchwork: allow setting user-agent from config Konstantin suggests using "kdevops-ci/{version} (contact@address.here)" for "good bots". Signed-off-by: Jakub Kicinski --- mailbot.py | 14 +++++++++++--- pw/patchwork.py | 4 ++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/mailbot.py b/mailbot.py index 5782c37..9535347 100755 --- a/mailbot.py +++ b/mailbot.py @@ -32,6 +32,7 @@ auto_changes_requested = set() auto_awaiting_upstream = set() delay_actions = [] # contains tuples of (datetime, email) +http_headers = None pw_act_active = { @@ -189,7 +190,8 @@ def load_section(self, location, name): self.loc_map[name] = location - r = requests.get(f'https://www.kernel.org/doc/html/next/%7Blocation%7D.html') + r = requests.get(f'https://www.kernel.org/doc/html/next/%7Blocation%7D.html', + headers=http_headers) data = r.content.decode('utf-8') offs = 0 @@ -208,7 +210,7 @@ def load_section(self, location, name): # Now populate the plain text contents url = f'https://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git/plain/Documentation/%7Blocation%7D.rst' - r = requests.get(url) + r = requests.get(url, headers=http_headers) data = r.content.decode('utf-8') lines = data.split('\n') @@ -379,7 +381,8 @@ def _resolve_thread(self, pw): self._series_id = pw_obj[0]['series'][0]['id'] - r = requests.get(f'https://lore.kernel.org/all/%7Bmid%7D/raw') + r = requests.get(f'https://lore.kernel.org/all/%7Bmid%7D/raw', + headers=http_headers) data = r.content.decode('utf-8') msg = email.message_from_string(data, policy=default) self._series_author = msg.get('From') @@ -709,6 +712,11 @@ def main(): signal.signal(signal.SIGTERM, handler) signal.signal(signal.SIGINT, handler) + global http_headers + ua = config.get('patchwork', 'user-agent', fallback='') + if ua: + http_headers = {"user-agent":ua} + global authorized_users users = config.get('mailbot', 'authorized') authorized_users.update(set(users.split(','))) diff --git a/pw/patchwork.py b/pw/patchwork.py index 1c30f3a..4f2a0a1 100644 --- a/pw/patchwork.py +++ b/pw/patchwork.py @@ -42,6 +42,10 @@ def __init__(self, config): self._token = config.get('patchwork', 'token', fallback='') self._user = config.get('patchwork', 'user', fallback='') + ua = config.get('patchwork', 'user-agent', fallback='') + if ua: + self._session.headers.update({"user-agent":ua}) + config_project = config.get('patchwork', 'project') pw_project = self.get_project(config_project) if pw_project: From 77708a8ef9b4d532c35ccbacd1746ae94746cea8 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 16 Apr 2025 10:40:05 -0700 Subject: [PATCH 307/429] deploy: contest: add io-uring io-uring is now used for ZC tests. Signed-off-by: Jakub Kicinski --- deploy/contest/remote/worker-setup.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/deploy/contest/remote/worker-setup.sh b/deploy/contest/remote/worker-setup.sh index c7938c4..437066e 100644 --- a/deploy/contest/remote/worker-setup.sh +++ b/deploy/contest/remote/worker-setup.sh @@ -244,3 +244,10 @@ cp packetdrill ~/tools/fs/usr/bin/ # Net tests need pyroute2 (for OvS tests) sudo dnf install python3-pyroute2.noarch + +# uring (needs ZC) + git clone https://github.com/axboe/liburing/ + cd liburing + ./configure --prefix=/usr + make -j + sudo make install From 9f49004a7572b654ff3110d6da3a2cfb5d53ee90 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 16 Apr 2025 10:40:47 -0700 Subject: [PATCH 308/429] form-letter: update for 6.15 Signed-off-by: Jakub Kicinski --- form-letters/net-next-closed | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/form-letters/net-next-closed b/form-letters/net-next-closed index f46841e..03cef54 100644 --- a/form-letters/net-next-closed +++ b/form-letters/net-next-closed @@ -1,8 +1,8 @@ -The merge window for v6.10 has begun and we have already posted our pull -request. Therefore net-next is closed for new drivers, features, code -refactoring and optimizations. We are currently accepting bug fixes only. +Linus already pulled net-next material v6.15 and therefore net-next is closed +for new drivers, features, code refactoring and optimizations. We are currently +accepting bug fixes only. -Please repost when net-next reopens after May 26th. +Please repost when net-next reopens after Apr 7th. RFC patches sent for review only are obviously welcome at any time. From 1b0aacaa0ccf0347054bec9edcffe7779a12bda3 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 16 Apr 2025 16:27:32 -0700 Subject: [PATCH 309/429] contest: vm: let user add qemu options Setting QEMU options via virtme_opts is hard, because QEMU opts may contain , which virtme_opts treat as separator. Signed-off-by: Jakub Kicinski --- contest/remote/lib/vm.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contest/remote/lib/vm.py b/contest/remote/lib/vm.py index fae58bd..d43d494 100644 --- a/contest/remote/lib/vm.py +++ b/contest/remote/lib/vm.py @@ -37,6 +37,7 @@ configs=relative/path/config,another/config init_prompt=expected_on-boot# virtme_opt=--opt,--another one +qemu_opt=--opt,this is --same one default_timeout=15 boot_timeout=45 slowdown=2.5 # mark the machine as slow and multiply the ksft timeout by 2.5 @@ -181,6 +182,9 @@ def start(self, cwd=None): opts = self.config.get('vm', 'virtme_opt', fallback="") cmd += opts.split(',') if opts else [] + opts = self.config.get('vm', 'qemu_opt', fallback="") + cmd += ["-o", " " + opts] if opts else [] + cpus = self.config.get('vm', 'cpus', fallback="") if cpus: cmd += ["--cpus", cpus] From faee581a0fe5a07ecc12d7b9aeac7828bb1dac70 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 16 Apr 2025 16:29:39 -0700 Subject: [PATCH 310/429] contest: switch to enp0sx names for virtio Use PCI virtio devices, they are more capable. Signed-off-by: Jakub Kicinski --- contest/scripts/vm-virtio-loop.sh | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/contest/scripts/vm-virtio-loop.sh b/contest/scripts/vm-virtio-loop.sh index d15f8b0..63d8a82 100755 --- a/contest/scripts/vm-virtio-loop.sh +++ b/contest/scripts/vm-virtio-loop.sh @@ -3,7 +3,10 @@ # Expect we were booted into a virtme-ng VM with "--net loop" -for ifc in eth0 eth1; do +IFC0=enp0s1 +IFC1=enp0s2 + +for ifc in $IFC0 $IFC1; do if ! ethtool -i "$ifc" | grep -q virtio; then echo "Error: $ifc is not virtio" exit 1 @@ -11,25 +14,25 @@ for ifc in eth0 eth1; do done ip netns add ns-remote -ip link set dev eth1 netns ns-remote +ip link set dev $IFC1 netns ns-remote export REMOTE_TYPE=netns export REMOTE_ARGS=ns-remote -ip link set dev eth0 up -ip -netns ns-remote link set dev eth1 up -export NETIF=eth0 +ip link set dev $IFC0 up +ip -netns ns-remote link set dev $IFC1 up +export NETIF=$IFC0 -ip addr add dev eth0 192.0.2.1/24 -ip -netns ns-remote addr add dev eth1 192.0.2.2/24 +ip addr add dev $IFC0 192.0.2.1/24 +ip -netns ns-remote addr add dev $IFC1 192.0.2.2/24 export LOCAL_V4=192.0.2.1 export REMOTE_V4=192.0.2.2 -ip addr add dev eth0 2001:db8::1/64 nodad -ip -netns ns-remote addr add dev eth1 2001:db8::2/64 nodad +ip addr add dev $IFC0 2001:db8::1/64 nodad +ip -netns ns-remote addr add dev $IFC1 2001:db8::2/64 nodad export LOCAL_V6=2001:db8::1 export REMOTE_V6=2001:db8::2 -sysctl -w net.ipv6.conf.eth0.keep_addr_on_down=1 +sysctl -w net.ipv6.conf.$IFC0.keep_addr_on_down=1 # We don't bring remote down, it'd break remote via SSH sleep 1 From c9f64419958867064721b5f7f66b889726f168d0 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Tue, 22 Apr 2025 19:01:07 -0700 Subject: [PATCH 311/429] mailbot: plumb thru the user agent to Maintainers() We fetch the MAINTAINERS file form Linus, set the user agent otherwise kernel.org is unpleasant to us. Signed-off-by: Jakub Kicinski --- core/maintainers.py | 10 ++++++++-- mailbot.py | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/core/maintainers.py b/core/maintainers.py index 79c014f..a8b8574 100755 --- a/core/maintainers.py +++ b/core/maintainers.py @@ -38,9 +38,15 @@ def __eq__(self, other): class Maintainers: - def __init__(self, *, file=None, url=None): + def __init__(self, *, file=None, url=None, config=None): self.entries = MaintainersList() + self.http_headers = None + if config: + ua = config.get('patchwork', 'user-agent', fallback='') + if ua: + self.http_headers = {"user-agent":ua} + if file: self._load_from_file(file) elif url: @@ -72,7 +78,7 @@ def _load_from_file(self, file): self._load_from_lines(f.read().split('\n')) def _load_from_url(https://github.com/self,%20url): - r = requests.get(url) + r = requests.get(url, headers=self.http_headers) data = r.content.decode('utf-8') self._load_from_lines(data.split('\n')) diff --git a/mailbot.py b/mailbot.py index 9535347..ba09387 100755 --- a/mailbot.py +++ b/mailbot.py @@ -752,7 +752,7 @@ def main(): if (req_time - doc_load_time).total_seconds() > 24 * 60 * 60: global maintainers - maintainers = Maintainers(url='https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/plain/MAINTAINERS') + maintainers = Maintainers(url='https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/plain/MAINTAINERS', config=config) dr = DocRefs() dr.load_section('process/maintainer-netdev', 'net') From 4ec10ed646317eeaa51e464a181e0f85d03cbcb5 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 23 Apr 2025 12:46:11 -0700 Subject: [PATCH 312/429] py: remove unnecessary globals The github linter decided to become strict about unnecessary globals (when variable isn't overwritten). Remove those. Signed-off-by: Jakub Kicinski --- contest/backend/query.py | 8 -------- core/lifetime.py | 3 +-- core/logger.py | 8 -------- mailbot.py | 10 ---------- pw_brancher.py | 2 -- pw_upload.py | 2 -- system-status.py | 2 -- tests/patch/cc_maintainers/test.py | 2 -- 8 files changed, 1 insertion(+), 36 deletions(-) diff --git a/contest/backend/query.py b/contest/backend/query.py index 32340a0..c2b8a77 100644 --- a/contest/backend/query.py +++ b/contest/backend/query.py @@ -26,8 +26,6 @@ def hello(): @app.route('/branches') def branches(): - global psql - with psql.cursor() as cur: cur.execute(f"SELECT branch, t_date, base, url FROM branches ORDER BY t_date DESC LIMIT 40") rows = [{"branch": r[0], "date": r[1].isoformat() + "+00:00", "base": r[2], "url": r[3]} for r in cur.fetchall()] @@ -36,8 +34,6 @@ def branches(): def branches_to_rows(br_cnt, remote, br_pfx=None): - global psql - cnt = 0 with psql.cursor() as cur: remote_k = ",remote" if remote else "" @@ -77,8 +73,6 @@ def result_as_l2(raw): @app.route('/results') def results(): - global psql - limit = 0 where = [] log = "" @@ -151,8 +145,6 @@ def results(): @app.route('/remotes') def remotes(): - global psql - t1 = datetime.datetime.now() with psql.cursor() as cur: diff --git a/core/lifetime.py b/core/lifetime.py index dd623e7..546ed8c 100644 --- a/core/lifetime.py +++ b/core/lifetime.py @@ -23,6 +23,7 @@ def sig_init(): if not sig_initialized: signal.signal(signal.SIGUSR1, sig_handler) + sig_initialized = True def nipa_git_version(): @@ -55,8 +56,6 @@ def __init__(self, config): self._restart = False def next_poll(self, wait_time=None): - global got_sigusr1 - if self._first_run: self._first_run = False return True diff --git a/core/logger.py b/core/logger.py index 0ab491d..166b964 100644 --- a/core/logger.py +++ b/core/logger.py @@ -220,24 +220,16 @@ def log_init(name, path, force_single_thread=False): def log_fini(): - global tls - tls.logger.fini() def log_open_sec(header): - global tls - tls.logger.open_sec(header) def log_end_sec(): - global tls - tls.logger.end_sec() def log(header, data=''): - global tls - tls.logger.log(header, data) diff --git a/mailbot.py b/mailbot.py index ba09387..a5132c5 100755 --- a/mailbot.py +++ b/mailbot.py @@ -317,7 +317,6 @@ def _resolve_authorized(self, pw): continue file_names.add(line[6:]) - global maintainers maintainer_matches = maintainers.find_by_paths(file_names).find_by_owner(self.msg.get('From')) if len(maintainer_matches): self._authorized = repr(maintainer_matches) @@ -340,7 +339,6 @@ def auto_awaiting_upstream(self): return False tags = subject[1:tags_end] - global auto_awaiting_upstream for designation in auto_awaiting_upstream: if designation in tags: return True @@ -538,7 +536,6 @@ def handler(signum, _): def pw_state_log(fields): - global config log_name = config.get('mailbot', 'change-log') if not log_name: return @@ -551,8 +548,6 @@ def pw_state_log(fields): def weak_act_should_ignore(msg, series, want): - global pw_act_active - if msg.user_authorized(): return None current = series.state() @@ -652,7 +647,6 @@ def do_mail_file(msg_path, pw, dr): try: do_mail(msg, pw, dr) except MlDelayActions as e: - global delay_actions msg.flush_actions() # avoid duplicates, actions will get re-parsed delay_actions.append((e.when, msg, )) @@ -717,15 +711,12 @@ def main(): if ua: http_headers = {"user-agent":ua} - global authorized_users users = config.get('mailbot', 'authorized') authorized_users.update(set(users.split(','))) - global auto_changes_requested users = config.get('mailbot', 'error-bots') auto_changes_requested.update(set(users.split(','))) - global auto_awaiting_upstream users = config.get('mailbot', 'awaiting-upstream') auto_awaiting_upstream.update(set(users.split(','))) @@ -746,7 +737,6 @@ def main(): doc_load_time = datetime.datetime.fromtimestamp(0) dr = None - global should_stop while not should_stop: req_time = datetime.datetime.now() diff --git a/pw_brancher.py b/pw_brancher.py index 1d511bc..e848a16 100755 --- a/pw_brancher.py +++ b/pw_brancher.py @@ -177,8 +177,6 @@ def apply_local_patches(config, tree) -> List: def db_insert(config, state, name): - global psql_conn - # Branches usually have a trailing separator pfx = config.get("target", "branch_pfx")[:-1] pub_url = config.get('target', 'public_url') diff --git a/pw_upload.py b/pw_upload.py index ff356ba..c02d7ee 100755 --- a/pw_upload.py +++ b/pw_upload.py @@ -166,8 +166,6 @@ def initial_scan(self): break def watch(self): - global should_stop - if self.main_wd is None: raise Exception('Not initialized') diff --git a/system-status.py b/system-status.py index a1ad8cc..dca3afc 100755 --- a/system-status.py +++ b/system-status.py @@ -51,8 +51,6 @@ def pre_strip(line, needle): def add_one_tree(result, pfx, name): - global char_filter - log_file = os.path.join(pfx, name) stat = os.stat(log_file) diff --git a/tests/patch/cc_maintainers/test.py b/tests/patch/cc_maintainers/test.py index 8d7dd8f..91e8bd1 100644 --- a/tests/patch/cc_maintainers/test.py +++ b/tests/patch/cc_maintainers/test.py @@ -116,8 +116,6 @@ def is_stale(self, e, since_months, dbg=None): def get_stale(sender_from, missing, out): - global stale_db - sender_corp = None for corp in corp_suffix: if sender_from.endswith(corp): From 79af2eee14abdf54f74b4d83b0a2d08b9cb2da11 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 24 Apr 2025 12:08:04 -0700 Subject: [PATCH 313/429] contest: loadavg: avoid false negatives when waiting for idle kunit is sensitive to overload. We currently ungate it too early, we seem to kick off before other workers start. Wait a few cycles to confirm the machine is actually idle. Signed-off-by: Jakub Kicinski --- contest/remote/lib/loadavg.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/contest/remote/lib/loadavg.py b/contest/remote/lib/loadavg.py index 7600e7d..35d2a26 100644 --- a/contest/remote/lib/loadavg.py +++ b/contest/remote/lib/loadavg.py @@ -4,12 +4,21 @@ import time -def wait_loadavg(target, check_ival=30): +def wait_loadavg(target, check_ival=30, stable_cnt=4): + """ + Wait for loadavg to drop but be careful at the start, the load + may have not ramped up, yet, so if we ungate early whoever is waiting + will experience the overload. + """ while target is not None: load, _, _ = os.getloadavg() if load <= target: - break + if stable_cnt == 0: + break + stable_cnt -= 1 + else: + stable_cnt = 0 - print(f"Waiting for loadavg to decrease: {load} > {target}") + print(f"Waiting for loadavg to decrease: {load} > {target} ({stable_cnt})") time.sleep(check_ival) From db9c2da240ab27c705e45dc2270c6bccd282ecc6 Mon Sep 17 00:00:00 2001 From: Johannes Berg Date: Wed, 23 Apr 2025 10:01:16 +0200 Subject: [PATCH 314/429] pw_poller: poll events, not series It's much easier since events get a timestamp from the server, not from the email. It should also be cheaper for the server since we can limit to the last event we saw before. Signed-off-by: Johannes Berg --- pw/patchwork.py | 15 +++++++++++-- pw_poller.py | 60 ++++++++----------------------------------------- 2 files changed, 22 insertions(+), 53 deletions(-) diff --git a/pw/patchwork.py b/pw/patchwork.py index 4f2a0a1..f4cb527 100644 --- a/pw/patchwork.py +++ b/pw/patchwork.py @@ -178,10 +178,21 @@ def get_patches_all(self, delegate=None, project=None, since=None, action_requir query['archived'] = 'false' return self.get_all('patches', query) - def get_series_all(self, project=None, since=None): + def get_new_series(self, project=None, since=None): if project is None: project = self._project - return self.get_all('series', {'project': project, 'since': since}) + event_params = { + 'project': project, + 'since': since, + 'order': 'date', + 'category': 'series-created', + } + events = self.get_all('events', event_params) + if not events: + return [], since + since = events[-1]['date'] + series = [self.get('series', e['payload']['series']['id']) for e in events] + return series, since def post_check(self, patch, name, state, url, desc): headers = {} diff --git a/pw_poller.py b/pw_poller.py index 0176e97..26483e0 100755 --- a/pw_poller.py +++ b/pw_poller.py @@ -70,12 +70,10 @@ def __init__(self, config) -> None: self._pw = Patchwork(config) self._state = { - 'last_poll': (datetime.datetime.now() - datetime.timedelta(hours=2)).timestamp(), - 'done_series': [], + 'last_event_ts': (datetime.datetime.now() - + datetime.timedelta(hours=2)).strftime('%Y-%m-%dT%H:%M:%S'), } self.init_state_from_disk() - self.seen_series = set(self._state['done_series']) - self.done_series = self.seen_series.copy() self._recheck_period = config.getint('poller', 'recheck_period', fallback=3) self._recheck_lookback = config.getint('poller', 'recheck_lookback', fallback=9) @@ -152,10 +150,6 @@ def series_determine_tree(self, s: PwSeries) -> str: return ret def _process_series(self, pw_series) -> None: - if pw_series['id'] in self.seen_series: - log(f"Already seen {pw_series['id']}", "") - return - s = PwSeries(self._pw, pw_series) log("Series info", @@ -184,8 +178,6 @@ def _process_series(self, pw_series) -> None: core.write_tree_selection_result(self.result_dir, s, comment) core.mark_done(self.result_dir, s) - self.seen_series.add(s['id']) - def process_series(self, pw_series) -> None: log_open_sec(f"Checking series {pw_series['id']} with {pw_series['total']} patches") try: @@ -194,59 +186,26 @@ def process_series(self, pw_series) -> None: log_end_sec() def run(self, life) -> None: - partial_series = {} + since = self._state['last_event_ts'] - prev_big_scan = datetime.datetime.fromtimestamp(self._state['last_poll']) - prev_req_time = datetime.datetime.now() - - # We poll every 2 minutes, for series from last 10 minutes - # Every 3 hours we do a larger check of series of last 12 hours to make sure we didn't miss anything - # apparently patchwork uses the time from the email headers and people back date their emails, a lot - # We keep a history of the series we've seen in and since the last big poll to not process twice try: + # We poll every 2 minutes after this secs = 0 while life.next_poll(secs): - this_poll_seen = set() req_time = datetime.datetime.now() - - # Decide if this is a normal 4 minute history poll or big scan of last 12 hours - if prev_big_scan + datetime.timedelta(hours=self._recheck_period) < req_time: - big_scan = True - since = prev_big_scan - datetime.timedelta(hours=self._recheck_lookback) - log_open_sec(f"Big scan of last 12 hours at {req_time} since {since}") - else: - big_scan = False - since = prev_req_time - datetime.timedelta(minutes=10) - log_open_sec(f"Checking at {req_time} since {since}") - - json_resp = self._pw.get_series_all(since=since) + json_resp, since = self._pw.get_new_series(since=since) log(f"Loaded {len(json_resp)} series", "") - had_partial_series = False for pw_series in json_resp: try: self.process_series(pw_series) - this_poll_seen.add(pw_series['id']) except IncompleteSeries: - partial_series.setdefault(pw_series['id'], 0) - if partial_series[pw_series['id']] < 5: - had_partial_series = True - partial_series[pw_series['id']] += 1 - - if big_scan: - prev_req_time = req_time - prev_big_scan = req_time - # Shorten the history of series we've seen to just the last 12 hours - self.seen_series = this_poll_seen - self.done_series &= self.seen_series - elif had_partial_series: - log("Partial series, not moving time forward", "") - else: - prev_req_time = req_time + # didn't make it to the list fully, patchwork + # shouldn't have had this event at all though + pass while not self._done_queue.empty(): s = self._done_queue.get() - self.done_series.add(s['id']) log(f"Testing complete for series {s['id']}", "") secs = 120 - (datetime.datetime.now() - req_time).total_seconds() @@ -257,8 +216,7 @@ def run(self, life) -> None: pass # finally will still run, but don't splat finally: # Dump state before trying to stop workers, in case they hang - self._state['last_poll'] = prev_big_scan.timestamp() - self._state['done_series'] = list(self.seen_series) + self._state['last_event_ts'] = since with open('poller.state', 'w') as f: json.dump(self._state, f) From 5a20de504bb3c185e484bd6595305bd5625cd576 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Mon, 28 Apr 2025 16:26:27 -0700 Subject: [PATCH 315/429] pw-poller: restore log section open Commit db9c2da240ab (pw_poller: poll events, not series, 2025-04-23) unintentionally removed a log opening section, while deleting some unused code. The log_open_sec() and log_end_sec() calls must be balanced. Signed-off-by: Jakub Kicinski --- pw_poller.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pw_poller.py b/pw_poller.py index 26483e0..d767e77 100755 --- a/pw_poller.py +++ b/pw_poller.py @@ -193,6 +193,7 @@ def run(self, life) -> None: secs = 0 while life.next_poll(secs): req_time = datetime.datetime.now() + log_open_sec(f"Querying patchwork at {req_time} since {since}") json_resp, since = self._pw.get_new_series(since=since) log(f"Loaded {len(json_resp)} series", "") From 0c7c03f7975bafd8bad48b4f7b388d673adc2456 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Mon, 28 Apr 2025 16:34:00 -0700 Subject: [PATCH 316/429] pw-poller: make sure we don't double-process the same series We seem to be fetching the same series multiple times right now. This is likely because PW uses >= for time comparison and we use the date of last fetched event as the since date. Signed-off-by: Jakub Kicinski --- pw_poller.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pw_poller.py b/pw_poller.py index d767e77..78159be 100755 --- a/pw_poller.py +++ b/pw_poller.py @@ -197,6 +197,11 @@ def run(self, life) -> None: json_resp, since = self._pw.get_new_series(since=since) log(f"Loaded {len(json_resp)} series", "") + # Advance the time by 1 usec, pw does >= for time comparison + since = datetime.datetime.fromisoformat(since) + since += datetime.timedelta(microseconds=1) + since = since.isoformat() + for pw_series in json_resp: try: self.process_series(pw_series) From c4f3c4b90327342a33237fc8a49bf65298fd55e0 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Tue, 29 Apr 2025 06:39:16 -0700 Subject: [PATCH 317/429] contest: collector: don't discard results with crashes due to instability Some tests always fail but they may start to trigger crashes. In that case we want to make sure the crashes / warnings are noticed. Never filter out results with crashes based on stability. Signed-off-by: Jakub Kicinski --- contest/results-collector.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contest/results-collector.py b/contest/results-collector.py index 0be43ba..4ed1561 100755 --- a/contest/results-collector.py +++ b/contest/results-collector.py @@ -300,12 +300,18 @@ def filter_l1(test): # Defer filtering to L2 if test.get("results"): return True + # Crashes must always be reported + if test.get("crashes"): + return True return (test['group'], test['test'], None) not in unstable[u_key] def trim_l2(test): # Skip over pure L1s if "results" not in test: return test + # Crashes must always be reported + if test.get("crashes"): + return test def filter_l1_l2(case): return (test['group'], test['test'], case['test']) not in unstable[u_key] From 87edd44a14c31ce3039927e98da4d1f7ee716084 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Mon, 5 May 2025 11:04:02 -0700 Subject: [PATCH 318/429] contest: add virtio deploy files virtio deployment is a bit more complex than the basic VNG. Add the relevant scripts from the deployment to the tree. Signed-off-by: Jakub Kicinski --- deploy/contest/setup_net.sh | 14 ++++++++++++++ deploy/contest/virtio-hw.config | 22 ++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100755 deploy/contest/setup_net.sh create mode 100644 deploy/contest/virtio-hw.config diff --git a/deploy/contest/setup_net.sh b/deploy/contest/setup_net.sh new file mode 100755 index 0000000..66cdd34 --- /dev/null +++ b/deploy/contest/setup_net.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +for i in `seq 0 3`; do + sudo ip tuntap add name tap$((i * 2 )) mode tap multi_queue group virtme + sudo ip tuntap add name tap$((i * 2 + 1)) mode tap multi_queue group virtme + + sudo ip li add name br$i type bridge + sudo ip link set dev tap$((i * 2 )) master br$i + sudo ip link set dev tap$((i * 2 + 1)) master br$i + + sudo ip link set dev br$i up + sudo ip link set dev tap$((i * 2 )) up + sudo ip link set dev tap$((i * 2 + 1)) up +done diff --git a/deploy/contest/virtio-hw.config b/deploy/contest/virtio-hw.config new file mode 100644 index 0000000..b964dd8 --- /dev/null +++ b/deploy/contest/virtio-hw.config @@ -0,0 +1,22 @@ +[executor] +name=## +deadline_minutes=479 +[remote] +branches=## +[local] +tree_path=## +base_path=## +[www] +url=## +[vm] +cpus=4 +setup=. #nipa/contest/scripts/vm-virtio-loop.sh +qemu_opt=-device virtio-net-pci,netdev=n0,iommu_platform=on,disable-legacy=on,mq=on,vectors=18 -netdev tap,id=n0,ifname=tap4,vhost=on,script=no,downscript=no,queues=8 -device virtio-net-pci,netdev=n1,iommu_platform=on,disable-legacy=on,mq=on,vectors=18 -netdev tap,id=n1,ifname=tap5,vhost=on,script=no,downscript=no,queues=8 +[ksft] +target=drivers/net drivers/net/hw +nested_tests=on +[device] +info_script=#nipa/contest/scripts/vm-virtio-dev-info.sh +[cfg] +thread_cnt=1 +thread_spawn_delay=2 From eff30c59dcf13413efef6cbf58b7a99a2c3e0c0b Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Mon, 5 May 2025 17:09:24 -0700 Subject: [PATCH 319/429] pw_poller: switch from series-created to series-completed for polling We want to deal with complete series only. series-created fires before all patches have been received. Signed-off-by: Jakub Kicinski --- pw/patchwork.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pw/patchwork.py b/pw/patchwork.py index f4cb527..35e2dc5 100644 --- a/pw/patchwork.py +++ b/pw/patchwork.py @@ -185,7 +185,7 @@ def get_new_series(self, project=None, since=None): 'project': project, 'since': since, 'order': 'date', - 'category': 'series-created', + 'category': 'series-completed', } events = self.get_all('events', event_params) if not events: From cf80758eec1c7b5df1b2b7b5cb0160013b7420d3 Mon Sep 17 00:00:00 2001 From: Johannes Berg Date: Tue, 6 May 2025 22:07:24 +0200 Subject: [PATCH 320/429] pw_poller: use configured trees for subject tag match There's no need to hard-code the list of trees when we actually have it in the configuration. Signed-off-by: Johannes Berg --- netdev/tree_match.py | 4 ++-- pw_poller.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/netdev/tree_match.py b/netdev/tree_match.py index 9212f96..a997ee8 100644 --- a/netdev/tree_match.py +++ b/netdev/tree_match.py @@ -9,8 +9,8 @@ from core import log, log_open_sec, log_end_sec -def series_tree_name_direct(series): - for t in ['net-next', 'bpf-next', 'net', 'bpf']: +def series_tree_name_direct(conf_trees, series): + for t in conf_trees: if re.match(r'\[.*{pfx}.*\]'.format(pfx=t), series.subject): return t diff --git a/pw_poller.py b/pw_poller.py index 78159be..28fdc57 100755 --- a/pw_poller.py +++ b/pw_poller.py @@ -92,7 +92,7 @@ def init_state_from_disk(self) -> None: pass def _series_determine_tree(self, s: PwSeries) -> str: - s.tree_name = self.list_module.series_tree_name_direct(s) + s.tree_name = self.list_module.series_tree_name_direct(self._trees.keys(), s) s.tree_mark_expected = True s.tree_marked = bool(s.tree_name) From f2608ab8eb9be400e8ef2cfd9cac30b24c979f3b Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 8 May 2025 13:32:50 -0700 Subject: [PATCH 321/429] contest: increase the MTU of the TAPs for virtio Some tests use jumbo, we need to make sure the 9k frames go thru. Signed-off-by: Jakub Kicinski --- deploy/contest/setup_net.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deploy/contest/setup_net.sh b/deploy/contest/setup_net.sh index 66cdd34..f85ae29 100755 --- a/deploy/contest/setup_net.sh +++ b/deploy/contest/setup_net.sh @@ -8,7 +8,7 @@ for i in `seq 0 3`; do sudo ip link set dev tap$((i * 2 )) master br$i sudo ip link set dev tap$((i * 2 + 1)) master br$i - sudo ip link set dev br$i up - sudo ip link set dev tap$((i * 2 )) up - sudo ip link set dev tap$((i * 2 + 1)) up + sudo ip link set dev br$i mtu 12000 up + sudo ip link set dev tap$((i * 2 )) mtu 12000 up + sudo ip link set dev tap$((i * 2 + 1)) mtu 12000 up done From f7862b51f37efe22c263b7bb6611250815e06c97 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 15 May 2025 06:44:54 -0700 Subject: [PATCH 322/429] netdev: tree_match: include devicetree/bindings/net/ Signed-off-by: Jakub Kicinski --- netdev/tree_match.py | 1 + 1 file changed, 1 insertion(+) diff --git a/netdev/tree_match.py b/netdev/tree_match.py index a997ee8..cecb2e0 100644 --- a/netdev/tree_match.py +++ b/netdev/tree_match.py @@ -48,6 +48,7 @@ def _tree_name_should_be_local_files(raw_email): 'drivers/vhost/', } required_files = { + 'Documentation/devicetree/bindings/net/', 'Documentation/netlink/', 'Documentation/networking/', 'include/linux/netdevice.h', From 77e4b2b7e445aba8b5b246d99d4ede56c84076de Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 17 May 2025 10:59:41 -0700 Subject: [PATCH 323/429] ui: flakes: split the result filtering from the matrix construction Minor refactoring, split the loop that constructs the result matrix into two, first which checks whether result is filtered, and second which actually constructs the array ignoring filtered. This will make it easy to add a step in between. Signed-off-by: Jakub Kicinski --- ui/flakes.js | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/ui/flakes.js b/ui/flakes.js index b862153..dd8975e 100644 --- a/ui/flakes.js +++ b/ui/flakes.js @@ -37,16 +37,15 @@ function load_result_table(data_raw) branch_pfx_set.add(br_pfx); } - // Build the result map + // Annotate which results will be visible var pw_n = document.getElementById("pw-n").checked; var pw_y = document.getElementById("pw-y").checked; let needle = document.getElementById("tn-needle").value; - var test_row = {}; - let tn_urls = {}; - $.each(data_raw, function(i, v) { $.each(v.results, function(j, r) { + r.visible = false; + if (pw_y == false && nipa_pw_reported(v, r) == true) return 1; if (pw_n == false && nipa_pw_reported(v, r) == false) @@ -56,6 +55,20 @@ function load_result_table(data_raw) if (needle && !tn.includes(needle)) return 1; + r.visible = true; + }); + }); + + // Build the result map + var test_row = {}; + let tn_urls = {}; + + $.each(data_raw, function(i, v) { + $.each(v.results, function(j, r) { + if (!r.visible) + return 1; + + const tn = v.remote + '/' + r.group + '/' + r.test; tn_urls[tn] = "executor=" + v.executor + "&test=" + r.test; if (!(tn in test_row)) { From 0b58e23a5fe590a9e05580fab663471398ec09e9 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 17 May 2025 11:01:10 -0700 Subject: [PATCH 324/429] ui: flakes: hide branch prefixes which have no branches with results Now that we have multiple branch prefixes flakes often shows completely empty columns. Hide branch prefixes which have no results. Signed-off-by: Jakub Kicinski --- ui/flakes.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/ui/flakes.js b/ui/flakes.js index dd8975e..ce3063b 100644 --- a/ui/flakes.js +++ b/ui/flakes.js @@ -21,12 +21,10 @@ function load_result_table(data_raw) $.each(data_raw, function(i, v) { branch_set.add(v.branch); }); - let br_cnt = document.getElementById("br-cnt").value; - const branches = Array.from(branch_set).slice(0, br_cnt); // Populate the load filters with prefixes let select_br_pfx = document.getElementById("br-pfx"); - for (const br of branches) { + for (const br of branch_set) { const br_pfx = nipa_br_pfx_get(br); if (select_br_pfx.length == 0) @@ -41,6 +39,7 @@ function load_result_table(data_raw) var pw_n = document.getElementById("pw-n").checked; var pw_y = document.getElementById("pw-y").checked; let needle = document.getElementById("tn-needle").value; + let br_pfx_with_data = new Set(); $.each(data_raw, function(i, v) { $.each(v.results, function(j, r) { @@ -56,9 +55,20 @@ function load_result_table(data_raw) return 1; r.visible = true; + + const br_pfx = nipa_br_pfx_get(v.branch); + br_pfx_with_data.add(br_pfx); }); }); + // Hide all the branches with prefixes which saw no data + let br_cnt = document.getElementById("br-cnt").value; + var branches = Array.from(branch_set); + branches = branches.filter( + (name) => br_pfx_with_data.has(nipa_br_pfx_get(name)) + ); + branches = branches.slice(0, br_cnt); + // Build the result map var test_row = {}; let tn_urls = {}; From 9828cb5a5f397e28d3fda68ed1dd1c5c24f40ee2 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 17 May 2025 11:52:13 -0700 Subject: [PATCH 325/429] mailbot: record time we refreshed maintainers We intended to refresh maintainers every 24h but the refresh time is not updated, so we wait 24h the first time, and then refresh on every mail fetch (every 2min). Signed-off-by: Jakub Kicinski --- mailbot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mailbot.py b/mailbot.py index a5132c5..2f5cce1 100755 --- a/mailbot.py +++ b/mailbot.py @@ -753,6 +753,8 @@ def main(): dr.alias_section('submitting-patches', 'submit') dr.alias_section('submitting-patches', 'sub') + doc_load_time = req_time + for t in mail_repos.values(): check_new(t, pw, dr) From 357f9854b809bd35bfdcf04350202c78d4679703 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 17 May 2025 11:50:23 -0700 Subject: [PATCH 326/429] maintainers: fix up tabs vs spaces People repeatedly mis-format MAINTAINERS, just fix it up. Previously we'd overwrite the group. Signed-off-by: Jakub Kicinski --- core/maintainers.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/maintainers.py b/core/maintainers.py index a8b8574..0167d53 100755 --- a/core/maintainers.py +++ b/core/maintainers.py @@ -61,6 +61,11 @@ def _load_from_lines(self, lines): if not started: continue + # Fix up tabs vs spaces + if len(line) > 5 and line[0].isupper() and line[1:4] == ': ': + print("Bad attr line:", group, line.strip()) + line = line[:2] + '\t' + line[2:].strip() + if line == '': if len(group) > 1: self.entries.add(MaintainersEntry(group)) From 20beb3d986072c599e9915529e8a2ddd9d852e0a Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 17 May 2025 12:44:53 -0700 Subject: [PATCH 327/429] check_fetcher: request all checks contest updates its checks too often, we end up with pagination of checks. Make sure we fetch them all, the earlier ones are far more likely to be failures. Signed-off-by: Jakub Kicinski --- check_fetcher.py | 2 +- pw/patchwork.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/check_fetcher.py b/check_fetcher.py index 98e4751..826eb28 100755 --- a/check_fetcher.py +++ b/check_fetcher.py @@ -68,7 +68,7 @@ def main(): continue seen_pids.add(p["id"]) - checks = pw.request(p["checks"]) + checks = pw.request_all(p["checks"]) for c in checks: info = { "id": p["id"], diff --git a/pw/patchwork.py b/pw/patchwork.py index 35e2dc5..c08e07c 100644 --- a/pw/patchwork.py +++ b/pw/patchwork.py @@ -73,6 +73,24 @@ def _request(self, url): def request(self, url): return self._request(url).json() + def request_all(self, url): + items = [] + + while url: + response = self._request(url) + items += response.json() + + if 'Link' not in response.headers: + break + url = '' + links = response.headers['Link'].split(',') + for link in links: + info = link.split(';') + if info[1].strip() == 'rel="next"': + url = info[0][1:-1] + + return items + def get(self, object_type, identifier): return self._get(f'{object_type}/{identifier}/').json() From 3873169426fda9ed746b993a30bf9d72381bc9ef Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 17 May 2025 12:59:56 -0700 Subject: [PATCH 328/429] pw_contest: limit the number of updates on the first run We seem to report 12 checks for the first branch that contains a patch, because the test case count grows slowly. This happens mostly for branches which have failures, as failure is declared as soon as one remote fails, even if others are pending. So limit the updates of test count for otherwise equivalent state if some remotes are still running. Signed-off-by: Jakub Kicinski --- pw_contest.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pw_contest.py b/pw_contest.py index 2e132b1..fbfedaa 100755 --- a/pw_contest.py +++ b/pw_contest.py @@ -124,6 +124,7 @@ def branch_summarize(filters: dict, results_by_branch: dict) -> dict: for name, branch in results_by_branch.items(): code = 0 test_cnt = 0 + pending = 0 for remote in filters["remotes"]: new_code = Codes.PENDING if remote in branch: @@ -132,7 +133,11 @@ def branch_summarize(filters: dict, results_by_branch: dict) -> dict: new_code = branch[remote]['code'] test_cnt += branch[remote]["cnt"] code = max(code, new_code) + if new_code == Codes.PENDING: + pending += 1 summary[name] = {'result': code_to_str[code], 'code': code, 'cnt': test_cnt} + if pending: + summary[name]['pending'] = pending return summary @@ -153,7 +158,11 @@ def result_upgrades(states: dict, item_id: str, outcome: dict, branch: str): if prev['code'] > outcome['code']: return True if prev['code'] == outcome['code']: - return prev['cnt'] < outcome['cnt'] + # use the run with most reported results, but wait for it to finish + # otherwise first contest for a patch updates the check every time + # a remote finishes and bumps the test case count (~12 updates) + if prev['cnt'] < outcome['cnt']: + return not outcome.get('pending') return False From e1520313af9563001b434d9dd0adf10d902b425e Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 17 May 2025 13:19:19 -0700 Subject: [PATCH 329/429] check_fetcher: only record the latest check Ignore older checks, only show the latest. Signed-off-by: Jakub Kicinski --- check_fetcher.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/check_fetcher.py b/check_fetcher.py index 826eb28..6f138cd 100755 --- a/check_fetcher.py +++ b/check_fetcher.py @@ -57,6 +57,7 @@ def main(): json_resp = pw.get_patches_all(delegate=delegate, since=since) jdb = [] old_unchanged = 0 + check_updates = 0 seen_pids = set() for p in json_resp: pdate = datetime.datetime.fromisoformat(p["date"]) @@ -68,8 +69,14 @@ def main(): continue seen_pids.add(p["id"]) + seen_checks = set() checks = pw.request_all(p["checks"]) - for c in checks: + for c in reversed(checks): + if c["context"] in seen_checks: + check_updates += 1 + continue + seen_checks.add(c["context"]) + info = { "id": p["id"], "date": p["date"], @@ -101,7 +108,7 @@ def main(): new_db.append(row) new_db += jdb print(f'Old db: {len(old_db)}, retained: {old_stayed}') - print(f'Fetching: patches: {len(json_resp)}, patches old-unchanged: {old_unchanged}, checks fetched: {len(jdb)}') + print(f'Fetching: patches: {len(json_resp)}, patches old-unchanged: {old_unchanged}, checks fetched: {len(jdb)}, checks were updates: {check_updates}') print(f'Writing: refreshed: {skipped}, new: {len(new_db) - old_stayed}, expired: {horizon_gc} new len: {len(new_db)}') with open(tgt_json, "w") as fp: From 8e2d457fd654b23d3916e1e523c0b710857bbd41 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 17 May 2025 15:30:19 -0700 Subject: [PATCH 330/429] contest: collector: record device info into a table Store device info in a table, for ease of reporting the driver name in the UI. Signed-off-by: Jakub Kicinski --- contest/results-collector.py | 24 ++++++++++++++++++++++++ deploy/contest/db | 8 ++++++++ 2 files changed, 32 insertions(+) diff --git a/contest/results-collector.py b/contest/results-collector.py index 4ed1561..7062867 100755 --- a/contest/results-collector.py +++ b/contest/results-collector.py @@ -221,11 +221,35 @@ def psql_insert_stability(self, data): stability["pass_cur"], stability["fail_cur"], stability["passing"], now)).decode('utf-8') + self.psql_stability_selector(cur, data, row)) + def psql_insert_device(self, data): + if 'device' not in data: + return + + with self.psql_conn.cursor() as cur: + cur.execute(f"SELECT info FROM devices_info WHERE " + + cur.mogrify("remote = %s AND executor = %s", + (data["remote"], data["executor"], )).decode('utf-8') + + "ORDER BY changed DESC LIMIT 1") + rows = cur.fetchall() + if rows: + info = rows[0][0] + else: + info = 'x' + if info == data['device']: + return + + with self.psql_conn.cursor() as cur: + cur.execute(f"INSERT INTO devices_info (remote, executor, changed, info) " + + cur.mogrify("VALUES(%s, %s, %s, %s)", + (data["remote"], data["executor"], + data["start"], data["device"])).decode('utf-8')) + def insert_real(self, remote, run): data = run.copy() data["remote"] = remote["name"] self.psql_insert_stability(data) + self.psql_insert_device(data) with self.psql_conn.cursor() as cur: if not self.psql_has_wip(remote, run): diff --git a/deploy/contest/db b/deploy/contest/db index eddae3f..9f678f0 100644 --- a/deploy/contest/db +++ b/deploy/contest/db @@ -85,3 +85,11 @@ CREATE TABLE stability ( last_update timestamp, passing timestamp ); + + +CREATE TABLE devices_info ( + remote varchar(80), + executor varchar(80), + changed timestamp, + info text +); From 60e60d7c7c929cd3664d171d51dbc8861114d6a7 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 17 May 2025 15:46:07 -0700 Subject: [PATCH 331/429] contest: backend: support querying stability and device info Basic queries of stability and device info. Signed-off-by: Jakub Kicinski --- contest/backend/query.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/contest/backend/query.py b/contest/backend/query.py index c2b8a77..d53d45f 100644 --- a/contest/backend/query.py +++ b/contest/backend/query.py @@ -155,3 +155,38 @@ def remotes(): print(f"Query for remotes: {str(t2-t1)}") return rows + + +@app.route('/stability') +def stability(): + # auto = query only tests which NIPA ignores based on stability + auto = request.args.get('auto') + + where = "" + if auto == "y" or auto == '1' or auto == 't': + where = "WHERE autoignore = true"; + elif auto == "n" or auto == '0' or auto == 'f': + where = "WHERE autoignore = false"; + + with psql.cursor() as cur: + cur.execute(f"SELECT * FROM stability {where}") + + columns = [desc[0] for desc in cur.description] + rows = cur.fetchall() + # Convert each row to a dictionary with column names as keys + data = [{columns[i]: value for i, value in enumerate(row)} for row in rows] + + return data + + +@app.route('/device-info') +def dev_info(): + with psql.cursor() as cur: + cur.execute(f"SELECT * FROM devices_info") + + columns = [desc[0] for desc in cur.description] + rows = cur.fetchall() + # Convert each row to a dictionary with column names as keys + data = [{columns[i]: value for i, value in enumerate(row)} for row in rows] + + return data From 4974eae2bf9563e0fc3600c803797bd15980ec79 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 17 May 2025 16:03:22 -0700 Subject: [PATCH 332/429] ui: device: render basic device info and stability Signed-off-by: Jakub Kicinski --- ui/devices.html | 17 +++++++- ui/devices.js | 108 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 2 deletions(-) diff --git a/ui/devices.html b/ui/devices.html index bcc0c25..b6dfdfb 100644 --- a/ui/devices.html +++ b/ui/devices.html @@ -19,9 +19,22 @@ +
+
+

Latest device info

+ +
+
+
+
+
+

Test case status

+ +
diff --git a/ui/devices.js b/ui/devices.js index 3e49077..3544a23 100644 --- a/ui/devices.js +++ b/ui/devices.js @@ -1,3 +1,111 @@ +let xfr_todo = 2; +let dev_info = null; +let stability = null; + +function load_tables() +{ + // Turn stability into matrix by executor + let rn_seen = new Set(); + let tn_db = []; + let sta_db = {}; + + for (ste of stability) { + let tn = ste.grp + ':' + ste.test + ':' + ste.subtest; + if (ste.subtest == null) + tn = ste.grp + ':' + ste.test + ':'; + let rn = ste.remote + ste.executor; + + if (!(tn in sta_db)) { + sta_db[tn] = {}; + tn_db.push(tn); + } + + sta_db[tn][rn] = ste; + rn_seen.add(rn); + } + + // Simple sort by name + tn_db.sort(); + + // Render device info + let display_names = {}; + let dev_table = document.getElementById("device_info"); + + for (dev of dev_info) { + let rn = dev.remote + dev.executor; + if (!rn_seen.has(rn)) + continue; + + let row = dev_table.insertRow(); + + row.insertCell(0).innerText = dev.remote; + row.insertCell(1).innerText = dev.executor; + + const info = JSON.parse(dev.info); + const driver = info.driver; + row.insertCell(2).innerText = driver; + + delete info.driver; + const versions = JSON.stringify(info); + row.insertCell(3).innerText = versions; + + display_names[dev.remote + dev.executor] = + dev.remote + '
' + dev.executor + '
' + driver; + } + + // Create headers + let sta_tb = document.getElementById("stability"); + + const hdr = sta_tb.createTHead().insertRow(); + hdr.insertCell().innerText = 'Group'; + hdr.insertCell().innerText = 'Test'; + hdr.insertCell().innerText = 'Subtest'; + for (rn of Object.keys(display_names)) { + let cell = hdr.insertCell(); + + cell.innerHTML = display_names[rn]; + cell.setAttribute("style", "writing-mode: tb-rl;"); + } + + // Display + for (tn of tn_db) { + let row = sta_tb.insertRow(); + + row.insertCell(0).innerText = tn.split(':')[0]; + row.insertCell(1).innerText = tn.split(':')[1]; + let cell = row.insertCell(2); + if (tn.split(':').length == 3) + cell.innerText = tn.split(':')[2]; + + let i = 3; + for (rn of Object.keys(display_names)) { + cell = row.insertCell(i++); + if (rn in sta_db[tn]) { + let ste = sta_db[tn][rn]; + + if (ste.passing) + cell.setAttribute("class", "box-pass"); + else + cell.setAttribute("class", "box-skip"); + } + } + } +} + function do_it() { + $(document).ready(function() { + $.get("query/device-info", function(data_raw) { + dev_info = data_raw; + if (!--xfr_todo) + load_tables(); + }) + }); + $(document).ready(function() { + $.get("query/stability?auto=1", function(data_raw) { + stability = data_raw; + if (!--xfr_todo) + load_tables(); + }) + }); } From f634ab263be2cfdc2007609229a92bf65dadb4fa Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sun, 18 May 2025 08:18:58 -0700 Subject: [PATCH 333/429] ui: devices: list the pass pct This helps find failed cases which may be supported but are flaky. Also it fixes the column width on Firefox which doesn't like empty cells. Signed-off-by: Jakub Kicinski --- ui/devices.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/ui/devices.js b/ui/devices.js index 3544a23..b80446c 100644 --- a/ui/devices.js +++ b/ui/devices.js @@ -83,10 +83,17 @@ function load_tables() if (rn in sta_db[tn]) { let ste = sta_db[tn][rn]; - if (ste.passing) + pct = 100 * ste.pass_cnt / (ste.fail_cnt + ste.pass_cnt); + pct = Math.round(pct); + if (ste.passing) { cell.setAttribute("class", "box-pass"); - else + if (pct != 100) + cell.innerText = pct + "%"; + } else { cell.setAttribute("class", "box-skip"); + if (pct != 0) + cell.innerText = pct + "%"; + } } } } From d0fa575ff85c52d3f58ad50a3773d2ee2736b4f9 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 17 May 2025 18:56:36 -0700 Subject: [PATCH 334/429] contest: collector: fix recording JSON device_info Signed-off-by: Jakub Kicinski --- contest/results-collector.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/contest/results-collector.py b/contest/results-collector.py index 7062867..09c3fcc 100755 --- a/contest/results-collector.py +++ b/contest/results-collector.py @@ -235,14 +235,18 @@ def psql_insert_device(self, data): info = rows[0][0] else: info = 'x' - if info == data['device']: + + new_info = data["device"] + if isinstance(new_info, dict): + new_info = json.dumps(new_info) + if info == new_info: return with self.psql_conn.cursor() as cur: cur.execute(f"INSERT INTO devices_info (remote, executor, changed, info) " + cur.mogrify("VALUES(%s, %s, %s, %s)", (data["remote"], data["executor"], - data["start"], data["device"])).decode('utf-8')) + data["start"], new_info)).decode('utf-8')) def insert_real(self, remote, run): data = run.copy() From 6de590e59b288a9ecff40101a41345275ba66a2c Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sun, 18 May 2025 08:37:35 -0700 Subject: [PATCH 335/429] ui: devices: split old tests out to a separate table Signed-off-by: Jakub Kicinski --- ui/devices.html | 7 +++++++ ui/devices.js | 36 +++++++++++++++++++++++++++--------- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/ui/devices.html b/ui/devices.html index b6dfdfb..0a58807 100644 --- a/ui/devices.html +++ b/ui/devices.html @@ -37,5 +37,12 @@

Test case status

+
+
+

Old test cases (no reports for 2 weeks+)

+ +
+
+
diff --git a/ui/devices.js b/ui/devices.js index b80446c..56fa0b2 100644 --- a/ui/devices.js +++ b/ui/devices.js @@ -8,6 +8,10 @@ function load_tables() let rn_seen = new Set(); let tn_db = []; let sta_db = {}; + // Test age + let tn_time = {}; + let year_ago = new Date(); + year_ago.setFullYear(year_ago.getFullYear() - 1); for (ste of stability) { let tn = ste.grp + ':' + ste.test + ':' + ste.subtest; @@ -18,10 +22,14 @@ function load_tables() if (!(tn in sta_db)) { sta_db[tn] = {}; tn_db.push(tn); + tn_time[tn] = year_ago; } sta_db[tn][rn] = ste; rn_seen.add(rn); + let d = new Date(ste.last_update); + if (d > tn_time[tn]) + tn_time[tn] = d; } // Simple sort by name @@ -55,21 +63,31 @@ function load_tables() // Create headers let sta_tb = document.getElementById("stability"); + let sta_to = document.getElementById("stability-old"); - const hdr = sta_tb.createTHead().insertRow(); - hdr.insertCell().innerText = 'Group'; - hdr.insertCell().innerText = 'Test'; - hdr.insertCell().innerText = 'Subtest'; - for (rn of Object.keys(display_names)) { - let cell = hdr.insertCell(); + for (tbl of [sta_tb, sta_to]) { + const hdr = tbl.createTHead().insertRow(); + hdr.insertCell().innerText = 'Group'; + hdr.insertCell().innerText = 'Test'; + hdr.insertCell().innerText = 'Subtest'; + for (rn of Object.keys(display_names)) { + let cell = hdr.insertCell(); - cell.innerHTML = display_names[rn]; - cell.setAttribute("style", "writing-mode: tb-rl;"); + cell.innerHTML = display_names[rn]; + cell.setAttribute("style", "writing-mode: tb-rl;"); + } } // Display + let two_weeks_ago = new Date().setDate(new Date().getDate() - 14); + for (tn of tn_db) { - let row = sta_tb.insertRow(); + let row = null; + + if (tn_time[tn] > two_weeks_ago) + row = sta_tb.insertRow(); + else + row = sta_to.insertRow(); row.insertCell(0).innerText = tn.split(':')[0]; row.insertCell(1).innerText = tn.split(':')[1]; From e5143b0e0d37e364868a6604a2454b225d6b180a Mon Sep 17 00:00:00 2001 From: Johannes Berg Date: Fri, 16 May 2025 08:55:21 +0200 Subject: [PATCH 336/429] tests: kernel-doc: handle pull requests better For pull requests, it only tests files changed during the merge commit itself, which is a bit weird, especially as the automatic merge won't be able to resolve conflicts. Use "git diff" instead of "git show" to build the list of changed files. Signed-off-by: Johannes Berg --- tests/patch/kdoc/kdoc.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/patch/kdoc/kdoc.sh b/tests/patch/kdoc/kdoc.sh index a4f96ad..ac36d1b 100755 --- a/tests/patch/kdoc/kdoc.sh +++ b/tests/patch/kdoc/kdoc.sh @@ -8,7 +8,7 @@ tmpfile_o=$(mktemp) tmpfile_n=$(mktemp) rc=0 -files=$(git show --pretty="" --name-only HEAD) +files=$(git diff HEAD^ --pretty= --name-only) HEAD=$(git rev-parse HEAD) From a541cecc3499827ff8e06998a2db45191742f030 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 22 May 2025 06:27:01 -0700 Subject: [PATCH 337/429] contest: loadavg: wait for dirty page writeback It looks like AWS machines stall very painfully for some time after the builds because of pending disk IO. The b/w cap was increased on our instances but still there is a few minutes of lag between when we decide the that the load has gone down and when machine has recovered from the builds. Wait for dirty memory to go down to ~100MB. Since we have two conditions now lower the stable count to 3. Signed-off-by: Jakub Kicinski --- contest/remote/lib/loadavg.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/contest/remote/lib/loadavg.py b/contest/remote/lib/loadavg.py index 35d2a26..733a990 100644 --- a/contest/remote/lib/loadavg.py +++ b/contest/remote/lib/loadavg.py @@ -4,21 +4,32 @@ import time -def wait_loadavg(target, check_ival=30, stable_cnt=4): +def get_dirty_mem(): + """ Get amount of dirty mem, returns value in MB """ + with open("/proc/meminfo", "r") as fp: + lines = fp.read().split("\n") + dirty = list(filter(lambda a: "Dirty" in a, lines))[0] + return int(dirty.split(" ")[-2]) / 1000 + + +def wait_loadavg(target, dirty_max=100, check_ival=30, stable_cnt=3): """ Wait for loadavg to drop but be careful at the start, the load may have not ramped up, yet, so if we ungate early whoever is waiting will experience the overload. """ + + seen_stable = 0 while target is not None: load, _, _ = os.getloadavg() + dirty = get_dirty_mem() - if load <= target: - if stable_cnt == 0: + if load <= target and dirty <= dirty_max: + if seen_stable >= stable_cnt: break - stable_cnt -= 1 + seen_stable += 1 else: - stable_cnt = 0 + seen_stable = 0 - print(f"Waiting for loadavg to decrease: {load} > {target} ({stable_cnt})") + print(f"Waiting for loadavg to decrease: CPU: {load} > {target} Dirty Mem: {dirty} > {dirty_max} MB ({seen_stable})") time.sleep(check_ival) From f9c19b999249092f89357f985ae46c2b6efb5649 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 22 May 2025 13:43:15 -0700 Subject: [PATCH 338/429] netdev: tree_match: include Bluetooth and wireless After recent improvements to PR matching we are now "correctly" not matching bluetooth PRs. After we excluded wireless and bluetooth via maintainers the cross-posts are relatively rare. Let them be tested. Signed-off-by: Jakub Kicinski --- netdev/tree_match.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/netdev/tree_match.py b/netdev/tree_match.py index cecb2e0..b1868b2 100644 --- a/netdev/tree_match.py +++ b/netdev/tree_match.py @@ -59,6 +59,7 @@ def _tree_name_should_be_local_files(raw_email): 'lib/', 'net/', 'drivers/atm/', + 'drivers/bluetooth/', 'drivers/dpll/', 'drivers/isdn/', 'drivers/net/', @@ -71,9 +72,7 @@ def _tree_name_should_be_local_files(raw_email): 'tools/net/', 'tools/testing/selftests/net/', } - excluded_files = { - 'drivers/net/wireless/', - } + excluded_files = set() all_files = acceptable_files.union(required_files) required_found = False foreign_found = False From 67d300d87970063378a4a68c1945eeb109a7fb55 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 31 May 2025 10:11:40 -0700 Subject: [PATCH 339/429] logging: support setting log dir poller creates log files based on tree name. To move the logs out of the nipa dir (e.g. to /var/logs/) we need the ability to specify the log dir, but not the log file name. Signed-off-by: Jakub Kicinski --- check_fetcher.py | 3 ++- core/tester.py | 3 ++- mailbot.py | 3 ++- pw_contest.py | 3 ++- pw_poller.py | 3 ++- pw_upload.py | 3 ++- 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/check_fetcher.py b/check_fetcher.py index 6f138cd..7b811fc 100755 --- a/check_fetcher.py +++ b/check_fetcher.py @@ -34,8 +34,9 @@ def main(): config = configparser.ConfigParser() config.read(['nipa.config', 'pw.config', 'checks.config']) + log_dir = config.get('log', 'dir', fallback=NIPA_DIR) log_init(config.get('log', 'type', fallback='org'), - config.get('log', 'file', fallback=os.path.join(NIPA_DIR, "checks.org")), + config.get('log', 'file', fallback=os.path.join(log_dir, "checks.org")), force_single_thread=True) rdir = config.get('dirs', 'results', fallback=os.path.join(NIPA_DIR, "results")) diff --git a/core/tester.py b/core/tester.py index 49d3e7f..268f465 100644 --- a/core/tester.py +++ b/core/tester.py @@ -68,9 +68,10 @@ def run(self) -> None: self.config = configparser.ConfigParser() self.config.read(['nipa.config', 'pw.config', 'tester.config']) + log_dir = self.config.get('log', 'dir', fallback=core.NIPA_DIR) core.log_init( self.config.get('log', 'type', fallback='org'), - self.config.get('log', 'file', fallback=os.path.join(core.NIPA_DIR, f"{self.tree.name}.org"))) + self.config.get('log', 'file', fallback=os.path.join(log_dir, f"{self.tree.name}.org"))) core.log_open_sec("Tester init") if not os.path.exists(self.result_dir): diff --git a/mailbot.py b/mailbot.py index 2f5cce1..68608ad 100755 --- a/mailbot.py +++ b/mailbot.py @@ -697,8 +697,9 @@ def main(): config = configparser.ConfigParser() config.read(['nipa.config', 'pw.config', 'mailbot.config']) + log_dir = config.get('log', 'dir', fallback=NIPA_DIR) log_init(config.get('log', 'type', fallback='org'), - config.get('log', 'file', fallback=os.path.join(NIPA_DIR, "mailbot.org")), + config.get('log', 'file', fallback=os.path.join(log_dir, "mailbot.org")), force_single_thread=True) pw = Patchwork(config) diff --git a/pw_contest.py b/pw_contest.py index fbfedaa..bdd8434 100755 --- a/pw_contest.py +++ b/pw_contest.py @@ -295,8 +295,9 @@ def parse_configs(): def main() -> None: config = parse_configs() + log_dir = config.get('log', 'dir', fallback=NIPA_DIR) log_init(config.get('log', 'type', fallback='org'), - config.get('log', 'file', fallback=os.path.join(NIPA_DIR, "contest.org")), + config.get('log', 'file', fallback=os.path.join(log_dir, "contest.org")), force_single_thread=True) pw = Patchwork(config) diff --git a/pw_poller.py b/pw_poller.py index 28fdc57..937a9cf 100755 --- a/pw_poller.py +++ b/pw_poller.py @@ -242,8 +242,9 @@ def run(self, life) -> None: config = configparser.ConfigParser() config.read(['nipa.config', 'pw.config', 'poller.config']) + log_dir = config.get('log', 'dir', fallback=NIPA_DIR) log_init(config.get('log', 'type', fallback='org'), - config.get('log', 'file', fallback=os.path.join(NIPA_DIR, "poller.org"))) + config.get('log', 'file', fallback=os.path.join(log_dir, "poller.org"))) life = NipaLifetime(config) poller = PwPoller(config) diff --git a/pw_upload.py b/pw_upload.py index c02d7ee..6b7c508 100755 --- a/pw_upload.py +++ b/pw_upload.py @@ -192,8 +192,9 @@ def main(): config = configparser.ConfigParser() config.read(['nipa.config', 'pw.config', 'upload.config']) + log_dir = config.get('log', 'dir', fallback=NIPA_DIR) log_init(config.get('log', 'type', fallback='org'), - config.get('log', 'file', fallback=os.path.join(NIPA_DIR, "upload.org")), + config.get('log', 'file', fallback=os.path.join(log_dir, "upload.org")), force_single_thread=True) results_dir = config.get('results', 'dir', fallback=os.path.join(NIPA_DIR, "results")) From 769b4f83b414ec3689300c4f31d93b30005a6473 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 31 May 2025 10:28:53 -0700 Subject: [PATCH 340/429] contest: collector: don't crash when remote has invalid JSON Remotes sometimes serve us invalid JSON files, usually due to race conditions between writing and us fetching. Skip those, we will retry on the next poll. And in case of persistent failure we'll not report anything and contest will report that remote is dead. Signed-off-by: Jakub Kicinski --- contest/results-collector.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/contest/results-collector.py b/contest/results-collector.py index 09c3fcc..c9b4519 100755 --- a/contest/results-collector.py +++ b/contest/results-collector.py @@ -276,13 +276,19 @@ def write_json_atomic(path, data): def fetch_remote_run(fetcher, remote, run_info, remote_state): r = requests.get(run_info['url']) - data = json.loads(r.content.decode('utf-8')) + try: + data = json.loads(r.content.decode('utf-8')) + except json.decoder.JSONDecodeError: + print('WARN: Failed to decode results from remote:', remote['name'], + 'invalid JSON at', run_info['url']) + return False fetcher.insert_real(remote, data) file = os.path.join(remote_state['dir'], os.path.basename(run_info['url'])) with open(file, "w") as fp: json.dump(data, fp) + return True def fetch_remote(fetcher, remote, seen): @@ -291,7 +297,7 @@ def fetch_remote(fetcher, remote, seen): try: manifest = json.loads(r.content.decode('utf-8')) except json.decoder.JSONDecodeError: - print('Failed to decode manifest from remote:', remote['name']) + print('WARN: Failed to decode manifest from remote:', remote['name']) return remote_state = seen[remote['name']] @@ -305,8 +311,8 @@ def fetch_remote(fetcher, remote, seen): continue print('Fetching run', run['branch']) - fetch_remote_run(fetcher, remote, run, remote_state) - fetcher.fetched = True + if fetch_remote_run(fetcher, remote, run, remote_state): + fetcher.fetched = True with open(os.path.join(remote_state['dir'], 'results.json'), "w") as fp: json.dump(manifest, fp) From 54e060c9094e33bffe356b5d3e25853e22235d49 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 31 May 2025 11:14:39 -0700 Subject: [PATCH 341/429] tests: shellcheck: add shellcheck Signed-off-by: Jakub Kicinski --- tests/patch/shellcheck/info.json | 3 ++ tests/patch/shellcheck/shellcheck.sh | 78 ++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 tests/patch/shellcheck/info.json create mode 100755 tests/patch/shellcheck/shellcheck.sh diff --git a/tests/patch/shellcheck/info.json b/tests/patch/shellcheck/info.json new file mode 100644 index 0000000..fa95e9e --- /dev/null +++ b/tests/patch/shellcheck/info.json @@ -0,0 +1,3 @@ +{ + "run": ["shellcheck.sh"] +} diff --git a/tests/patch/shellcheck/shellcheck.sh b/tests/patch/shellcheck/shellcheck.sh new file mode 100755 index 0000000..9e311b8 --- /dev/null +++ b/tests/patch/shellcheck/shellcheck.sh @@ -0,0 +1,78 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 + +HEAD=$(git rev-parse HEAD) +tmpfile_o=$(mktemp) +tmpfile_n=$(mktemp) +rc=0 + +pr() { + echo " ====== $@ ======" | tee -a /dev/stderr +} + +# If it doesn't touch .sh files, don't bother. Ignore created and deleted. +if ! git show --diff-filter=M --pretty="" --name-only HEAD | grep -q -E "\.sh$" +then + echo "No shell scripts touched, skip" >&$DESC_FD + exit 0 +fi + +echo "Redirect to $tmpfile_o and $tmpfile_n" + +echo "Tree base:" +git log -1 --pretty='%h ("%s")' HEAD~ +echo "Now at:" +git log -1 --pretty='%h ("%s")' HEAD + +pr "Checking before the patch" +git checkout -q HEAD~ + +for f in $(git show --diff-filter=M --pretty="" --name-only HEAD | grep -E "\.sh$"); do + ( + echo "Checking $f" + echo + + cd $(dirname $f) + shellcheck -x $(basename $f) | tee -a $tmpfile_o + echo + ) +done + +incumbent=$(grep -i -c "(error)" $tmpfile_o) +incumbent_w=$(grep -i -c "SC[0-9]* (" $tmpfile_o) + +pr "Building the tree with the patch" +git checkout -q $HEAD + +for f in $(git show --diff-filter=AM --pretty="" --name-only HEAD | grep -E "\.sh$"); do + ( + echo "Checking $f" + echo + + cd $(dirname $f) + shellcheck -x $(basename $f) | tee -a $tmpfile_n + echo + ) +done + +current=$(grep -i -c "(error)" $tmpfile_n) +current_w=$(grep -i -c "SC[0-9]* (" $tmpfile_n) + +echo "Errors before: $incumbent (+warn: $incumbent_w) this patch: $current (+warn: $current_w)" >&$DESC_FD + +if [ $current -gt $incumbent ]; then + echo "New errors added" 1>&2 + diff -U 0 $tmpfile_o $tmpfile_n 1>&2 + + rc=1 +fi + +if [ $current_w -gt $incumbent_w ]; then + echo "New warnings added" 1>&2 + + rc=250 +fi + +rm "$tmpfile_o" "$tmpfile_n" + +exit $rc From c0fe53ae533d19c19d2e00955403fb57c3679084 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Tue, 3 Jun 2025 11:37:49 -0700 Subject: [PATCH 342/429] tests: pylint: add pylint Add a pylint integration, we have increasing amount of python.. Signed-off-by: Jakub Kicinski --- tests/patch/pylint/info.json | 3 ++ tests/patch/pylint/pylint.sh | 64 ++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 tests/patch/pylint/info.json create mode 100755 tests/patch/pylint/pylint.sh diff --git a/tests/patch/pylint/info.json b/tests/patch/pylint/info.json new file mode 100644 index 0000000..176fa1e --- /dev/null +++ b/tests/patch/pylint/info.json @@ -0,0 +1,3 @@ +{ + "run": ["pylint.sh"] +} diff --git a/tests/patch/pylint/pylint.sh b/tests/patch/pylint/pylint.sh new file mode 100755 index 0000000..1a4b93a --- /dev/null +++ b/tests/patch/pylint/pylint.sh @@ -0,0 +1,64 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 + +HEAD=$(git rev-parse HEAD) +tmpfile_o=$(mktemp) +tmpfile_n=$(mktemp) +rc=0 + +pr() { + echo " ====== $@ ======" | tee -a /dev/stderr +} + +# If it doesn't touch .py files, don't bother. Ignore created and deleted. +if ! git show --diff-filter=AM --pretty="" --name-only HEAD | grep -q -E "\.py$" +then + echo "No python scripts touched, skip" >&$DESC_FD + exit 0 +fi + +echo "Redirect to $tmpfile_o and $tmpfile_n" + +echo "Tree base:" +git log -1 --pretty='%h ("%s")' HEAD~ +echo "Now at:" +git log -1 --pretty='%h ("%s")' HEAD + +pr "Checking before the patch" +git checkout -q HEAD~ + +for f in $(git show --diff-filter=M --pretty="" --name-only HEAD | grep -E "\.py$"); do + pylint $f | tee -a $tmpfile_o +done + +incumbent=$(grep -i -c ": E[0-9][0-9][0-9][0-9]: " $tmpfile_o) +incumbent_w=$(grep -i -c ": [WC][0-9][0-9][0-9][0-9]: " $tmpfile_o) + +pr "Checking the tree with the patch" +git checkout -q $HEAD + +for f in $(git show --diff-filter=AM --pretty="" --name-only HEAD | grep -E "\.py$"); do + pylint $f | tee -a $tmpfile_n +done + +current=$(grep -i -c ": E[0-9][0-9][0-9][0-9]: " $tmpfile_n) +current_w=$(grep -i -c ": [WC][0-9][0-9][0-9][0-9]: " $tmpfile_n) + +echo "Errors before: $incumbent (+warn: $incumbent_w) this patch: $current (+warn: $current_w)" >&$DESC_FD + +if [ $current -gt $incumbent ]; then + echo "New errors added" 1>&2 + diff -U 0 $tmpfile_o $tmpfile_n 1>&2 + + rc=1 +fi + +if [ $current_w -gt $incumbent_w ]; then + echo "New warnings added" 1>&2 + + rc=250 +fi + +rm "$tmpfile_o" "$tmpfile_n" + +exit $rc From a6d11dfc455cb4ffbe07b0436a8fdd87933c4f6e Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Tue, 3 Jun 2025 11:38:43 -0700 Subject: [PATCH 343/429] tests: shellcheck: run for added files The skipping check is not considering added files. I initially thought we can't test them because there is no before. Then I realized before is empty. But I didn't adjust the skip check. Signed-off-by: Jakub Kicinski --- tests/patch/shellcheck/shellcheck.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/patch/shellcheck/shellcheck.sh b/tests/patch/shellcheck/shellcheck.sh index 9e311b8..e4ae58e 100755 --- a/tests/patch/shellcheck/shellcheck.sh +++ b/tests/patch/shellcheck/shellcheck.sh @@ -11,7 +11,7 @@ pr() { } # If it doesn't touch .sh files, don't bother. Ignore created and deleted. -if ! git show --diff-filter=M --pretty="" --name-only HEAD | grep -q -E "\.sh$" +if ! git show --diff-filter=AM --pretty="" --name-only HEAD | grep -q -E "\.sh$" then echo "No shell scripts touched, skip" >&$DESC_FD exit 0 From 41a20fdbb2ccf5d635f4a3e35faeeb217d889295 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Tue, 3 Jun 2025 11:42:42 -0700 Subject: [PATCH 344/429] tests: create temp files after skip check We have a bunch of skip checks, which prevent tests from running if the test doesn't apply (e.g. shellcheck when commit has no shell scripts). Make sure we create temp files for output only after the check, otherwise we won't delete them. Signed-off-by: Jakub Kicinski --- tests/patch/build_clang_rust/build_clang_rust.sh | 5 +++-- tests/patch/build_tools/build_tools.sh | 5 +++-- tests/patch/pylint/pylint.sh | 5 +++-- tests/patch/shellcheck/shellcheck.sh | 5 +++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/patch/build_clang_rust/build_clang_rust.sh b/tests/patch/build_clang_rust/build_clang_rust.sh index a69b0d9..25b3451 100755 --- a/tests/patch/build_clang_rust/build_clang_rust.sh +++ b/tests/patch/build_clang_rust/build_clang_rust.sh @@ -7,8 +7,6 @@ cc=clang output_dir=build_clang_rust/ ncpu=$(grep -c processor /proc/cpuinfo) build_flags="-Oline -j $ncpu W=1" -tmpfile_o=$(mktemp) -tmpfile_n=$(mktemp) rc=0 prep_config() { @@ -94,6 +92,9 @@ echo "Baseline building the tree" prep_config make LLVM=1 O=$output_dir $build_flags +tmpfile_o=$(mktemp) +tmpfile_n=$(mktemp) + git checkout -q HEAD~ echo "Building the tree before the patch" diff --git a/tests/patch/build_tools/build_tools.sh b/tests/patch/build_tools/build_tools.sh index 5c08db3..efa10aa 100755 --- a/tests/patch/build_tools/build_tools.sh +++ b/tests/patch/build_tools/build_tools.sh @@ -4,8 +4,6 @@ output_dir=build_tools/ ncpu=$(grep -c processor /proc/cpuinfo) build_flags="-Oline -j $ncpu" -tmpfile_o=$(mktemp) -tmpfile_n=$(mktemp) rc=0 pr() { @@ -18,6 +16,9 @@ if ! git diff --name-only HEAD~ | grep -q -E "^(include)|(tools)/"; then exit 0 fi +tmpfile_o=$(mktemp) +tmpfile_n=$(mktemp) + # Looks like tools inherit WERROR, otherwise make O=$output_dir allmodconfig ./scripts/config --file $output_dir/.config -d werror diff --git a/tests/patch/pylint/pylint.sh b/tests/patch/pylint/pylint.sh index 1a4b93a..515fe19 100755 --- a/tests/patch/pylint/pylint.sh +++ b/tests/patch/pylint/pylint.sh @@ -2,8 +2,6 @@ # SPDX-License-Identifier: GPL-2.0 HEAD=$(git rev-parse HEAD) -tmpfile_o=$(mktemp) -tmpfile_n=$(mktemp) rc=0 pr() { @@ -17,6 +15,9 @@ then exit 0 fi +tmpfile_o=$(mktemp) +tmpfile_n=$(mktemp) + echo "Redirect to $tmpfile_o and $tmpfile_n" echo "Tree base:" diff --git a/tests/patch/shellcheck/shellcheck.sh b/tests/patch/shellcheck/shellcheck.sh index e4ae58e..095c689 100755 --- a/tests/patch/shellcheck/shellcheck.sh +++ b/tests/patch/shellcheck/shellcheck.sh @@ -2,8 +2,6 @@ # SPDX-License-Identifier: GPL-2.0 HEAD=$(git rev-parse HEAD) -tmpfile_o=$(mktemp) -tmpfile_n=$(mktemp) rc=0 pr() { @@ -17,6 +15,9 @@ then exit 0 fi +tmpfile_o=$(mktemp) +tmpfile_n=$(mktemp) + echo "Redirect to $tmpfile_o and $tmpfile_n" echo "Tree base:" From 255ee0295a096ee7096bebd9d640388acc590da0 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Tue, 3 Jun 2025 12:00:44 -0700 Subject: [PATCH 345/429] tests: yamllint: add YAML linting Signed-off-by: Jakub Kicinski --- tests/patch/yamllint/info.json | 3 ++ tests/patch/yamllint/yamllint.sh | 65 ++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 tests/patch/yamllint/info.json create mode 100755 tests/patch/yamllint/yamllint.sh diff --git a/tests/patch/yamllint/info.json b/tests/patch/yamllint/info.json new file mode 100644 index 0000000..1dea919 --- /dev/null +++ b/tests/patch/yamllint/info.json @@ -0,0 +1,3 @@ +{ + "run": ["yamllint.sh"] +} diff --git a/tests/patch/yamllint/yamllint.sh b/tests/patch/yamllint/yamllint.sh new file mode 100755 index 0000000..99efb34 --- /dev/null +++ b/tests/patch/yamllint/yamllint.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 + +HEAD=$(git rev-parse HEAD) +rc=0 + +pr() { + echo " ====== $@ ======" | tee -a /dev/stderr +} + +# If it doesn't touch .yaml files, don't bother. Ignore created and deleted. +if ! git show --diff-filter=AM --pretty="" --name-only HEAD | grep -q -E "\.yaml$" +then + echo "No YAML files touched, skip" >&$DESC_FD + exit 0 +fi + +tmpfile_o=$(mktemp) +tmpfile_n=$(mktemp) + +echo "Redirect to $tmpfile_o and $tmpfile_n" + +echo "Tree base:" +git log -1 --pretty='%h ("%s")' HEAD~ +echo "Now at:" +git log -1 --pretty='%h ("%s")' HEAD + +pr "Checking before the patch" +git checkout -q HEAD~ + +for f in $(git show --diff-filter=M --pretty="" --name-only HEAD | grep -E "\.yaml$"); do + yamllint $f | tee -a $tmpfile_o +done + +incumbent=$(grep -i -c " error " $tmpfile_o) +incumbent_w=$(grep -i -c " warning " $tmpfile_o) + +pr "Checking the tree with the patch" +git checkout -q $HEAD + +for f in $(git show --diff-filter=AM --pretty="" --name-only HEAD | grep -E "\.yaml$"); do + yamllint $f | tee -a $tmpfile_n +done + +current=$(grep -i -c " error " $tmpfile_n) +current_w=$(grep -i -c " warning " $tmpfile_n) + +echo "Errors before: $incumbent (+warn: $incumbent_w) this patch: $current (+warn: $current_w)" >&$DESC_FD + +if [ $current -gt $incumbent ]; then + echo "New errors added" 1>&2 + diff -U 0 $tmpfile_o $tmpfile_n 1>&2 + + rc=1 +fi + +if [ $current_w -gt $incumbent_w ]; then + echo "New warnings added" 1>&2 + + rc=250 +fi + +rm "$tmpfile_o" "$tmpfile_n" + +exit $rc From 8f21b09eaab9f43cd06d82ffedd894a44ecef8b5 Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Tue, 3 Jun 2025 21:29:06 +0200 Subject: [PATCH 346/429] tests: use correct HEAD to list files When checking the files before the patch, after a checkout to 'HEAD~', it is required to use the previous HEAD, not the new one to look at the same files and not others. While at it, always use the $HEAD variable, and also fix a related comment + added an extra one to explain the diff. Signed-off-by: Matthieu Baerts (NGI0) --- tests/patch/pylint/pylint.sh | 9 +++++---- tests/patch/shellcheck/shellcheck.sh | 9 +++++---- tests/patch/yamllint/yamllint.sh | 9 +++++---- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/tests/patch/pylint/pylint.sh b/tests/patch/pylint/pylint.sh index 515fe19..6b90b2c 100755 --- a/tests/patch/pylint/pylint.sh +++ b/tests/patch/pylint/pylint.sh @@ -8,8 +8,8 @@ pr() { echo " ====== $@ ======" | tee -a /dev/stderr } -# If it doesn't touch .py files, don't bother. Ignore created and deleted. -if ! git show --diff-filter=AM --pretty="" --name-only HEAD | grep -q -E "\.py$" +# If it doesn't touch .py files, don't bother. Ignore deleted. +if ! git show --diff-filter=AM --pretty="" --name-only "${HEAD}" | grep -q -E "\.py$" then echo "No python scripts touched, skip" >&$DESC_FD exit 0 @@ -28,7 +28,8 @@ git log -1 --pretty='%h ("%s")' HEAD pr "Checking before the patch" git checkout -q HEAD~ -for f in $(git show --diff-filter=M --pretty="" --name-only HEAD | grep -E "\.py$"); do +# Also ignore created, as not present in the parent commit +for f in $(git show --diff-filter=M --pretty="" --name-only "${HEAD}" | grep -E "\.py$"); do pylint $f | tee -a $tmpfile_o done @@ -38,7 +39,7 @@ incumbent_w=$(grep -i -c ": [WC][0-9][0-9][0-9][0-9]: " $tmpfile_o) pr "Checking the tree with the patch" git checkout -q $HEAD -for f in $(git show --diff-filter=AM --pretty="" --name-only HEAD | grep -E "\.py$"); do +for f in $(git show --diff-filter=AM --pretty="" --name-only "${HEAD}" | grep -E "\.py$"); do pylint $f | tee -a $tmpfile_n done diff --git a/tests/patch/shellcheck/shellcheck.sh b/tests/patch/shellcheck/shellcheck.sh index 095c689..a96f648 100755 --- a/tests/patch/shellcheck/shellcheck.sh +++ b/tests/patch/shellcheck/shellcheck.sh @@ -8,8 +8,8 @@ pr() { echo " ====== $@ ======" | tee -a /dev/stderr } -# If it doesn't touch .sh files, don't bother. Ignore created and deleted. -if ! git show --diff-filter=AM --pretty="" --name-only HEAD | grep -q -E "\.sh$" +# If it doesn't touch .sh files, don't bother. Ignore deleted. +if ! git show --diff-filter=AM --pretty="" --name-only "${HEAD}" | grep -q -E "\.sh$" then echo "No shell scripts touched, skip" >&$DESC_FD exit 0 @@ -28,7 +28,8 @@ git log -1 --pretty='%h ("%s")' HEAD pr "Checking before the patch" git checkout -q HEAD~ -for f in $(git show --diff-filter=M --pretty="" --name-only HEAD | grep -E "\.sh$"); do +# Also ignore created, as not present in the parent commit +for f in $(git show --diff-filter=M --pretty="" --name-only "${HEAD}" | grep -E "\.sh$"); do ( echo "Checking $f" echo @@ -45,7 +46,7 @@ incumbent_w=$(grep -i -c "SC[0-9]* (" $tmpfile_o) pr "Building the tree with the patch" git checkout -q $HEAD -for f in $(git show --diff-filter=AM --pretty="" --name-only HEAD | grep -E "\.sh$"); do +for f in $(git show --diff-filter=AM --pretty="" --name-only "${HEAD}" | grep -E "\.sh$"); do ( echo "Checking $f" echo diff --git a/tests/patch/yamllint/yamllint.sh b/tests/patch/yamllint/yamllint.sh index 99efb34..099e9fc 100755 --- a/tests/patch/yamllint/yamllint.sh +++ b/tests/patch/yamllint/yamllint.sh @@ -8,8 +8,8 @@ pr() { echo " ====== $@ ======" | tee -a /dev/stderr } -# If it doesn't touch .yaml files, don't bother. Ignore created and deleted. -if ! git show --diff-filter=AM --pretty="" --name-only HEAD | grep -q -E "\.yaml$" +# If it doesn't touch .yaml files, don't bother. Ignore deleted. +if ! git show --diff-filter=AM --pretty="" --name-only "${HEAD}" | grep -q -E "\.yaml$" then echo "No YAML files touched, skip" >&$DESC_FD exit 0 @@ -28,7 +28,8 @@ git log -1 --pretty='%h ("%s")' HEAD pr "Checking before the patch" git checkout -q HEAD~ -for f in $(git show --diff-filter=M --pretty="" --name-only HEAD | grep -E "\.yaml$"); do +# Also ignore created, as not present in the parent commit +for f in $(git show --diff-filter=M --pretty="" --name-only "${HEAD}" | grep -E "\.yaml$"); do yamllint $f | tee -a $tmpfile_o done @@ -38,7 +39,7 @@ incumbent_w=$(grep -i -c " warning " $tmpfile_o) pr "Checking the tree with the patch" git checkout -q $HEAD -for f in $(git show --diff-filter=AM --pretty="" --name-only HEAD | grep -E "\.yaml$"); do +for f in $(git show --diff-filter=AM --pretty="" --name-only "${HEAD}" | grep -E "\.yaml$"); do yamllint $f | tee -a $tmpfile_n done From 238e7a9338a2437580f6e1a65d6b3b7223d72713 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Tue, 3 Jun 2025 18:12:24 -0700 Subject: [PATCH 347/429] contest: virtio: don't use the same addresses as ksft In my infinite wisdom I copied the addresses which netdevsim uses in ksft. But some tests assume these addresses are unused and want to use these them on tunnels. Signed-off-by: Jakub Kicinski --- contest/scripts/vm-virtio-loop.sh | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/contest/scripts/vm-virtio-loop.sh b/contest/scripts/vm-virtio-loop.sh index 63d8a82..1ebb425 100755 --- a/contest/scripts/vm-virtio-loop.sh +++ b/contest/scripts/vm-virtio-loop.sh @@ -22,15 +22,15 @@ ip link set dev $IFC0 up ip -netns ns-remote link set dev $IFC1 up export NETIF=$IFC0 -ip addr add dev $IFC0 192.0.2.1/24 -ip -netns ns-remote addr add dev $IFC1 192.0.2.2/24 -export LOCAL_V4=192.0.2.1 -export REMOTE_V4=192.0.2.2 - -ip addr add dev $IFC0 2001:db8::1/64 nodad -ip -netns ns-remote addr add dev $IFC1 2001:db8::2/64 nodad -export LOCAL_V6=2001:db8::1 -export REMOTE_V6=2001:db8::2 +ip addr add dev $IFC0 192.0.3.1/24 +ip -netns ns-remote addr add dev $IFC1 192.0.3.2/24 +export LOCAL_V4=192.0.3.1 +export REMOTE_V4=192.0.3.2 + +ip addr add dev $IFC0 2001:db8:1::1/64 nodad +ip -netns ns-remote addr add dev $IFC1 2001:db8:1::2/64 nodad +export LOCAL_V6=2001:db8:1::1 +export REMOTE_V6=2001:db8:1::2 sysctl -w net.ipv6.conf.$IFC0.keep_addr_on_down=1 # We don't bring remote down, it'd break remote via SSH From 4c8449dbaf35c8eb2d740c2076126a34e9cb8d00 Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Wed, 4 Jun 2025 13:29:54 +0200 Subject: [PATCH 348/429] tests: shellcheck: restrict warnings to warnings only And not lower severities like info and style. For the moment, there are too many info messages, e.g. not using double quotes everywhere or having functions that are not directly called. Let's not push people to fix those and create massive cleanup patches not fixing actual problems. Signed-off-by: Matthieu Baerts (NGI0) --- tests/patch/shellcheck/shellcheck.sh | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/patch/shellcheck/shellcheck.sh b/tests/patch/shellcheck/shellcheck.sh index a96f648..03a9c99 100755 --- a/tests/patch/shellcheck/shellcheck.sh +++ b/tests/patch/shellcheck/shellcheck.sh @@ -40,8 +40,10 @@ for f in $(git show --diff-filter=M --pretty="" --name-only "${HEAD}" | grep -E ) done -incumbent=$(grep -i -c "(error)" $tmpfile_o) -incumbent_w=$(grep -i -c "SC[0-9]* (" $tmpfile_o) +# ex: SC3045 (warning): In POSIX sh, printf -v is undefined. +# severity: error, warning, info, style +incumbent=$(grep -c " (error):" $tmpfile_o) +incumbent_w=$(grep -c " (warning):" $tmpfile_o) pr "Building the tree with the patch" git checkout -q $HEAD @@ -57,8 +59,9 @@ for f in $(git show --diff-filter=AM --pretty="" --name-only "${HEAD}" | grep -E ) done -current=$(grep -i -c "(error)" $tmpfile_n) -current_w=$(grep -i -c "SC[0-9]* (" $tmpfile_n) +# severity: error, warning, info, style +current=$(grep -c " (error):" $tmpfile_n) +current_w=$(grep -c " (warning):" $tmpfile_n) echo "Errors before: $incumbent (+warn: $incumbent_w) this patch: $current (+warn: $current_w)" >&$DESC_FD From 3a489d42f295398e311330b384bc2b721fec7782 Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Wed, 4 Jun 2025 14:53:06 +0200 Subject: [PATCH 349/429] tests: shellcheck: keep files compliant If a file was shellcheck compliant before or is new, it is interesting to tell the reviewers when this status changes or if the new file is not shellcheck compliant from the beginning. A new dedicated file is used for each modified shell script, using a hash of the file path to cope with files with the same base name. If the status is different than before, warnings + info + style are added to the errors count. Note: it is not clear if the log message about a file no longer being shellcheck compliant any more should be more visible or not, i.e. sent to $DESC_FD. Signed-off-by: Matthieu Baerts (NGI0) --- tests/patch/shellcheck/shellcheck.sh | 35 ++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/tests/patch/shellcheck/shellcheck.sh b/tests/patch/shellcheck/shellcheck.sh index 03a9c99..83b33e8 100755 --- a/tests/patch/shellcheck/shellcheck.sh +++ b/tests/patch/shellcheck/shellcheck.sh @@ -31,11 +31,14 @@ git checkout -q HEAD~ # Also ignore created, as not present in the parent commit for f in $(git show --diff-filter=M --pretty="" --name-only "${HEAD}" | grep -E "\.sh$"); do ( - echo "Checking $f" + sha=$(echo $f | sha256sum | awk '{print $1}') + echo "Checking $f - $sha" echo cd $(dirname $f) - shellcheck -x $(basename $f) | tee -a $tmpfile_o + sha="${tmpfile_o}_${sha}" + rm -f "${sha}" + shellcheck -x $(basename $f) | tee -a "${tmpfile_o}" "${sha}" echo ) done @@ -50,11 +53,14 @@ git checkout -q $HEAD for f in $(git show --diff-filter=AM --pretty="" --name-only "${HEAD}" | grep -E "\.sh$"); do ( - echo "Checking $f" + sha=$(echo $f | sha256sum | awk '{print $1}') + echo "Checking $f - $sha" echo cd $(dirname $f) - shellcheck -x $(basename $f) | tee -a $tmpfile_n + sha="${tmpfile_n}_${sha}" + rm -f "${sha}" + shellcheck -x $(basename $f) | tee -a "${tmpfile_n}" "${sha}" echo ) done @@ -63,6 +69,25 @@ done current=$(grep -c " (error):" $tmpfile_n) current_w=$(grep -c " (warning):" $tmpfile_n) +# if a file was compliant before or is new, mark everything as error to keep it good. +for f in "${tmpfile_n}_"*; do + [ ! -s "${f}" ] && continue # still compliant + + sha="${f:${#tmpfile_n}+1}" + old="${tmpfile_o}_${sha}" + [ -s "${old}" ] && continue # wasn't compliant + + fname=$(head -n2 "${f}" | tail -n1 | sed "s/^In \(\S\+\.sh\) line [0-9]\+:/\1/g") + if [ -f "${old}" ]; then + echo "${fname} was shellcheck compliant, not anymore" 1>&2 + else + echo "${fname} is a new file, but not shellcheck compliant" 1>&2 + fi + + extra=$(grep -c -E " \((warning|info|style)\):" "${f}") + current=$((current + extra)) +done + echo "Errors before: $incumbent (+warn: $incumbent_w) this patch: $current (+warn: $current_w)" >&$DESC_FD if [ $current -gt $incumbent ]; then @@ -78,6 +103,6 @@ if [ $current_w -gt $incumbent_w ]; then rc=250 fi -rm "$tmpfile_o" "$tmpfile_n" +rm "$tmpfile_o"* "$tmpfile_n"* exit $rc From 58e7f5b042de555bf34d1dc1441074627f91d70c Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Wed, 4 Jun 2025 16:35:36 +0200 Subject: [PATCH 350/429] tests: error rc in case of errors and warnings Before, in case of new errors and new warnings, the script was exiting with rc=250 (warning) instead of rc=1 (error). This is fixed simply by looking at the warnings counter before the errors one, so rc=1 will be set last. Signed-off-by: Matthieu Baerts (NGI0) --- tests/patch/pylint/pylint.sh | 12 ++++++------ tests/patch/shellcheck/shellcheck.sh | 12 ++++++------ tests/patch/yamllint/yamllint.sh | 12 ++++++------ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/patch/pylint/pylint.sh b/tests/patch/pylint/pylint.sh index 6b90b2c..59d2ad3 100755 --- a/tests/patch/pylint/pylint.sh +++ b/tests/patch/pylint/pylint.sh @@ -48,6 +48,12 @@ current_w=$(grep -i -c ": [WC][0-9][0-9][0-9][0-9]: " $tmpfile_n) echo "Errors before: $incumbent (+warn: $incumbent_w) this patch: $current (+warn: $current_w)" >&$DESC_FD +if [ $current_w -gt $incumbent_w ]; then + echo "New warnings added" 1>&2 + + rc=250 +fi + if [ $current -gt $incumbent ]; then echo "New errors added" 1>&2 diff -U 0 $tmpfile_o $tmpfile_n 1>&2 @@ -55,12 +61,6 @@ if [ $current -gt $incumbent ]; then rc=1 fi -if [ $current_w -gt $incumbent_w ]; then - echo "New warnings added" 1>&2 - - rc=250 -fi - rm "$tmpfile_o" "$tmpfile_n" exit $rc diff --git a/tests/patch/shellcheck/shellcheck.sh b/tests/patch/shellcheck/shellcheck.sh index 83b33e8..d0a574a 100755 --- a/tests/patch/shellcheck/shellcheck.sh +++ b/tests/patch/shellcheck/shellcheck.sh @@ -90,6 +90,12 @@ done echo "Errors before: $incumbent (+warn: $incumbent_w) this patch: $current (+warn: $current_w)" >&$DESC_FD +if [ $current_w -gt $incumbent_w ]; then + echo "New warnings added" 1>&2 + + rc=250 +fi + if [ $current -gt $incumbent ]; then echo "New errors added" 1>&2 diff -U 0 $tmpfile_o $tmpfile_n 1>&2 @@ -97,12 +103,6 @@ if [ $current -gt $incumbent ]; then rc=1 fi -if [ $current_w -gt $incumbent_w ]; then - echo "New warnings added" 1>&2 - - rc=250 -fi - rm "$tmpfile_o"* "$tmpfile_n"* exit $rc diff --git a/tests/patch/yamllint/yamllint.sh b/tests/patch/yamllint/yamllint.sh index 099e9fc..51f80a9 100755 --- a/tests/patch/yamllint/yamllint.sh +++ b/tests/patch/yamllint/yamllint.sh @@ -48,6 +48,12 @@ current_w=$(grep -i -c " warning " $tmpfile_n) echo "Errors before: $incumbent (+warn: $incumbent_w) this patch: $current (+warn: $current_w)" >&$DESC_FD +if [ $current_w -gt $incumbent_w ]; then + echo "New warnings added" 1>&2 + + rc=250 +fi + if [ $current -gt $incumbent ]; then echo "New errors added" 1>&2 diff -U 0 $tmpfile_o $tmpfile_n 1>&2 @@ -55,12 +61,6 @@ if [ $current -gt $incumbent ]; then rc=1 fi -if [ $current_w -gt $incumbent_w ]; then - echo "New warnings added" 1>&2 - - rc=250 -fi - rm "$tmpfile_o" "$tmpfile_n" exit $rc From cb66c52a0b499d4e3dc68035026cde43264528c2 Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Wed, 4 Jun 2025 16:47:19 +0200 Subject: [PATCH 351/429] tests: pylint/shellcheck/yamllint: shellcheck compliant It sounds good to have the script checking for shellcheck compliance to be shellcheck compliant :) While at it, pylint and yamllint scripts have also be modified because they are inspired by the shellcheck one. Before the modifications, shellcheck was complaining about: 53 note: Double quote to prevent globbing and word splitting. [SC2086] 4 warning: Quote this to prevent word splitting. [SC2046] 3 error: Argument mixes string and array. Use * or separate argument. [SC2145] 2 warning: Use 'cd ... || exit' or 'cd ... || return' in case cd fails. [SC2164] Not any more. Also fix the indentation around the 'diff' at the end of each script, and in shellcheck.sh, s/Building/Checking/ like the others. Signed-off-by: Matthieu Baerts (NGI0) --- tests/patch/pylint/pylint.sh | 30 ++++++++++----------- tests/patch/shellcheck/shellcheck.sh | 40 ++++++++++++++-------------- tests/patch/yamllint/yamllint.sh | 30 ++++++++++----------- 3 files changed, 50 insertions(+), 50 deletions(-) diff --git a/tests/patch/pylint/pylint.sh b/tests/patch/pylint/pylint.sh index 59d2ad3..f8b3980 100755 --- a/tests/patch/pylint/pylint.sh +++ b/tests/patch/pylint/pylint.sh @@ -5,13 +5,13 @@ HEAD=$(git rev-parse HEAD) rc=0 pr() { - echo " ====== $@ ======" | tee -a /dev/stderr + echo " ====== $* ======" | tee -a /dev/stderr } # If it doesn't touch .py files, don't bother. Ignore deleted. if ! git show --diff-filter=AM --pretty="" --name-only "${HEAD}" | grep -q -E "\.py$" then - echo "No python scripts touched, skip" >&$DESC_FD + echo "No python scripts touched, skip" >&"$DESC_FD" exit 0 fi @@ -30,35 +30,35 @@ git checkout -q HEAD~ # Also ignore created, as not present in the parent commit for f in $(git show --diff-filter=M --pretty="" --name-only "${HEAD}" | grep -E "\.py$"); do - pylint $f | tee -a $tmpfile_o + pylint "$f" | tee -a "$tmpfile_o" done -incumbent=$(grep -i -c ": E[0-9][0-9][0-9][0-9]: " $tmpfile_o) -incumbent_w=$(grep -i -c ": [WC][0-9][0-9][0-9][0-9]: " $tmpfile_o) +incumbent=$(grep -i -c ": E[0-9][0-9][0-9][0-9]: " "$tmpfile_o") +incumbent_w=$(grep -i -c ": [WC][0-9][0-9][0-9][0-9]: " "$tmpfile_o") pr "Checking the tree with the patch" -git checkout -q $HEAD +git checkout -q "$HEAD" for f in $(git show --diff-filter=AM --pretty="" --name-only "${HEAD}" | grep -E "\.py$"); do - pylint $f | tee -a $tmpfile_n + pylint "$f" | tee -a "$tmpfile_n" done -current=$(grep -i -c ": E[0-9][0-9][0-9][0-9]: " $tmpfile_n) -current_w=$(grep -i -c ": [WC][0-9][0-9][0-9][0-9]: " $tmpfile_n) +current=$(grep -i -c ": E[0-9][0-9][0-9][0-9]: " "$tmpfile_n") +current_w=$(grep -i -c ": [WC][0-9][0-9][0-9][0-9]: " "$tmpfile_n") -echo "Errors before: $incumbent (+warn: $incumbent_w) this patch: $current (+warn: $current_w)" >&$DESC_FD +echo "Errors before: $incumbent (+warn: $incumbent_w) this patch: $current (+warn: $current_w)" >&"$DESC_FD" -if [ $current_w -gt $incumbent_w ]; then +if [ "$current_w" -gt "$incumbent_w" ]; then echo "New warnings added" 1>&2 rc=250 fi -if [ $current -gt $incumbent ]; then - echo "New errors added" 1>&2 - diff -U 0 $tmpfile_o $tmpfile_n 1>&2 +if [ "$current" -gt "$incumbent" ]; then + echo "New errors added" 1>&2 + diff -U 0 "$tmpfile_o" "$tmpfile_n" 1>&2 - rc=1 + rc=1 fi rm "$tmpfile_o" "$tmpfile_n" diff --git a/tests/patch/shellcheck/shellcheck.sh b/tests/patch/shellcheck/shellcheck.sh index d0a574a..1d647a0 100755 --- a/tests/patch/shellcheck/shellcheck.sh +++ b/tests/patch/shellcheck/shellcheck.sh @@ -5,13 +5,13 @@ HEAD=$(git rev-parse HEAD) rc=0 pr() { - echo " ====== $@ ======" | tee -a /dev/stderr + echo " ====== $* ======" | tee -a /dev/stderr } # If it doesn't touch .sh files, don't bother. Ignore deleted. if ! git show --diff-filter=AM --pretty="" --name-only "${HEAD}" | grep -q -E "\.sh$" then - echo "No shell scripts touched, skip" >&$DESC_FD + echo "No shell scripts touched, skip" >&"$DESC_FD" exit 0 fi @@ -31,43 +31,43 @@ git checkout -q HEAD~ # Also ignore created, as not present in the parent commit for f in $(git show --diff-filter=M --pretty="" --name-only "${HEAD}" | grep -E "\.sh$"); do ( - sha=$(echo $f | sha256sum | awk '{print $1}') + sha=$(echo "$f" | sha256sum | awk '{print $1}') echo "Checking $f - $sha" echo - cd $(dirname $f) + cd "$(dirname "$f")" || exit 1 sha="${tmpfile_o}_${sha}" rm -f "${sha}" - shellcheck -x $(basename $f) | tee -a "${tmpfile_o}" "${sha}" + shellcheck -x "$(basename "$f")" | tee -a "${tmpfile_o}" "${sha}" echo ) done # ex: SC3045 (warning): In POSIX sh, printf -v is undefined. # severity: error, warning, info, style -incumbent=$(grep -c " (error):" $tmpfile_o) -incumbent_w=$(grep -c " (warning):" $tmpfile_o) +incumbent=$(grep -c " (error):" "$tmpfile_o") +incumbent_w=$(grep -c " (warning):" "$tmpfile_o") -pr "Building the tree with the patch" -git checkout -q $HEAD +pr "Checking the tree with the patch" +git checkout -q "$HEAD" for f in $(git show --diff-filter=AM --pretty="" --name-only "${HEAD}" | grep -E "\.sh$"); do ( - sha=$(echo $f | sha256sum | awk '{print $1}') + sha=$(echo "$f" | sha256sum | awk '{print $1}') echo "Checking $f - $sha" echo - cd $(dirname $f) + cd "$(dirname "$f")" || exit 1 sha="${tmpfile_n}_${sha}" rm -f "${sha}" - shellcheck -x $(basename $f) | tee -a "${tmpfile_n}" "${sha}" + shellcheck -x "$(basename "$f")" | tee -a "${tmpfile_n}" "${sha}" echo ) done # severity: error, warning, info, style -current=$(grep -c " (error):" $tmpfile_n) -current_w=$(grep -c " (warning):" $tmpfile_n) +current=$(grep -c " (error):" "$tmpfile_n") +current_w=$(grep -c " (warning):" "$tmpfile_n") # if a file was compliant before or is new, mark everything as error to keep it good. for f in "${tmpfile_n}_"*; do @@ -88,19 +88,19 @@ for f in "${tmpfile_n}_"*; do current=$((current + extra)) done -echo "Errors before: $incumbent (+warn: $incumbent_w) this patch: $current (+warn: $current_w)" >&$DESC_FD +echo "Errors before: $incumbent (+warn: $incumbent_w) this patch: $current (+warn: $current_w)" >&"$DESC_FD" -if [ $current_w -gt $incumbent_w ]; then +if [ "$current_w" -gt "$incumbent_w" ]; then echo "New warnings added" 1>&2 rc=250 fi -if [ $current -gt $incumbent ]; then - echo "New errors added" 1>&2 - diff -U 0 $tmpfile_o $tmpfile_n 1>&2 +if [ "$current" -gt "$incumbent" ]; then + echo "New errors added" 1>&2 + diff -U 0 "$tmpfile_o" "$tmpfile_n" 1>&2 - rc=1 + rc=1 fi rm "$tmpfile_o"* "$tmpfile_n"* diff --git a/tests/patch/yamllint/yamllint.sh b/tests/patch/yamllint/yamllint.sh index 51f80a9..2688ccc 100755 --- a/tests/patch/yamllint/yamllint.sh +++ b/tests/patch/yamllint/yamllint.sh @@ -5,13 +5,13 @@ HEAD=$(git rev-parse HEAD) rc=0 pr() { - echo " ====== $@ ======" | tee -a /dev/stderr + echo " ====== $* ======" | tee -a /dev/stderr } # If it doesn't touch .yaml files, don't bother. Ignore deleted. if ! git show --diff-filter=AM --pretty="" --name-only "${HEAD}" | grep -q -E "\.yaml$" then - echo "No YAML files touched, skip" >&$DESC_FD + echo "No YAML files touched, skip" >&"$DESC_FD" exit 0 fi @@ -30,35 +30,35 @@ git checkout -q HEAD~ # Also ignore created, as not present in the parent commit for f in $(git show --diff-filter=M --pretty="" --name-only "${HEAD}" | grep -E "\.yaml$"); do - yamllint $f | tee -a $tmpfile_o + yamllint "$f" | tee -a "$tmpfile_o" done -incumbent=$(grep -i -c " error " $tmpfile_o) -incumbent_w=$(grep -i -c " warning " $tmpfile_o) +incumbent=$(grep -i -c " error " "$tmpfile_o") +incumbent_w=$(grep -i -c " warning " "$tmpfile_o") pr "Checking the tree with the patch" -git checkout -q $HEAD +git checkout -q "$HEAD" for f in $(git show --diff-filter=AM --pretty="" --name-only "${HEAD}" | grep -E "\.yaml$"); do - yamllint $f | tee -a $tmpfile_n + yamllint "$f" | tee -a "$tmpfile_n" done -current=$(grep -i -c " error " $tmpfile_n) -current_w=$(grep -i -c " warning " $tmpfile_n) +current=$(grep -i -c " error " "$tmpfile_n") +current_w=$(grep -i -c " warning " "$tmpfile_n") -echo "Errors before: $incumbent (+warn: $incumbent_w) this patch: $current (+warn: $current_w)" >&$DESC_FD +echo "Errors before: $incumbent (+warn: $incumbent_w) this patch: $current (+warn: $current_w)" >&"$DESC_FD" -if [ $current_w -gt $incumbent_w ]; then +if [ "$current_w" -gt "$incumbent_w" ]; then echo "New warnings added" 1>&2 rc=250 fi -if [ $current -gt $incumbent ]; then - echo "New errors added" 1>&2 - diff -U 0 $tmpfile_o $tmpfile_n 1>&2 +if [ "$current" -gt "$incumbent" ]; then + echo "New errors added" 1>&2 + diff -U 0 "$tmpfile_o" "$tmpfile_n" 1>&2 - rc=1 + rc=1 fi rm "$tmpfile_o" "$tmpfile_n" From 39353c0acb7b1ae3df515c45e772d126838c5548 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 5 Jun 2025 09:29:45 -0700 Subject: [PATCH 352/429] tests: kdoc: fix warnings when new files are added Use the git incantation we used for linters to avoid trying to include new files in the "before" counts. Signed-off-by: Jakub Kicinski --- tests/patch/kdoc/kdoc.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/patch/kdoc/kdoc.sh b/tests/patch/kdoc/kdoc.sh index ac36d1b..63ae6eb 100755 --- a/tests/patch/kdoc/kdoc.sh +++ b/tests/patch/kdoc/kdoc.sh @@ -8,20 +8,21 @@ tmpfile_o=$(mktemp) tmpfile_n=$(mktemp) rc=0 -files=$(git diff HEAD^ --pretty= --name-only) +files_mod=$(git show --diff-filter=M --pretty="" --name-only "HEAD") +files_all=$(git show --diff-filter=AM --pretty="" --name-only "HEAD") HEAD=$(git rev-parse HEAD) echo "Checking the tree before the patch" git checkout -q HEAD~ -./scripts/kernel-doc -Wall -none $files 2> >(tee $tmpfile_o >&2) +./scripts/kernel-doc -Wall -none $files_mod 2> >(tee $tmpfile_o >&2) incumbent=$(grep -v 'Error: Cannot open file ' $tmpfile_o | wc -l) echo "Checking the tree with the patch" git checkout -q $HEAD -./scripts/kernel-doc -Wall -none $files 2> >(tee $tmpfile_n >&2) +./scripts/kernel-doc -Wall -none $files_all 2> >(tee $tmpfile_n >&2) current=$(grep -v 'Error: Cannot open file ' $tmpfile_n | wc -l) From 5b2e098a5d83c744581374c40cae354fe0ab8649 Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Mon, 9 Jun 2025 10:26:18 +0200 Subject: [PATCH 353/429] tests: lint: print versions The linters might show different results depending on the version. Then, it looks interesting to print these versions to help people reproducing issues on their side. If there is an issue to print the version, exit with an error: this will also help to identify if the linter is missing or if there is something wrong with it. Signed-off-by: Matthieu Baerts (NGI0) --- tests/patch/pylint/pylint.sh | 2 ++ tests/patch/shellcheck/shellcheck.sh | 2 ++ tests/patch/yamllint/yamllint.sh | 2 ++ 3 files changed, 6 insertions(+) diff --git a/tests/patch/pylint/pylint.sh b/tests/patch/pylint/pylint.sh index f8b3980..e50e83f 100755 --- a/tests/patch/pylint/pylint.sh +++ b/tests/patch/pylint/pylint.sh @@ -15,6 +15,8 @@ then exit 0 fi +pylint --version || exit 1 + tmpfile_o=$(mktemp) tmpfile_n=$(mktemp) diff --git a/tests/patch/shellcheck/shellcheck.sh b/tests/patch/shellcheck/shellcheck.sh index 1d647a0..54b6b70 100755 --- a/tests/patch/shellcheck/shellcheck.sh +++ b/tests/patch/shellcheck/shellcheck.sh @@ -15,6 +15,8 @@ then exit 0 fi +shellcheck --version || exit 1 + tmpfile_o=$(mktemp) tmpfile_n=$(mktemp) diff --git a/tests/patch/yamllint/yamllint.sh b/tests/patch/yamllint/yamllint.sh index 2688ccc..dd25314 100755 --- a/tests/patch/yamllint/yamllint.sh +++ b/tests/patch/yamllint/yamllint.sh @@ -15,6 +15,8 @@ then exit 0 fi +yamllint --version || exit 1 + tmpfile_o=$(mktemp) tmpfile_n=$(mktemp) From 9361c52fcfc6f40ca7393bc8262e973663950621 Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Mon, 9 Jun 2025 10:41:44 +0200 Subject: [PATCH 354/429] tests: shellcheck: log shellcheck compliant files To be clearer and to make sure the script is working properly. Signed-off-by: Matthieu Baerts (NGI0) --- tests/patch/shellcheck/shellcheck.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/patch/shellcheck/shellcheck.sh b/tests/patch/shellcheck/shellcheck.sh index 54b6b70..d6fe83c 100755 --- a/tests/patch/shellcheck/shellcheck.sh +++ b/tests/patch/shellcheck/shellcheck.sh @@ -73,9 +73,9 @@ current_w=$(grep -c " (warning):" "$tmpfile_n") # if a file was compliant before or is new, mark everything as error to keep it good. for f in "${tmpfile_n}_"*; do - [ ! -s "${f}" ] && continue # still compliant - sha="${f:${#tmpfile_n}+1}" + [ ! -s "${f}" ] && echo "${sha} is shellcheck compliant" && continue + old="${tmpfile_o}_${sha}" [ -s "${old}" ] && continue # wasn't compliant From 136a1134f4e0a59b655a8b249843356f442a84ce Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Mon, 9 Jun 2025 10:52:25 +0200 Subject: [PATCH 355/429] tests: shellcheck: print full path in logs It is clearer, and helpful when multiple files have the same base name. Even more when we only have the SHA to print. To get that, we can simply use an associative array variable. While at it, move some code out of the sub-shells, the same way before and after the patch, to only use these sub-shells when changing directories. Signed-off-by: Matthieu Baerts (NGI0) --- tests/patch/shellcheck/shellcheck.sh | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/tests/patch/shellcheck/shellcheck.sh b/tests/patch/shellcheck/shellcheck.sh index d6fe83c..328fd64 100755 --- a/tests/patch/shellcheck/shellcheck.sh +++ b/tests/patch/shellcheck/shellcheck.sh @@ -32,11 +32,11 @@ git checkout -q HEAD~ # Also ignore created, as not present in the parent commit for f in $(git show --diff-filter=M --pretty="" --name-only "${HEAD}" | grep -E "\.sh$"); do - ( - sha=$(echo "$f" | sha256sum | awk '{print $1}') - echo "Checking $f - $sha" - echo + sha=$(echo "$f" | sha256sum | awk '{print $1}') + echo "Checking $f - $sha" + echo + ( cd "$(dirname "$f")" || exit 1 sha="${tmpfile_o}_${sha}" rm -f "${sha}" @@ -53,12 +53,14 @@ incumbent_w=$(grep -c " (warning):" "$tmpfile_o") pr "Checking the tree with the patch" git checkout -q "$HEAD" +declare -A files for f in $(git show --diff-filter=AM --pretty="" --name-only "${HEAD}" | grep -E "\.sh$"); do - ( - sha=$(echo "$f" | sha256sum | awk '{print $1}') - echo "Checking $f - $sha" - echo + sha=$(echo "$f" | sha256sum | awk '{print $1}') + files[${sha}]="${f}" + echo "Checking $f - $sha" + echo + ( cd "$(dirname "$f")" || exit 1 sha="${tmpfile_n}_${sha}" rm -f "${sha}" @@ -74,16 +76,16 @@ current_w=$(grep -c " (warning):" "$tmpfile_n") # if a file was compliant before or is new, mark everything as error to keep it good. for f in "${tmpfile_n}_"*; do sha="${f:${#tmpfile_n}+1}" - [ ! -s "${f}" ] && echo "${sha} is shellcheck compliant" && continue + fpath="${files[${sha}]}" + [ ! -s "${f}" ] && echo "${fpath} is shellcheck compliant" && continue old="${tmpfile_o}_${sha}" [ -s "${old}" ] && continue # wasn't compliant - fname=$(head -n2 "${f}" | tail -n1 | sed "s/^In \(\S\+\.sh\) line [0-9]\+:/\1/g") if [ -f "${old}" ]; then - echo "${fname} was shellcheck compliant, not anymore" 1>&2 + echo "${fpath} was shellcheck compliant, not anymore" 1>&2 else - echo "${fname} is a new file, but not shellcheck compliant" 1>&2 + echo "${fpath} is a new file, but not shellcheck compliant" 1>&2 fi extra=$(grep -c -E " \((warning|info|style)\):" "${f}") From a77f070db0b7511316fe1fdc37a4e46f12ccee26 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Tue, 10 Jun 2025 14:31:03 -0700 Subject: [PATCH 356/429] tests: linters: generate diff on warnings Signed-off-by: Jakub Kicinski --- tests/patch/pylint/pylint.sh | 11 +++++------ tests/patch/shellcheck/shellcheck.sh | 11 +++++------ tests/patch/yamllint/yamllint.sh | 11 +++++------ 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/tests/patch/pylint/pylint.sh b/tests/patch/pylint/pylint.sh index e50e83f..bac6df8 100755 --- a/tests/patch/pylint/pylint.sh +++ b/tests/patch/pylint/pylint.sh @@ -50,17 +50,16 @@ current_w=$(grep -i -c ": [WC][0-9][0-9][0-9][0-9]: " "$tmpfile_n") echo "Errors before: $incumbent (+warn: $incumbent_w) this patch: $current (+warn: $current_w)" >&"$DESC_FD" -if [ "$current_w" -gt "$incumbent_w" ]; then - echo "New warnings added" 1>&2 - - rc=250 -fi - if [ "$current" -gt "$incumbent" ]; then echo "New errors added" 1>&2 diff -U 0 "$tmpfile_o" "$tmpfile_n" 1>&2 rc=1 +elif [ "$current_w" -gt "$incumbent_w" ]; then + echo "New warnings added" 1>&2 + diff -U 0 "$tmpfile_o" "$tmpfile_n" 1>&2 + + rc=250 fi rm "$tmpfile_o" "$tmpfile_n" diff --git a/tests/patch/shellcheck/shellcheck.sh b/tests/patch/shellcheck/shellcheck.sh index 328fd64..41e4976 100755 --- a/tests/patch/shellcheck/shellcheck.sh +++ b/tests/patch/shellcheck/shellcheck.sh @@ -94,17 +94,16 @@ done echo "Errors before: $incumbent (+warn: $incumbent_w) this patch: $current (+warn: $current_w)" >&"$DESC_FD" -if [ "$current_w" -gt "$incumbent_w" ]; then - echo "New warnings added" 1>&2 - - rc=250 -fi - if [ "$current" -gt "$incumbent" ]; then echo "New errors added" 1>&2 diff -U 0 "$tmpfile_o" "$tmpfile_n" 1>&2 rc=1 +elif [ "$current_w" -gt "$incumbent_w" ]; then + echo "New warnings added" 1>&2 + diff -U 0 "$tmpfile_o" "$tmpfile_n" 1>&2 + + rc=250 fi rm "$tmpfile_o"* "$tmpfile_n"* diff --git a/tests/patch/yamllint/yamllint.sh b/tests/patch/yamllint/yamllint.sh index dd25314..ffb401a 100755 --- a/tests/patch/yamllint/yamllint.sh +++ b/tests/patch/yamllint/yamllint.sh @@ -50,17 +50,16 @@ current_w=$(grep -i -c " warning " "$tmpfile_n") echo "Errors before: $incumbent (+warn: $incumbent_w) this patch: $current (+warn: $current_w)" >&"$DESC_FD" -if [ "$current_w" -gt "$incumbent_w" ]; then - echo "New warnings added" 1>&2 - - rc=250 -fi - if [ "$current" -gt "$incumbent" ]; then echo "New errors added" 1>&2 diff -U 0 "$tmpfile_o" "$tmpfile_n" 1>&2 rc=1 +elif [ "$current_w" -gt "$incumbent_w" ]; then + echo "New warnings added" 1>&2 + diff -U 0 "$tmpfile_o" "$tmpfile_n" 1>&2 + + rc=250 fi rm "$tmpfile_o" "$tmpfile_n" From 23b74dd194413a5a8a7183ee48d885bdb96aa183 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 13 Jun 2025 17:25:53 -0700 Subject: [PATCH 357/429] tests: shellcheck: disable unreachable code warning Signed-off-by: Jakub Kicinski --- tests/patch/shellcheck/shellcheck.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/patch/shellcheck/shellcheck.sh b/tests/patch/shellcheck/shellcheck.sh index 41e4976..8a93c03 100755 --- a/tests/patch/shellcheck/shellcheck.sh +++ b/tests/patch/shellcheck/shellcheck.sh @@ -4,6 +4,9 @@ HEAD=$(git rev-parse HEAD) rc=0 +# SC2317 = unreachable code, gets confused by test case definitions +SC_FLAGS="-x -e SC2317" + pr() { echo " ====== $* ======" | tee -a /dev/stderr } @@ -40,7 +43,7 @@ for f in $(git show --diff-filter=M --pretty="" --name-only "${HEAD}" | grep -E cd "$(dirname "$f")" || exit 1 sha="${tmpfile_o}_${sha}" rm -f "${sha}" - shellcheck -x "$(basename "$f")" | tee -a "${tmpfile_o}" "${sha}" + shellcheck $SC_FLAGS "$(basename "$f")" | tee -a "${tmpfile_o}" "${sha}" echo ) done @@ -64,7 +67,7 @@ for f in $(git show --diff-filter=AM --pretty="" --name-only "${HEAD}" | grep -E cd "$(dirname "$f")" || exit 1 sha="${tmpfile_n}_${sha}" rm -f "${sha}" - shellcheck -x "$(basename "$f")" | tee -a "${tmpfile_n}" "${sha}" + shellcheck $SC_FLAGS "$(basename "$f")" | tee -a "${tmpfile_n}" "${sha}" echo ) done From 00851a7bab1ec7efef5c3ecae2e491e8105db3c7 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 14 Jun 2025 11:50:28 -0700 Subject: [PATCH 358/429] tests: build_tools: exclude more broken selftest builds Signed-off-by: Jakub Kicinski --- tests/patch/build_tools/build_tools.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/patch/build_tools/build_tools.sh b/tests/patch/build_tools/build_tools.sh index efa10aa..98a7aa4 100755 --- a/tests/patch/build_tools/build_tools.sh +++ b/tests/patch/build_tools/build_tools.sh @@ -33,7 +33,7 @@ echo "Now at:" git log -1 --pretty='%h ("%s")' HEAD # These are either very slow or don't build -export SKIP_TARGETS="bpf dt kvm landlock livepatch lsm sched_ext user_events mm powerpc" +export SKIP_TARGETS="bpf dt kvm landlock livepatch lsm sched_ext user_events mm powerpc filesystems/mount-notify ublk" pr "Cleaning" make O=$output_dir $build_flags -C tools/testing/selftests/ clean From 394c02ed350167154f35a254fad9ada162792186 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 14 Jun 2025 13:00:08 -0700 Subject: [PATCH 359/429] tests: yamllint: descend into the relevant dir Descend into the dir where the file is, that way local .yamllint files are taken into account. yamllint searches for configs down the fs chain but only from CWD location. Signed-off-by: Jakub Kicinski --- tests/patch/yamllint/yamllint.sh | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/patch/yamllint/yamllint.sh b/tests/patch/yamllint/yamllint.sh index ffb401a..37d014c 100755 --- a/tests/patch/yamllint/yamllint.sh +++ b/tests/patch/yamllint/yamllint.sh @@ -32,7 +32,11 @@ git checkout -q HEAD~ # Also ignore created, as not present in the parent commit for f in $(git show --diff-filter=M --pretty="" --name-only "${HEAD}" | grep -E "\.yaml$"); do - yamllint "$f" | tee -a "$tmpfile_o" + ( + cd "$(dirname "$f")" || exit 1 + + yamllint "$(basename "$f")" | tee -a "$tmpfile_o" + ) done incumbent=$(grep -i -c " error " "$tmpfile_o") @@ -42,7 +46,11 @@ pr "Checking the tree with the patch" git checkout -q "$HEAD" for f in $(git show --diff-filter=AM --pretty="" --name-only "${HEAD}" | grep -E "\.yaml$"); do - yamllint "$f" | tee -a "$tmpfile_n" + ( + cd "$(dirname "$f")" || exit 1 + + yamllint "$(basename "$f")" | tee -a "$tmpfile_n" + ) done current=$(grep -i -c " error " "$tmpfile_n") From d541fd7b69304d24e67e21e5e8c94c12ebf4d040 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 20 Jun 2025 17:58:15 -0700 Subject: [PATCH 360/429] contest: collector: fix input for marking as passing from srk to cur We should only mark test as passing based on cur[rent streak] not the [max] s[t]r[ea]k. Because if a test is incorrectly considered as passing we want to be able to force is back to filtered. But unless we also clear the max streak it will come back to passing on the next collection. Signed-off-by: Jakub Kicinski --- contest/results-collector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contest/results-collector.py b/contest/results-collector.py index c9b4519..8c071c0 100755 --- a/contest/results-collector.py +++ b/contest/results-collector.py @@ -210,7 +210,7 @@ def psql_insert_stability(self, data): stability[key_pfx + "_srk"] = max(stability[key_pfx + "_cur"], stability[key_pfx + "_srk"]) now = datetime.datetime.now().isoformat() + "+00:00" - if stability["pass_srk"] > 15 and not stability["passing"]: # 5 clean days for HW + if stability["pass_cur"] > 15 and not stability["passing"]: # 5 clean days for HW print("Test reached stability", data["remote"], row["test"], row["subtest"]) stability["passing"] = now From 86e5195515fe78674b57c4a14a7db82a10d1ea90 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 27 Jun 2025 18:32:47 -0700 Subject: [PATCH 361/429] deploy: contest: add bpftrace Signed-off-by: Jakub Kicinski --- deploy/contest/remote/worker-setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/contest/remote/worker-setup.sh b/deploy/contest/remote/worker-setup.sh index 437066e..35627b0 100644 --- a/deploy/contest/remote/worker-setup.sh +++ b/deploy/contest/remote/worker-setup.sh @@ -13,7 +13,7 @@ git config --global --add safe.directory /opt/nipa sudo dnf install pip meson -sudo dnf install perf +sudo dnf install perf bpftrace sudo dnf install nftables.x86_64 sudo dnf install pixman-devel.x86_64 pixman.x86_64 libgudev.x86_64 sudo dnf install libpcap-devel libpcap cmake From d4828909f4a0bdc7b740359cac6d52fdb2e1edd0 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 27 Jun 2025 18:33:22 -0700 Subject: [PATCH 362/429] contest: apply stability to retries as well The UI shows a lot of retries as failing (even tho the initial test passed). This is because sub-cases are failing but we know they are unstable. But we lack the update logic for retries with sub-cases. Run over the list. Signed-off-by: Jakub Kicinski --- contest/results-collector.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/contest/results-collector.py b/contest/results-collector.py index 8c071c0..44aca0c 100755 --- a/contest/results-collector.py +++ b/contest/results-collector.py @@ -361,6 +361,13 @@ def filter_l1_l2(case): all_pass = functools.reduce(lambda x, y: x and y["result"].lower() == "pass", test["results"], all_pass) if all_pass: test["result"] = "pass" + # Same logic for retries + all_pass = True + all_pass &= not test.get("crashes") + if test.get("retry", "pass").lower() != "pass": + all_pass = functools.reduce(lambda x, y: x and y.get("retry", "fail").lower() == "pass", test["results"], all_pass) + if all_pass: + test["retry"] = "pass" return test data["results"] = list(filter(filter_l1, data["results"])) From ec9a1b341e828c61f9aa3efb88bffe7c74c2f3e0 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 28 Jun 2025 10:50:14 -0700 Subject: [PATCH 363/429] contest: cli: cidiff: add a Python version with HTML output Signed-off-by: Jakub Kicinski --- contest/cidiff.py | 506 ++++++++++++++++++++++++++++++++++++++++++++++ pw_brancher.py | 7 + ui/status.js | 2 +- 3 files changed, 514 insertions(+), 1 deletion(-) create mode 100755 contest/cidiff.py diff --git a/contest/cidiff.py b/contest/cidiff.py new file mode 100755 index 0000000..c2dcbc7 --- /dev/null +++ b/contest/cidiff.py @@ -0,0 +1,506 @@ +#!/usr/bin/env python3 + +import argparse +import os +import subprocess +import tempfile +import sys +import re +from datetime import datetime, timedelta + +html_template = """ + + + + + NIPA {branch2} info + + + + + +
+

NIPA Branch {branch2}

+
+
+ Branches +
+ {prev_button} + {next_button} +
+
+
{branch2_html} (current)\n {branch1_html} (comparison){compare_link}
+
+ +
+
Base trees
+
{ancestor_info}\n{base_diff}
+
+ +
+
+ Tested patches +
+ +
+
+
{commit_diff}
+
+
+ + +""" + + +def parse_branch_datetime(branch_name): + """Extract date and time from branch name format like 'net-next-2025-06-28--21-00'.""" + match = re.search(r'(.*?)(\d{4}-\d{2}-\d{2})--(\d{2})-(\d{2})', branch_name) + if match: + date_str = match.group(2) + hour_str = match.group(3) + minute_str = match.group(4) + try: + return match.group(1), datetime.strptime(f"{date_str} {hour_str}:{minute_str}", "%Y-%m-%d %H:%M") + except ValueError: + return None, None + return None, None + +def generate_next_branch_name(branch1, branch2): + """Generate the next branch name based on the time difference between branch1 and branch2.""" + # Parse datetime from branch names + prefix, dt1 = parse_branch_datetime(branch1) + prefix, dt2 = parse_branch_datetime(branch2) + if not prefix or not dt1 or not dt2: + return None + + # Calculate time difference + time_diff = dt2 - dt1 + next_dt = dt2 + time_diff + return f"{prefix}{next_dt.strftime('%Y-%m-%d--%H-%M')}" + +# Format branch names for display and file paths +def branch_name_clear(name): + if not name: + return None + name = name.strip() + if name.startswith('remotes/') and name.count('/') >= 2: + name = "/".join(name.split('/')[2:]) + return name + +def generate_html(args, branch1, branch2, base_diff_output, commit_diff_output, + ancestor_info=None, committed=None): + """Generate HTML output for the diff.""" + # Generate next branch name + branch1 = branch_name_clear(branch1) + branch2 = branch_name_clear(branch2) + next_branch = generate_next_branch_name(branch1, branch2) + + prev_file = f"{branch1}.html" + next_file = f"{next_branch}.html" if next_branch else None + + # Process diff output to add HTML styling + def process_diff(diff_text): + if not diff_text: + return "

No differences found.

" + + lines = [] + for line in diff_text.split('\n'): + if line.startswith('---') or line.startswith('+++') or line.startswith('index') or line.startswith('diff --git'): + pass + elif line.startswith('+') and not line.startswith('+++'): + lines.append(f'
[+] {line[1:]}
') + elif line.startswith('-') and not line.startswith('---'): + title = line[1:] + if title in committed: + lines.append(f'
[c] {title}
') + else: + lines.append(f'
[-] {line[1:]}
') + elif line.startswith('@@'): + lines.append(f'
{line}
') + else: + lines.append(f'
{line}
') + + return ''.join(lines) + + # Process the diff outputs + processed_ancestor_info = process_diff(ancestor_info) + processed_commit_diff = process_diff(commit_diff_output) + compare_link = "" + + github_url = args.github_url + if github_url: + # Remove trailing slash if present + if github_url.endswith('/'): + github_url = github_url[:-1] + + compare_link = f'' + + branch1_html = f'{branch1}' + branch2_html = f'{branch2}' + else: + branch1_html = branch1 + branch2_html = branch2 + compare_link = "" + + # Create navigation buttons + prev_button = f'' + next_button = f'' if next_file else '' + + # Generate the HTML + html = html_template.format( + branch1=branch1, + branch2=branch2, + branch1_html=branch1_html, + branch2_html=branch2_html, + compare_link=compare_link, + ancestor_info=processed_ancestor_info, + base_diff=base_diff_output, + commit_diff=processed_commit_diff, + prev_button=prev_button, + next_button=next_button + ) + + return html + +def text_print(args: argparse.Namespace, message: str) -> None: + """Print message to stdout only if HTML output is not requested.""" + if not args.html: + print(message) + +def run_command(cmd): + """Run a shell command and return its output.""" + result = subprocess.run(cmd, shell=True, capture_output=True, text=True) + return result.stdout.strip() + +def get_base(branch): + """Get the base commit for a branch.""" + cmd = f"git log -1 --format='%h' --grep=\"Merge git://git.kernel.org/pub/scm/linux/kernel/git/netdev/net\" {branch}" + return run_command(cmd) + +def get_common_ancestor(commit1, commit2): + """Find the common ancestor of two commits.""" + cmd = f"git merge-base {commit1} {commit2}" + return run_command(cmd) + +def get_commit_list(start_commit, end_commit): + """Get a list of commits between start_commit and end_commit.""" + cmd = f"git log --format='%h#%s' {start_commit}..{end_commit}" + commits = run_command(cmd) + # Skip the first line, it's the net/main merge commit + return [x.split("#") for x in reversed(commits.split('\n')[1:])] + +def get_base_diff(base1, base2): + """Get the diff between two base commits.""" + # Find common ancestor between the base commits + common_ancestor = get_common_ancestor(base1, base2) + + # Get commit lists between common ancestor and base commits + commits1 = get_commit_list(common_ancestor, base1) + commits2 = get_commit_list(common_ancestor, base2) + + committed = set() + diff_list = [] + + set1 = set([x for x, _ in commits1]) + set2 = set([x for x, _ in commits2]) + for h, s in commits1: + if h not in set2: + diff_list.append("-" + s) + for h, s in commits2: + if h not in set1: + diff_list.append("+" + s) + committed.add(s) + return "\n".join(diff_list), committed + +def main(): + parser = argparse.ArgumentParser(description='Compare two git branches.') + parser.add_argument('branch1', nargs='?', default=None, help='First branch to compare') + parser.add_argument('branch2', nargs='?', default=None, help='Second branch to compare') + parser.add_argument('--html', '-H', action='https://github.com/store_true', help='Generate HTML output') + parser.add_argument('--output', '-o', help='Output file for HTML (default: cidiff_result.html)') + parser.add_argument('--github-url', '-g', help='GitHub repository URL (to create branch links in HTML output)') + args = parser.parse_args() + + branch1 = args.branch1 + branch2 = args.branch2 + + # Determine which branches to compare + if not branch1 and not branch2: + text_print(args, "No branches specified, using two most recent:") + branches = run_command("git branch -a | tail -2").split('\n') + branch1 = branches[0].strip() + branch2 = branches[1].strip() if len(branches) > 1 else None + elif branch1 and not branch2: + text_print(args, "Single branch specified, using that and the previous one:") + branches = run_command(f"git branch -a | grep -B1 \"{branch1}\"").split('\n') + branch1 = branches[0].strip() + branch2 = branches[1].strip() if len(branches) > 1 else None + + if not branch2: + print("Error: Could not determine second branch.") + sys.exit(1) + + text_print(args, f" {branch1} ({run_command(f'git describe {branch1}')})") + text_print(args, f" {branch2} ({run_command(f'git describe {branch2}')})") + text_print(args, "") + + # Get base commits + base1 = get_base(branch1) + base2 = get_base(branch2) + + # Compare base commits + result = subprocess.run(f"git diff --exit-code --stat {base1} {base2}", + shell=True, capture_output=True, text=True) + + base_diff_output = "" + base_diff_status = "" + if result.returncode == 0: + base_diff_status = "==== BASE IDENTICAL ====" + base_diff_list, committed = "", set() + else: + base_diff_status = "==== BASE DIFF ====" + base_diff_output = run_command(f"git --no-pager diff --stat {base1} {base2}") + base_diff_list, committed = get_base_diff(base1, base2) + + text_print(args, base_diff_status) + if base_diff_output: + text_print(args, base_diff_output) + text_print(args, "") + text_print(args, base_diff_list + "\n") + + # Create temporary files with commit messages + with tempfile.NamedTemporaryFile(mode='w+', delete=False) as tmp1, \ + tempfile.NamedTemporaryFile(mode='w+', delete=False) as tmp2: + + tmp1_path = tmp1.name + tmp2_path = tmp2.name + + tmp1.write(run_command(f"git log --format=\"%s\" {base1}..{branch1}")) + tmp2.write(run_command(f"git log --format=\"%s\" {base2}..{branch2}")) + tmp1.write("\n") + tmp2.write("\n") + + # Compare commit lists + if not args.html: + print("==== COMMIT DIFF ====") + subprocess.run(f"git --no-pager diff --no-index {tmp1_path} {tmp2_path}", shell=True) + else: + commit_diff_result = subprocess.run( + f"git --no-pager diff -U500 --no-index {tmp1_path} {tmp2_path}", + shell=True, capture_output=True, text=True + ) + commit_diff_output = commit_diff_result.stdout if commit_diff_result.stdout else commit_diff_result.stderr + + html_output = generate_html(args, branch1, branch2, base_diff_output, + commit_diff_output, + base_diff_list, committed) + if args.output: + with open(args.output, 'w') as f: + f.write(html_output) + print(f"HTML output written to {args.output}") + else: + print(html_output) + + # Clean up temporary files + os.unlink(tmp1_path) + os.unlink(tmp2_path) + +if __name__ == "__main__": + main() diff --git a/pw_brancher.py b/pw_brancher.py index e848a16..8171fe0 100755 --- a/pw_brancher.py +++ b/pw_brancher.py @@ -205,6 +205,13 @@ def generate_deltas(config, tree, name): with open(outfile, 'w') as fp: subprocess.run([cidiff, name], cwd=tree.path, stdout=fp, check=True) + outfile += ".html" + cidiff = os.path.join(os.path.dirname(__file__), "contest", "cidiff.py") + # pub_url is for git, so it most likely ends with ".git" + pub_url = config.get('target', 'public_url')[:-4] + subprocess.run([cidiff, name, '-H', '-o', outfile, '-g', pub_url], + cwd=tree.path, check=True) + def get_change_from_last(tree, branch_list) -> bool: branch_list = list(sorted(branch_list)) diff --git a/ui/status.js b/ui/status.js index 6c510c7..ce9dc93 100644 --- a/ui/status.js +++ b/ui/status.js @@ -717,7 +717,7 @@ function load_result_table_one(data_raw, table, reported, avgs) br_pull = " (pull: " + v.pull_status + ")"; branch.innerHTML = a + v.branch + "" + br_pull; branch.setAttribute("colspan", "2"); - res.innerHTML = "cidiff"; + res.innerHTML = "cidiff"; res.setAttribute("style", "text-align: right;"); } }); From 8e46dacba45e72d79c1ff188543dd86f8f0a41e1 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 28 Jun 2025 16:44:28 -0700 Subject: [PATCH 364/429] brancher: support using full directories for local patches We ended up keeping local patches in a directory. Support using a dir path rather than listing the patches one by one. Signed-off-by: Jakub Kicinski --- pw_brancher.py | 42 +++++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/pw_brancher.py b/pw_brancher.py index 8171fe0..914e703 100755 --- a/pw_brancher.py +++ b/pw_brancher.py @@ -158,20 +158,44 @@ def apply_pending_patches(pw, config, tree) -> Tuple[List, List]: return list(applied_series), list(applied_prs) -def apply_local_patches(config, tree) -> List: - extras = [] - for entry in config.get("local", "patches", fallback="").split(','): - with open(entry, "r") as fp: +def _apply_local_patch(path, tree, dir_path=None) -> bool: + """Apply a single patch file to the tree.""" + log_msg = "Applying: " + path + if dir_path: + log_msg += f" (dir: {dir_path})" + log_open_sec(log_msg) + try: + with open(path, "r") as fp: data = fp.read() - - log_open_sec("Applying: " + entry) p = Patch(data) try: tree.apply(p) - extras.append(entry) + success = True except PatchApplyError: - pass - log_end_sec() + success = False + except Exception as e: + log(f"Error reading or applying patch {path}: {str(e)}") + success = False + log_end_sec() + return success + +def apply_local_patches(config, tree) -> List: + extras = [] + for entry in config.get("local", "patches", fallback="").split(','): + if not entry: + continue + + if os.path.isdir(entry): + # If entry is a directory, apply all .patch files in it + for filename in os.listdir(entry): + if filename.endswith(".patch"): + patch_path = os.path.join(entry, filename) + if _apply_local_patch(patch_path, tree, entry): + extras.append(patch_path) + else: + # Regular file handling + if _apply_local_patch(entry, tree): + extras.append(entry) return extras From ae6ca21637d12b1349d80d5ab6ec8737a08806dd Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 28 Jun 2025 17:49:45 -0700 Subject: [PATCH 365/429] ui: contest: random HTML fixes Signed-off-by: Jakub Kicinski --- ui/contest.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/contest.html b/ui/contest.html index 184d9af..0e67f29 100644 --- a/ui/contest.html +++ b/ui/contest.html @@ -2,7 +2,7 @@ - PW status + PW contest @@ -27,11 +27,11 @@
-
 
+
 

- -
 
+ +
 
From 1da0170e4fea09639c958884253801c82655d465 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 28 Jun 2025 17:54:08 -0700 Subject: [PATCH 366/429] ui: embed contest listing in branch results Add an iframe in branch results listing all the results for given branch. Tell contest.html with embed=1 that its embedded so that it doesn't display navigation. Signed-off-by: Jakub Kicinski --- contest/cidiff.py | 17 ++++++++++++++++- ui/contest.html | 7 +++++-- ui/contest.js | 24 ++++++++++++++++++++++++ ui/nipa.css | 39 +++++++++++++++++++++++++++++++++------ 4 files changed, 78 insertions(+), 9 deletions(-) diff --git a/contest/cidiff.py b/contest/cidiff.py index c2dcbc7..2ede932 100755 --- a/contest/cidiff.py +++ b/contest/cidiff.py @@ -6,6 +6,7 @@ import tempfile import sys import re +import urllib.parse from datetime import datetime, timedelta html_template = """ @@ -243,6 +244,16 @@
{commit_diff}
+ +
+
+ Test results +
+
+ +
+
@@ -292,6 +303,9 @@ def generate_html(args, branch1, branch2, base_diff_output, commit_diff_output, branch2 = branch_name_clear(branch2) next_branch = generate_next_branch_name(branch1, branch2) + # URL encode branch2 for the contest results iframe + branch2_encoded = urllib.parse.quote(branch2) + prev_file = f"{branch1}.html" next_file = f"{next_branch}.html" if next_branch else None @@ -354,7 +368,8 @@ def process_diff(diff_text): base_diff=base_diff_output, commit_diff=processed_commit_diff, prev_button=prev_button, - next_button=next_button + next_button=next_button, + branch2_encoded=branch2_encoded ) return html diff --git a/ui/contest.html b/ui/contest.html index 0e67f29..07554b8 100644 --- a/ui/contest.html +++ b/ui/contest.html @@ -22,7 +22,7 @@
-
+
Loading:
@@ -97,8 +97,11 @@
diff --git a/ui/contest.js b/ui/contest.js index 3ae5d67..7df46db 100644 --- a/ui/contest.js +++ b/ui/contest.js @@ -262,10 +262,34 @@ function reload_data(event) }); } +function embedded_mode() { + $('#loading-fieldset').hide(); + $('#sitemap').hide(); + + $('#open-full-page').show(); + + // Set up click handler for the "Open in full page" link + $('#open-full-page-link').on('click', function(e) { + e.preventDefault(); + + // Create a new URL without the embed parameter + const currentUrl = new URL(window.location.href); + currentUrl.searchParams.delete('embed'); + + // Open in a new tab + window.open(currentUrl.toString(), '_blank'); + }); +} + function do_it() { const urlParams = new URLSearchParams(window.location.search); + // embed=1 means we in an iframe in another page, hide navigation + if (urlParams.get("embed") === "1") { + embedded_mode(); + } + nipa_input_set_from_url("https://github.com/ld-pw"); /* The filter is called "branch" the load selector is called "ld_branch" * auto-copy will not work, but we want them to match, initially. diff --git a/ui/nipa.css b/ui/nipa.css index e013ee1..5c52d37 100644 --- a/ui/nipa.css +++ b/ui/nipa.css @@ -58,18 +58,34 @@ tr:nth-child(even) { right: 1em; } -.box p { - border: 1px solid grey; - padding: 1em; - border-radius: 0.2em; -} - #contest-filters { margin: 1em; padding: 1em; border: solid grey 1px; } +.nipa-button { + background-color: #0366d6; + border: 1px solid #0366d6; + border-radius: 3px; + padding: 5px 10px; + cursor: pointer; + font-size: 0.9em; + color: white; + text-decoration: none; + display: inline-block; + transition: all 0.2s ease; +} + +.nipa-button:hover { + background-color: #0056b3; + text-decoration: none; +} + +.nipa-button:active { + background-color: #004494; +} + @media (prefers-color-scheme: dark) { body { color: #b8b8b8; @@ -96,4 +112,15 @@ tr:nth-child(even) { .column-sorted { background-color: #484848; } + .nipa-button { + background-color: #2c5282; + border-color: #2c5282; + color: #e2e8f0; + } + .nipa-button:hover { + background-color: #2b4c7e; + } + .nipa-button:active { + background-color: #1e3a5f; + } } From 948ef3da3534cbae8b4a3ea81347a22abde0b5b7 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 28 Jun 2025 18:41:41 -0700 Subject: [PATCH 367/429] ui: status: use the cidiff as the main branch info link Signed-off-by: Jakub Kicinski --- ui/status.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ui/status.js b/ui/status.js index ce9dc93..fadd843 100644 --- a/ui/status.js +++ b/ui/status.js @@ -705,20 +705,18 @@ function load_result_table_one(data_raw, table, reported, avgs) time.setAttribute("colspan", "3"); } } else { - let res = row.insertCell(2); let br_pull = ""; if (v.start) remote.innerHTML = v.start.toLocaleString(); else remote.innerHTML = "unknown"; - remote.setAttribute("colspan", "2"); + remote.setAttribute("colspan", "3"); if (v.pull_status != "okay") br_pull = " (pull: " + v.pull_status + ")"; + a = ""; branch.innerHTML = a + v.branch + "" + br_pull; branch.setAttribute("colspan", "2"); - res.innerHTML = "cidiff"; - res.setAttribute("style", "text-align: right;"); } }); From 88e3823b1d0334095349a7dc7060400cdfa8e83c Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 11 Jul 2025 18:15:49 -0700 Subject: [PATCH 368/429] tests: check_selftest: include Python scripts in the check Signed-off-by: Jakub Kicinski --- tests/patch/check_selftest/check_selftest.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/patch/check_selftest/check_selftest.sh b/tests/patch/check_selftest/check_selftest.sh index e6ce905..43c4040 100755 --- a/tests/patch/check_selftest/check_selftest.sh +++ b/tests/patch/check_selftest/check_selftest.sh @@ -5,13 +5,18 @@ rt=0 -files=$(git show --pretty="" --name-only -- tools/testing/selftests*.sh) +files=$(git show --pretty="" --name-only -- \ + tools/testing/selftests*.sh \ + tools/testing/selftests*.py \ + | grep -v "/lib/" + ) if [ -z "$files" ]; then echo "No net selftest shell script" >&$DESC_FD exit $rt fi for file in $files; do + echo "Checking $file" f=$(basename $file) d=$(dirname $file) if [ -f "${d}/Makefile" ] && ! grep -P "[\t| ]${f}" ${d}/Makefile; then From 84f56fad33869329b6e472e7e574098f3dfd3e8b Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 12 Jul 2025 08:42:46 -0700 Subject: [PATCH 369/429] brancher: merge trees with --no-ff to make it clear they are present cidiff uses the net merge point to find the branch base. If net is already merged we end up with no commit, so cidiff gets confused. Always create the merge commit. Signed-off-by: Jakub Kicinski --- core/tree.py | 14 +++++++++----- pw_brancher.py | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/core/tree.py b/core/tree.py index d7640c4..e81385c 100644 --- a/core/tree.py +++ b/core/tree.py @@ -81,8 +81,12 @@ def git(self, args: List[str]): def git_am(self, patch): return self.git(["am", "-s", "--", patch]) - def git_pull(self, pull_url): + def git_pull(self, pull_url, ff=None): cmd = ["pull", "--no-edit", "--signoff"] + if ff == True: + cmd.append('--ff-only') + elif ff == False: + cmd.append('--no-ff') cmd += pull_url.split() return self.git(cmd) @@ -234,9 +238,9 @@ def check_applies(self, thing): return ret - def _pull_safe(self, pull_url, trust_rerere): + def _pull_safe(self, pull_url, trust_rerere, ff): try: - self.git_pull(pull_url) + self.git_pull(pull_url, ff=ff) except CMD.CmdError as e: try: # If rerere fixed it, just commit @@ -253,11 +257,11 @@ def _pull_safe(self, pull_url, trust_rerere): pass raise PullError(e) from e - def pull(self, pull_url, reset=True, trust_rerere=None): + def pull(self, pull_url, reset=True, trust_rerere=None, ff=None): core.log_open_sec("Pulling " + pull_url) try: if reset: self.reset() - self._pull_safe(pull_url, trust_rerere) + self._pull_safe(pull_url, trust_rerere, ff) finally: core.log_end_sec() diff --git a/pw_brancher.py b/pw_brancher.py index 914e703..e07eb4d 100755 --- a/pw_brancher.py +++ b/pw_brancher.py @@ -266,7 +266,7 @@ def create_new(pw, config, state, tree, tgt_remote) -> None: log_open_sec("Pulling in other trees") for url in pull_list.split(','): try: - tree.pull(url, reset=False) + tree.pull(url, reset=False, ff=False) state["info"][branch_name]["base-pulls"][url] = "okay" except PullError: try: From be564dfa568c3051d11ff8c33e54812775a98a64 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 12 Jul 2025 08:51:42 -0700 Subject: [PATCH 370/429] ui: cidiff: add sitemap Add sitemap to the cidiff generated pages. Signed-off-by: Jakub Kicinski --- contest/cidiff.py | 7 +++++++ ui/nipa.js | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/contest/cidiff.py b/contest/cidiff.py index 2ede932..7e59236 100755 --- a/contest/cidiff.py +++ b/contest/cidiff.py @@ -215,8 +215,15 @@ toggleButton.addEventListener('click', toggleUnchangedLines); }}); + + + + +

NIPA Branch {branch2}

diff --git a/ui/nipa.js b/ui/nipa.js index bf32824..1f60ecb 100644 --- a/ui/nipa.js +++ b/ui/nipa.js @@ -168,7 +168,7 @@ function nipa_pw_reported(v, r) function nipa_load_sitemap() { $(document).ready(function() { - $("#sitemap").load("sitemap.html") + $("#sitemap").load("/sitemap.html") }); } From 7959f67780647ba7edd0d44f718c389994b27056 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 12 Jul 2025 09:01:33 -0700 Subject: [PATCH 371/429] ui: cidiff: hide the Next button if the Next page does not exist To make it clear that we are at the newest branch hide the "Next" button if we are on the newest branch. Signed-off-by: Jakub Kicinski --- contest/cidiff.py | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/contest/cidiff.py b/contest/cidiff.py index 7e59236..cec5961 100755 --- a/contest/cidiff.py +++ b/contest/cidiff.py @@ -213,6 +213,22 @@ // Add click event listener to toggle button toggleButton.addEventListener('click', toggleUnchangedLines); + + // Check if Next button link is dead and hide it if so + const nextUrl = '{next_url}'; + const nextButton = document.getElementById('next-button'); + if (nextUrl && nextButton) {{ + fetch(nextUrl, {{ method: 'HEAD' }}) + .then(response => {{ + if (!response.ok) {{ + nextButton.style.display = 'none'; + }} + }}) + .catch(() => {{ + // If fetch fails (e.g., network error, 404), hide the button + nextButton.style.display = 'none'; + }}); + }} }}); @@ -230,8 +246,8 @@
Branches
- {prev_button} - {next_button} + +
{branch2_html} (current)\n {branch1_html} (comparison){compare_link}
@@ -244,9 +260,9 @@
- Tested patches + New patches
- +
{commit_diff}
@@ -313,9 +329,6 @@ def generate_html(args, branch1, branch2, base_diff_output, commit_diff_output, # URL encode branch2 for the contest results iframe branch2_encoded = urllib.parse.quote(branch2) - prev_file = f"{branch1}.html" - next_file = f"{next_branch}.html" if next_branch else None - # Process diff output to add HTML styling def process_diff(diff_text): if not diff_text: @@ -360,10 +373,6 @@ def process_diff(diff_text): branch2_html = branch2 compare_link = "" - # Create navigation buttons - prev_button = f'' - next_button = f'' if next_file else '' - # Generate the HTML html = html_template.format( branch1=branch1, @@ -374,8 +383,8 @@ def process_diff(diff_text): ancestor_info=processed_ancestor_info, base_diff=base_diff_output, commit_diff=processed_commit_diff, - prev_button=prev_button, - next_button=next_button, + prev_url=f"{branch1}.html", + next_url=f"{next_branch}.html" if next_branch else '', branch2_encoded=branch2_encoded ) From 4679ece2236ef48270418c7cdb7482236023d7c7 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 12 Jul 2025 09:19:53 -0700 Subject: [PATCH 372/429] ui: contest: show filtered count Show count of filtered out results. This will be useful especially in branch info where the filters are easy to miss. Signed-off-by: Jakub Kicinski --- ui/contest.html | 3 +++ ui/contest.js | 36 +++++++++++++++++++++++++----------- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/ui/contest.html b/ui/contest.html index 07554b8..5aa86c6 100644 --- a/ui/contest.html +++ b/ui/contest.html @@ -91,6 +91,9 @@
+
+ +
Loading... diff --git a/ui/contest.js b/ui/contest.js index 7df46db..8a73f10 100644 --- a/ui/contest.js +++ b/ui/contest.js @@ -64,6 +64,8 @@ function load_result_table(data_raw) form = "&ld-cases=1"; let rows = []; + let total_results = 0; + let filtered_results = 0; $.each(data_raw, function(i, v) { if (rows.length >= 5000) { @@ -71,19 +73,17 @@ function load_result_table(data_raw) return 0; } - if (branch_filter && - branch_filter != v.branch) - return 1; - if (exec_filter && - exec_filter != v.executor) - return 1; - if (remote_filter && - remote_filter != v.remote) - return 1; + let branch_matches = !branch_filter || branch_filter == v.branch; + let exec_matches = !exec_filter || exec_filter == v.executor; + let remote_matches = !remote_filter || remote_filter == v.remote; $.each(v.results, function(j, r) { - if (test_filter && - r.test != test_filter) + total_results++; + + if (!branch_matches || !exec_matches || !remote_matches) + return 1; + + if (test_filter && r.test != test_filter) return 1; if (result_filter[r.result] == false) return 1; @@ -92,10 +92,24 @@ function load_result_table(data_raw) if (pw_n == false && nipa_pw_reported(v, r) == false) return 1; + filtered_results++; rows.push({"v": v, "r": r}); }); }); + // Display filtering information + let filter_info_elem = document.getElementById("filter-info"); + if (total_results > 0) { + let filtered_out = total_results - filtered_results; + if (filtered_out > 0) { + filter_info_elem.innerHTML = `${total_results} results
(${filtered_out} filtered out)`; + } else { + filter_info_elem.innerHTML = `${total_results} results`; + } + } else { + filter_info_elem.innerHTML = ""; + } + // Trim the time, so that sort behavior matches what user sees for (const result of rows) { if (result.r.time) From ef053513304a197e8745d2c4cd00eb64c8b53db4 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 12 Jul 2025 09:27:00 -0700 Subject: [PATCH 373/429] ui: cidiff: hide the pass results Most of the time we care about failures in branch info. Now that we show filtered count we can default to showing just failures. Signed-off-by: Jakub Kicinski --- contest/cidiff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contest/cidiff.py b/contest/cidiff.py index cec5961..d1b8447 100755 --- a/contest/cidiff.py +++ b/contest/cidiff.py @@ -273,7 +273,7 @@ Test results
-
From 37e4f7c54eeaa104db8e1e90229e30bec8bd915a Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Mon, 21 Jul 2025 13:14:36 -0700 Subject: [PATCH 374/429] contest: vm: sleep 5 sec before triggering kmemleak Stan reports that kmemleak has a 5 sec grace period. This is probably also why we always catch leaks on the second scan. Add a sleep. Signed-off-by: Jakub Kicinski --- contest/remote/lib/vm.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contest/remote/lib/vm.py b/contest/remote/lib/vm.py index d43d494..3e9a142 100644 --- a/contest/remote/lib/vm.py +++ b/contest/remote/lib/vm.py @@ -402,6 +402,9 @@ def check_health(self): if self.fail_state: return if self.has_kmemleak: + # kmemleak needs objects to be at least MSECS_MIN_AGE (5000) + # before it considers them to have been leaked + sleep(5) self.cmd("echo scan > /sys/kernel/debug/kmemleak && cat /sys/kernel/debug/kmemleak") self.drain_to_prompt() # Do it twice, kmemleak likes to hide the leak on the first attempt From df5eac58e7de6c89a47e7692ab97631a629c1a31 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 24 Jul 2025 16:24:29 -0700 Subject: [PATCH 375/429] ui: cidiff: avoid splitting commit message Turns out rust uses # in commit messages quite a lot. We use {hash}#{subject} as the git format when getting a list. Make sure we split the lines at most once to keep the subject in once piece. Signed-off-by: Jakub Kicinski --- contest/cidiff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contest/cidiff.py b/contest/cidiff.py index d1b8447..80ee139 100755 --- a/contest/cidiff.py +++ b/contest/cidiff.py @@ -415,7 +415,7 @@ def get_commit_list(start_commit, end_commit): cmd = f"git log --format='%h#%s' {start_commit}..{end_commit}" commits = run_command(cmd) # Skip the first line, it's the net/main merge commit - return [x.split("#") for x in reversed(commits.split('\n')[1:])] + return [x.split("#", 1) for x in reversed(commits.split('\n')[1:])] def get_base_diff(base1, base2): """Get the diff between two base commits.""" From 8f2a4b9a0359e594949fc6ec4119c6908510fc36 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 24 Jul 2025 17:17:17 -0700 Subject: [PATCH 376/429] mailbot: strip down From lines of user bots syzbot adds some noise to its email address. Remove it when checking for auto-cr. Signed-off-by: Jakub Kicinski --- mailbot.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mailbot.py b/mailbot.py index 68608ad..c42d0d0 100755 --- a/mailbot.py +++ b/mailbot.py @@ -324,7 +324,10 @@ def _resolve_authorized(self, pw): self._authorized = False def user_bot(self): - return self.msg.get('From') in auto_changes_requested + sender = self.msg.get('From') + # strip down bla+01234@email.com to bla@email.com, for syzbot + sender = re.sub(r"\+[a-zA-Z0-9_-]*@", "@", sender) + return sender in auto_changes_requested def auto_awaiting_upstream(self): # Try to operate only on the first message in the thread From b908a0c4ec6d6df41580bbb924d6b26852e95f92 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 26 Jul 2025 11:40:16 -0700 Subject: [PATCH 377/429] pw-brancher: don't fail if cidiff crashed cidiff is a bit buggy, it will probably fail during the merge window. Let's avoid missing branches for something that can be re-run manually. Signed-off-by: Jakub Kicinski --- pw_brancher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pw_brancher.py b/pw_brancher.py index e07eb4d..0ac022f 100755 --- a/pw_brancher.py +++ b/pw_brancher.py @@ -234,7 +234,7 @@ def generate_deltas(config, tree, name): # pub_url is for git, so it most likely ends with ".git" pub_url = config.get('target', 'public_url')[:-4] subprocess.run([cidiff, name, '-H', '-o', outfile, '-g', pub_url], - cwd=tree.path, check=True) + cwd=tree.path, check=False) def get_change_from_last(tree, branch_list) -> bool: From ddbc9ed461a9f76974dc4601df5fd7ccc84ef654 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Tue, 29 Jul 2025 07:11:24 -0700 Subject: [PATCH 378/429] form-letters: update net-next form letter for 6.17 Signed-off-by: Jakub Kicinski --- form-letters/net-next-closed | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/form-letters/net-next-closed b/form-letters/net-next-closed index 03cef54..2e23180 100644 --- a/form-letters/net-next-closed +++ b/form-letters/net-next-closed @@ -1,8 +1,8 @@ -Linus already pulled net-next material v6.15 and therefore net-next is closed -for new drivers, features, code refactoring and optimizations. We are currently -accepting bug fixes only. +We have already submitted our pull request with net-next material for v6.17, +and therefore net-next is closed for new drivers, features, code refactoring +and optimizations. We are currently accepting bug fixes only. -Please repost when net-next reopens after Apr 7th. +Please repost when net-next reopens after Aug 11th. RFC patches sent for review only are obviously welcome at any time. From 9028c860d27850900a562f26523f4f754b8e7834 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Tue, 29 Jul 2025 07:11:43 -0700 Subject: [PATCH 379/429] contest: backend: add flakiness query Signed-off-by: Jakub Kicinski --- contest/backend/query.py | 85 ++++++++++++++++++++++++++++++++++++++++ ui/status.html | 12 ++++++ ui/status.js | 34 ++++++++++++++++ 3 files changed, 131 insertions(+) diff --git a/contest/backend/query.py b/contest/backend/query.py index d53d45f..dc85829 100644 --- a/contest/backend/query.py +++ b/contest/backend/query.py @@ -18,6 +18,9 @@ psql = psycopg2.connect(database=db_name) psql.autocommit = True +# How many branches to query to get flakes for last month +flake_cnt = 300 + @app.route('/') def hello(): @@ -190,3 +193,85 @@ def dev_info(): data = [{columns[i]: value for i, value in enumerate(row)} for row in rows] return data + + +@app.route('/flaky-tests') +def flaky_tests(): + """ + Returns tests that are flaky (first try fails, retry passes, and no crash). + """ + global flake_cnt + limit = request.args.get('limit') + try: + limit = int(limit) + month = False + except: + month = True # Default to querying last month + limit = flake_cnt # Default limit + + # Find branches with incomplete results, psql JSON helpers fail for them + t = datetime.datetime.now() + with psql.cursor() as cur: + query = f""" + SELECT branch + FROM results + WHERE json_normal NOT LIKE '%"results": [%' + GROUP BY branch; + """ + + cur.execute(query) + rows = cur.fetchall() + branches = "" + if rows: + branches = " AND branch != ".join([""] + [f"'{r[0]}'" for r in rows]) + print(f"Query for in-prog execs took: {str(datetime.datetime.now() - t)}") + + t = datetime.datetime.now() + with psql.cursor() as cur: + # Query for tests where first try failed, retry passed, and no crash + query = f""" + SELECT remote, executor, test, branch, branch_date + FROM results, jsonb_to_recordset(json_normal::jsonb->'results') as + x(test text, result text, retry text, crashes text) + WHERE x.result = 'fail' + AND x.retry = 'pass' + AND x.crashes IS NULL + {branches} + ORDER BY branch_date DESC LIMIT {limit}; + """ + + cur.execute(query) + rows = cur.fetchall() + + print(f"Query for flaky tests took: {str(datetime.datetime.now() - t)}") + + target_date = datetime.datetime.now() - datetime.timedelta(days=14) + two_weeks = target_date.strftime("%Y-%m-%d--%H-%M") + target_date = datetime.datetime.now() - datetime.timedelta(days=28) + four_weeks = target_date.strftime("%Y-%m-%d--%H-%M") + cnt = 0 + res = {} + for row in rows: + rem, exe, test, branch, br_date = row + key = (rem, exe, test) + if not month: + res[key] = res.get(key, 0) + 1 + else: + if key not in res: + res[key] = [0, 0] + if br_date >= two_weeks: + res[key][0] += 1 + elif br_date >= four_weeks: + res[key][1] += 1 + else: + break + cnt += 1 + # JSON needs a simple array, not a dict + data = [] + for k, v in res.items(): + data.append({"remote": k[0], "executor": k[1], "test": k[2], "count": v}) + + if month: + # Overcount by 30 to account for fluctuation in flakiness + flake_cnt = cnt + 30 + return data diff --git a/ui/status.html b/ui/status.html index 7d5b77d..9f58b7f 100644 --- a/ui/status.html +++ b/ui/status.html @@ -52,6 +52,18 @@

Build processing

Memory Use +
+

Flakiest tests

+ + + + + + + + + +
RemoteExecutorTestFlakes (now - 2w)Flakes (2w - 4w)Ignored

Continuous testing results

diff --git a/ui/status.js b/ui/status.js index fadd843..0e583de 100644 --- a/ui/status.js +++ b/ui/status.js @@ -1005,6 +1005,37 @@ function branches_loaded(data_raw) loaded_one(); } +function flakes_doit(data_raw) +{ + let flakes = document.getElementById("flakes"); + + data_raw.sort(function(a, b){ + if (a["count"][0] != b["count"][0]) + return b["count"][0] - a["count"][0]; + if (a["count"][1] != b["count"][1]) + return b["count"][1] - a["count"][1]; + return 0; + }) + + $.each(data_raw, function(i, v) { + let row = flakes.insertRow(); + let reported = nipa_pw_reported(v, v); + let ignored = ""; + + if (v["count"][0] < 3 && reported) + return 1; + if (!reported) + ignored = "yes"; + + row.insertCell(0).innerText = v["remote"]; + row.insertCell(1).innerText = v["executor"]; + row.insertCell(2).innerText = v["test"]; + row.insertCell(3).innerText = v["count"][0]; + row.insertCell(4).innerText = v["count"][1]; + row.insertCell(5).innerText = ignored; + }); +} + function do_it() { /* @@ -1028,4 +1059,7 @@ function do_it() $(document).ready(function() { $.get("static/nipa/branches-info.json", branches_loaded) }); + $(document).ready(function() { + $.get("query/flaky-tests", flakes_doit) + }); } From 9efdb98f4e0f5679d9450cf326f0ab28f875fa16 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 31 Jul 2025 11:50:13 -0700 Subject: [PATCH 380/429] contest: status: break down flakiness query by week (not 2 weeks) Signed-off-by: Jakub Kicinski --- contest/backend/query.py | 22 ++++++++++++---------- ui/status.html | 3 +-- ui/status.js | 8 ++++++-- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/contest/backend/query.py b/contest/backend/query.py index dc85829..55b9ce7 100644 --- a/contest/backend/query.py +++ b/contest/backend/query.py @@ -245,10 +245,11 @@ def flaky_tests(): print(f"Query for flaky tests took: {str(datetime.datetime.now() - t)}") - target_date = datetime.datetime.now() - datetime.timedelta(days=14) - two_weeks = target_date.strftime("%Y-%m-%d--%H-%M") - target_date = datetime.datetime.now() - datetime.timedelta(days=28) - four_weeks = target_date.strftime("%Y-%m-%d--%H-%M") + weeks_ago = [] + for weeks in range(1, 5): + target_date = datetime.datetime.now() - datetime.timedelta(weeks=weeks) + weeks_ago.append(target_date.strftime("%Y-%m-%d--%H-%M")) + cnt = 0 res = {} for row in rows: @@ -258,13 +259,14 @@ def flaky_tests(): res[key] = res.get(key, 0) + 1 else: if key not in res: - res[key] = [0, 0] - if br_date >= two_weeks: - res[key][0] += 1 - elif br_date >= four_weeks: - res[key][1] += 1 + res[key] = [0, 0, 0, 0] + + for i in range(len(weeks_ago)): + if br_date >= weeks_ago[i]: + res[key][i] += 1 + break else: - break + break # stop looking at rows, the records are sorted by date cnt += 1 # JSON needs a simple array, not a dict data = [] diff --git a/ui/status.html b/ui/status.html index 9f58b7f..c4de7e0 100644 --- a/ui/status.html +++ b/ui/status.html @@ -59,8 +59,7 @@

Flakiest tests

Remote Executor Test - Flakes (now - 2w) - Flakes (2w - 4w) + Flakes (by week: this, 1, 2, 3 ago) Ignored diff --git a/ui/status.js b/ui/status.js index 0e583de..3e608ed 100644 --- a/ui/status.js +++ b/ui/status.js @@ -1014,6 +1014,8 @@ function flakes_doit(data_raw) return b["count"][0] - a["count"][0]; if (a["count"][1] != b["count"][1]) return b["count"][1] - a["count"][1]; + if (a["count"][2] != b["count"][2]) + return b["count"][2] - a["count"][2]; return 0; }) @@ -1022,7 +1024,7 @@ function flakes_doit(data_raw) let reported = nipa_pw_reported(v, v); let ignored = ""; - if (v["count"][0] < 3 && reported) + if (v["count"][0] + v["count"][1] + v["count"][2] < 4 && reported) return 1; if (!reported) ignored = "yes"; @@ -1032,7 +1034,9 @@ function flakes_doit(data_raw) row.insertCell(2).innerText = v["test"]; row.insertCell(3).innerText = v["count"][0]; row.insertCell(4).innerText = v["count"][1]; - row.insertCell(5).innerText = ignored; + row.insertCell(5).innerText = v["count"][2]; + row.insertCell(6).innerText = v["count"][3]; + row.insertCell(7).innerText = ignored; }); } From 9ba29bb2297f713c047549dc99f2013c59e02dee Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 6 Aug 2025 17:06:31 -0700 Subject: [PATCH 381/429] contest: crash: don't overlap crashes We include previous 5 lines in the crash to capture all of the intro lines to the warning / splat. Tianyi reports that for short errors like refleak this leads to previous stack trace leaking into the new one. Reset the "previous lines" to make sure this doesn't happen. Signed-off-by: Jakub Kicinski --- contest/remote/lib/crash.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/contest/remote/lib/crash.py b/contest/remote/lib/crash.py index b283428..38fdc02 100755 --- a/contest/remote/lib/crash.py +++ b/contest/remote/lib/crash.py @@ -55,6 +55,7 @@ def extract_crash(outputs, prompt, get_filters): in_crash &= line[-2:] != '] ' in_crash &= not line.startswith(prompt) if not in_crash: + last5 = [""] * 5 finger_prints.add(crash_finger_print(get_filters(), crash_lines[start:])) else: @@ -108,11 +109,9 @@ def test_refleak(self): lines, fingers = extract_crash(TestCrashes.refleak, "xx__->", lambda : None) self.assertGreater(len(lines), 50) self.assertEqual(fingers, - {'dev_hard_start_xmit:__dev_queue_xmit:ip6_finish_output2:ip6_finish_output:netdev_get_by_index', - '___sys_sendmsg:__sys_sendmsg:do_syscall_64:dst_init:dst_alloc', - 'dst_init:dst_alloc:ip6_dst_alloc:ip6_rt_pcpu_alloc:ip6_pol_route', - '___sys_sendmsg:__sys_sendmsg:do_syscall_64:ipv6_add_dev:addrconf_notify', - 'dev_hard_start_xmit:__dev_queue_xmit:arp_solicit:neigh_probe:dst_init'}) + {'netdev_get_by_index:fib6_nh_init:nh_create_ipv6:nexthop_create:rtm_new_nexthop', + 'ipv6_add_dev:addrconf_notify:notifier_call_chain:register_netdevice:veth_newlink', + 'dst_init:dst_alloc:ip6_dst_alloc:ip6_rt_pcpu_alloc:ip6_pol_route'}) def test_hung_task(self): self.assertTrue(has_crash(TestCrashes.hung_task)) From 154bc4e7efdd5749813ae5fd6444c051980db252 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 8 Aug 2025 09:24:19 -0700 Subject: [PATCH 382/429] contest: don't create local branches Move to a git commit from remove but don't create a local branch. Looks like nothing actually requires it. Signed-off-by: Jakub Kicinski --- contest/remote/gh.py | 3 ++- contest/remote/lib/fetcher.py | 39 +++++++++++++++++++++-------------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/contest/remote/gh.py b/contest/remote/gh.py index 0b3d6af..69b95e8 100755 --- a/contest/remote/gh.py +++ b/contest/remote/gh.py @@ -151,7 +151,8 @@ def test_run(binfo, rinfo, cbarg, config, start): base = config.get('gh', 'base') subprocess.run('git checkout ' + base, cwd=tree_path, shell=True, check=True) - res = subprocess.run('git merge ' + binfo['branch'], cwd=tree_path, shell=True) + res = subprocess.run('git merge ' + rinfo['branch-ref'], + cwd=tree_path, shell=True) if res.returncode != 0: # If rerere fixed it, just commit res = subprocess.run('git diff -s --exit-code', cwd=tree_path, shell=True) diff --git a/contest/remote/lib/fetcher.py b/contest/remote/lib/fetcher.py index 29c8786..2ffcc74 100644 --- a/contest/remote/lib/fetcher.py +++ b/contest/remote/lib/fetcher.py @@ -85,12 +85,15 @@ def _write_result(self, data, run_cookie): return self._url_path + '/' + file_name - def _run_test(self, binfo): + def _run_test(self, binfo, ref): self._result_set(binfo['branch'], None) start = datetime.datetime.now(datetime.UTC) run_id_cookie = str(int(start.timestamp() / 60) % 1000000) - rinfo = {'run-cookie': run_id_cookie} + rinfo = { + 'run-cookie': run_id_cookie, + 'branch-ref': ref, + } results = self._cb(binfo, rinfo, self._cbarg) end = datetime.datetime.now(datetime.UTC) @@ -109,19 +112,23 @@ def _run_test(self, binfo): self._result_set(binfo['branch'], url) - def _clean_old_branches(self, remote, current): - ret = subprocess.run('git branch', - cwd=self._tree_path, shell=True, - capture_output=True, check=True) + def _find_branch(self, name): + ret = subprocess.run(['git', 'describe', 'main'], + check=False, capture_output=True) + if ret.returncode == 0: + # git found a direct hit for the name, use as is + return name - existing = set([x.strip() for x in ret.stdout.decode('utf-8').split('\n')]) + # Try to find the branch in one of the remotes (will return remote/name) + ret = subprocess.run(['git', 'branch', '-r', '-l', '*/' + name], + cwd=self._tree_path, + capture_output=True, check=True) - for b in remote: - if b["branch"] in existing and b["branch"] != current: - print("Clean up old branch", b["branch"]) - subprocess.run('git branch -D ' + b["branch"], - cwd=self._tree_path, shell=True, - check=True) + branches = ret.stdout.decode('utf-8').strip() + branches = [x.strip() for x in branches.split('\n')] + if len(branches) != 1: + print("Unexpected number of branches found:", branches) + return branches[0] def _run_once(self): r = requests.get(self._branches_url) @@ -160,7 +167,8 @@ def _run_once(self): print("HEAD is still locked! Sleeping..") time.sleep(0.2) - subprocess.run('git checkout ' + to_test["branch"], + ref = self._find_branch(to_test["branch"]) + subprocess.run('git checkout --detach ' + ref, cwd=self._tree_path, shell=True, check=True) if self._patches_path is not None: @@ -169,8 +177,7 @@ def _run_once(self): subprocess.run('git apply -v {}'.format(realpath), cwd=self._tree_path, shell=True) - self._clean_old_branches(branches, to_test["branch"]) - self._run_test(to_test) + self._run_test(to_test, ref) def run(self): while self.life.next_poll(): From b24e151aad7ce967c424a9da92c1af6d94613dcd Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 9 Aug 2025 22:02:05 -0700 Subject: [PATCH 383/429] ui: status: add reminder and total lines to the flakes Looks like there is quite a few low rate flakes. Add a reminder line to flag those that don't qualify for frequent flaking. Also add a total / sum while at it. Signed-off-by: Jakub Kicinski --- ui/status.js | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/ui/status.js b/ui/status.js index 3e608ed..8d979bd 100644 --- a/ui/status.js +++ b/ui/status.js @@ -1005,6 +1005,18 @@ function branches_loaded(data_raw) loaded_one(); } +function flakes_add_summary(table, name, data) +{ + let row = table.insertRow(); + let cell = row.insertCell(0); + cell.innerHTML = name; + cell.setAttribute("colspan", "3"); + cell.setAttribute("style", "text-align: right; font-style: italic;"); + for (let n = 0; n < 4; n++) + row.insertCell(n + 1).innerHTML = "" + data[n] + ""; + row.insertCell(5).innerText = ""; +} + function flakes_doit(data_raw) { let flakes = document.getElementById("flakes"); @@ -1019,13 +1031,22 @@ function flakes_doit(data_raw) return 0; }) + let reminder = [0, 0, 0, 0]; + let total = [0, 0, 0, 0]; + $.each(data_raw, function(i, v) { let row = flakes.insertRow(); let reported = nipa_pw_reported(v, v); let ignored = ""; - if (v["count"][0] + v["count"][1] + v["count"][2] < 4 && reported) + for (let n = 0; n < 4; n++) + total[n] += v["count"][n]; + + if (v["count"][0] + v["count"][1] + v["count"][2] < 4 && reported) { + for (let n = 0; n < 4; n++) + reminder[n] += v["count"][n]; return 1; + } if (!reported) ignored = "yes"; @@ -1038,6 +1059,9 @@ function flakes_doit(data_raw) row.insertCell(6).innerText = v["count"][3]; row.insertCell(7).innerText = ignored; }); + + flakes_add_summary(flakes, "reminder", reminder); + flakes_add_summary(flakes, "total", total); } function do_it() From 595ebe43ddda5f9aeed3379b3e037c949655fb47 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 15 Aug 2025 16:41:17 -0700 Subject: [PATCH 384/429] ingest_mdir: break lines to make pylint happy Break up long lines. Pure refactor, no functional changes intended. Signed-off-by: Jakub Kicinski --- ingest_mdir.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/ingest_mdir.py b/ingest_mdir.py index e6fbd46..81025aa 100755 --- a/ingest_mdir.py +++ b/ingest_mdir.py @@ -25,13 +25,16 @@ config = configparser.ConfigParser() config.read(['nipa.config', "tester.config"]) -results_dir = config.get('results', 'dir', fallback=os.path.join(NIPA_DIR, "results")) +results_dir = config.get('results', 'dir', + fallback=os.path.join(NIPA_DIR, "results")) # TODO: use config parser = argparse.ArgumentParser() -parser.add_argument('--mdir', required=True, help='path to the directory with the patches') +parser.add_argument('--mdir', required=True, + help='path to the directory with the patches') parser.add_argument('--tree', required=True, help='path to the tree to test on') -parser.add_argument('--tree-name', default='unknown', help='the tree name to expect') +parser.add_argument('--tree-name', default='unknown', + help='the tree name to expect') parser.add_argument('--tree-branch', default='main', help='the branch or commit to use as a base for applying patches') parser.add_argument('--result-dir', default=results_dir, @@ -41,7 +44,8 @@ args.mdir = os.path.abspath(args.mdir) args.tree = os.path.abspath(args.tree) -log_init(config.get('log', 'type'), config.get('log', 'path'), force_single_thread=True) +log_init(config.get('log', 'type'), config.get('log', 'path'), + force_single_thread=True) log_open_sec("Loading patches") try: @@ -81,5 +85,8 @@ tester.join() # Summary hack -os.system(f'for i in $(find {args.result_dir} -type f -name summary); do dir=$(dirname "$i"); head -n2 "$dir"/summary; cat "$dir"/desc 2>/dev/null; done' - ) +os.system(f'for i in $(find {args.result_dir} -type f -name summary); do ' + + 'dir=$(dirname "$i"); ' + + 'head -n2 "$dir"/summary; ' + + 'cat "$dir"/desc 2>/dev/null; done' +) From 746026c959e1a6551f6d953363ca1941128f077d Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 20 Aug 2025 15:27:33 -0700 Subject: [PATCH 385/429] ingest_mdir: fix three pylint complaints Fix: ingest_mdir.py:31:1: W0511: TODO: use config (fixme) ingest_mdir.py:58:13: W1514: Using open without explicitly specifying an encoding (unspecified-encoding) ingest_mdir.py:19:0: W0611: Unused log imported from core (unused-import) No functional changes intended. Signed-off-by: Jakub Kicinski --- ingest_mdir.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ingest_mdir.py b/ingest_mdir.py index 81025aa..5c0d988 100755 --- a/ingest_mdir.py +++ b/ingest_mdir.py @@ -16,7 +16,7 @@ import queue from core import NIPA_DIR -from core import log, log_open_sec, log_end_sec, log_init +from core import log_open_sec, log_end_sec, log_init from core import Patch from core import Series from core import Tree @@ -28,7 +28,6 @@ results_dir = config.get('results', 'dir', fallback=os.path.join(NIPA_DIR, "results")) -# TODO: use config parser = argparse.ArgumentParser() parser.add_argument('--mdir', required=True, help='path to the directory with the patches') @@ -55,7 +54,7 @@ series.tree_mark_expected = False for f in files: - with open(f, 'r') as fp: + with open(f, 'r', encoding="utf-8") as fp: data = fp.read() if re.search(r"\[.* 0+/\d.*\]", data) and \ not re.search(r"\n@@ -\d", data): From 0fb552db830b2e6c180f81f10b02743521e01f2f Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 15 Aug 2025 16:46:18 -0700 Subject: [PATCH 386/429] ingest_mdir: support testing a single patch Support testing a single patch, rather than a while series (directory of patches). For multiple patches the dir is the only option (rather than allowing --patch to be set multiple times) because we need the cover letter, too. Signed-off-by: Jakub Kicinski --- ingest_mdir.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/ingest_mdir.py b/ingest_mdir.py index 5c0d988..8ca1bc5 100755 --- a/ingest_mdir.py +++ b/ingest_mdir.py @@ -29,8 +29,11 @@ fallback=os.path.join(NIPA_DIR, "results")) parser = argparse.ArgumentParser() -parser.add_argument('--mdir', required=True, - help='path to the directory with the patches') + +patch_arg = parser.add_mutually_exclusive_group(required=True) +patch_arg.add_argument('--patch', help='path to the patch file') +patch_arg.add_argument('--mdir', help='path to the directory with the patches') + parser.add_argument('--tree', required=True, help='path to the tree to test on') parser.add_argument('--tree-name', default='unknown', help='the tree name to expect') @@ -40,7 +43,6 @@ help='the directory where results will be generated') args = parser.parse_args() -args.mdir = os.path.abspath(args.mdir) args.tree = os.path.abspath(args.tree) log_init(config.get('log', 'type'), config.get('log', 'path'), @@ -48,7 +50,12 @@ log_open_sec("Loading patches") try: - files = [os.path.join(args.mdir, f) for f in sorted(os.listdir(args.mdir))] + if args.mdir: + mdir = os.path.abspath(args.mdir) + files = [os.path.join(mdir, f) for f in sorted(os.listdir(mdir))] + else: + files = [os.path.abspath(args.patch)] + series = Series() series.tree_selection_comment = "ingest_mdir" series.tree_mark_expected = False From 000a1cda5741dd96ec88996fe02fe6acb793e2ab Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 20 Aug 2025 16:19:12 -0700 Subject: [PATCH 387/429] ingest_mdir: don't load local configs Don't try to load real config files. NIPA expects configs because it's geared towards service execution. But for CLI this is a pain. Create an empty config and populate it as needed from command line arguments. Signed-off-by: Jakub Kicinski --- ingest_mdir.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ingest_mdir.py b/ingest_mdir.py index 8ca1bc5..821c836 100755 --- a/ingest_mdir.py +++ b/ingest_mdir.py @@ -23,7 +23,12 @@ from core import Tester config = configparser.ConfigParser() -config.read(['nipa.config', "tester.config"]) + +config.add_section('dirs') +config.add_section('log') + +config.set('log', 'type', 'org') +config.set('log', 'path', '.nipa.log') results_dir = config.get('results', 'dir', fallback=os.path.join(NIPA_DIR, "results")) From 604f0b08d72f921620001619f8b42ac46b89b85b Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 20 Aug 2025 16:20:21 -0700 Subject: [PATCH 388/429] tester: allow passing config to the tester Tester has its own thread, so to avoid ownership issues it instantiates its own config object. But in ingest_mdir the config is only used at init, and it's fake so we're better off passing it in. Signed-off-by: Jakub Kicinski --- core/tester.py | 9 +++++---- ingest_mdir.py | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/core/tester.py b/core/tester.py index 268f465..4a9d5b9 100644 --- a/core/tester.py +++ b/core/tester.py @@ -49,7 +49,7 @@ def mark_done(result_dir, series): class Tester(threading.Thread): - def __init__(self, result_dir, tree, queue, done_queue): + def __init__(self, result_dir, tree, queue, done_queue, config=None): threading.Thread.__init__(self) self.tree = tree @@ -57,7 +57,7 @@ def __init__(self, result_dir, tree, queue, done_queue): self.done_queue = done_queue self.should_die = False self.result_dir = result_dir - self.config = None + self.config = config self.include = None self.exclude = None @@ -65,8 +65,9 @@ def __init__(self, result_dir, tree, queue, done_queue): self.patch_tests = [] def run(self) -> None: - self.config = configparser.ConfigParser() - self.config.read(['nipa.config', 'pw.config', 'tester.config']) + if self.config is None: + self.config = configparser.ConfigParser() + self.config.read(['nipa.config', 'pw.config', 'tester.config']) log_dir = self.config.get('log', 'dir', fallback=core.NIPA_DIR) core.log_init( diff --git a/ingest_mdir.py b/ingest_mdir.py index 821c836..82277ba 100755 --- a/ingest_mdir.py +++ b/ingest_mdir.py @@ -84,12 +84,12 @@ try: done = queue.Queue() pending = queue.Queue() - tester = Tester(args.result_dir, tree, pending, done) + tester = Tester(args.result_dir, tree, pending, done, + config=config) tester.start() pending.put(series) pending.put(None) - except: tester.should_die = True finally: From b3e5cbdfc7eab2dd3f7d1942ef333b4afe281467 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 15 Aug 2025 17:19:19 -0700 Subject: [PATCH 389/429] ingest_mdir: split up into functions Refactor the code so that it's split into functions, with real control flow.. No functional changes intended. Signed-off-by: Jakub Kicinski --- ingest_mdir.py | 134 +++++++++++++++++++++++++++++-------------------- 1 file changed, 79 insertions(+), 55 deletions(-) diff --git a/ingest_mdir.py b/ingest_mdir.py index 82277ba..40cf4b1 100755 --- a/ingest_mdir.py +++ b/ingest_mdir.py @@ -46,58 +46,82 @@ help='the branch or commit to use as a base for applying patches') parser.add_argument('--result-dir', default=results_dir, help='the directory where results will be generated') -args = parser.parse_args() - -args.tree = os.path.abspath(args.tree) - -log_init(config.get('log', 'type'), config.get('log', 'path'), - force_single_thread=True) - -log_open_sec("Loading patches") -try: - if args.mdir: - mdir = os.path.abspath(args.mdir) - files = [os.path.join(mdir, f) for f in sorted(os.listdir(mdir))] - else: - files = [os.path.abspath(args.patch)] - - series = Series() - series.tree_selection_comment = "ingest_mdir" - series.tree_mark_expected = False - - for f in files: - with open(f, 'r', encoding="utf-8") as fp: - data = fp.read() - if re.search(r"\[.* 0+/\d.*\]", data) and \ - not re.search(r"\n@@ -\d", data): - series.set_cover_letter(data) - else: - series.add_patch(Patch(data)) -finally: - log_end_sec() - -tree = Tree(args.tree_name, args.tree_name, args.tree, branch=args.tree_branch) -if not tree.check_applies(series): - print("Patch series does not apply cleanly to the tree") - os.sys.exit(1) - -try: - done = queue.Queue() - pending = queue.Queue() - tester = Tester(args.result_dir, tree, pending, done, - config=config) - tester.start() - - pending.put(series) - pending.put(None) -except: - tester.should_die = True -finally: - tester.join() - -# Summary hack -os.system(f'for i in $(find {args.result_dir} -type f -name summary); do ' + - 'dir=$(dirname "$i"); ' + - 'head -n2 "$dir"/summary; ' + - 'cat "$dir"/desc 2>/dev/null; done' -) + + +def run_tester(args, tree, series): + """ Run the tester, report results as they appear """ + + try: + done = queue.Queue() + pending = queue.Queue() + tester = Tester(args.result_dir, tree, pending, done, + config=config) + tester.start() + + pending.put(series) + pending.put(None) + except: + tester.should_die = True + finally: + tester.join() + + +def load_patches(args): + """ Load patches from specified location on disk """ + + log_open_sec("Loading patches") + try: + if args.mdir: + mdir = os.path.abspath(args.mdir) + files = [os.path.join(mdir, f) for f in sorted(os.listdir(mdir))] + else: + files = [os.path.abspath(args.patch)] + + series = Series() + series.tree_selection_comment = "ingest_mdir" + series.tree_mark_expected = False + + for f in files: + with open(f, 'r', encoding="utf-8") as fp: + data = fp.read() + if re.search(r"\[.* 0+/\d.*\]", data) and \ + not re.search(r"\n@@ -\d", data): + series.set_cover_letter(data) + else: + series.add_patch(Patch(data)) + finally: + log_end_sec() + + return series + + +def main(): + """ Main function """ + + args = parser.parse_args() + + args.tree = os.path.abspath(args.tree) + + log_init(config.get('log', 'type'), config.get('log', 'path'), + force_single_thread=True) + + series = load_patches(args) + + tree = Tree(args.tree_name, args.tree_name, args.tree, + branch=args.tree_branch) + if not tree.check_applies(series): + print("Patch series does not apply cleanly to the tree") + os.sys.exit(1) + + run_tester(args, tree, series) + + # Summary hack + os.system(f'for i in $(find {args.result_dir} -type f -name summary); do ' + + 'dir=$(dirname "$i"); ' + + 'head -n2 "$dir"/summary; ' + + 'cat "$dir"/desc 2>/dev/null; done' + ) + + +if __name__ == "__main__": + main() From bf902222c62c730ffbf76aaa158d6a944e630bb3 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 15 Aug 2025 17:01:56 -0700 Subject: [PATCH 390/429] ingest_mdir: use current branch in the kernel tree Instead of letting user specify the tree branch use what's checked out. This is slightly less convenient, but it will be less error prone. It will avoid accidental changes to the state of the tree. Detach when we start testing to avoid overriding the branch. Then return to the branch at the end, discarding the tested patches. Signed-off-by: Jakub Kicinski --- core/tree.py | 17 ++++++++++++++++- ingest_mdir.py | 18 ++++++++++++++---- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/core/tree.py b/core/tree.py index e81385c..07c90e7 100644 --- a/core/tree.py +++ b/core/tree.py @@ -35,9 +35,11 @@ class Tree: """The git tree class Git tree class which controls a git tree + + current_branch: use whathever is currently checked out as branch """ def __init__(self, name, pfx, fspath, remote=None, branch=None, - wt_id=None, parent=None): + wt_id=None, parent=None, current_branch=False): self.name = name self.pfx = pfx self.path = os.path.abspath(fspath) @@ -49,6 +51,8 @@ def __init__(self, name, pfx, fspath, remote=None, branch=None, else: self.lock = multiprocessing.RLock() + if current_branch: + self.branch = self.current_branch() if remote and not branch: self.branch = remote + "/main" @@ -81,6 +85,9 @@ def git(self, args: List[str]): def git_am(self, patch): return self.git(["am", "-s", "--", patch]) + def git_checkout(self, ref): + return self.git(["checkout", ref]) + def git_pull(self, pull_url, ff=None): cmd = ["pull", "--no-edit", "--signoff"] if ff == True: @@ -133,6 +140,14 @@ def _check_tree(self): finally: core.log_end_sec() + def current_branch(self): + out = self.git(["symbolic-ref", "-q", "HEAD"]) + if out: + out = out.strip() + if out.startswith('refs/heads/'): + out = out[11:] + return out + def head_hash(self): return self.git(['rev-parse', 'HEAD']).strip() diff --git a/ingest_mdir.py b/ingest_mdir.py index 40cf4b1..4e326e0 100755 --- a/ingest_mdir.py +++ b/ingest_mdir.py @@ -15,6 +15,7 @@ import re import queue +from core import cmd from core import NIPA_DIR from core import log_open_sec, log_end_sec, log_init from core import Patch @@ -42,8 +43,6 @@ parser.add_argument('--tree', required=True, help='path to the tree to test on') parser.add_argument('--tree-name', default='unknown', help='the tree name to expect') -parser.add_argument('--tree-branch', default='main', - help='the branch or commit to use as a base for applying patches') parser.add_argument('--result-dir', default=results_dir, help='the directory where results will be generated') @@ -107,13 +106,24 @@ def main(): series = load_patches(args) - tree = Tree(args.tree_name, args.tree_name, args.tree, - branch=args.tree_branch) + try: + tree = Tree(args.tree_name, args.tree_name, args.tree, + current_branch=True) + except cmd.CmdError: + print("Can't assertain tree state, is a valid branch checked out?") + raise + head = tree.head_hash() + tree.git_checkout(head) + if not tree.check_applies(series): print("Patch series does not apply cleanly to the tree") os.sys.exit(1) + tree.git_reset(head, hard=True) + run_tester(args, tree, series) + tree.git_checkout(tree.branch) + tree.git_reset(head, hard=True) # Summary hack os.system(f'for i in $(find {args.result_dir} -type f -name summary); do ' + From c8a192c35b4306627eaea177b3bea0c7f0e5e0c0 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 20 Aug 2025 14:05:44 -0700 Subject: [PATCH 391/429] ingest_mdir: try to extract tree name from the patches Right now we default to using "unknown" as the tree name, which makes fixes_present test false positive (it expects a Fixes tag if the tree does not have -next in its name). The patches we ingest should have a tree name annotation. Try to extract it from there. Signed-off-by: Jakub Kicinski --- ingest_mdir.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/ingest_mdir.py b/ingest_mdir.py index 4e326e0..073262f 100755 --- a/ingest_mdir.py +++ b/ingest_mdir.py @@ -41,8 +41,7 @@ patch_arg.add_argument('--mdir', help='path to the directory with the patches') parser.add_argument('--tree', required=True, help='path to the tree to test on') -parser.add_argument('--tree-name', default='unknown', - help='the tree name to expect') +parser.add_argument('--tree-name', help='the tree name to expect') parser.add_argument('--result-dir', default=results_dir, help='the directory where results will be generated') @@ -106,9 +105,23 @@ def main(): series = load_patches(args) + tree_name = args.tree_name + if tree_name is None: + # Try to guess tree name from the patch subject, expecting subject + # to be something like [PATCH tree-name 2/N]. + tags = re.search( + r'Subject: \[(?:PATCH|RFC) (?:v\d+ )?([a-zA-Z-]+)(?: v\d+)?(?: \d*\/\d*)?\]', + series.patches[0].raw_patch + ) + if tags: + tree_name = tags.group(1).strip() + print("Tree name extracted from patches:", tree_name) + else: + tree_name = "unknown" + print("Tree name unknown") + try: - tree = Tree(args.tree_name, args.tree_name, args.tree, - current_branch=True) + tree = Tree(tree_name, tree_name, args.tree, current_branch=True) except cmd.CmdError: print("Can't assertain tree state, is a valid branch checked out?") raise From cfa3f8068297be8e5226544e87ab6b3d946d3f7b Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 15 Aug 2025 17:30:17 -0700 Subject: [PATCH 392/429] ingest_mdir: automatically create a temp dir for results, save logs there Save logs to the results dir. If user doesn't specify one create a tempdir rather than polluting the local directory. Signed-off-by: Jakub Kicinski --- ingest_mdir.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/ingest_mdir.py b/ingest_mdir.py index 073262f..8071512 100755 --- a/ingest_mdir.py +++ b/ingest_mdir.py @@ -14,9 +14,9 @@ import os import re import queue +import tempfile from core import cmd -from core import NIPA_DIR from core import log_open_sec, log_end_sec, log_init from core import Patch from core import Series @@ -28,12 +28,6 @@ config.add_section('dirs') config.add_section('log') -config.set('log', 'type', 'org') -config.set('log', 'path', '.nipa.log') - -results_dir = config.get('results', 'dir', - fallback=os.path.join(NIPA_DIR, "results")) - parser = argparse.ArgumentParser() patch_arg = parser.add_mutually_exclusive_group(required=True) @@ -42,7 +36,7 @@ parser.add_argument('--tree', required=True, help='path to the tree to test on') parser.add_argument('--tree-name', help='the tree name to expect') -parser.add_argument('--result-dir', default=results_dir, +parser.add_argument('--result-dir', help='the directory where results will be generated') @@ -100,7 +94,16 @@ def main(): args.tree = os.path.abspath(args.tree) - log_init(config.get('log', 'type'), config.get('log', 'path'), + if args.result_dir is None: + args.result_dir = tempfile.mkdtemp() + print("Saving output and logs to:", args.result_dir) + + config.set('log', 'type', 'org') + config.set('log', 'dir', args.result_dir) + config.set('log', 'path', "nipa.log") + + log_init(config.get('log', 'type'), + os.path.join(args.result_dir, 'nipa.log'), force_single_thread=True) series = load_patches(args) From 856197e3b66f6217ab9b63d845ea7fb3f0611834 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Mon, 18 Aug 2025 12:34:28 -0700 Subject: [PATCH 393/429] ingest_mdir: assign series ID automatically If result dir is reused, and we always use series ID 1 the tester will think the series is already tested and do nothing. Assign a series ID which does not exist. Signed-off-by: Jakub Kicinski --- ingest_mdir.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/ingest_mdir.py b/ingest_mdir.py index 8071512..f2fdac6 100755 --- a/ingest_mdir.py +++ b/ingest_mdir.py @@ -40,6 +40,15 @@ help='the directory where results will be generated') +def get_series_id(result_dir): + """ Find an unused series ID. """ + + i = 1 + while os.path.exists(os.path.join(result_dir, str(i))): + i += 1 + return i + + def run_tester(args, tree, series): """ Run the tester, report results as they appear """ @@ -61,6 +70,8 @@ def run_tester(args, tree, series): def load_patches(args): """ Load patches from specified location on disk """ + series_id = get_series_id(args.result_dir) + log_open_sec("Loading patches") try: if args.mdir: @@ -69,7 +80,7 @@ def load_patches(args): else: files = [os.path.abspath(args.patch)] - series = Series() + series = Series(ident=series_id) series.tree_selection_comment = "ingest_mdir" series.tree_mark_expected = False @@ -142,7 +153,7 @@ def main(): tree.git_reset(head, hard=True) # Summary hack - os.system(f'for i in $(find {args.result_dir} -type f -name summary); do ' + + os.system(f'for i in $(find {args.result_dir}/{series.id} -type f -name summary); do ' + 'dir=$(dirname "$i"); ' + 'head -n2 "$dir"/summary; ' + 'cat "$dir"/desc 2>/dev/null; done' From 67fd6c78679d836e2699520fd90a0d59cf801a39 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Mon, 18 Aug 2025 12:36:11 -0700 Subject: [PATCH 394/429] ingest_mdir: print test results as we go Major improvement to the printing of results. Scan the tree for results and output as tester spits them out. Signed-off-by: Jakub Kicinski --- ingest_mdir.py | 164 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 157 insertions(+), 7 deletions(-) diff --git a/ingest_mdir.py b/ingest_mdir.py index f2fdac6..2194154 100755 --- a/ingest_mdir.py +++ b/ingest_mdir.py @@ -14,7 +14,9 @@ import os import re import queue +import shutil import tempfile +import time from core import cmd from core import log_open_sec, log_end_sec, log_init @@ -23,6 +25,13 @@ from core import Tree from core import Tester +CONSOLE_WIDTH = None +BOLD = '\033[1m' +RED = '\033[31m' +GREEN = '\033[32m' +YELLOW = '\033[33m' +RESET = '\033[0m' + config = configparser.ConfigParser() config.add_section('dirs') @@ -40,6 +49,20 @@ help='the directory where results will be generated') +def get_console_width(): + """ Get console width to avoid line wraps where we can. """ + + global CONSOLE_WIDTH + + if CONSOLE_WIDTH is None: + try: + terminal_size = shutil.get_terminal_size() + CONSOLE_WIDTH = terminal_size.columns + except OSError: + CONSOLE_WIDTH = 80 + return CONSOLE_WIDTH + + def get_series_id(result_dir): """ Find an unused series ID. """ @@ -49,9 +72,134 @@ def get_series_id(result_dir): return i +def print_summary_singleton(print_state, files, full_path, patch_id): + """ + Print summaries, single patch mode. + Output differs if we have one patch vs many because tester will + run the same test on all the patches in sequence. + """ + + if len(print_state['seen']) == 1: + print() + print(BOLD + "Series level tests:") + + if patch_id != print_state['last_patch']: + print_state['last_patch'] = patch_id + print(BOLD + "Patch level tests:") + + test_name = os.path.basename(full_path) + with open(os.path.join(full_path, "retcode"), "r", encoding="utf-8") as fp: + retcode = int(fp.read()) + desc = None + if "desc" in files: + with open(os.path.join(full_path, "desc"), "r", encoding="utf-8") as fp: + desc = fp.read().strip().replace('\n', ' ') + + print(BOLD + f" {test_name:32}", end='') + if retcode == 0: + print(GREEN + "OKAY " + RESET, end='') + elif retcode == 250: + print(YELLOW + "WARNING" + RESET, end='') + else: + print(RED + "FAIL " + RESET + f"({retcode})", end='') + + if desc: + if len(desc) > get_console_width() - 41: + print() + print("", desc, end='') + print('', flush=True) + + +def print_summary_series(print_state, files, full_path, patch_id): + """ Print summaries, series mode (more than one patch). """ + + test_name = os.path.basename(full_path) + if test_name != print_state.get('last_test'): + print_state['last_test'] = test_name + print() + print(BOLD + test_name) + + with open(os.path.join(full_path, "retcode"), "r", encoding="utf-8") as fp: + retcode = int(fp.read()) + desc = None + if "desc" in files: + with open(os.path.join(full_path, "desc"), "r", encoding="utf-8") as fp: + desc = fp.read().strip().replace('\n', ' ') + + if patch_id >= 0: + patch_str = f"Patch {patch_id + 1:<6}" + else: + patch_str = "Full series " + + failed = False + print(BOLD + " " + patch_str, end='') + if retcode == 0: + print(GREEN + "OKAY " + RESET, end='') + elif retcode == 250: + print(YELLOW + "WARNING" + RESET, end='') + else: + print(RED + "FAIL " + RESET + f"({retcode})", end='') + failed = True + + if failed or (desc and len(desc) > get_console_width() - 21): + print("\n", end=" ") + if desc: + print("", desc, end='') + if failed: + print("\n", end=" ") + if failed: + print(" Outputs:", full_path, end='') + print('', flush=True) + + +def print_test_summary(args, series, print_state): + """ + Report results based on files created by the tester in the filesystem. + Track which files we have already as this function should be called + periodically to check for new results, as the tester runs. + """ + + seen = print_state.get('seen', set()) + print_state['seen'] = seen + print_state['last_patch'] = print_state.get('last_patch', -1) + + for full_path, _, files in os.walk(os.path.join(args.result_dir, + str(series.id))): + if full_path in seen: + continue + if "summary" not in files: + continue + seen.add(full_path) + + rel_path = full_path[len(args.result_dir) + 1:].split('/') + + patch_id = -1 + if len(rel_path) == 3: + patch_id = int(rel_path[-2]) - 1 + + if len(series.patches) == 1: + print_summary_singleton(print_state, files, full_path, patch_id) + else: + print_summary_series(print_state, files, full_path, patch_id) + + +def print_series_info(series): + """ Print list of patches """ + + if len(series.patches) > 2 and series.cover_letter is None: + print(BOLD + "No cover letter" + RESET) + elif series.cover_letter: + print(BOLD + series.title + RESET) + + for p in series.patches: + print(" " + f"[{p.id}] " + p.title) + + def run_tester(args, tree, series): """ Run the tester, report results as they appear """ + summary_seen = {} + try: done = queue.Queue() pending = queue.Queue() @@ -61,8 +209,15 @@ def run_tester(args, tree, series): pending.put(series) pending.put(None) + + while done.empty(): + print_test_summary(args, series, summary_seen) + time.sleep(0.2) except: + print("Error / Interrupt detected, asking runner to stop") tester.should_die = True + tester.join() + raise finally: tester.join() @@ -134,6 +289,8 @@ def main(): tree_name = "unknown" print("Tree name unknown") + print_series_info(series) + try: tree = Tree(tree_name, tree_name, args.tree, current_branch=True) except cmd.CmdError: @@ -152,13 +309,6 @@ def main(): tree.git_checkout(tree.branch) tree.git_reset(head, hard=True) - # Summary hack - os.system(f'for i in $(find {args.result_dir}/{series.id} -type f -name summary); do ' + - 'dir=$(dirname "$i"); ' + - 'head -n2 "$dir"/summary; ' + - 'cat "$dir"/desc 2>/dev/null; done' - ) - if __name__ == "__main__": main() From 1bce35168dac858f354741ecee80b708fdd4e35c Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 20 Aug 2025 09:24:59 -0700 Subject: [PATCH 395/429] ingest_mdir: dbg: support printing results for existing runs Support printing results for existing runs for quicker testing of output format changes. Signed-off-by: Jakub Kicinski --- ingest_mdir.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/ingest_mdir.py b/ingest_mdir.py index 2194154..157873e 100755 --- a/ingest_mdir.py +++ b/ingest_mdir.py @@ -47,6 +47,7 @@ parser.add_argument('--tree-name', help='the tree name to expect') parser.add_argument('--result-dir', help='the directory where results will be generated') +parser.add_argument('--dbg-print-run', help='print results of previous run') def get_console_width(): @@ -152,7 +153,7 @@ def print_summary_series(print_state, files, full_path, patch_id): print('', flush=True) -def print_test_summary(args, series, print_state): +def print_test_summary(args, series, print_state, tests=None): """ Report results based on files created by the tester in the filesystem. Track which files we have already as this function should be called @@ -172,6 +173,10 @@ def print_test_summary(args, series, print_state): seen.add(full_path) rel_path = full_path[len(args.result_dir) + 1:].split('/') + test_name = os.path.basename(full_path) + + if tests and test_name not in tests: + continue patch_id = -1 if len(rel_path) == 3: @@ -225,7 +230,10 @@ def run_tester(args, tree, series): def load_patches(args): """ Load patches from specified location on disk """ - series_id = get_series_id(args.result_dir) + if args.dbg_print_run is None: + series_id = get_series_id(args.result_dir) + else: + series_id = int(args.dbg_print_run) log_open_sec("Loading patches") try: @@ -291,6 +299,10 @@ def main(): print_series_info(series) + if args.dbg_print_run: + print_test_summary(args, series, {}, tests={'ynl', 'build_clang'}) + return + try: tree = Tree(tree_name, tree_name, args.tree, current_branch=True) except cmd.CmdError: From 56ddfdd2a1d385bca9c729daf18b4074dc83a141 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 21 Aug 2025 12:51:40 -0700 Subject: [PATCH 396/429] contest: remove: add support for env checks Driver tests seem to fail quite a lot due to other tests not cleaning up after themselves. Add a hook point and a script for checking that the env is sane. The expectation is that we'll run contest/scripts/env_check.py in each netns before the test (as part of setup), and then after each test. If post-test check fails VM is destroyed and new one gets started. Signed-off-by: Jakub Kicinski --- contest/remote/lib/vm.py | 7 +- contest/remote/vmksft-p.py | 17 ++++- contest/scripts/env_check.py | 120 +++++++++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+), 4 deletions(-) create mode 100755 contest/scripts/env_check.py diff --git a/contest/remote/lib/vm.py b/contest/remote/lib/vm.py index 3e9a142..5ba324f 100644 --- a/contest/remote/lib/vm.py +++ b/contest/remote/lib/vm.py @@ -149,8 +149,11 @@ def _set_env(self): self.cmd("export LD_LIBRARY_PATH=" + self.config.get('vm', 'ld_paths') + ':$LD_LIBRARY_PATH') self.drain_to_prompt() - if self.config.get('vm', 'setup', fallback=None): - self.cmd(self.config.get('vm', 'setup')) + setup_scripts = self.config.get('vm', 'setup', fallback='').split(',') + for setup in setup_scripts: + if not setup: + continue + self.cmd(setup) self.drain_to_prompt() exports = self.config.get('vm', 'exports', fallback=None) diff --git a/contest/remote/vmksft-p.py b/contest/remote/vmksft-p.py index e34acf5..7cfa9d1 100755 --- a/contest/remote/vmksft-p.py +++ b/contest/remote/vmksft-p.py @@ -222,7 +222,20 @@ def _vm_thread(config, results_path, thr_id, hard_stop, in_queue, out_queue): print(f"INFO: thr-{thr_id} {prog} >> nested tests: {len(nested_tests)}") - if not is_retry and result == 'fail': + can_retry = not is_retry + + post_check = config.get('ksft', 'post_check', fallback=None) + if post_check and not vm.fail_state: + vm.cmd(post_check) + vm.drain_to_prompt() + pc = vm.bash_prev_retcode() + if pc != 0: + vm.fail_state = "env-check-fail" + if result == 'pass': + result = 'fail' + can_retry = False # Don't waste time, the test is buggy + + if can_retry and result == 'fail': in_queue.put(outcome) else: out_queue.put(outcome) @@ -232,7 +245,7 @@ def _vm_thread(config, results_path, thr_id, hard_stop, in_queue, out_queue): "found": indicators, "vm_state": vm.fail_state}) if vm.fail_state: - print(f"INFO: thr-{thr_id} VM kernel crashed, destroying it") + print(f"INFO: thr-{thr_id} VM {vm.fail_state}, destroying it") vm.stop() vm.dump_log(results_path + f'/vm-stop-thr{thr_id}-{vm_id}') vm = None diff --git a/contest/scripts/env_check.py b/contest/scripts/env_check.py new file mode 100755 index 0000000..73ce916 --- /dev/null +++ b/contest/scripts/env_check.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +""" +Collect system state info. Save it to a JSON file, +if file already exists, compare it first and report deltas. +""" + +import json +import os +import subprocess +import sys + + +def run_cmd_text(cmd): + """Execute a shell command and return its output as text.""" + result = subprocess.run(cmd, shell=True, check=False, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + universal_newlines=True) + return result.stdout + + +def run_cmd_json(cmd): + """Execute a shell command and return its output parsed as JSON.""" + result = subprocess.run(cmd, shell=True, check=False, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + universal_newlines=True) + if result.returncode != 0: + return {"error": result.stderr.strip()} + + ret = json.loads(result.stdout) + # "decapsulate" the one element arrays that ip and ethtool like return + if isinstance(ret, list) and len(ret) == 1: + ret = ret[0] + return ret + + +def collect_system_state(): + """Collect network interface information.""" + state = { + "links": {}, + "chans": {}, + "feat": {}, + "rings": {}, + "rss": {}, + "ntuple": {}, + } + + interfaces = run_cmd_json("ip -j -d link show") + + for iface in interfaces: + ifname = iface['ifname'] + + state["links"][ifname] = iface + + state["chans"][ifname] = run_cmd_json(f"ethtool -j -l {ifname}") + state["feat" ][ifname] = run_cmd_json(f"ethtool -j -k {ifname}") + state["rings"][ifname] = run_cmd_json(f"ethtool -j -g {ifname}") + state["rss" ][ifname] = run_cmd_json(f"ethtool -j -x {ifname}") + if "rss-hash-key" in state["rss"][ifname]: + del state["rss"][ifname]["rss-hash-key"] + state["ntuple"][ifname] = run_cmd_text(f"ethtool -n {ifname}") + + return state + + +def compare_states(current, saved, path=""): + """Compare current system state with saved state.""" + + ret = 0 + + if isinstance(current, dict) and isinstance(saved, dict): + for k in current.keys() | saved.keys(): + if k in current and k in saved: + ret |= compare_states(current[k], saved[k], path=f"{path}.{k}") + else: + print(f"Saved {path}.{k}:", saved.get(k)) + print(f"Current {path}.{k}:", current.get(k)) + ret = 1 + else: + if current != saved: + print(f"Saved {path}:", saved) + print(f"Current {path}:", current) + ret = 1 + + return ret + + +def main(): + """Main function to collect and compare network interface states.""" + output_file = "/tmp/nipa-env-state.json" + if len(sys.argv) > 1: + output_file = sys.argv[1] + + # Collect current system state + current_state = collect_system_state() + exit_code = 0 + + # Check if the file already exists + if os.path.exists(output_file): + print("Comparing to existing state file: ", end="") + try: + with open(output_file, 'r', encoding='utf-8') as f: + saved_state = json.load(f) + + # Compare states + exit_code = compare_states(current_state, saved_state) + if exit_code == 0: + print("no differences detected.") + except (json.JSONDecodeError, IOError, OSError) as e: + print("Error loading or comparing:") + print(e) + # Save current state to file + with open(output_file, 'w', encoding='utf-8') as f: + json.dump(current_state, f, indent=2) + print(f"Current system state saved to {output_file}") + + sys.exit(exit_code) + + +if __name__ == "__main__": + main() From de7c9111cb2f2c8ed5e14e46bdd4edf8cef94c5e Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 21 Aug 2025 14:01:27 -0700 Subject: [PATCH 397/429] contest: env_check: try to ignore carrier state changes Carrier state changes are not a real problem. Signed-off-by: Jakub Kicinski --- contest/scripts/env_check.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/contest/scripts/env_check.py b/contest/scripts/env_check.py index 73ce916..c6f376a 100755 --- a/contest/scripts/env_check.py +++ b/contest/scripts/env_check.py @@ -62,6 +62,19 @@ def collect_system_state(): return state +def is_linkstate(a, b, path): + """System state key is related to carrier (whether link has come up, yet)""" + + if path.startswith(".links."): + if path.endswith(".operstate"): + return True + if path.endswith(".flags"): + a = set(a) + b = set(b) + diff = a ^ b + return not (diff - {'NO-CARRIER', 'LOWER_UP'}) + return False + def compare_states(current, saved, path=""): """Compare current system state with saved state.""" @@ -79,7 +92,8 @@ def compare_states(current, saved, path=""): if current != saved: print(f"Saved {path}:", saved) print(f"Current {path}:", current) - ret = 1 + + ret |= not is_linkstate(current, saved, path) return ret From c15d2bb217e5a500f2e052e22a58b6c4b56e914f Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 22 Aug 2025 07:02:33 -0700 Subject: [PATCH 398/429] tree_match: move drivers/phy to acceptable We don't take pure drivers/phy changes, move them to acceptable files. A change under net/ or drivers/net is needed in the same series for us to care. Signed-off-by: Jakub Kicinski --- netdev/tree_match.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netdev/tree_match.py b/netdev/tree_match.py index b1868b2..9046c37 100644 --- a/netdev/tree_match.py +++ b/netdev/tree_match.py @@ -45,6 +45,7 @@ def _tree_name_should_be_local_files(raw_email): 'include/', 'rust/', 'tools/', + 'drivers/phy/', 'drivers/vhost/', } required_files = { @@ -65,7 +66,6 @@ def _tree_name_should_be_local_files(raw_email): 'drivers/net/', 'drivers/dsa/', 'drivers/nfc/', - 'drivers/phy/', 'drivers/ptp/', 'drivers/net/ethernet/', 'kernel/bpf/', From 8e974af8dc071b7a24d2bdae11f47b0554f24e24 Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Wed, 3 Sep 2025 12:30:44 +0200 Subject: [PATCH 399/429] contest: vm: kmemleak: eat, sleep, rave, repeat As recommended by Catalin (and Fatboy Slim???), it seems better to scan, wait for the grace period, scan again, then look for leaks. This should catch most issues, and avoid most of the false positives. Link: https://lore.kernel.org/aLdfOrQ4O4rnD5M9@arm.com Signed-off-by: Matthieu Baerts (NGI0) --- contest/remote/lib/vm.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/contest/remote/lib/vm.py b/contest/remote/lib/vm.py index 5ba324f..8e7efbe 100644 --- a/contest/remote/lib/vm.py +++ b/contest/remote/lib/vm.py @@ -405,12 +405,13 @@ def check_health(self): if self.fail_state: return if self.has_kmemleak: + # First scan, to identify possible leaks + self.cmd("echo scan > /sys/kernel/debug/kmemleak") + self.drain_to_prompt() # kmemleak needs objects to be at least MSECS_MIN_AGE (5000) # before it considers them to have been leaked sleep(5) - self.cmd("echo scan > /sys/kernel/debug/kmemleak && cat /sys/kernel/debug/kmemleak") - self.drain_to_prompt() - # Do it twice, kmemleak likes to hide the leak on the first attempt + # Second scan, to identify what has really leaked self.cmd("echo scan > /sys/kernel/debug/kmemleak && cat /sys/kernel/debug/kmemleak") self.drain_to_prompt() From 09bbb20cea92fb84a36f925f8f7f66f492cf8418 Mon Sep 17 00:00:00 2001 From: "Matthieu Baerts (NGI0)" Date: Tue, 9 Sep 2025 22:55:37 +0200 Subject: [PATCH 400/429] tests: python: add ruff When looking at 'ynl', 'ruff' found out at least one interesting error: one variable was used but not defined. It looks then interesting to add it. Its usage is very similar to pylint. Some says it can also be used instead of pylint, but I'm not experienced enough to judge. Note that by default, ruff will only check for "interesting" errors. Signed-off-by: Matthieu Baerts (NGI0) --- tests/patch/ruff/info.json | 3 ++ tests/patch/ruff/ruff.sh | 60 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 tests/patch/ruff/info.json create mode 100755 tests/patch/ruff/ruff.sh diff --git a/tests/patch/ruff/info.json b/tests/patch/ruff/info.json new file mode 100644 index 0000000..03f4d40 --- /dev/null +++ b/tests/patch/ruff/info.json @@ -0,0 +1,3 @@ +{ + "run": ["ruff.sh"] +} diff --git a/tests/patch/ruff/ruff.sh b/tests/patch/ruff/ruff.sh new file mode 100755 index 0000000..fa25fde --- /dev/null +++ b/tests/patch/ruff/ruff.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 + +HEAD=$(git rev-parse HEAD) +rc=0 + +pr() { + echo " ====== $* ======" | tee -a /dev/stderr +} + +# If it doesn't touch .py files, don't bother. Ignore deleted. +if ! git show --diff-filter=AM --pretty="" --name-only "${HEAD}" | grep -q -E "\.py$" +then + echo "No python scripts touched, skip" >&"$DESC_FD" + exit 0 +fi + +ruff --version || exit 1 + +tmpfile_o=$(mktemp) +tmpfile_n=$(mktemp) + +echo "Redirect to $tmpfile_o and $tmpfile_n" + +echo "Tree base:" +git log -1 --pretty='%h ("%s")' HEAD~ +echo "Now at:" +git log -1 --pretty='%h ("%s")' HEAD + +pr "Checking before the patch" +git checkout -q HEAD~ + +# Also ignore created, as not present in the parent commit +for f in $(git show --diff-filter=M --pretty="" --name-only "${HEAD}" | grep -E "\.py$"); do + ruff check --output-format pylint "$f" | tee -a "$tmpfile_o" +done + +incumbent=$(wc -l < "$tmpfile_o") + +pr "Checking the tree with the patch" +git checkout -q "$HEAD" + +for f in $(git show --diff-filter=AM --pretty="" --name-only "${HEAD}" | grep -E "\.py$"); do + ruff check --output-format pylint "$f" | tee -a "$tmpfile_n" +done + +current=$(wc -l < "$tmpfile_n") + +echo "Errors before: $incumbent ; this patch: $current" >&"$DESC_FD" + +if [ "$current" -gt "$incumbent" ]; then + echo "New errors added" 1>&2 + diff -U 0 "$tmpfile_o" "$tmpfile_n" 1>&2 + + rc=1 +fi + +rm "$tmpfile_o" "$tmpfile_n" + +exit $rc From f207196dc80ebb43d34488b8952b0f19c893b6c4 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Mon, 25 Aug 2025 09:28:56 -0700 Subject: [PATCH 401/429] build_tools: add more broken build targets Add build targets that broke in 6.16. Signed-off-by: Jakub Kicinski --- tests/patch/build_tools/build_tools.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/patch/build_tools/build_tools.sh b/tests/patch/build_tools/build_tools.sh index 98a7aa4..369bbb8 100755 --- a/tests/patch/build_tools/build_tools.sh +++ b/tests/patch/build_tools/build_tools.sh @@ -33,7 +33,7 @@ echo "Now at:" git log -1 --pretty='%h ("%s")' HEAD # These are either very slow or don't build -export SKIP_TARGETS="bpf dt kvm landlock livepatch lsm sched_ext user_events mm powerpc filesystems/mount-notify ublk" +export SKIP_TARGETS="bpf dt kvm landlock livepatch lsm sched_ext user_events mm powerpc filesystems/mount-notify ublk sgx nolibc nsfs" pr "Cleaning" make O=$output_dir $build_flags -C tools/testing/selftests/ clean From 685cca6a4b8223819edeba11a5ae18f6d36c4650 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 30 Aug 2025 13:13:18 -0700 Subject: [PATCH 402/429] ui: add buttons to update the URL with the current filters Add buttons to update URL from currently set filters. This is very useful when filtered down list needs to be shared over email. Signed-off-by: Jakub Kicinski --- ui/contest.html | 4 ++++ ui/contest.js | 60 +++++++++++++++++++++++++++++++++++++++++++++++++ ui/flakes.html | 3 +++ ui/flakes.js | 54 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 121 insertions(+) diff --git a/ui/contest.html b/ui/contest.html index 5aa86c6..3cfee4d 100644 --- a/ui/contest.html +++ b/ui/contest.html @@ -93,6 +93,10 @@
+

+

+ Update URL +

diff --git a/ui/contest.js b/ui/contest.js index 8a73f10..9670e10 100644 --- a/ui/contest.js +++ b/ui/contest.js @@ -276,6 +276,61 @@ function reload_data(event) }); } +function update_url_from_filters() +{ + const result_filter = { + "pass": document.getElementById("pass").checked, + "skip": document.getElementById("skip").checked, + "warn": document.getElementById("warn").checked, + "fail": document.getElementById("fail").checked + }; + const branch_filter = document.getElementById("branch").value; + const exec_filter = document.getElementById("executor").value; + const remote_filter = document.getElementById("remote").value; + const test_filter = document.getElementById("test").value; + const pw_n = document.getElementById("pw-n").checked; + const pw_y = document.getElementById("pw-y").checked; + const ld_cases = document.getElementById("ld-cases").checked; + + // Create new URL with current filters + const currentUrl = new URL(window.location.href); + + // Clear existing filter parameters + const filterParams = ['pass', 'skip', 'warn', 'fail', 'branch', 'executor', + 'remote', 'test', 'pw-n', 'pw-y', 'ld-cases']; + filterParams.forEach(param => currentUrl.searchParams.delete(param)); + + // Add current filter states to URL + if (!result_filter.pass) + currentUrl.searchParams.set('pass', '0'); + if (!result_filter.skip) + currentUrl.searchParams.set('skip', '0'); + if (!result_filter.warn) + currentUrl.searchParams.set('warn', '0'); + if (!result_filter.fail) + currentUrl.searchParams.set('fail', '0'); + + if (branch_filter) + currentUrl.searchParams.set('branch', branch_filter); + if (exec_filter) + currentUrl.searchParams.set('executor', exec_filter); + if (remote_filter) + currentUrl.searchParams.set('remote', remote_filter); + if (test_filter) + currentUrl.searchParams.set('test', test_filter); + + if (!pw_n) + currentUrl.searchParams.set('pw-n', '0'); + if (!pw_y) + currentUrl.searchParams.set('pw-y', '0'); + + if (ld_cases) + currentUrl.searchParams.set('ld-cases', '1'); + + // Update the browser URL without reloading the page + window.history.pushState({}, '', currentUrl.toString()); +} + function embedded_mode() { $('#loading-fieldset').hide(); $('#sitemap').hide(); @@ -313,6 +368,11 @@ function do_it() document.getElementById("ld_cnt").value = 1; } + $('#update-url-button').on('click', function (e) { + e.preventDefault(); + update_url_from_filters(); + }); + nipa_sort_cb = results_update; /* diff --git a/ui/flakes.html b/ui/flakes.html index 9ec16b1..98dd6a3 100644 --- a/ui/flakes.html +++ b/ui/flakes.html @@ -69,6 +69,9 @@
+

+ Update URL +

diff --git a/ui/flakes.js b/ui/flakes.js index ce3063b..e92c435 100644 --- a/ui/flakes.js +++ b/ui/flakes.js @@ -209,6 +209,55 @@ function remotes_loaded(data_raw) loaded_one(); } +function update_url_from_filters() +{ + const tn_needle = document.getElementById("tn-needle").value; + const min_flip = document.getElementById("min-flip").value; + const pw_n = document.getElementById("pw-n").checked; + const pw_y = document.getElementById("pw-y").checked; + const sort_streak = document.getElementById("sort-streak").checked; + const br_cnt = document.getElementById("br-cnt").value; + const br_pfx = document.getElementById("br-pfx").value; + const ld_remote = document.getElementById("ld-remote").value; + const ld_cases = document.getElementById("ld-cases").checked; + + // Create new URL with current filters + const currentUrl = new URL(window.location.href); + + // Clear existing filter parameters + const filterParams = ['tn-needle', 'min-flip', 'pw-n', 'pw-y', 'sort-flips', + 'sort-streak', 'br-cnt', 'br-pfx', + 'ld-remote', 'ld-cases']; + filterParams.forEach(param => currentUrl.searchParams.delete(param)); + + // Add current filter states to URL + if (tn_needle) + currentUrl.searchParams.set('tn-needle', tn_needle); + if (min_flip && min_flip !== '1') + currentUrl.searchParams.set('min-flip', min_flip); + + if (!pw_n) + currentUrl.searchParams.set('pw-n', '0'); + if (!pw_y) + currentUrl.searchParams.set('pw-y', '0'); + + if (sort_streak) + currentUrl.searchParams.set('sort-streak', '1'); + + if (br_cnt && br_cnt !== '100') + currentUrl.searchParams.set('br-cnt', br_cnt); + if (br_pfx) + currentUrl.searchParams.set('br-pfx', br_pfx); + if (ld_remote) + currentUrl.searchParams.set('ld-remote', ld_remote); + + if (ld_cases) + currentUrl.searchParams.set('ld-cases', '1'); + + // Update the browser URL without reloading the page + window.history.pushState({}, '', currentUrl.toString()); +} + function reload_data() { const format_l2 = document.getElementById("ld-cases"); @@ -238,6 +287,11 @@ function do_it() nipa_filters_enable(results_update, "fl-pw"); nipa_input_set_from_url("https://github.com/ld-pw"); + $('#update-url-button').on('click', function (e) { + e.preventDefault(); + update_url_from_filters(); + }); + /* * Please remember to keep these assets in sync with `scripts/ui_assets.sh` */ From e1c10661a5d173aff750e345496bec917c78d71e Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Mon, 1 Sep 2025 14:46:14 -0700 Subject: [PATCH 403/429] ui: show more flakes Signed-off-by: Jakub Kicinski --- ui/status.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/status.js b/ui/status.js index 8d979bd..82244d4 100644 --- a/ui/status.js +++ b/ui/status.js @@ -1042,7 +1042,7 @@ function flakes_doit(data_raw) for (let n = 0; n < 4; n++) total[n] += v["count"][n]; - if (v["count"][0] + v["count"][1] + v["count"][2] < 4 && reported) { + if (v["count"][0] + v["count"][1] + v["count"][2] < 3 && reported) { for (let n = 0; n < 4; n++) reminder[n] += v["count"][n]; return 1; From 61dbb3c05f5a1549a516e4d1a0a925e2e0030adc Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 22 Aug 2025 08:34:02 -0700 Subject: [PATCH 404/429] ingest_mdir: try to default to disabling tests we disable in NIPA Signed-off-by: Jakub Kicinski --- ingest_mdir.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ingest_mdir.py b/ingest_mdir.py index 157873e..c64baad 100755 --- a/ingest_mdir.py +++ b/ingest_mdir.py @@ -36,6 +36,7 @@ config.add_section('dirs') config.add_section('log') +config.add_section('tests') parser = argparse.ArgumentParser() @@ -47,6 +48,8 @@ parser.add_argument('--tree-name', help='the tree name to expect') parser.add_argument('--result-dir', help='the directory where results will be generated') +parser.add_argument('-d', '--disable-test', nargs='+', + help='disable test, can be specified multiple times') parser.add_argument('--dbg-print-run', help='print results of previous run') @@ -297,6 +300,11 @@ def main(): tree_name = "unknown" print("Tree name unknown") + # Default settings for networking trees: + if tree_name.startswith('net'): + if not args.disable_test: + config.set('tests', 'exclude', 'patch/signed') + print_series_info(series) if args.dbg_print_run: From b7d57ae9782b100f8c2b65f12172f94bb30faee8 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Mon, 1 Sep 2025 14:43:10 -0700 Subject: [PATCH 405/429] ingest_mdir: use common printing for dir and patch Signed-off-by: Jakub Kicinski --- ingest_mdir.py | 74 +++++++++++++++++++++----------------------------- 1 file changed, 31 insertions(+), 43 deletions(-) diff --git a/ingest_mdir.py b/ingest_mdir.py index c64baad..62ac8e2 100755 --- a/ingest_mdir.py +++ b/ingest_mdir.py @@ -76,6 +76,35 @@ def get_series_id(result_dir): return i +def __print_summary_result(offset, files, full_path): + with open(os.path.join(full_path, "retcode"), "r", encoding="utf-8") as fp: + retcode = int(fp.read()) + desc = None + if "desc" in files: + with open(os.path.join(full_path, "desc"), "r", encoding="utf-8") as fp: + desc = fp.read().strip().replace('\n', ' ') + + failed = False + + if retcode == 0: + print(GREEN + "OKAY " + RESET, end='') + elif retcode == 250: + print(YELLOW + "WARNING" + RESET, end='') + else: + print(RED + "FAIL " + RESET + f"({retcode})", end='') + failed = True + + if failed or (desc and len(desc) + offset > get_console_width()): + print("\n", end=" ") + if desc: + print("", desc, end='') + if failed: + print("\n", end=" ") + if failed: + print(" Outputs:", full_path, end='') + print('', flush=True) + + def print_summary_singleton(print_state, files, full_path, patch_id): """ Print summaries, single patch mode. @@ -92,26 +121,9 @@ def print_summary_singleton(print_state, files, full_path, patch_id): print(BOLD + "Patch level tests:") test_name = os.path.basename(full_path) - with open(os.path.join(full_path, "retcode"), "r", encoding="utf-8") as fp: - retcode = int(fp.read()) - desc = None - if "desc" in files: - with open(os.path.join(full_path, "desc"), "r", encoding="utf-8") as fp: - desc = fp.read().strip().replace('\n', ' ') print(BOLD + f" {test_name:32}", end='') - if retcode == 0: - print(GREEN + "OKAY " + RESET, end='') - elif retcode == 250: - print(YELLOW + "WARNING" + RESET, end='') - else: - print(RED + "FAIL " + RESET + f"({retcode})", end='') - - if desc: - if len(desc) > get_console_width() - 41: - print() - print("", desc, end='') - print('', flush=True) + __print_summary_result(41, files, full_path) def print_summary_series(print_state, files, full_path, patch_id): @@ -123,37 +135,13 @@ def print_summary_series(print_state, files, full_path, patch_id): print() print(BOLD + test_name) - with open(os.path.join(full_path, "retcode"), "r", encoding="utf-8") as fp: - retcode = int(fp.read()) - desc = None - if "desc" in files: - with open(os.path.join(full_path, "desc"), "r", encoding="utf-8") as fp: - desc = fp.read().strip().replace('\n', ' ') - if patch_id >= 0: patch_str = f"Patch {patch_id + 1:<6}" else: patch_str = "Full series " - failed = False print(BOLD + " " + patch_str, end='') - if retcode == 0: - print(GREEN + "OKAY " + RESET, end='') - elif retcode == 250: - print(YELLOW + "WARNING" + RESET, end='') - else: - print(RED + "FAIL " + RESET + f"({retcode})", end='') - failed = True - - if failed or (desc and len(desc) > get_console_width() - 21): - print("\n", end=" ") - if desc: - print("", desc, end='') - if failed: - print("\n", end=" ") - if failed: - print(" Outputs:", full_path, end='') - print('', flush=True) + __print_summary_result(21, files, full_path) def print_test_summary(args, series, print_state, tests=None): From 468d3487587aabe5a7071a37f3a562bbcffe719f Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 12 Sep 2025 09:48:16 -0700 Subject: [PATCH 406/429] README: add more info about local testing Signed-off-by: Jakub Kicinski --- README.rst | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 4d29e37..893b68d 100644 --- a/README.rst +++ b/README.rst @@ -36,11 +36,34 @@ Having everyone test their patches locally allows for better scaling (no need for big central infrastructure) and hopefully creates an incentive for contributing. +Running locally +=============== + +`ingest_mdir.py` can ingest patches and run the checks locally +(by the developers on their machines). `ingest_mdir.py` should be pointed +at a directory and run all the checks on patches that directory contains +(patches are expected to be generated by `git format-patch`). + +Example: + +.. code-block:: bash + + cd $linux + git format-patch HEAD~4.. -o /tmp/my-series/ --subject-prefix="PATCH net-next" + git checkout net-next/master -b test + + cd $nipa + ./ingest_mdir.py --mdir /tmp/my-series/ --tree $linux + +Note that we need to check out the linux tree to a branch that matches the base +on which we intend the patches to be applied. NIPA does not know what to reset +the tree to, it will just try to apply the patches to whatever branch is +currently checked out in the `$linux` repo. + Structure ========= -The project is split into multiple programs with different -uses. +The project is split into multiple programs with different uses. `pw_poller.py` fetches emails from patchwork and runs tests in worker threads. There is one worker thread for each tree, enabling testing @@ -52,11 +75,7 @@ and sub-dirs for each patch. Once tests are done another daemon - `pw_upload.py` uploads the results as checks to patchwork. -`ingest_mdir.py` is supposed to serve the purpose of testing -patches locally, it can be pointed at a directory and run all the -checks on patches that directory contains (patches are expected to -be generated by `git format-patch`). `ingest_mdir.py` has not been -tested in a while so it's probably broken. +`ingest_mdir.py` combines all the stages for local use. Configuration ============= From 273be942d5341bd9148fcab0c266ebec931d978a Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 27 Sep 2025 12:31:58 -0700 Subject: [PATCH 407/429] ingest_mdir: add the ability to run only specific tests Build tests are very slow, when writing Python tests it's useful to be able to run just the Python checks. Signed-off-by: Jakub Kicinski --- core/tester.py | 21 +++++++++++++++++++++ ingest_mdir.py | 19 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/core/tester.py b/core/tester.py index 4a9d5b9..7152a7e 100644 --- a/core/tester.py +++ b/core/tester.py @@ -100,6 +100,27 @@ def run(self) -> None: core.log("Tester exiting") + def get_test_names(self, annotate=True) -> list[str]: + tests_dir = os.path.abspath(core.CORE_DIR + "../../tests") + location = self.config.get('dirs', 'tests', fallback=tests_dir) + + self.include = [x.strip() for x in re.split(r'[,\n]', self.config.get('tests', 'include', fallback="")) if len(x)] + self.exclude = [x.strip() for x in re.split(r'[,\n]', self.config.get('tests', 'exclude', fallback="")) if len(x)] + + tests = [] + for name in ["series", "patch"]: + tests_subdir = os.path.join(location, name) + for td in os.listdir(tests_subdir): + test = f'{name}/{td}' + if not annotate: + pass # don't annotate + elif test in self.exclude or \ + (len(self.include) != 0 and test not in self.include): + test += ' [excluded]' + tests.append(test) + + return tests + def load_tests(self, name): core.log_open_sec(name.capitalize() + " tests") tests_subdir = os.path.join(self.config.get('dirs', 'tests'), name) diff --git a/ingest_mdir.py b/ingest_mdir.py index 62ac8e2..f043f92 100755 --- a/ingest_mdir.py +++ b/ingest_mdir.py @@ -15,6 +15,7 @@ import re import queue import shutil +import sys import tempfile import time @@ -48,8 +49,12 @@ parser.add_argument('--tree-name', help='the tree name to expect') parser.add_argument('--result-dir', help='the directory where results will be generated') +parser.add_argument('--list-tests', action='https://github.com/store_true', + help='print all available tests and exit') parser.add_argument('-d', '--disable-test', nargs='+', help='disable test, can be specified multiple times') +parser.add_argument('-t', '--test', nargs='+', + help='run only specified tests. Note: full test name is needed, e.g. "patch/pylint" or "series/ynl" not just "pylint" or "ynl"') parser.add_argument('--dbg-print-run', help='print results of previous run') @@ -252,6 +257,13 @@ def load_patches(args): return series +def list_tests(args, config): + """ List all available tests and exit """ + + tester = Tester(args.result_dir, None, None, None, config=config) + print(' ', '\n '.join(tester.get_test_names())) + + def main(): """ Main function """ @@ -259,6 +271,13 @@ def main(): args.tree = os.path.abspath(args.tree) + if args.test: + config.set('tests', 'include', ','.join(args.test)) + + if args.list_tests: + list_tests(args, config) + return + if args.result_dir is None: args.result_dir = tempfile.mkdtemp() print("Saving output and logs to:", args.result_dir) From fdc96b813a473e2692904e8ae1615d88b20ec9d2 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 27 Sep 2025 12:51:40 -0700 Subject: [PATCH 408/429] ingest_mdir: make sure we print the result of the last test We exit as soon as we find out that the tester finished. Make sure we do one more scan and printing, otherwise the last test may not be printed. Signed-off-by: Jakub Kicinski --- ingest_mdir.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ingest_mdir.py b/ingest_mdir.py index f043f92..a966622 100755 --- a/ingest_mdir.py +++ b/ingest_mdir.py @@ -214,6 +214,8 @@ def run_tester(args, tree, series): while done.empty(): print_test_summary(args, series, summary_seen) time.sleep(0.2) + # Finish, print the last test's result + print_test_summary(args, series, summary_seen) except: print("Error / Interrupt detected, asking runner to stop") tester.should_die = True From 23af093be3f2ed8abf64960d60729e94b3135e87 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 20 Sep 2025 12:00:46 -0700 Subject: [PATCH 409/429] contest: backend: fix lint errors Signed-off-by: Jakub Kicinski --- contest/backend/query.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/contest/backend/query.py b/contest/backend/query.py index 55b9ce7..77ee0b2 100644 --- a/contest/backend/query.py +++ b/contest/backend/query.py @@ -30,7 +30,7 @@ def hello(): @app.route('/branches') def branches(): with psql.cursor() as cur: - cur.execute(f"SELECT branch, t_date, base, url FROM branches ORDER BY t_date DESC LIMIT 40") + cur.execute("SELECT branch, t_date, base, url FROM branches ORDER BY t_date DESC LIMIT 40") rows = [{"branch": r[0], "date": r[1].isoformat() + "+00:00", "base": r[2], "url": r[3]} for r in cur.fetchall()] rows.reverse() return rows @@ -100,7 +100,7 @@ def results(): br_cnt = request.args.get('branches') try: br_cnt = int(br_cnt) - except: + except (TypeError, ValueError): br_cnt = None if not br_cnt: br_cnt = 10 @@ -151,7 +151,7 @@ def remotes(): t1 = datetime.datetime.now() with psql.cursor() as cur: - cur.execute(f"SELECT remote FROM results GROUP BY remote LIMIT 50") + cur.execute("SELECT remote FROM results GROUP BY remote LIMIT 50") rows = [r[0] for r in cur.fetchall()] t2 = datetime.datetime.now() @@ -167,9 +167,9 @@ def stability(): where = "" if auto == "y" or auto == '1' or auto == 't': - where = "WHERE autoignore = true"; + where = "WHERE autoignore = true" elif auto == "n" or auto == '0' or auto == 'f': - where = "WHERE autoignore = false"; + where = "WHERE autoignore = false" with psql.cursor() as cur: cur.execute(f"SELECT * FROM stability {where}") @@ -185,7 +185,7 @@ def stability(): @app.route('/device-info') def dev_info(): with psql.cursor() as cur: - cur.execute(f"SELECT * FROM devices_info") + cur.execute("SELECT * FROM devices_info") columns = [desc[0] for desc in cur.description] rows = cur.fetchall() @@ -205,14 +205,14 @@ def flaky_tests(): try: limit = int(limit) month = False - except: + except (TypeError, ValueError): month = True # Default to querying last month limit = flake_cnt # Default limit # Find branches with incomplete results, psql JSON helpers fail for them t = datetime.datetime.now() with psql.cursor() as cur: - query = f""" + query = """ SELECT branch FROM results WHERE json_normal NOT LIKE '%"results": [%' From 4d02275bcfa9ce7a8e73d5df08af720bf919e5be Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 20 Sep 2025 13:07:31 -0700 Subject: [PATCH 410/429] results-fetcher: fix lint Signed-off-by: Jakub Kicinski --- contest/results-collector.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/contest/results-collector.py b/contest/results-collector.py index 44aca0c..f7573b4 100755 --- a/contest/results-collector.py +++ b/contest/results-collector.py @@ -10,8 +10,6 @@ import psycopg2 import requests import time -import traceback -import uuid """ @@ -226,7 +224,7 @@ def psql_insert_device(self, data): return with self.psql_conn.cursor() as cur: - cur.execute(f"SELECT info FROM devices_info WHERE " + + cur.execute("SELECT info FROM devices_info WHERE " + cur.mogrify("remote = %s AND executor = %s", (data["remote"], data["executor"], )).decode('utf-8') + "ORDER BY changed DESC LIMIT 1") @@ -243,7 +241,7 @@ def psql_insert_device(self, data): return with self.psql_conn.cursor() as cur: - cur.execute(f"INSERT INTO devices_info (remote, executor, changed, info) " + + cur.execute("INSERT INTO devices_info (remote, executor, changed, info) " + cur.mogrify("VALUES(%s, %s, %s, %s)", (data["remote"], data["executor"], data["start"], new_info)).decode('utf-8')) From e5d176f9f29a629aff8d71d3726ca4a576f48d03 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 20 Sep 2025 12:10:53 -0700 Subject: [PATCH 411/429] contest: store pending results in a separate table We used to store an entry with NULL results in the main result table for runners which reported that they kicked off but haven't finished testing, yet. This was convenient for the status UI which wants to show results for last 10 runs, and unfinished one show up as "pending". However, mixing the NULLs in the main table makes using json helpers in PostreSQL very annoying as we need to filter them out. Move the pending results to their own table. Add a option to query interface to add them in as in practice it's only the status UI that wants to show pending runners. Signed-off-by: Jakub Kicinski --- contest/backend/query.py | 30 ++++++++++++++++++-- contest/results-collector.py | 55 +++++++++++++++++------------------- deploy/contest/db | 12 ++++++++ ui/status.js | 2 +- 4 files changed, 66 insertions(+), 33 deletions(-) diff --git a/contest/backend/query.py b/contest/backend/query.py index 77ee0b2..efdca5b 100644 --- a/contest/backend/query.py +++ b/contest/backend/query.py @@ -43,9 +43,15 @@ def branches_to_rows(br_cnt, remote, br_pfx=None): # Slap the -2 in here as the first letter of the date, to avoid prefix of prefix matches pfx_flt = f"WHERE branch LIKE '{br_pfx}-2%' " if br_pfx else "" - q = f"SELECT branch,count(*),branch_date{remote_k} FROM results {pfx_flt} GROUP BY branch,branch_date{remote_k} ORDER BY branch_date DESC LIMIT {br_cnt}" + # Count from both results and results_pending tables + q_results = f"SELECT branch,count(*),branch_date{remote_k} FROM results {pfx_flt} GROUP BY branch,branch_date{remote_k} ORDER BY branch_date DESC LIMIT {br_cnt}" + q_pending = f"SELECT branch,count(*),branch_date{remote_k} FROM results_pending {pfx_flt} GROUP BY branch,branch_date{remote_k} ORDER BY branch_date DESC LIMIT {br_cnt}" - cur.execute(q) + cur.execute(q_results) + for r in cur.fetchall(): + cnt += r[1] + + cur.execute(q_pending) for r in cur.fetchall(): cnt += r[1] return cnt @@ -81,6 +87,7 @@ def results(): log = "" form = request.args.get('format') + pending = request.args.get('pending') in {'1', 'y', 'yes', 'true'} remote = request.args.get('remote') if remote and re.match(r'^[\w_ -]+$', remote) is None: remote = None @@ -123,9 +130,26 @@ def results(): if not form or form == "normal": with psql.cursor() as cur: cur.execute(f"SELECT json_normal FROM results {where} ORDER BY branch_date DESC LIMIT {limit}") - rows = "[" + ",".join([r[0] for r in cur.fetchall()]) + "]" + all_rows = [r[0] for r in cur.fetchall()] + + if pending: + # Get pending results from results_pending table + cur.execute(f""" + SELECT json_build_object( + 'branch', branch, + 'remote', remote, + 'executor', executor, + 'start', (t_start AT TIME ZONE 'UTC')::text, + 'end', null, + 'results', null + )::text + FROM results_pending {where} ORDER BY branch_date DESC LIMIT {limit} + """) + all_rows += [r[0] for r in cur.fetchall()] + rows = "[" + ",".join(all_rows) + "]" elif form == "l2": with psql.cursor() as cur: + # Get completed results only, pending + l2 makes no sense cur.execute(f"SELECT json_normal, json_full FROM results {where} ORDER BY branch_date DESC LIMIT {limit}") rows = "[" for r in cur.fetchall(): diff --git a/contest/results-collector.py b/contest/results-collector.py index f7573b4..82af61a 100755 --- a/contest/results-collector.py +++ b/contest/results-collector.py @@ -27,6 +27,7 @@ db=db-name stability-name=table-name results-name=table-name +wip-name=table-name branches-name=table-name """ @@ -66,6 +67,7 @@ def __init__(self): self.tbl_stb = self.config.get("db", "stability-name", fallback="stability") self.tbl_res = self.config.get("db", "results-name", fallback="results") + self.tbl_wip = self.config.get("db", "wip-name", fallback="results_pending") self.tbl_brn = self.config.get("db", "branches-name", fallback="branches") db_name = self.config.get("db", "db") @@ -83,36 +85,39 @@ def psql_run_selector(self, cur, remote, run): (run['branch'], remote["name"], run["executor"],)).decode('utf-8') def psql_has_wip(self, remote, run): + """ Check if there is an entry in the WIP/pending table for the run """ with self.psql_conn.cursor() as cur: - cur.execute(f"SELECT branch FROM {self.tbl_res} " + self.psql_run_selector(cur, remote, run)) + cur.execute(f"SELECT branch FROM {self.tbl_wip} " + self.psql_run_selector(cur, remote, run)) rows = cur.fetchall() return rows and len(rows) > 0 - def insert_result_psql(self, cur, data): - fields = "(branch, branch_date, remote, executor, t_start, t_end, json_normal, json_full)" - normal, full = self.psql_json_split(data) - arg = cur.mogrify("(%s,%s,%s,%s,%s,%s,%s,%s)", - (data["branch"], data["branch"][-17:], data["remote"], data["executor"], - data["start"], data["end"], normal, full)) - cur.execute(f"INSERT INTO {self.tbl_res} {fields} VALUES " + arg.decode('utf-8')) + def psql_clear_wip(self, remote, run): + """ Delete entry in the WIP/pending table for the run """ + with self.psql_conn.cursor() as cur: + cur.execute(f"DELETE FROM {self.tbl_wip} " + self.psql_run_selector(cur, remote, run)) - def insert_wip(self, remote, run): + def psql_insert_wip(self, remote, run): + """ + Add entry in the WIP/pending table for the run, if one doesn't exist + """ if self.psql_has_wip(remote, run): - # no point, we have no interesting info to add return branch_info = self.get_branch(run["branch"]) - - data = run.copy() - data["remote"] = remote["name"] when = datetime.datetime.fromisoformat(branch_info['date']) - data["start"] = str(when) - when += datetime.timedelta(hours=2, minutes=58) - data["end"] = str(when) - data["results"] = None with self.psql_conn.cursor() as cur: - self.insert_result_psql(cur, data) + cur.execute(f"INSERT INTO {self.tbl_wip} (branch, remote, executor, branch_date, t_start) VALUES (%s, %s, %s, %s, %s)", + (run["branch"], remote["name"], run["executor"], run["branch"][-17:], str(when))) + + def insert_result_psql(self, data): + with self.psql_conn.cursor() as cur: + fields = "(branch, branch_date, remote, executor, t_start, t_end, json_normal, json_full)" + normal, full = self.psql_json_split(data) + arg = cur.mogrify("(%s,%s,%s,%s,%s,%s,%s,%s)", + (data["branch"], data["branch"][-17:], data["remote"], data["executor"], + data["start"], data["end"], normal, full)) + cur.execute(f"INSERT INTO {self.tbl_res} {fields} VALUES " + arg.decode('utf-8')) def psql_json_split(self, data): # return "normal" and "full" as json string or None @@ -253,16 +258,8 @@ def insert_real(self, remote, run): self.psql_insert_stability(data) self.psql_insert_device(data) - with self.psql_conn.cursor() as cur: - if not self.psql_has_wip(remote, run): - self.insert_result_psql(cur, data) - else: - normal, full = self.psql_json_split(data) - vals = cur.mogrify("SET t_start = %s, t_end = %s, json_normal = %s, json_full = %s", - (data["start"], data["end"], normal, full)).decode('utf-8') - selector = self.psql_run_selector(cur, remote, run) - q = f"UPDATE {self.tbl_res} " + vals + ' ' + selector - cur.execute(q) + self.psql_clear_wip(remote, run) + self.insert_result_psql(data) def write_json_atomic(path, data): @@ -304,7 +301,7 @@ def fetch_remote(fetcher, remote, seen): continue if not run['url']: # Executor has not finished, yet if run['branch'] not in remote_state['wip']: - fetcher.insert_wip(remote, run) + fetcher.psql_insert_wip(remote, run) fetcher.fetched = True continue diff --git a/deploy/contest/db b/deploy/contest/db index 9f678f0..ed174a6 100644 --- a/deploy/contest/db +++ b/deploy/contest/db @@ -23,6 +23,9 @@ psql GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO "nipa"; \q +GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO "nipa-brancher"; +GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO "nipa-brancher"; + # Read-only users createuser flask psql @@ -61,6 +64,15 @@ CREATE TABLE branches ( CREATE INDEX by_branch ON results (branch DESC); CREATE INDEX by_branch_date ON results (branch_date DESC); +CREATE TABLE results_pending ( + id serial primary key, + branch varchar(80), + remote varchar(80), + executor varchar(80), + branch_date varchar(17), + t_start timestamp +); + CREATE TABLE db_monitor ( id serial primary key, ts timestamp not null, diff --git a/ui/status.js b/ui/status.js index 82244d4..f798c23 100644 --- a/ui/status.js +++ b/ui/status.js @@ -1082,7 +1082,7 @@ function do_it() $.get("static/nipa/branch-results.json", branch_res_doit) }); $(document).ready(function() { - $.get("query/results?branches=10", results_loaded) + $.get("query/results?branches=10&pending=y", results_loaded) }); $(document).ready(function() { $.get("static/nipa/branches-info.json", branches_loaded) From 423efeab1165bb6adc1ee8fc9fda3caf63b5de1c Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 20 Sep 2025 12:44:00 -0700 Subject: [PATCH 412/429] contest: backend: use branch_date to trim results For status UI we want to show results from last 10 branches. This is slightly tricky as we have a row for each runner, so there are multiple rows per branch. We used to try to estimate how many rows we need to match 10 branches exactly. Relying on LIMIT to separate branches doesn't work very well because we have multiple branch streams. The rows from two branches may be interleaved. Use matching on branch_date instead. Signed-off-by: Jakub Kicinski --- contest/backend/query.py | 49 ++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/contest/backend/query.py b/contest/backend/query.py index efdca5b..5b5a3c7 100644 --- a/contest/backend/query.py +++ b/contest/backend/query.py @@ -36,25 +36,38 @@ def branches(): return rows -def branches_to_rows(br_cnt, remote, br_pfx=None): - cnt = 0 +def get_oldest_branch_date(br_cnt, br_pfx=None): + """ + Find the branch_date of the oldest branch that should be included + based on the requested number of branches. + Returns the cutoff date string or None if no limit should be applied. + """ with psql.cursor() as cur: - remote_k = ",remote" if remote else "" - # Slap the -2 in here as the first letter of the date, to avoid prefix of prefix matches + # Slap the -2 in here as the first letter of the date, + # to avoid prefix of prefix matches pfx_flt = f"WHERE branch LIKE '{br_pfx}-2%' " if br_pfx else "" - # Count from both results and results_pending tables - q_results = f"SELECT branch,count(*),branch_date{remote_k} FROM results {pfx_flt} GROUP BY branch,branch_date{remote_k} ORDER BY branch_date DESC LIMIT {br_cnt}" - q_pending = f"SELECT branch,count(*),branch_date{remote_k} FROM results_pending {pfx_flt} GROUP BY branch,branch_date{remote_k} ORDER BY branch_date DESC LIMIT {br_cnt}" + order_limit = f"ORDER BY branch_date DESC LIMIT {br_cnt}" + + # Get unique branch dates from both tables, ordered by date descending + # We use UNION to combine unique branch_dates from both tables + # Make sure to limit both sides to avoid a huge merge + query = f""" + (SELECT DISTINCT branch_date FROM results {pfx_flt} {order_limit}) + UNION + (SELECT DISTINCT branch_date FROM results_pending {pfx_flt} {order_limit}) + {order_limit} + """ + + cur.execute(query) + rows = cur.fetchall() - cur.execute(q_results) - for r in cur.fetchall(): - cnt += r[1] + if len(rows) < br_cnt: + # DB doesn't have enough data, no need to limit + return None - cur.execute(q_pending) - for r in cur.fetchall(): - cnt += r[1] - return cnt + # Return the oldest branch_date from our limit + return rows[-1][0] # Last row is the oldest due to DESC order def result_as_l2(raw): @@ -117,10 +130,16 @@ def results(): # Slap the -2 in here as the first letter of the date, to avoid prefix of prefix matches where.append(f"branch LIKE '{br_pfx}-2%'") - limit = branches_to_rows(br_cnt, remote, br_pfx) + # Get the cutoff date for the requested number of branches + cutoff_date = get_oldest_branch_date(br_cnt, br_pfx) + if cutoff_date: + where.append(f"branch_date >= '{cutoff_date}'") t2 = datetime.datetime.now() + # Set a reasonable limit to prevent runaway queries + limit = 10000 + if remote: where.append(f"remote = '{remote}'") log += ', remote' From b03918e0724e105c4e559e6fff45b6146efde343 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 20 Sep 2025 14:35:09 -0700 Subject: [PATCH 413/429] contest: backend: remove WIP filtering from flake query WIP / pending results are in a separate table now. We don't have to worry about filtering them out when using json() helpers. Signed-off-by: Jakub Kicinski --- contest/backend/query.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/contest/backend/query.py b/contest/backend/query.py index 5b5a3c7..9e73c5b 100644 --- a/contest/backend/query.py +++ b/contest/backend/query.py @@ -252,23 +252,6 @@ def flaky_tests(): month = True # Default to querying last month limit = flake_cnt # Default limit - # Find branches with incomplete results, psql JSON helpers fail for them - t = datetime.datetime.now() - with psql.cursor() as cur: - query = """ - SELECT branch - FROM results - WHERE json_normal NOT LIKE '%"results": [%' - GROUP BY branch; - """ - - cur.execute(query) - rows = cur.fetchall() - branches = "" - if rows: - branches = " AND branch != ".join([""] + [f"'{r[0]}'" for r in rows]) - print(f"Query for in-prog execs took: {str(datetime.datetime.now() - t)}") - t = datetime.datetime.now() with psql.cursor() as cur: # Query for tests where first try failed, retry passed, and no crash @@ -279,7 +262,6 @@ def flaky_tests(): WHERE x.result = 'fail' AND x.retry = 'pass' AND x.crashes IS NULL - {branches} ORDER BY branch_date DESC LIMIT {limit}; """ From 6292a818c856ac5d21429fa2fc5aedb0e60cd799 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 27 Sep 2025 16:07:15 -0700 Subject: [PATCH 414/429] contest: change db-monitor into a generic key/value table System monitoring currently has a table with explicit columns. Make it more of a generic key/value table to be able to store more data there in the future without having to modify the DB. Signed-off-by: Jakub Kicinski --- deploy/contest/db | 9 +++++---- system-status.py | 50 +++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/deploy/contest/db b/deploy/contest/db index ed174a6..6f86c34 100644 --- a/deploy/contest/db +++ b/deploy/contest/db @@ -73,12 +73,13 @@ CREATE TABLE results_pending ( t_start timestamp ); -CREATE TABLE db_monitor ( +CREATE TABLE metrics ( id serial primary key, ts timestamp not null, - size int not null check (size > 0), - disk_pct REAL, - disk_pct_metal REAL + source varchar(40), + category varchar(40), + name varchar(40), + value double precision ); CREATE TABLE stability ( diff --git a/system-status.py b/system-status.py index dca3afc..94a49b8 100755 --- a/system-status.py +++ b/system-status.py @@ -172,9 +172,21 @@ def add_remote_services(result, remote): result["remote"][remote["name"]] = data +def get_metric_values(db_connection, source, category, name, limit=120): + """ Query metrics from the DB """ + with db_connection.cursor() as cur: + cur.execute(""" + SELECT ts, value + FROM metrics + WHERE source = %s AND category = %s AND name = %s + ORDER BY ts DESC + LIMIT %s + """, (source, category, name, limit)) + return cur.fetchall() + + def add_db(result, cfg): db_name = cfg["db"]["name"] - tbl = cfg["db"]["table"] psql = psycopg2.connect(database=db_name) psql.autocommit = True @@ -188,13 +200,35 @@ def add_db(result, cfg): for _, remote in result["remote"].items(): remote_disk = remote["disk-use"] - arg = cur.mogrify("(NOW(),%s,%s,%s)", (size, result["disk-use"], remote_disk)) - cur.execute(f"INSERT INTO {tbl}(ts, size, disk_pct, disk_pct_metal) VALUES" + arg.decode('utf-8')) - - with psql.cursor() as cur: - cur.execute(f"SELECT ts,size,disk_pct,disk_pct_metal FROM {tbl} ORDER BY id DESC LIMIT 40") - result["db"]["data"] = [ {'ts': t.isoformat(), 'size': s, 'disk': d, 'disk_remote': dr} - for t, s, d, dr in reversed(cur.fetchall()) ] + # Insert metrics data + metrics_data = [ + ("system", "db", "size", size), + ("system", "disk", "util", result["disk-use"]), + ("system-metal", "disk", "util", remote_disk) + ] + + for source, category, name, value in metrics_data: + cur.execute(f"INSERT INTO metrics(ts, source, category, name, value) VALUES(NOW(), '{source}', '{category}', '{name}', %s)", (value,)) + + # Retrieve display data - query each metric individually + size_data = get_metric_values(psql, "system", "db", "size", limit=40) + disk_data = get_metric_values(psql, "system", "disk", "util", limit=40) + disk_remote_data = get_metric_values(psql, "system-metal", "disk", "util", limit=40) + + # Since they're inserted with the same timestamp, we can just zip them together + result["db"]["data"] = [ + { + 'ts': ts.isoformat(), + 'size': size, + 'disk': disk, + 'disk_remote': disk_remote + } + for (ts, size), (_, disk), (_, disk_remote) in zip(size_data, disk_data, disk_remote_data) + ] + # Reverse to get chronological order (oldest first) + result["db"]["data"].reverse() + + psql.close() def main(): From 1888931c7162199ccc00a0423ed348802197c51e Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 27 Sep 2025 16:42:42 -0700 Subject: [PATCH 415/429] ui: checks: sort check outputs by rate of occurrence in accepted patches Signed-off-by: Jakub Kicinski --- ui/checks.html | 3 ++- ui/checks.js | 27 +++++++++++++++------------ 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/ui/checks.html b/ui/checks.html index 2667774..b1b291d 100644 --- a/ui/checks.html +++ b/ui/checks.html @@ -119,7 +119,8 @@

Top 20 check outputs

Check Output - Hits + Acc + Tot
diff --git a/ui/checks.js b/ui/checks.js index a810fa2..bc3848f 100644 --- a/ui/checks.js +++ b/ui/checks.js @@ -217,18 +217,19 @@ function load_outputs(data) var top_out = []; var top_out_cnt = {}; $.each(data, function(i, v) { - if (v.result != "success") { - if (top_out_cnt[v.description]) { - top_out_cnt[v.description]++; - } else { - top_out.push(v); - top_out_cnt[v.description] = 1; - } + if (v.result == "success") + return 1; + + if (!(v.description in top_out_cnt)) { + top_out.push(v); + top_out_cnt[v.description] = {true: 0, false: 0}; } + + top_out_cnt[v.description][v.state == "accepted"]++; }); top_out.sort(function(a, b) { - return top_out_cnt[b.description] - top_out_cnt[a.description]; + return top_out_cnt[b.description][true] - top_out_cnt[a.description][true]; }); for (let i = 0; i < 20; i++) { @@ -237,11 +238,13 @@ function load_outputs(data) var row = table.insertRow(); var check = row.insertCell(0); var output = row.insertCell(1); - var hits = row.insertCell(2); + var a_hits = row.insertCell(2); + var t_hits = row.insertCell(3); - check.innerHTML = v.check; - output.innerHTML = v.description; - hits.innerHTML = top_out_cnt[v.description]; + check.innerText = v.check; + output.innerText = v.description; + a_hits.innerText = top_out_cnt[v.description][true]; + t_hits.innerText = top_out_cnt[v.description][false]; } } From c700a1cbd575178875a8517f23ebcc16215efc31 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 27 Sep 2025 16:43:25 -0700 Subject: [PATCH 416/429] core: patch: fix lint warnings No functional change. Signed-off-by: Jakub Kicinski --- core/patch.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/core/patch.py b/core/patch.py index 4b47b97..4bd1150 100644 --- a/core/patch.py +++ b/core/patch.py @@ -9,10 +9,8 @@ import core -patch_id_gen = 0 - -class Patch(object): +class Patch: """Patch class Class representing a patch with references to postings etc. @@ -29,6 +27,9 @@ class Patch(object): write_out(fp) Write the raw patch into the given file pointer. """ + + PATCH_ID_GEN = 0 + def __init__(self, raw_patch, ident=None, title="", series=None): self.raw_patch = raw_patch self.title = title @@ -48,13 +49,13 @@ def __init__(self, raw_patch, ident=None, title="", series=None): core.log_open_sec("Patch init: " + self.title) core.log_end_sec() - global patch_id_gen if ident is not None: self.id = ident else: - patch_id_gen += 1 - self.id = patch_id_gen + Patch.PATCH_ID_GEN += 1 + self.id = Patch.PATCH_ID_GEN def write_out(self, fp): + """ Write patch contents to a file """ fp.write(self.raw_patch.encode('utf-8')) fp.flush() From 19c2685ca60f9a195547792d151ccc0c613f899a Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 27 Sep 2025 17:50:29 -0700 Subject: [PATCH 417/429] tests: add a better test for maintainers file coverage Add a test which checks if new files added need a MAINTAINERS entry. Signed-off-by: Jakub Kicinski --- tests/patch/cc_maintainers/test.py | 22 ++- tests/patch/checkpatch/checkpatch.sh | 3 +- tests/patch/maintainers/info.json | 4 - tests/patch/maintainers/maintainers.sh | 42 ------ tests/series/maintainers/info.json | 4 + tests/series/maintainers/test.py | 185 +++++++++++++++++++++++++ 6 files changed, 207 insertions(+), 53 deletions(-) delete mode 100644 tests/patch/maintainers/info.json delete mode 100755 tests/patch/maintainers/maintainers.sh create mode 100644 tests/series/maintainers/info.json create mode 100644 tests/series/maintainers/test.py diff --git a/tests/patch/cc_maintainers/test.py b/tests/patch/cc_maintainers/test.py index 91e8bd1..2fe9f43 100644 --- a/tests/patch/cc_maintainers/test.py +++ b/tests/patch/cc_maintainers/test.py @@ -2,16 +2,19 @@ # # Copyright (c) 2020 Facebook -from typing import Tuple +""" +Test if relevant maintainers were CCed +""" + import datetime import email import email.utils -import subprocess -import tempfile +import json import os import re -import json -""" Test if relevant maintainers were CCed """ +import subprocess +import tempfile +from typing import Tuple emailpat = re.compile(r'([^ <"]*@[^ >"]*)') @@ -39,6 +42,9 @@ local_map = ["Vladimir Oltean ", "Alexander Duyck "] +# +# Maintainer auto-staleness checking +# class StalenessEntry: def __init__(self, e, since_months): @@ -132,14 +138,18 @@ def get_stale(sender_from, missing, out): ret.add(e) return ret +# +# Main +# def cc_maintainers(tree, thing, result_dir) -> Tuple[int, str, str]: + """ Main test entry point """ out = [] raw_gm = [] patch = thing if patch.series and patch.series.cover_pull: - return 0, f"Pull request co-post, skipping", "" + return 0, "Pull request co-post, skipping", "" msg = email.message_from_string(patch.raw_patch) addrs = msg.get_all('to', []) diff --git a/tests/patch/checkpatch/checkpatch.sh b/tests/patch/checkpatch/checkpatch.sh index 2a95905..886404f 100755 --- a/tests/patch/checkpatch/checkpatch.sh +++ b/tests/patch/checkpatch/checkpatch.sh @@ -9,7 +9,8 @@ MACRO_ARG_REUSE,\ ALLOC_SIZEOF_STRUCT,\ NO_AUTHOR_SIGN_OFF,\ GIT_COMMIT_ID,\ -CAMELCASE +CAMELCASE,\ +FILE_PATH_CHANGES tmpfile=$(mktemp) diff --git a/tests/patch/maintainers/info.json b/tests/patch/maintainers/info.json deleted file mode 100644 index 26f8179..0000000 --- a/tests/patch/maintainers/info.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "run": ["maintainers.sh"], - "disabled": true -} diff --git a/tests/patch/maintainers/maintainers.sh b/tests/patch/maintainers/maintainers.sh deleted file mode 100755 index bb50ca6..0000000 --- a/tests/patch/maintainers/maintainers.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash -# SPDX-License-Identifier: GPL-2.0 - -if git diff-index --quiet --name-only HEAD~ -- MAINTAINERS; then - echo "MAINTAINERS not touched" >&$DESC_FD - exit 0 -fi - -tmpfile_o=$(mktemp) -tmpfile_n=$(mktemp) -rc=0 - -echo "MAINTAINERS self-test: redirect to $tmpfile_o and $tmpfile_n" - -HEAD=$(git rev-parse HEAD) - -git checkout -q HEAD~ - -echo "Checking old warning count" - -./scripts/get_maintainer.pl --self-test 2> >(tee $tmpfile_o >&2) -incumbent=$(grep -i -c "\(warn\|error\)" $tmpfile_o) - -echo "Checking new warning count" - -git checkout -q $HEAD - -./scripts/get_maintainer.pl --self-test 2> >(tee $tmpfile_n >&2) -current=$(grep -i -c "\(warn\|error\)" $tmpfile_n) - -echo "Errors and warnings before: $incumbent this patch: $current" >&$DESC_FD - -if [ $current -gt $incumbent ]; then - echo "New errors added" 1>&2 - diff $tmpfile_o $tmpfile_n 1>&2 - - rc=1 -fi - -rm $tmpfile_o $tmpfile_n - -exit $rc diff --git a/tests/series/maintainers/info.json b/tests/series/maintainers/info.json new file mode 100644 index 0000000..cce6da5 --- /dev/null +++ b/tests/series/maintainers/info.json @@ -0,0 +1,4 @@ +{ + "pymod": "test", + "pyfunc": "maintainers" +} diff --git a/tests/series/maintainers/test.py b/tests/series/maintainers/test.py new file mode 100644 index 0000000..1926ada --- /dev/null +++ b/tests/series/maintainers/test.py @@ -0,0 +1,185 @@ +# SPDX-License-Identifier: GPL-2.0 + +""" Test if the MAINTAINERS file needs an update """ + +import os +import subprocess +from typing import Tuple + +# +# Checking for needed new MAINTAINERS entries +# + +new_file_ignore_pfx = [ 'Documentation/', 'tools/testing/'] + +def extract_files(series): + """Extract paths of new files being added by the series.""" + + new_files = set() + mod_files = set() + lines = [] + for patch in series.patches: + lines += patch.raw_patch.split("\n") + + # Walk lines, skip last since it doesn't have next + for i, line in enumerate(lines[:-1]): + next_line = lines[i + 1] + + if not next_line.startswith("+++ b/"): + continue + file_path = next_line[6:] + + # .startswith() can take a while array of alternatives + if file_path.startswith(tuple(new_file_ignore_pfx)): + continue + + if line == "--- /dev/null": + new_files.add(file_path) + else: + mod_files.add(file_path) + + # We're testing a series, same file may appear multiple times + mod_files -= new_files + return list(new_files), list(mod_files) + + +def count_files_for_maintainer_entry(tree, maintainer_entry): + """Count how many files are covered by a specific maintainer entry.""" + patterns = [] + + # Extract file patterns from the maintainer entry + for line in maintainer_entry.split("\n"): + if line.startswith("F:"): + pattern = line[2:].strip() + patterns.append(pattern) + if not patterns: + return 0 + + # Count files matching these patterns + total_files = 0 + for pattern in patterns: + if pattern[-1] == '/': + where = pattern + what = '*' + elif '/' in pattern: + where = os.path.dirname(pattern) + what = os.path.basename(pattern) + else: + where = "." + what = pattern + cmd = ["find", where, "-name", what, "-type", "f"] + result = subprocess.run(cmd, cwd=tree.path, capture_output=True, + text=True, check=False) + if result.returncode == 0: + total_files += result.stdout.count("\n") + + return total_files + + +def get_maintainer_entry_for_file(tree, file_path): + """Get the full MAINTAINERS entry for a specific file.""" + + cmd = ["./scripts/get_maintainer.pl", "--sections", file_path] + result = subprocess.run(cmd, cwd=tree.path, capture_output=True, text=True, + check=False) + + if result.returncode == 0: + return result.stdout + return "" + + +def check_maintainer_coverage(tree, new_files, out): + """Check if new files should have an MAINTAINERS entry.""" + has_miss = False + has_fail = False + has_warn = False + warnings = [] + + # Ideal entry size is <50. But if someone is adding a Kconfig file, + # chances are they should be a maintainer. + pass_target = 50 + if 'Kconfig' in new_files: + pass_target = 3 + + for file_path in new_files: + out.append("\nChecking coverage for a new file: " + file_path) + + maintainer_info = get_maintainer_entry_for_file(tree, file_path) + + # This should not happen, Linus catches all + if not maintainer_info.strip(): + warnings.append(f"Failed to fetch MAINTAINERS for {file_path}") + has_warn = True + continue + + # Parse the maintainer sections + sections = [] + current_section = [] + + prev = "" + for line in maintainer_info.split("\n"): + if len(line) > 1 and line[1] == ':': + if not current_section: + current_section = [prev] + current_section.append(line) + elif len(line) < 2: + if current_section: + sections.append("\n".join(current_section)) + current_section = [] + prev = line + + if current_section: + sections.append("\n".join(current_section)) + + # Check each maintainer section + min_cnt = 999999 + for section in sections: + name = section.split("\n")[0] + # Count files for this maintainer entry + file_count = count_files_for_maintainer_entry(tree, section) + out.append(f" Section {name} covers ~{file_count} files") + + if 0 < file_count < pass_target: + out.append("PASS") + break + min_cnt = min(min_cnt, file_count) + else: + # Intel and nVidia drivers have 400+ files, just warn for these + # sort of sizes. More files than 500 means we fell down to subsystem + # level of entries. + out.append(f" MIN {min_cnt}") + has_miss = True + if min_cnt < 500: + has_warn = True + else: + has_fail = True + + if has_miss: + warnings.append("Expecting a new MAINTAINERS entry") + else: + warnings.append("MAINTAINERS coverage looks sufficient") + + ret = 0 + if has_fail: + ret = 1 + elif has_warn: + ret = 250 + + return ret, "; ".join(warnings) + + +def maintainers(tree, series, _result_dir) -> Tuple[int, str, str]: + """ Main function / entry point """ + + # Check for new files in the series + new_files, mod_files = extract_files(series) + + ret = 0 + log = ["New files:"] + new_files + ["", "Modified files:"] + mod_files + + if not new_files: + desc = "No new files, skip" + else: + ret, desc = check_maintainer_coverage(tree, new_files, log) + + return ret, desc, "\n".join(log) From af02b9f6fd7da9939f19ef285dae7c2121ce3c9a Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 2 Oct 2025 11:47:16 -0700 Subject: [PATCH 418/429] tests: make check_selftests check format of Makefiles and configs Signed-off-by: Jakub Kicinski --- tests/patch/check_selftest/check_selftest.sh | 30 --- tests/patch/check_selftest/info.json | 3 +- tests/patch/check_selftest/test.py | 177 ++++++++++++++ .../check_selftest/validate_config_format.py | 117 +++++++++ .../validate_makefile_format.py | 225 ++++++++++++++++++ 5 files changed, 521 insertions(+), 31 deletions(-) delete mode 100755 tests/patch/check_selftest/check_selftest.sh create mode 100644 tests/patch/check_selftest/test.py create mode 100755 tests/patch/check_selftest/validate_config_format.py create mode 100755 tests/patch/check_selftest/validate_makefile_format.py diff --git a/tests/patch/check_selftest/check_selftest.sh b/tests/patch/check_selftest/check_selftest.sh deleted file mode 100755 index 43c4040..0000000 --- a/tests/patch/check_selftest/check_selftest.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash -# SPDX-License-Identifier: GPL-2.0 -# -# Check if the shell selftest scripts are in correspond Makefile - -rt=0 - -files=$(git show --pretty="" --name-only -- \ - tools/testing/selftests*.sh \ - tools/testing/selftests*.py \ - | grep -v "/lib/" - ) -if [ -z "$files" ]; then - echo "No net selftest shell script" >&$DESC_FD - exit $rt -fi - -for file in $files; do - echo "Checking $file" - f=$(basename $file) - d=$(dirname $file) - if [ -f "${d}/Makefile" ] && ! grep -P "[\t| ]${f}" ${d}/Makefile; then - echo "Script ${f} not found in ${d}/Makefile" >&$DESC_FD - rt=1 - fi -done - -[ ${rt} -eq 0 ] && echo "net selftest script(s) already in Makefile" >&$DESC_FD - -exit $rt diff --git a/tests/patch/check_selftest/info.json b/tests/patch/check_selftest/info.json index 615779f..4b3c251 100644 --- a/tests/patch/check_selftest/info.json +++ b/tests/patch/check_selftest/info.json @@ -1,3 +1,4 @@ { - "run": ["check_selftest.sh"] + "pymod": "test", + "pyfunc": "check_selftest" } diff --git a/tests/patch/check_selftest/test.py b/tests/patch/check_selftest/test.py new file mode 100644 index 0000000..5a5ba68 --- /dev/null +++ b/tests/patch/check_selftest/test.py @@ -0,0 +1,177 @@ +# SPDX-License-Identifier: GPL-2.0 + +""" Test Makefile, .gitignore and config format """ + +import os +import subprocess +from typing import Tuple + + +LOCAL_DIR = os.path.dirname(__file__) + + +def ret_merge(ret, nret): + """ merge results """ + if ret[0] == 0 or nret[0] == 0: + val = 0 + else: + val = min(ret[0], nret[0]) + + desc = "" + if ret[1] and nret[1]: + desc = ret[1] + "; " + nret[1] + else: + desc = ret[1] + nret[1] + return (val, desc) + + +def check_new_files_makefile(tree, new_files, log): + """ Make sure new files are listed in a Makefile, somewhere """ + + ret = (0, "") + cnt = 0 + + for path in new_files: + if path.endswith(('.sh', '.py')): + needle = path + elif path.endswith(('.c')): + needle = path.split('.')[0] + else: + log.append("makefile inclusion check ignoring " + path) + continue + + makefile = os.path.dirname(path) + "/Makefile" + + cmd = ["git", "grep", "--exit-code", needle, "---", makefile] + result = subprocess.run(cmd, cwd=tree.path, check=False) + log.append(" ".join(cmd) + f":: {result.returncode}") + if result.returncode: + ret_merge(ret, (1, path + " not found in Makefile")) + cnt += 1 + + if not ret[0] and cnt: + ret = (0, f"New files in Makefile checked ({cnt})") + + return ret + + +def check_new_files_gitignore(tree, new_files, log): + """ Make sure new binaries are listed in .gitignore """ + + ret = (0, "") + cnt = 0 + + for path in new_files: + if path.endswith(('.c')): + needle = path.split('.')[0] + else: + log.append("gitignore check ignoring " + path) + continue + + target = os.path.dirname(path) + "/.gitignore" + + cmd = ["git", "grep", "--exit-code", needle, "---", target] + result = subprocess.run(cmd, cwd=tree.path, check=False) + log.append(" ".join(cmd) + f":: {result.returncode}") + if result.returncode: + ret_merge(ret, (1, needle + " not found in .gitignore")) + cnt += 1 + + if not ret[0] and cnt: + ret = (0, f"New files in gitignore checked ({cnt})") + + return ret + + +def _check_file_fmt(tree, path, script, result_dir, ident): + cmd = [script, os.path.join(tree.path, path)] + + result = subprocess.run(cmd, cwd=LOCAL_DIR, capture_output=True, + text=True, check=False) + with open(os.path.join(result_dir, ident), "w", encoding="utf-8") as fp: + fp.write(result.stdout) + return result.returncode + + +def check_file_formats(tree, file_list, log, result_dir): + """ Validate sort order of all touched files """ + + ret = (0, "") + i = 0 + for path in file_list: + if path.endswith("/config"): + script = "validate_config_format.py" + fmt = f"fmt-config-{i}" + elif path.endswith("/.gitignore"): + script = "validate_config_format.py" + fmt = f"fmt-gitignore-{i}" + elif path.endswith("/Makefile"): + script = "validate_Makefile_format.py" + fmt = f"fmt-makefile-{i}" + else: + log.append("format check ignoring " + path) + continue + + if _check_file_fmt(tree, path, script, result_dir, fmt): + ret = ret_merge(ret, (1, "Bad format: " + path)) + + if not ret[0] and i: + ret = (0, f"Good format ({i})") + + return ret + + +def extract_files(patch): + """Extract paths of new files being added by the series.""" + + new_files = set() + mod_files = set() + lines = patch.raw_patch.split("\n") + + # Walk lines, skip last since it doesn't have next + for i, line in enumerate(lines[:-1]): + next_line = lines[i + 1] + + if not next_line.startswith("+++ b/"): + continue + if 'tools/testing/selftests/' not in next_line: + continue + + file_path = next_line[6:] + + if line == "--- /dev/null": + new_files.add(file_path) + else: + mod_files.add(file_path) + + # We're testing a series, same file may appear multiple times + mod_files -= new_files + return list(new_files), list(mod_files) + + +def check_selftest(tree, patch, result_dir) -> Tuple[int, str, str]: + """ Main function / entry point """ + + # Check for new files in the series + new_files, mod_files = extract_files(patch) + + ret = (0, "") + log = ["New files:"] + new_files + ["", "Modified files:"] + mod_files + [""] + + if not new_files and not mod_files: + ret = (0, "No changes to selftests") + else: + nret = check_file_formats(tree, new_files + mod_files, log, result_dir) + ret = ret_merge(ret, nret) + + if new_files: + nret = check_new_files_makefile(tree, new_files, log) + ret = ret_merge(ret, nret) + + nret = check_new_files_gitignore(tree, new_files, log) + ret = ret_merge(ret, nret) + + if not ret[0] and not ret[1]: + ret = (0, f"New files {len(new_files)}, modified {len(mod_files)}, no checks") + + return ret[0], ret[1], "\n".join(log) diff --git a/tests/patch/check_selftest/validate_config_format.py b/tests/patch/check_selftest/validate_config_format.py new file mode 100755 index 0000000..87d49fa --- /dev/null +++ b/tests/patch/check_selftest/validate_config_format.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 + +import sys + + +def extract_key(raw): + k = raw.split("=")[0] + k = k.strip() + k = k.replace('_', '') + return k + + +def check_one(a, b, line): + _a = extract_key(a) + _b = extract_key(b) + + if _a >= _b: + return None + + return f"Lines {line}-{line+1} invalid order, {a} should be after {b}" + + +def validate_config(file_path): + """Validate a Makefile for proper variable assignment format.""" + + with open(file_path, "r", encoding="utf-8") as f: + content = f.read() + + lines = content.split("\n") + + all_errors = [] + + prev = "" + for i, line in enumerate(lines): + # ignore comments + if line.strip().startswith('#'): + continue + # ignore bad lines + if "=" not in line: + continue + if not prev: + prev = line + continue + + err = check_one(line, prev, i) + if err: + all_errors.append(err) + + prev = line + + if all_errors: + print(f"Validation errors in {file_path}:") + for error in all_errors: + print(error) + return False + + print(f"✓ {file_path} is properly formatted") + return True + + +def fix(file_path): + """Fix the config file by sorting entries alphabetically.""" + + with open(file_path, "r", encoding="utf-8") as f: + content = f.read() + + lines = content.split("\n") + + output = [] + + while lines: + idx = 0 + first = lines[0] + for i, line in enumerate(lines): + # ignore comments + if line.strip().startswith('#'): + continue + # ignore bad lines + if "=" not in line: + continue + + err = check_one(line, first, i) + if err: + first = line + idx = i + output.append(first) + lines.pop(idx) + + # Write the fixed content back to the file + with open(file_path, "w", encoding="utf-8") as f: + f.write("\n".join(output)) + + print(f"✓ Fixed {file_path} - config entries sorted alphabetically") + + +def main(): + """Main entry point for the script.""" + if len(sys.argv) < 2: + print("Usage: validate_config_format.py ") + sys.exit(1) + + file_path = sys.argv[1] + if file_path == "--fix": + file_path = sys.argv[2] + + code = 0 + if not validate_config(file_path): + code = 1 + if sys.argv[1] == "--fix": + fix(file_path) + + sys.exit(code) + + +if __name__ == "__main__": + main() diff --git a/tests/patch/check_selftest/validate_makefile_format.py b/tests/patch/check_selftest/validate_makefile_format.py new file mode 100755 index 0000000..fad086c --- /dev/null +++ b/tests/patch/check_selftest/validate_makefile_format.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 +""" +Script to validate Makefile variable assignment format. + +Expected format: +- Variable assignment starts with "VARIABLE = \", "VARIABLE := \", or + "VARIABLE += \" (with optional space) +- Each item on its own line, indented with a tab +- Each line ends with " \" (except the last item line) +- Items are sorted alphabetically +- Last line is a comment starting with "#" allowing the previous line to end with "\" +- Variables should only be assigned once (no duplicate assignments) +""" + +import re +import sys + + +def _extract_items(lines, line_nums): + """Extract and validate items from the middle lines.""" + errors = [] + items = [] + + # Skip last line if it's the terminating comment + end = len(lines) + if lines[-1].strip().startswith("#"): + end = -1 + + for i, line in enumerate(lines[:end]): + line_num = line_nums[i] + + # Check indentation (should be a tab) + if not line.startswith("\t"): + errors.append(f"Line {line_num}: Should start with tab, got '{line[:1]}'") + + # Remove tab and trailing " \" + item = line[1:] # Remove tab + if item.endswith(" \\"): + item = item[:-2].strip() + + if ' ' in item and '$' not in item: + errors.append(f"Line {line_num}: contains a splace, multiple values? '{item}'") + + items.append((item, line_num)) + + return items, errors + + +def _directory_sort_key(item): + """Generate sort key considering directory depth first, then alphabetical order.""" + directory_count = item.count("/") + return (directory_count, item.lower()) + + +def _validate_sorting(items): + """Validate directory-aware alphabetical sorting of items.""" + errors = [] + + # Filter out function calls (items starting with $) as they don't need sorting + sortable_items = [] + for item, line_num in items: + if not item.startswith("$"): + sortable_items.append((item, line_num)) + + # Only validate sorting among sortable items + for i in range(len(sortable_items) - 1): + current_item, current_line = sortable_items[i] + next_item, next_line = sortable_items[i + 1] + + if current_item < next_item: + continue + + current_key = _directory_sort_key(current_item) + next_key = _directory_sort_key(next_item) + + if current_key > next_key: + current_dirs = current_item.count("/") + next_dirs = next_item.count("/") + + if current_dirs != next_dirs: + errors.append( + f"Lines {current_line}-{next_line}: Items not in directory-aware order: " + f"'{current_item}' ({current_dirs} dirs) should come after " + f"'{next_item}' ({next_dirs} dirs)" + ) + else: + errors.append( + f"Lines {current_line}-{next_line}: Items not in alphabetical order: " + f"'{current_item}' should come after '{next_item}'" + ) + return errors + + +def validate_variable_block(var_name, lines, line_nums): + """Validate a single variable assignment block.""" + errors = [] + + if not lines: + return errors + + # Extract and validate items from the middle lines + items, item_errors = _extract_items(lines, line_nums) + errors.extend(item_errors) + + # Check last line starts with "#" + if len(lines) > 1: + if not lines[-1].strip().startswith("#"): + errors.append( + f"Line {line_nums[-1]}: Trailing comment should start with '#'," + f" got '{lines[-1].strip()}'" + ) + elif len(lines[-1].strip()) > 5 and var_name not in lines[-1]: + errors.append( + f"Line {line_nums[-1]}: Trailing comment should contain the " + f"variable name ({var_name}), got '{lines[-1].strip()}'" + ) + + # Check alphabetical sorting + if len(items) > 1: + errors.extend(_validate_sorting(items)) + + return errors + + +def check_multiple_blocks(var_name, lines, line_nums): + """Check for multiple variable assignment blocks.""" + errors = [] + + # Check for multiple blocks + for i, line_no in enumerate(line_nums): + if i == 0: + continue + if line_no != line_nums[i - 1] + 1: + errors.append(f"Line {line_no}: Multiple variable assignment blocks, first block starts at line {line_nums[0]}") + + return errors + + +def _process_entry(variable_blocks, var_name, entry, line_num): + """Process a single entry and update the variable_blocks dictionary.""" + if var_name not in variable_blocks: + variable_blocks[var_name] = ([], [], ) + variable_blocks[var_name][0].append(entry) + variable_blocks[var_name][1].append(line_num) + + +def parse_makefile(content): + """Parse Makefile and extract variable assignment blocks.""" + lines = content.split("\n") + variable_blocks = {} + + i = 0 + var_name = None + while i < len(lines): + # Look for variable assignment with backslash continuation (=, :=, +=) + match = re.match(r"^([A-Z_][A-Z0-9_]*)\s*(:?=|\+=)(.*)$", lines[i]) + if match: + var_name = match.group(1) + entry = match.group(3).strip() + if entry.startswith("$") and not entry.startswith("\\"): + # Special entry, probably for a good reason. Ignore completely. + var_name = None + elif len(var_name) < 3 or "FLAGS" in var_name or 'LIBS' in var_name: + # Special case for CFLAGS, which is often used for multiple values + # and is not sorted alphabetically. + var_name = None + elif entry.strip() != "\\": + _process_entry(variable_blocks, var_name, '\t' + entry, i + 1) + elif var_name: + _process_entry(variable_blocks, var_name, lines[i], i + 1) + + if var_name and not lines[i].endswith('\\'): + var_name = None + i += 1 + + return variable_blocks + + +def validate_makefile(file_path): + """Validate a Makefile for proper variable assignment format.""" + + with open(file_path, "r", encoding="utf-8") as f: + content = f.read() + + variable_blocks = parse_makefile(content) + + if not variable_blocks: + print(f"No multi-line variable assignments found in {file_path}") + return True + + all_errors = [] + + # Validate each variable block + for var_name, (block_lines, line_nums) in variable_blocks.items(): + errors = validate_variable_block(var_name, block_lines, line_nums) + errors += check_multiple_blocks(var_name, block_lines, line_nums) + if errors: + all_errors.extend( + [f"Variable {var_name}:"] + [f" {error}" for error in errors] + ) + + if all_errors: + print(f"Validation errors in {file_path}:") + for error in all_errors: + print(error) + return False + + print(f"✓ {file_path} is properly formatted") + return True + + +def main(): + """Main entry point for the script.""" + if len(sys.argv) != 2: + print("Usage: validate_makefile_format.py ") + sys.exit(1) + + file_path = sys.argv[1] + + if not validate_makefile(file_path): + sys.exit(1) + + +if __name__ == "__main__": + main() From c00528872851561843599c05d976718b137ffe82 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 3 Oct 2025 14:58:04 -0700 Subject: [PATCH 419/429] tests: check_selftest: fix running the helper scripts Signed-off-by: Jakub Kicinski --- tests/patch/check_selftest/test.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/patch/check_selftest/test.py b/tests/patch/check_selftest/test.py index 5a5ba68..cf78c2b 100644 --- a/tests/patch/check_selftest/test.py +++ b/tests/patch/check_selftest/test.py @@ -84,7 +84,7 @@ def check_new_files_gitignore(tree, new_files, log): def _check_file_fmt(tree, path, script, result_dir, ident): - cmd = [script, os.path.join(tree.path, path)] + cmd = [os.path.join(LOCAL_DIR, script), os.path.join(tree.path, path)] result = subprocess.run(cmd, cwd=LOCAL_DIR, capture_output=True, text=True, check=False) @@ -106,12 +106,13 @@ def check_file_formats(tree, file_list, log, result_dir): script = "validate_config_format.py" fmt = f"fmt-gitignore-{i}" elif path.endswith("/Makefile"): - script = "validate_Makefile_format.py" + script = "validate_makefile_format.py" fmt = f"fmt-makefile-{i}" else: log.append("format check ignoring " + path) continue + i += 1 if _check_file_fmt(tree, path, script, result_dir, fmt): ret = ret_merge(ret, (1, "Bad format: " + path)) From 6db3fbcc31e3df820db2af83490626dc2f1f3f27 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 16 Oct 2025 06:55:39 -0700 Subject: [PATCH 420/429] pw_poller: add local socket for patch series injection Create a simple local control socket to inject series into the testing queue. Example use: echo "1011459" | nc -U ./poller.sock or echo "12345 net-next;67890 bpf-next" | nc -U ./poller.sock This should help us easily manually correct missed series as well as series missed due to crashes and restarts. Signed-off-by: Jakub Kicinski --- pw_poller.py | 96 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 90 insertions(+), 6 deletions(-) diff --git a/pw_poller.py b/pw_poller.py index 937a9cf..6226b1a 100755 --- a/pw_poller.py +++ b/pw_poller.py @@ -9,6 +9,7 @@ import json import os import shutil +import socket import time import queue from typing import Dict @@ -81,6 +82,9 @@ def __init__(self, config) -> None: listmodname = config.get('list', 'module', fallback='netdev') self.list_module = import_module(listmodname) + self._local_sock = None + self._start_lock_sock(config) + def init_state_from_disk(self) -> None: try: with open('poller.state', 'r') as f: @@ -149,7 +153,7 @@ def series_determine_tree(self, s: PwSeries) -> str: return ret - def _process_series(self, pw_series) -> None: + def _process_series(self, pw_series, force_tree=None) -> None: s = PwSeries(self._pw, pw_series) log("Series info", @@ -160,10 +164,16 @@ def _process_series(self, pw_series) -> None: log(p['name'], "") log_end_sec() - if not s['received_all']: - raise IncompleteSeries + if force_tree: + comment = f"Force tree {force_tree}" + s.tree_name = force_tree + s.tree_mark_expected = None + s.tree_marked = True + else: + comment = self.series_determine_tree(s) + if not s['received_all']: + raise IncompleteSeries - comment = self.series_determine_tree(s) s.need_async = self.list_module.series_needs_async(s) if s.need_async: comment += ', async' @@ -178,11 +188,83 @@ def _process_series(self, pw_series) -> None: core.write_tree_selection_result(self.result_dir, s, comment) core.mark_done(self.result_dir, s) - def process_series(self, pw_series) -> None: + def process_series(self, pw_series, force_tree=None) -> None: log_open_sec(f"Checking series {pw_series['id']} with {pw_series['total']} patches") try: - self._process_series(pw_series) + self._process_series(pw_series, force_tree) + finally: + log_end_sec() + + def _start_lock_sock(self, config) -> None: + socket_path = config.get('poller', 'local_sock_path', fallback=None) + if not socket_path: + return + + if os.path.exists(socket_path): + os.unlink(socket_path) + + self._local_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self._local_sock.setblocking(False) + self._local_sock.bind(socket_path) + self._local_sock.listen(5) + + log(f"Socket listener started on {socket_path}", "") + + def _check_local_sock(self) -> None: + if not self._local_sock: + return + + try: + conn, _ = self._local_sock.accept() + except BlockingIOError: + return + + log_open_sec("Processing local socket connection") + try: + data = b"" + while True: + chunk = conn.recv(4096) + data += chunk + if len(chunk) < 4096: + break + + if data: + data = data.decode("utf-8") + series_ids = [] + items = data.split(";") + for item in items: + item = item.strip() + if not item: + continue + + # We accept "series [tree]; series [tree]; ..." + parts = item.rsplit(" ", 1) + if len(parts) == 2: + tree = parts[1].strip() + else: + tree = None + try: + s_id = int(parts[0].strip()) + series_ids.append((tree, s_id)) + log("Processing", series_ids[-1]) + except ValueError: + log("Invalid number in tuple", item) + continue + + for tree, series_id in series_ids: + try: + pw_series = self._pw.get("series", series_id) + self.process_series(pw_series, force_tree=tree) + conn.sendall(f"OK: {series_id}\n".encode("utf-8")) + except Exception as e: + log("Error processing series", str(e)) + conn.sendall(f"ERROR: {series_id}: {e}\n".encode("utf-8")) + else: + conn.sendall(b"DONE\n") + except Exception as e: + log("Error processing socket request", str(e)) finally: + conn.close() log_end_sec() def run(self, life) -> None: @@ -210,6 +292,8 @@ def run(self, life) -> None: # shouldn't have had this event at all though pass + self._check_local_sock() + while not self._done_queue.empty(): s = self._done_queue.get() log(f"Testing complete for series {s['id']}", "") From 1668e7ab07983d5d2738f14d6c8c7a3534b2f0c9 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Thu, 16 Oct 2025 13:16:10 -0700 Subject: [PATCH 421/429] maintainer: ignore the +xyz in emails when matching Mailbot is rejecting maintainers who have +str in their email. Try to cut that part out. Signed-off-by: Jakub Kicinski --- core/maintainers.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/maintainers.py b/core/maintainers.py index 0167d53..d56127e 100755 --- a/core/maintainers.py +++ b/core/maintainers.py @@ -25,6 +25,11 @@ def name_email_split(name_email): name_email = name_email[idx + 1:-1] name = '' email = name_email + if '+' in email and email.find('+') < email.find('@'): + pidx = email.find('+') + didx = email.find('@') + email = email[:idx] + email[didx:] + return name, email def __repr__(self): From 3b57fb5d09016c64ee79c8dcff54cb9a85bc7b3e Mon Sep 17 00:00:00 2001 From: Jacob Keller Date: Wed, 22 Oct 2025 10:54:22 -0700 Subject: [PATCH 422/429] tests: kdoc: re-implement in python The existing kdoc test is written in shell, based on a simple heuristic of checking whether the number of lines of output from scripts/kernel-doc increases. This test can pass in some cases even when new warnings are introduced, such as if a single patch adds some new warnings while removing others. In addition, the provided log output is not very useful to humans. It uses a traditional diff to compare the prior and current warnings. Because the warnings include line number data, this results in an unreadable mess. Implementing a proper comparison in the shell script is possible, but the resulting logic and code is difficult to maintain. Ultimately, what we want to do is a sort of set intersection on the warnings while ignoring line numbers. Implementing this in python is much more practical. As a first step, convert the kdoc test into a python module. Commands are executed via subprocess. The improved comparison algorithm will be implemented in a following change. Since we will be implementing a new comparison algorithm, do not bother porting the "diff" implementation forward. This results in a temporary regression as we no longer compute the difference or file summary for the user. This will be resolved with the following change implementing the improved algorithm. This version has a couple of minor changes worth nothing: 1) The log output is now in stdout instead of stderr, since the python-based tests do not support logging output to the stderr file. 2) We no longer print out "Checking the tree..." lines. 3) The description line is included within the stdout log text as well as the desc file. Signed-off-by: Jacob Keller --- tests/patch/kdoc/info.json | 3 +- tests/patch/kdoc/kdoc.sh | 52 --------------------- tests/patch/kdoc/test.py | 92 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 53 deletions(-) delete mode 100755 tests/patch/kdoc/kdoc.sh create mode 100644 tests/patch/kdoc/test.py diff --git a/tests/patch/kdoc/info.json b/tests/patch/kdoc/info.json index e6d6d9a..bfac5c9 100644 --- a/tests/patch/kdoc/info.json +++ b/tests/patch/kdoc/info.json @@ -1,4 +1,5 @@ { - "run": ["kdoc.sh"], + "pymod": "test", + "pyfunc": "kdoc", "pull-requests": true } diff --git a/tests/patch/kdoc/kdoc.sh b/tests/patch/kdoc/kdoc.sh deleted file mode 100755 index 63ae6eb..0000000 --- a/tests/patch/kdoc/kdoc.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/bin/bash -# SPDX-License-Identifier: GPL-2.0 -# -# Copyright (C) 2019 Netronome Systems, Inc. -# Copyright (c) 2020 Facebook - -tmpfile_o=$(mktemp) -tmpfile_n=$(mktemp) -rc=0 - -files_mod=$(git show --diff-filter=M --pretty="" --name-only "HEAD") -files_all=$(git show --diff-filter=AM --pretty="" --name-only "HEAD") - -HEAD=$(git rev-parse HEAD) - -echo "Checking the tree before the patch" -git checkout -q HEAD~ -./scripts/kernel-doc -Wall -none $files_mod 2> >(tee $tmpfile_o >&2) - -incumbent=$(grep -v 'Error: Cannot open file ' $tmpfile_o | wc -l) - -echo "Checking the tree with the patch" - -git checkout -q $HEAD -./scripts/kernel-doc -Wall -none $files_all 2> >(tee $tmpfile_n >&2) - -current=$(grep -v 'Error: Cannot open file ' $tmpfile_n | wc -l) - -echo "Errors and warnings before: $incumbent this patch: $current" >&$DESC_FD - -if [ $current -gt $incumbent ]; then - echo "New warnings added" 1>&2 - diff $tmpfile_o $tmpfile_n 1>&2 - - echo "Per-file breakdown" 1>&2 - tmpfile_fo=$(mktemp) - tmpfile_fn=$(mktemp) - - grep -i "\(warn\|error\)" $tmpfile_o | sed -n 's@\(^[/a-zA-Z0-9_.-]*.[ch]\):.*@\1@p' | sort | uniq -c \ - > $tmpfile_fo - grep -i "\(warn\|error\)" $tmpfile_n | sed -n 's@\(^[/a-zA-Z0-9_.-]*.[ch]\):.*@\1@p' | sort | uniq -c \ - > $tmpfile_fn - - diff $tmpfile_fo $tmpfile_fn 1>&2 - rm $tmpfile_fo $tmpfile_fn - - rc=1 -fi - -rm $tmpfile_o $tmpfile_n - -exit $rc diff --git a/tests/patch/kdoc/test.py b/tests/patch/kdoc/test.py new file mode 100644 index 0000000..ac53b30 --- /dev/null +++ b/tests/patch/kdoc/test.py @@ -0,0 +1,92 @@ +# SPDX-License-Identifier: GPL-2.0 + +""" Test if kernel-doc generates new warnings """ + +import os +import subprocess +from typing import List, Optional, Tuple + +def get_git_head(tree) -> str: + """ Get the git commit ID for head commit. """ + + cmd = ["git", "rev-parse", "HEAD"] + result = subprocess.run(cmd, cwd=tree.path, capture_output=True, text=True, + check=True) + + return result.stdout.strip() + +def run_kernel_doc(tree, commitish, files): + """ Run ./scripts/kdoc on a given commit and capture its results. """ + + cmd = ["git", "checkout", "-q", commitish] + subprocess.run(cmd, cwd=tree.path, capture_output=False, check=True) + + cmd = ["./scripts/kernel-doc", "-Wall", "-none"] + files + result = subprocess.run(cmd, cwd=tree.path, text=True, check=False, + stderr=subprocess.PIPE) + + return result.stderr.strip().split('\n') + +def extract_files(patch): + """Extract paths added or modified by the patch.""" + + all_files = set() + mod_files = set() + lines = patch.raw_patch.split("\n") + + # Walk lines, skip last since it doesn't have next + for i, line in enumerate(lines[:-1]): + next_line = lines[i + 1] + + if not next_line.startswith("+++ b/"): + continue + + file_path = next_line[6:] + + all_files.add(file_path) + + if line != "--- /dev/null": + mod_files.add(file_path) + + return list(mod_files), list(all_files) + +def kdoc(tree, patch, result_dir) -> Tuple[int, str, str]: + """ Main function / entry point """ + + mod_files, all_files = extract_files(patch) + + if not mod_files or not all_files: + return 1, "Patch has no modified files?", "" + + ret = 0 + desc = "" + log = [] + + head_commit = get_git_head(tree) + + try: + incumbent_warnings = run_kernel_doc(tree, "HEAD~", mod_files) + log += ["Warnings before patch:"] + log.extend(map(str, incumbent_warnings)) + + current_warnings = run_kernel_doc(tree, head_commit, all_files) + log += ["", "Current warnings:"] + log.extend(map(str, current_warnings)) + except subprocess.CalledProcessError as e: + desc = f'{e.cmd} failed with exit code {e.returncode}' + if e.stderr: + log += e.stderr.split('\n') + ret = 1 + + return ret, desc, "\n".join(log) + + incumbent_count = len(incumbent_warnings) + current_count = len(current_warnings) + + desc = f'Errors and warnings before: {incumbent_count} This patch: {current_count}' + log += ["", desc] + + if current_count > incumbent_count: + ret = 1 + + return ret, desc, "\n".join(log) From 8fc078fa59b54374bc96ca35a333070ab2a73912 Mon Sep 17 00:00:00 2001 From: Jacob Keller Date: Wed, 22 Oct 2025 12:16:38 -0700 Subject: [PATCH 423/429] tests: kdoc: improve test comparison and output The current algorithm for the kdoc test is a simple line number comparison of the output from ./scripts/kernel-doc. This is not guaranteed to be accurate, as a patch could fix some warnings while introducing others, resulting in the total number of lines not changing. In addition, it is difficult to quickly determine which warnings are new. Historically, a simple "diff" was used to compare the before and after results. This was difficult to parse because line numbers change due to code being added or moved. To fix this, use a set difference algorithm which compares based on data from the warning lines without including the line number. To do this, introduce a KdocWarning dataclass, which represents a warning based on its kind, file, line, and content. Extract these from the lines via a regular expression that looks for the expected pattern of output from ./scripts/kernel-doc. Using a @dataclass allows specifying which fields to compare against. In particular, the line number is not counted. Additionally, the original message as output from ./scripts/kernel-doc is preserved as its own (non-compared) field. Some warnings are spread over two lines, indicated by the first line ending in a semicolon. Handle this when iterating over the output of kernel-doc and converting to the new warning objects. Any output line which doesn't match the regular expression is converted so that its entire message becomes the contents. This ensures that such lines still get counted and still get compared in some form, rather than silently ignored. After obtaining the converted output from ./scripts/kernel-doc, convert the current_warnings and incumbent_warnings lists into sets. Calculate the set of new and removed warnings by iterating the original lists. For new_warnings, find the sequence of warnings which are in the current_warnings but not in the incumbent set. Do the same to calculate the set of removed warnings. This implementation is fast, as it can use the hash-based comparisons for efficient set lookup. Using the original list also preserves the order of the warnings as output by ./scripts/kernel-doc. Calculating both the new and removed counts provides useful data for users of the NIPA tests. It is much easier to see at a glance what warnings were added. Signed-off-by: Jacob Keller --- tests/patch/kdoc/test.py | 120 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 117 insertions(+), 3 deletions(-) diff --git a/tests/patch/kdoc/test.py b/tests/patch/kdoc/test.py index ac53b30..6715161 100644 --- a/tests/patch/kdoc/test.py +++ b/tests/patch/kdoc/test.py @@ -2,7 +2,10 @@ """ Test if kernel-doc generates new warnings """ +import collections +import dataclasses import os +import re import subprocess from typing import List, Optional, Tuple @@ -15,7 +18,84 @@ def get_git_head(tree) -> str: return result.stdout.strip() -def run_kernel_doc(tree, commitish, files): +@dataclasses.dataclass(frozen=True, eq=True, order=True, init=True) +class KdocWarning: + # The original warning message + message : str = dataclasses.field(repr=False, compare=False) + _ : dataclasses.KW_ONLY + # Kind of warning line, determined during init + kind : str = dataclasses.field(repr=True, compare=True) + # The file path, or None if unable to determine + file : Optional[str] = dataclasses.field(repr=True, compare=True) + # The line, or None if unable to determine + # Note: *not* part of comparison, or hash! + line : Optional[int] = dataclasses.field(repr=True, compare=False) + # The content of the warning (excluding kind, file, line) + content : str = dataclasses.field(repr=True, compare=True) + + @classmethod + def from_text(self, line, extra=None): + message = line + + if extra: + message += '\n' + extra + + parser = re.compile( + r""" + ^ # Start of string + (?Pwarning|error): # Severity + \s+ # Spacing + (?P[/a-z0-9_.-]*): # File path + (?P[0-9]+) # Line number + \s* # Spacing + (?P.*) # Warning content + $ # End of string + """, + re.VERBOSE | re.IGNORECASE) + + m = parser.match(line) + if m: + kind = m['kind'] + file = m['file'] + line = int(m['line']) + content = m['content'] + if extra: + content += '\n' + extra + else: + kind = 'Unknown' + file = None + line = None + content = message + + return KdocWarning(message, kind=kind, file=file, line=line, + content=content) + + def __str__(self): + return self.message + +def parse_warnings(lines) -> List[KdocWarning]: + skip = False + length = len(lines) + + warnings = [] + + # Walk through lines and convert to warning objects + for i, line in enumerate(lines): + if skip: + skip = False + continue + + if line.endswith(':') and i + 1 < length: + extra = lines[i + 1] + skip = True + else: + extra = None + + warnings.append(KdocWarning.from_text(line, extra)) + + return warnings + +def run_kernel_doc(tree, commitish, files) -> List[KdocWarning]: """ Run ./scripts/kdoc on a given commit and capture its results. """ cmd = ["git", "checkout", "-q", commitish] @@ -25,7 +105,9 @@ def run_kernel_doc(tree, commitish, files): result = subprocess.run(cmd, cwd=tree.path, text=True, check=False, stderr=subprocess.PIPE) - return result.stderr.strip().split('\n') + lines = result.stderr.strip().split('\n') + + return parse_warnings(lines) def extract_files(patch): """Extract paths added or modified by the patch.""" @@ -80,13 +162,45 @@ def kdoc(tree, patch, result_dir) -> Tuple[int, str, str]: return ret, desc, "\n".join(log) + current_set = set(current_warnings) + incumbent_set = set(incumbent_warnings) + + # This construction preserves ordering vs using set difference + new_warnings = [x for x in current_warnings if x not in incumbent_set] + rm_warnings = [x for x in incumbent_warnings if x not in current_set] + incumbent_count = len(incumbent_warnings) current_count = len(current_warnings) + new_count = len(new_warnings) + rm_count = len(rm_warnings) desc = f'Errors and warnings before: {incumbent_count} This patch: {current_count}' + if new_count: + desc += f' New: {new_count}' + if rm_count: + desc += f' Removed: {rm_count}' log += ["", desc] - if current_count > incumbent_count: + if rm_count: + log += ["", "Warnings removed:"] + log.extend(map(str, rm_warnings)) + + file_breakdown = collections.Counter((x.file for x in rm_warnings)) + + log += ["Per-file breakdown:"] + for f, count in file_breakdown.items(): + log += [f'{count:6} {f}'] + + if new_count: ret = 1 + log += ["", "New warnings added:"] + log.extend(map(str, new_warnings)) + + file_breakdown = collections.Counter((x.file for x in new_warnings)) + + log += ["Per-file breakdown:"] + for f, count in file_breakdown.items(): + log += [f'{count:6} {f}'] + return ret, desc, "\n".join(log) From 79f48a777d61b7fd7424b5321332522adefc1d88 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 24 Oct 2025 09:17:03 -0700 Subject: [PATCH 424/429] core: tree: retry fetches Fetching the trees has gotten flaky lately. Retry up to 10 times. Signed-off-by: Jakub Kicinski --- core/tree.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/core/tree.py b/core/tree.py index 07c90e7..5844b27 100644 --- a/core/tree.py +++ b/core/tree.py @@ -7,6 +7,7 @@ import multiprocessing import os import tempfile +import time from typing import List import core @@ -116,7 +117,14 @@ def git_merge_base(self, c1, c2, is_ancestor=False): return self.git(cmd) def git_fetch(self, remote): - return self.git(['fetch', remote]) + for i in range(10): + try: + return self.git(['fetch', remote]) + except CMD.CmdError as e: + core.log(f"Fetching failed (attempt {i + 1})", repr(e)) + time.sleep(30) + if i >= 9: + raise def git_reset(self, target, hard=False): cmd = ['reset', target] From fd0a3844618e2b9fd199c26d1c7d73aabd2d555e Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 24 Oct 2025 09:17:49 -0700 Subject: [PATCH 425/429] contest: worker-setup: add instruction for traceoute Signed-off-by: Jakub Kicinski --- deploy/contest/remote/worker-setup.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/deploy/contest/remote/worker-setup.sh b/deploy/contest/remote/worker-setup.sh index 35627b0..9288661 100644 --- a/deploy/contest/remote/worker-setup.sh +++ b/deploy/contest/remote/worker-setup.sh @@ -251,3 +251,12 @@ sudo dnf install python3-pyroute2.noarch ./configure --prefix=/usr make -j sudo make install + + # traceroute + get tar ball from: + https://sourceforge.net/projects/traceroute/files/ + untar + cd ... + make + cp -v ./traceroute/traceroute ../fs/usr/bin/ + cp -v ./traceroute/traceroute ../fs/usr/bin/traceroute6 From d3909811a1a64a9d1cbf2a57520ab650649d25e5 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 25 Oct 2025 10:21:21 -0700 Subject: [PATCH 426/429] tests: kdoc: fix some linter warnings Signed-off-by: Jakub Kicinski --- tests/patch/kdoc/test.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/patch/kdoc/test.py b/tests/patch/kdoc/test.py index 6715161..e4317e4 100644 --- a/tests/patch/kdoc/test.py +++ b/tests/patch/kdoc/test.py @@ -4,7 +4,6 @@ import collections import dataclasses -import os import re import subprocess from typing import List, Optional, Tuple @@ -132,7 +131,7 @@ def extract_files(patch): return list(mod_files), list(all_files) -def kdoc(tree, patch, result_dir) -> Tuple[int, str, str]: +def kdoc(tree, patch, _result_dir) -> Tuple[int, str, str]: """ Main function / entry point """ mod_files, all_files = extract_files(patch) From 0a1066f7b7dd8b47b014993208ffb43b895d3a66 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 25 Oct 2025 10:46:12 -0700 Subject: [PATCH 427/429] tests: kdoc: fix handling file removal Looks like the kdoc test struggles with file deletion as well as files which produce no warnings at all. Fix that. Add more logs for ease of troubleshooting. While at it shorten the outputs a little bit to make the summary fit on a line when running ingest_mdir Signed-off-by: Jakub Kicinski --- tests/patch/kdoc/test.py | 45 +++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/tests/patch/kdoc/test.py b/tests/patch/kdoc/test.py index e4317e4..e6d3216 100644 --- a/tests/patch/kdoc/test.py +++ b/tests/patch/kdoc/test.py @@ -72,7 +72,7 @@ def from_text(self, line, extra=None): def __str__(self): return self.message -def parse_warnings(lines) -> List[KdocWarning]: +def parse_warnings(lines, logs) -> List[KdocWarning]: skip = False length = len(lines) @@ -87,16 +87,24 @@ def parse_warnings(lines) -> List[KdocWarning]: if line.endswith(':') and i + 1 < length: extra = lines[i + 1] skip = True + elif not line.strip(): + continue else: + logs += [": " + line.strip()] extra = None warnings.append(KdocWarning.from_text(line, extra)) return warnings -def run_kernel_doc(tree, commitish, files) -> List[KdocWarning]: +def run_kernel_doc(tree, commitish, files, logs) -> List[KdocWarning]: """ Run ./scripts/kdoc on a given commit and capture its results. """ + logs += ["files: " + str(files)] + + if not files: + return [] + cmd = ["git", "checkout", "-q", commitish] subprocess.run(cmd, cwd=tree.path, capture_output=False, check=True) @@ -106,13 +114,13 @@ def run_kernel_doc(tree, commitish, files) -> List[KdocWarning]: lines = result.stderr.strip().split('\n') - return parse_warnings(lines) + return parse_warnings(lines, logs) def extract_files(patch): """Extract paths added or modified by the patch.""" - all_files = set() - mod_files = set() + before_files = set() + after_files = set() lines = patch.raw_patch.split("\n") # Walk lines, skip last since it doesn't have next @@ -124,19 +132,19 @@ def extract_files(patch): file_path = next_line[6:] - all_files.add(file_path) - - if line != "--- /dev/null": - mod_files.add(file_path) + if "/dev/null" not in line: + before_files.add(file_path) + if "/dev/null" not in next_line: + after_files.add(file_path) - return list(mod_files), list(all_files) + return list(before_files), list(after_files) def kdoc(tree, patch, _result_dir) -> Tuple[int, str, str]: """ Main function / entry point """ - mod_files, all_files = extract_files(patch) + before_files, after_files = extract_files(patch) - if not mod_files or not all_files: + if not before_files and not after_files: return 1, "Patch has no modified files?", "" ret = 0 @@ -146,12 +154,12 @@ def kdoc(tree, patch, _result_dir) -> Tuple[int, str, str]: head_commit = get_git_head(tree) try: - incumbent_warnings = run_kernel_doc(tree, "HEAD~", mod_files) log += ["Warnings before patch:"] + incumbent_warnings = run_kernel_doc(tree, "HEAD~", before_files, log) log.extend(map(str, incumbent_warnings)) - current_warnings = run_kernel_doc(tree, head_commit, all_files) log += ["", "Current warnings:"] + current_warnings = run_kernel_doc(tree, head_commit, after_files, log) log.extend(map(str, current_warnings)) except subprocess.CalledProcessError as e: desc = f'{e.cmd} failed with exit code {e.returncode}' @@ -173,11 +181,14 @@ def kdoc(tree, patch, _result_dir) -> Tuple[int, str, str]: new_count = len(new_warnings) rm_count = len(rm_warnings) - desc = f'Errors and warnings before: {incumbent_count} This patch: {current_count}' + desc = f'Warnings before: {incumbent_count} after: {current_count}' + brac = [] if new_count: - desc += f' New: {new_count}' + brac += [f'add: {new_count}'] if rm_count: - desc += f' Removed: {rm_count}' + brac += [f'del: {rm_count}'] + if brac: + desc += f' ({" ".join(brac)})' log += ["", desc] if rm_count: From f93bc615894620d0ffbd89f85183da9df678b5a9 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 25 Oct 2025 10:54:45 -0700 Subject: [PATCH 428/429] tests: maintainers: fix false positives on build files The build files are sometimes outside of the directory covered by the new MAINTAINERS entry. Signed-off-by: Jakub Kicinski --- tests/series/maintainers/test.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/series/maintainers/test.py b/tests/series/maintainers/test.py index 1926ada..6adb05f 100644 --- a/tests/series/maintainers/test.py +++ b/tests/series/maintainers/test.py @@ -102,6 +102,11 @@ def check_maintainer_coverage(tree, new_files, out): pass_target = 3 for file_path in new_files: + # The build files are sometimes outside of the directory covered + # by the new MAINTAINERS entry + if file_path.endswith(("/Makefile", "/Kconfig")): + continue + out.append("\nChecking coverage for a new file: " + file_path) maintainer_info = get_maintainer_entry_for_file(tree, file_path) From b4025acd2372300380a8f39cb7545a5efa7a839e Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Sat, 25 Oct 2025 11:57:54 -0700 Subject: [PATCH 429/429] tests: check_selftest: fix the Makefile, gitignore checks and result merging Multiple issues here: - git grep does not have --exit-code - capture output, otherwise it goes to stdout - needle must be stripped from the full path - result merging logic is wrong - result merge doesn't override input, caller must capture ret Signed-off-by: Jakub Kicinski --- tests/patch/check_selftest/test.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/tests/patch/check_selftest/test.py b/tests/patch/check_selftest/test.py index cf78c2b..17684cb 100644 --- a/tests/patch/check_selftest/test.py +++ b/tests/patch/check_selftest/test.py @@ -12,10 +12,12 @@ def ret_merge(ret, nret): """ merge results """ - if ret[0] == 0 or nret[0] == 0: + if ret[0] == 0 and nret[0] == 0: val = 0 + elif ret[0] == 1 or nret[0] == 1: + val = 1 else: - val = min(ret[0], nret[0]) + val = max(ret[0], nret[0]) desc = "" if ret[1] and nret[1]: @@ -41,12 +43,14 @@ def check_new_files_makefile(tree, new_files, log): continue makefile = os.path.dirname(path) + "/Makefile" + needle = os.path.basename(path) - cmd = ["git", "grep", "--exit-code", needle, "---", makefile] - result = subprocess.run(cmd, cwd=tree.path, check=False) + cmd = ["git", "grep", needle, "--", makefile] + result = subprocess.run(cmd, cwd=tree.path, capture_output=True, + check=False) log.append(" ".join(cmd) + f":: {result.returncode}") if result.returncode: - ret_merge(ret, (1, path + " not found in Makefile")) + ret = ret_merge(ret, (1, path + " not found in Makefile")) cnt += 1 if not ret[0] and cnt: @@ -69,12 +73,14 @@ def check_new_files_gitignore(tree, new_files, log): continue target = os.path.dirname(path) + "/.gitignore" + needle = os.path.basename(path) - cmd = ["git", "grep", "--exit-code", needle, "---", target] - result = subprocess.run(cmd, cwd=tree.path, check=False) + cmd = ["git", "grep", needle, "--", target] + result = subprocess.run(cmd, cwd=tree.path, capture_output=True, + check=False) log.append(" ".join(cmd) + f":: {result.returncode}") if result.returncode: - ret_merge(ret, (1, needle + " not found in .gitignore")) + ret = ret_merge(ret, (1, needle + " not found in .gitignore")) cnt += 1 if not ret[0] and cnt: