Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Tools/test_plan_visualization/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
FROM python:3.10-slim

WORKDIR /app

RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app/ app/
COPY run.sh .

RUN chmod +x run.sh

ENTRYPOINT ["./run.sh"]
217 changes: 217 additions & 0 deletions Tools/test_plan_visualization/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
# Checkbox Job Database

A web application to inspect, filter, and explore Checkbox jobs and test plans.

## Overview

This application parses the
[Checkbox](https://github.com/canonical/checkbox) repository and provides
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.
Comment on lines +9 to +17
- **Automated Parsing**: Scans `.pxu` files and extracts both job units
(including legacy `plugin:`-style entries) and test plan units.
- **Job Filters**: Filter by Provider, Category, Environ, Manifest Keys,
and Template ID presence.
- **Search**: Search by job ID, test plan ID, or plan name — searches across
both jobs and the test plans they belong to.
- **Test Plan Tree**: In the Test Plans view, expand plans to see nested
sub-plans and directly included jobs.
- **Exclude Support**: Jobs excluded by a test plan's `exclude:` field are
shown with a strikethrough and an EXCLUDED badge — they remain visible
but clearly marked.
- **Job Details Modal**: Click Details on any job to see its full attributes
and the complete test plan hierarchy it belongs to.
- **Plan Details Modal**: Click Details on any test plan card to see all raw
plan attributes, include/exclude patterns, and nested parts.
- **Dynamic Filters**: Dropdown options update dynamically based on
current selections.
- **Provider Resolution**: Automatically resolves provider namespaces
from `manage.py`.
- **Compare Plans**: Switch to the Compare view, enter two plan IDs, and click
Compare. An animated progress bar shows while the comparison runs. Results
are presented as a three-column diff — jobs only in Plan 1, jobs in both,
and jobs only in Plan 2 (excludes are applied before comparing). Each column
header shows the job count, its percentage of all unique jobs across both
plans, and an additional hint:
- *Only-in* columns: percentage of that plan's total jobs that are exclusive
to it (e.g. `42.5% of plan 1`).
- *In Both* column: Jaccard similarity — the overlap as a percentage of the
union of both plans (e.g. `Jaccard similarity: 31.2% overlap`).

## Getting Started

### Option A: Run Locally

**Prerequisites:** Python 3.10+, `git`

1. **Run the startup script** — it automatically creates a virtual
environment, installs dependencies, clones/updates the checkbox repo,
builds the database, and starts the server:

```bash
./run.sh
```

> To restart (e.g. after a code change):
>
> ```bash
> pkill -f "uvicorn app.main:app"; ./run.sh
> ```

2. **Access the Web Interface**:
Open your browser to [http://localhost:8888](http://localhost:8888).

---

### Option B: Run with Docker

**Prerequisites:** [Docker](https://docs.docker.com/get-docker/)

1. **Build the Docker image**:

```bash
sudo docker build -t checkbox-job-db .
```

2. **Run the container** (the startup script runs automatically
inside the container):

```bash
sudo docker run -p 8888:8888 checkbox-job-db
```

> If you get a "port already allocated" error, stop any existing
> container first:
>
> ```bash
> sudo docker stop $(sudo docker ps -q --filter publish=8888)
> ```
>
> If the port still appears stuck with no process using it, restart
> the Docker daemon:
>
> ```bash
> sudo systemctl restart docker
> ```

3. **Access the Web Interface**:
Open your browser to [http://localhost:8888](http://localhost:8888).

## Project Structure

- `app/`: Source code
- `main.py`: FastAPI server and API endpoints
- `parser.py`: PXU file parsing logic (jobs + test plans)
- `database.py`: SQLite schema (jobs and test_plans tables)
- `templates/`: HTML/CSS/JS frontend
- `providers/`: *(optional)* Local provider folders — place any custom or
OEM provider trees here. Jobs and test plans in this folder take
**priority** over the upstream checkbox repo; duplicates from the repo
are silently skipped. See [Local Providers](#local-providers) below.
- `Dockerfile`: Container definition
- `run.sh`: Startup script (venv → install → clone/pull → parse → serve)

## API Endpoints

<!-- markdownlint-disable MD013 -->
| Endpoint | Description |
| --- | --- |
| `GET /api/jobs` | List jobs with optional filters: `provider`, `category`, `environ`, `manifest`, `has_template_id`, `search` |
| `GET /api/options` | Get available filter values for the current filter selection |
| `GET /api/testplans?job_id=` | Get the full test plan ancestry for a given job ID |
| `GET /api/plan-tree?search=` | Search test plans and return their full nested tree with included jobs (excluded jobs marked) |
| `GET /api/plan-details?plan_id=` | Get all attributes, include/exclude patterns, and nested parts for a single test plan |
| `GET /api/compare-plans?plan1=&plan2=` | Compare effective job sets of two test plans (excludes applied), returning only-in-1, in-both, only-in-2 with job counts and Jaccard similarity |
<!-- markdownlint-enable MD013 -->

## Troubleshooting: Port 8888

**Check what is using port 8888:**

```bash
ss -tlnp sport = :8888 # shows process if owned by current user
sudo ss -tlnp sport = :8888 # shows process for all users
# (including Docker/root)
```

**Kill local uvicorn process:**

```bash
pkill -f "uvicorn app.main:app"
```

**Kill via port (requires root for Docker-owned processes):**

```bash
sudo fuser -k 8888/tcp
```

**Stop Docker container holding the port:**

```bash
sudo docker stop $(sudo docker ps -q --filter publish=8888)
```

**List running containers and their ports:**

```bash
sudo docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Ports}}"
```

**If port appears stuck with no process using it (stale Docker state):**

```bash
sudo systemctl restart docker
```

## Local Providers

Place any custom provider trees inside the `providers/` directory next to
`run.sh`. The directory structure mirrors a normal Checkbox provider:

```text
providers/
my-provider/
manage.py ← namespace is read from here
units/
foo/
jobs.pxu
test-plan.pxu
```

When the database is built:

1. All `.pxu` files under `providers/` are parsed **first** and their job
and test-plan IDs are recorded.
2. The upstream checkbox git repo is then scanned; any unit whose ID was
already loaded from `providers/` is skipped.

This means local providers can **override** upstream job definitions
(e.g. to add missing jobs or corrected summaries) without modifying the
checked-out checkbox repo.

> **Rebuild trigger**: if `providers/` contains any `.pxu` files, the
> database is always rebuilt on startup so local changes are picked up
> automatically.

## Notes

- The database is rebuilt every time the server starts
(both locally and in Docker).
- `unit: job`, legacy `plugin:`-style, and modern `flags: simple`
job blocks are all parsed correctly.
- Test plan nested hierarchy is traversed recursively;
cycle detection is built in.
- The `exclude:` field in test plans is parsed and stored. Excluded jobs
are shown with a strikethrough in the plan tree and are removed from
the effective job set when comparing plans.
- Environment variables referenced in a job's `command:` field
(e.g. `$SERIAL_PORTS_STATIC`) are automatically added to the Environ
filter even if no explicit `environ:` field is declared.
86 changes: 86 additions & 0 deletions Tools/test_plan_visualization/app/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from sqlalchemy import create_engine, Column, Integer, String, Text
from sqlalchemy.orm import declarative_base
from sqlalchemy.orm import sessionmaker

SQLALCHEMY_DATABASE_URL = "sqlite:///./checkbox_jobs.db"

engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()


class Job(Base):
"""
All runnable units: regular jobs (plugin: shell/manual/…),
resource jobs (plugin: resource), attachment jobs, and their
also-after-suspend mirror variants.
"""
__tablename__ = "jobs"

id = Column(Integer, primary_key=True, index=True)
job_id = Column(String, index=True)
provider = Column(String, index=True)
category_id = Column(String, index=True)
environ = Column(Text) # JSON list of env-var names
manifest = Column(Text) # JSON list of manifest keys from `requires`
command = Column(Text)
summary = Column(Text)
description = Column(Text)
unit_type = Column(String, index=True) # canonical unit: field value
plugin = Column(String, index=True) # plugin: field (shell/manual/resource/…)
data = Column(Text) # JSON of all raw PXU attributes


class ManifestEntry(Base):
"""
Manifest entry units (unit: manifest entry).
These describe hardware capability flags (e.g. has_bt_adapter).
"""
__tablename__ = "manifest_entries"

id = Column(Integer, primary_key=True, index=True)
entry_id = Column(String, index=True) # bare ID, e.g. has_bt_adapter
full_id = Column(String, index=True) # namespace::ID
provider = Column(String, index=True)
name = Column(Text) # _name field
value_type = Column(String) # boolean / natural / …
summary = Column(Text)
data = Column(Text) # JSON of all raw PXU attributes


class TestPlan(Base):
__tablename__ = "test_plans"

id = Column(Integer, primary_key=True, index=True)
plan_id = Column(String, index=True) # bare ID
full_id = Column(String, index=True) # namespace::ID
provider = Column(String, index=True)
name = Column(Text)
include = Column(Text) # JSON list of include patterns
exclude = Column(Text) # JSON list of exclude patterns
nested_part = Column(Text) # JSON list of nested plan refs
bootstrap_include = Column(Text) # JSON list of bootstrap resource refs
data = Column(Text) # JSON of all raw PXU attributes


class PlanMembership(Base):
"""
Precomputed effective job set per test plan (built at DB-update time).
Enables O(1) compare operations instead of recursive on-the-fly expansion.
"""
__tablename__ = "plan_membership"

id = Column(Integer, primary_key=True)
plan_full_id = Column(String, index=True)
job_id = Column(String, index=True)


def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
Loading
Loading