Add tool about visualiztion of checkbox test plans#137
Add tool about visualiztion of checkbox test plans#137rickwu666666 wants to merge 7 commits intomainfrom
Conversation
12d8072 to
039ea07
Compare
039ea07 to
a1974f2
Compare
There was a problem hiding this comment.
Pull request overview
Adds a local FastAPI-based tool under Tools/test_plan_visualization/ to parse Checkbox .pxu units into a SQLite DB and provide a web UI for browsing jobs, exploring test plan trees, and comparing plans.
Changes:
- Introduces a PXU parser + SQLite schema for jobs and test plans.
- Adds FastAPI endpoints to query/filter jobs, render plan trees, show plan details, and compare plans.
- Provides a self-contained run script, Dockerfile, and a single-page HTML/CSS/JS frontend.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| Tools/test_plan_visualization/run.sh | Bootstrap venv, clone/pull Checkbox repo, rebuild DB, start uvicorn |
| Tools/test_plan_visualization/requirements.txt | Python dependencies for the tool |
| Tools/test_plan_visualization/app/templates/index.html | Frontend UI for Jobs / Test Plans / Compare with modals |
| Tools/test_plan_visualization/app/parser.py | PXU parsing + DB rebuild logic (supports local providers priority) |
| Tools/test_plan_visualization/app/main.py | FastAPI app + API endpoints for jobs/options/plans/compare |
| Tools/test_plan_visualization/app/database.py | SQLAlchemy models + SQLite session management |
| Tools/test_plan_visualization/README.md | Usage and feature documentation |
| Tools/test_plan_visualization/Dockerfile | Containerization to run the tool with Docker |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| jobs = query.all() | ||
|
|
||
| if has_template_id is not None: | ||
| jobs = [ | ||
| j | ||
| for j in jobs | ||
| if bool( | ||
| (json.loads(j.data) if j.data else {}).get("template-id", "") | ||
| ) | ||
| == has_template_id | ||
| ] | ||
|
|
||
| if search: | ||
| sl = search.lower() | ||
|
|
||
| # helper: convert checkbox glob pattern to regex | ||
| def _matches(pattern: str, jid: str) -> bool: | ||
| try: | ||
| esc = _re.sub(r"\.(?!\*)", r"\\.", pattern) | ||
| esc = esc.replace(".*", "\x00") | ||
| esc = esc.replace(".", r"\.") | ||
| esc = esc.replace("\x00", ".*") | ||
| return bool(_re.match("^" + esc + "$", jid)) | ||
| except _re.error: | ||
| return pattern == jid | ||
|
|
||
| # 1. Jobs whose own ID contains the search term | ||
| job_id_set = {j.job_id for j in jobs if sl in j.job_id.lower()} | ||
|
|
||
| # 2. Jobs that belong to test plans whose ID contains the search term | ||
| all_plans = db.query(TestPlan).all() | ||
| by_full_id = {p.full_id: p for p in all_plans} | ||
| by_bare_id = {p.plan_id: p for p in all_plans} | ||
|
|
||
| def _resolve(ref: str): | ||
| return by_full_id.get(ref) or by_bare_id.get(ref.split("::")[-1]) | ||
|
|
||
| def _collect_patterns(plan, visited=None): | ||
| """Recursively collect include patterns from a plan | ||
| and all its nested_parts.""" | ||
| if visited is None: | ||
| visited = set() | ||
| if plan.full_id in visited: | ||
| return set() | ||
| visited.add(plan.full_id) | ||
| patterns = set(json.loads(plan.include) if plan.include else []) | ||
| for child_ref in ( | ||
| json.loads(plan.nested_part) if plan.nested_part else [] | ||
| ): | ||
| child = _resolve(child_ref) | ||
| if child: | ||
| patterns |= _collect_patterns(child, visited) | ||
| return patterns | ||
|
|
||
| matching_plans = [ | ||
| p | ||
| for p in all_plans | ||
| if sl in p.full_id.lower() or sl in p.plan_id.lower() | ||
| ] | ||
|
|
||
| plan_job_id_set = set() | ||
| for plan in matching_plans: | ||
| patterns = _collect_patterns(plan) | ||
| for job in jobs: | ||
| bare_job = job.job_id.split("::")[-1] |
| #!/bin/bash | ||
| set -e | ||
|
|
||
| REPO_URL="https://github.com/canonical/checkbox.git" | ||
| REPO_DIR="checkbox_repo" | ||
| VENV_DIR=".venv" | ||
|
|
| currentJobs.forEach((job, i) => { | ||
| const tr = document.createElement('tr'); | ||
| const envTags = job.environ.map(e => `<span class="tag env">${e}</span>`).join(''); | ||
| const manTags = job.manifest.map(m => `<span class="tag man">${m}</span>`).join(''); | ||
| const provider = job.provider.split('::').pop(); | ||
| const category = job.category.split('::').pop(); | ||
| const highlight = q | ||
| ? job.id.replace(new RegExp(`(${q.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')})`, 'gi'), | ||
| '<mark style="background:#fff3cd;border-radius:2px">$1</mark>') | ||
| : job.id; | ||
| tr.innerHTML = ` | ||
| <td style="color:#bbb;font-size:.73rem">${i + 1}</td> | ||
| <td><span class="job-id">${highlight}</span></td> | ||
| <td><span class="tmpl-id">${job.template_id || ''}</span></td> | ||
| <td style="font-size:.76rem;color:#667">${provider}</td> | ||
| <td style="font-size:.76rem;color:#667">${category}</td> | ||
| <td>${envTags}</td> | ||
| <td>${manTags}</td> | ||
| <td style="font-size:.8rem">${job.summary || ''}</td> | ||
| <td><button class="btn-details" onclick="showDetails(${i})">Details</button></td> | ||
| `; |
| app.mount("/static", StaticFiles(directory="app/static"), name="static") | ||
| templates = Jinja2Templates(directory="app/templates") |
|
|
||
| @app.get("/", response_class=HTMLResponse) | ||
| async def read_items(request: Request): | ||
| return templates.TemplateResponse(request=request, name="index.html") |
| if environ: | ||
| query = query.filter(Job.environ.contains(environ)) | ||
|
|
||
| if manifest: | ||
| query = query.filter(Job.manifest.contains(manifest)) | ||
|
|
| # Apply this plan's own excludes to the full accumulated set | ||
| if own_excludes: | ||
| result = { | ||
| jid | ||
| for jid in result | ||
| if not any( | ||
| _matches(ep.split("::")[-1], jid.split("::")[-1]) | ||
| or _matches(ep.split("::")[-1], jid) | ||
| for ep in own_excludes |
|
|
||
| if [ "$NEEDS_DB_UPDATE" = true ]; then | ||
| echo "Updating database..." | ||
| python3 -c "from app.parser import update_db; update_db('checkbox_repo', 'providers')" |
| let attrsHtml = '<p class="section-title" style="margin-top:20px">Job Attributes</p><div class="attr-grid">'; | ||
| for (const k of keys) { | ||
| const v = String(attrs[k]).trim(); | ||
| attrsHtml += `<div class="attr-key">${k}</div><div class="attr-val"><pre>${v}</pre></div>`; | ||
| } |
| a local search engine for job units and test plans. It supports two views: | ||
|
|
||
| - **Jobs View** — browse and filter individual job units | ||
| - **Test Plans View** — search test plans and explore their nested structure | ||
| down to included jobs | ||
|
|
||
| ## Features | ||
|
|
||
| - **Two-View UI**: Toggle between Jobs and Test Plans views from the header. |
|
Checkbox is going to migrate |
To adopt both pxu and yaml is a good point. However, this tool intend to not involve any checkbox function in case we have to touch the code in the future. |
I don't understand what this means. If you don't what to depend on any checkbox feature, how do you know the list of test cases is same as checkbox ? |
I want to move away from the checkbox function to prevent data errors caused by unexpected changes of checkbox. If the parser is implemented correctly, it should match the checkbox output since the job/plan definitions are clear. |
… than query from db.
Add a tool to parsing checkbox pxu file and display result on local webpage.