mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 09:13:58 +00:00
feat(installer): improve UI and add test coverage for core features
- Refactored installer UI with updated canvas rendering, sidebar navigation, and footer buttons. - Enhanced styling for macOS compatibility and consistent design across controls. - Added Pytest-based unit tests for `screen.py` and `config.py`. - Expanded dependency catalog with new tools like `tshark` and `pyshark`. - Improved error tolerance for background rendering and added placeholders for Kerberos configuration.
This commit is contained in:
parent
7b71d80053
commit
906392d462
11
.coveragerc
Normal file
11
.coveragerc
Normal file
@ -0,0 +1,11 @@
|
||||
[run]
|
||||
source =
|
||||
install.py
|
||||
installer/
|
||||
|
||||
omit =
|
||||
tests/*
|
||||
*/__init__.py
|
||||
|
||||
[report]
|
||||
show_missing = True
|
||||
46
.gitignore
vendored
Normal file
46
.gitignore
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
/.DS_Store
|
||||
/.gitignore
|
||||
/__pycache__/
|
||||
/bin/*
|
||||
!/bin/prole-env.sh
|
||||
!/bin/prole-kpf.sh
|
||||
/lib/
|
||||
/include/
|
||||
/.idea/
|
||||
/.vagrant/
|
||||
/.vscode/
|
||||
/.venv/
|
||||
/prole-tools-app/dist/
|
||||
/prole-tools-app/.build/
|
||||
/prole-tools-app/.build-cli/
|
||||
|
||||
# Secrets and local config
|
||||
*-password.txt
|
||||
*secret.yaml
|
||||
/secrets/
|
||||
/conf/
|
||||
/data/
|
||||
/logs/
|
||||
/storage/
|
||||
/target/
|
||||
/pyvenv.cfg
|
||||
.output.txt
|
||||
deploy/gcp/terraform-setup.txt
|
||||
.terraform/
|
||||
.terraform.lock.hcl
|
||||
|
||||
# Large binaries
|
||||
*.zip
|
||||
*.tar.gz
|
||||
|
||||
# Coverage and testing
|
||||
.coverage
|
||||
htmlcov/
|
||||
.pytest_cache/
|
||||
|
||||
# Unreal Engine artifacts
|
||||
/workstation/Prole/Binaries/
|
||||
/workstation/Prole/Intermediate/
|
||||
/workstation/Prole/Saved/
|
||||
/workstation/Prole/DerivedDataCache/
|
||||
/workstation/Prole/Build/
|
||||
11
bin/prole-env.sh
Normal file
11
bin/prole-env.sh
Normal file
@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# Prole environment file. Sourced by other scripts in ~/.prole/bin.
|
||||
# Customize PATH, KUBECONFIG, contexts, etc.
|
||||
|
||||
# Ensure common kubectl locations are available
|
||||
export PATH="/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:$PATH"
|
||||
|
||||
# Example: point to a specific kubeconfig if needed
|
||||
# export KUBECONFIG="$HOME/.kube/config"
|
||||
|
||||
# You can add any environment variables that your port-forward commands require here.
|
||||
76
bin/prole-kpf.sh
Normal file
76
bin/prole-kpf.sh
Normal file
@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
LABEL="org.prole.prole-db.kpf-dev"
|
||||
PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
|
||||
PROLE_HOME="$HOME/.prole"
|
||||
BIN_DIR="$PROLE_HOME/bin"
|
||||
RUN_DIR="$PROLE_HOME/run"
|
||||
PIDFILE="$RUN_DIR/kpf.pids"
|
||||
|
||||
# Source environment if present
|
||||
if [ -f "$BIN_DIR/prole-env.sh" ]; then
|
||||
# shellcheck source=/dev/null
|
||||
. "$BIN_DIR/prole-env.sh"
|
||||
fi
|
||||
|
||||
ensure_dirs() {
|
||||
mkdir -p "$BIN_DIR" "$RUN_DIR"
|
||||
}
|
||||
|
||||
list_cmds() {
|
||||
if /usr/libexec/PlistBuddy -c "Print :ProleCommands" "$PLIST" >/dev/null 2>&1; then
|
||||
local i=0
|
||||
while true; do
|
||||
if ! val=$(/usr/libexec/PlistBuddy -c "Print :ProleCommands:$i" "$PLIST" 2>/dev/null); then
|
||||
break
|
||||
fi
|
||||
echo "$val"
|
||||
i=$((i+1))
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
start() {
|
||||
ensure_dirs
|
||||
: > "$PIDFILE"
|
||||
# Iterate over commands and start each in background shell
|
||||
while IFS= read -r cmd; do
|
||||
[ -z "$cmd" ] && continue
|
||||
(sh -lc "$cmd") &
|
||||
echo $! >> "$PIDFILE"
|
||||
done < <(list_cmds)
|
||||
wait || true
|
||||
}
|
||||
|
||||
stop() {
|
||||
if [ -f "$PIDFILE" ]; then
|
||||
while read -r pid; do
|
||||
[ -z "$pid" ] && continue
|
||||
kill "$pid" 2>/dev/null || true
|
||||
done < "$PIDFILE"
|
||||
rm -f "$PIDFILE"
|
||||
fi
|
||||
}
|
||||
|
||||
status() {
|
||||
if [ ! -f "$PIDFILE" ]; then
|
||||
echo "not running"
|
||||
exit 3
|
||||
fi
|
||||
local alive=0 total=0
|
||||
while read -r pid; do
|
||||
[ -z "$pid" ] && continue
|
||||
total=$((total+1))
|
||||
if kill -0 "$pid" 2>/dev/null; then alive=$((alive+1)); fi
|
||||
done < "$PIDFILE"
|
||||
echo "$alive/$total running"
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
start) start ;;
|
||||
stop) stop ;;
|
||||
restart) stop; start ;;
|
||||
status) status ;;
|
||||
*) echo "Usage: $0 {start|stop|restart|status}" >&2; exit 2 ;;
|
||||
esac
|
||||
@ -280,8 +280,8 @@ case "$ACTION" in
|
||||
echo "ERROR: CNPG manifest not found at $CNPG_MANIFEST" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Starting CloudNative-PG cluster from $CNPG_MANIFEST ..."
|
||||
kubectl apply -f "$CNPG_MANIFEST"
|
||||
echo "Starting CloudNative-PG cluster from $CNPG_MANIFEST in namespace $NAMESPACE..."
|
||||
kubectl apply -n "$NAMESPACE" -f "$CNPG_MANIFEST"
|
||||
;;
|
||||
stop)
|
||||
ensure_tools
|
||||
|
||||
@ -33,9 +33,8 @@ fi
|
||||
ACTION=${1:-}
|
||||
|
||||
# Defaults
|
||||
NAMESPACE=${NAMESPACE:-default}
|
||||
NAMESPACE=${NAMESPACE:-prole}
|
||||
OPENBAO_NAME=${OPENBAO_NAME:-openbao}
|
||||
OPENBAO_IMAGE=${OPENBAO_IMAGE:-ghcr.io/openbao/openbao:latest}
|
||||
OPENBAO_MANIFEST_DIR="$SCRIPT_DIR/../k8s/openbao"
|
||||
|
||||
# Kerberos realm defaults
|
||||
@ -167,14 +166,23 @@ EOF
|
||||
|
||||
apply_k8s() {
|
||||
echo "Applying OpenBao manifest to namespace '$NAMESPACE' ..."
|
||||
kubectl apply -f "$OPENBAO_MANIFEST_DIR/deployment.yaml"
|
||||
if [[ -f "$SCRIPT_DIR/../k8s/prole/openbao-statefulset.yaml" ]]; then
|
||||
kubectl apply -n "$NAMESPACE" -f "$SCRIPT_DIR/../k8s/prole/openbao-statefulset.yaml"
|
||||
kubectl apply -n "$NAMESPACE" -f "$SCRIPT_DIR/../k8s/prole/openbao-service.yaml"
|
||||
else
|
||||
kubectl apply -n "$NAMESPACE" -f "$OPENBAO_MANIFEST_DIR/deployment.yaml"
|
||||
fi
|
||||
echo "Applying Kerberos ConfigMap (external realm) to namespace '$NAMESPACE' ..."
|
||||
kubectl apply -f "$OPENBAO_MANIFEST_DIR/kerberos-configmap.yaml"
|
||||
kubectl apply -n "$NAMESPACE" -f "$OPENBAO_MANIFEST_DIR/kerberos-configmap.yaml"
|
||||
}
|
||||
|
||||
wait_for_openbao() {
|
||||
echo "Waiting for OpenBao to become ready ..."
|
||||
kubectl rollout status deploy/$OPENBAO_NAME -n "$NAMESPACE" --timeout=120s
|
||||
if kubectl get statefulset/$OPENBAO_NAME -n "$NAMESPACE" >/dev/null 2>&1; then
|
||||
kubectl rollout status statefulset/$OPENBAO_NAME -n "$NAMESPACE" --timeout=120s
|
||||
else
|
||||
kubectl rollout status deploy/$OPENBAO_NAME -n "$NAMESPACE" --timeout=120s
|
||||
fi
|
||||
}
|
||||
|
||||
init_openbao_kv_and_store_admin_key() {
|
||||
@ -282,7 +290,19 @@ cmd_status() {
|
||||
fi
|
||||
|
||||
# K8s resources
|
||||
if kubectl -n "$NAMESPACE" get deploy "$OPENBAO_NAME" >/dev/null 2>&1; then
|
||||
if kubectl -n "$NAMESPACE" get statefulset "$OPENBAO_NAME" >/dev/null 2>&1; then
|
||||
local ready desired
|
||||
ready=$(kubectl -n "$NAMESPACE" get statefulset "$OPENBAO_NAME" -o jsonpath='{.status.readyReplicas}' 2>/dev/null || echo "0")
|
||||
desired=$(kubectl -n "$NAMESPACE" get statefulset "$OPENBAO_NAME" -o jsonpath='{.status.replicas}' 2>/dev/null || echo "0")
|
||||
ready=${ready:-0}
|
||||
desired=${desired:-0}
|
||||
if [[ "$ready" == "$desired" && "$ready" != "0" ]]; then
|
||||
echo "[OK] OpenBao StatefulSet running ($ready/$desired ready)"
|
||||
else
|
||||
echo "[WARN] OpenBao StatefulSet not fully ready ($ready/$desired)"
|
||||
ok=1
|
||||
fi
|
||||
elif kubectl -n "$NAMESPACE" get deploy "$OPENBAO_NAME" >/dev/null 2>&1; then
|
||||
# Get readiness
|
||||
local ready desired
|
||||
ready=$(kubectl -n "$NAMESPACE" get deploy "$OPENBAO_NAME" -o jsonpath='{.status.readyReplicas}' 2>/dev/null || echo "0")
|
||||
@ -296,7 +316,7 @@ cmd_status() {
|
||||
ok=1
|
||||
fi
|
||||
else
|
||||
echo "[MISSING] OpenBao Deployment '$OPENBAO_NAME' in namespace '$NAMESPACE'"
|
||||
echo "[MISSING] OpenBao Deployment or StatefulSet '$OPENBAO_NAME' in namespace '$NAMESPACE'"
|
||||
ok=1
|
||||
fi
|
||||
|
||||
@ -416,13 +436,13 @@ case "$ACTION" in
|
||||
kubectl create secret generic prole-db-user -n "$NAMESPACE" \
|
||||
--from-literal=username=prole \
|
||||
--from-literal=password="$db_pass" \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
--dry-run=client -o yaml | kubectl apply -n "$NAMESPACE" -f -
|
||||
|
||||
echo "Creating database superuser secret 'prole-db-superuser' ..."
|
||||
kubectl create secret generic prole-db-superuser -n "$NAMESPACE" \
|
||||
--from-literal=username=postgres \
|
||||
--from-literal=password="$db_pass" \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
--dry-run=client -o yaml | kubectl apply -n "$NAMESPACE" -f -
|
||||
fi
|
||||
|
||||
wait_for_openbao
|
||||
@ -442,8 +462,7 @@ case "$ACTION" in
|
||||
update|reload)
|
||||
generate_openbao_manifests
|
||||
generate_kerberos_configmap
|
||||
kubectl apply -f "$OPENBAO_MANIFEST_DIR/deployment.yaml"
|
||||
kubectl apply -f "$OPENBAO_MANIFEST_DIR/kerberos-configmap.yaml"
|
||||
apply_k8s
|
||||
echo "Re-applied manifests."
|
||||
;;
|
||||
*)
|
||||
|
||||
@ -6,7 +6,7 @@ set -u
|
||||
# Manages kubectl port-forward daemons defined in an XML file.
|
||||
|
||||
PROG="init_port_forwards"
|
||||
PROLE_HOME="/Users/chrisfu/dev/prole"
|
||||
PROLE_HOME="${PROLE_HOME:-/Users/chrisfu/dev/prole}"
|
||||
VERBOSE=0
|
||||
CONFIG_FILE="$PROLE_HOME/conf/port-mappings.properties"
|
||||
|
||||
|
||||
1382
install.py
1382
install.py
File diff suppressed because it is too large
Load Diff
@ -9,6 +9,7 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Tuple, Optional
|
||||
import os
|
||||
|
||||
@ -171,6 +172,24 @@ DEPENDENCIES = [
|
||||
"check_cmd": "ollama --version",
|
||||
"bin": "ollama",
|
||||
},
|
||||
{
|
||||
"id": "tshark",
|
||||
"name": "tshark (Wireshark)",
|
||||
"description": "Network protocol analyzer for network scanning",
|
||||
"url": "https://www.wireshark.org",
|
||||
"install_cmd": "brew install wireshark" if platform.system() == 'Darwin' else "sudo apt-get update && sudo apt-get install -y tshark",
|
||||
"check_cmd": "tshark --version",
|
||||
"bin": "tshark",
|
||||
},
|
||||
{
|
||||
"id": "pyshark",
|
||||
"name": "pyshark",
|
||||
"description": "Python wrapper for tshark",
|
||||
"url": "https://github.com/KimiNewt/pyshark",
|
||||
"install_cmd": f"'{sys.executable}' -m pip install pyshark",
|
||||
"check_cmd": f"'{sys.executable}' -c 'import pyshark'",
|
||||
"bin": None,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@ -190,6 +209,10 @@ def get_dep_info(dep: dict) -> Tuple[bool, Optional[str], Optional[str]]:
|
||||
res2 = subprocess.run(["bash", "-lc", check_cmd], capture_output=True, text=True)
|
||||
if res2.returncode == 0:
|
||||
version = " ".join(res2.stdout.strip().splitlines()[:1])
|
||||
# If check_cmd succeeded, consider it installed
|
||||
installed = True
|
||||
if not location:
|
||||
location = "Installed via Python"
|
||||
except Exception:
|
||||
pass
|
||||
return installed, location, version
|
||||
|
||||
@ -15,20 +15,20 @@ def render_title(app, text: str, y: int = 40):
|
||||
"""Render a section title on the main background canvas."""
|
||||
if getattr(app, 'bg_canvas', None) is None:
|
||||
return
|
||||
item = app.bg_canvas.create_text(40, y, anchor='nw', text=text, fill='#111', font=('Helvetica Neue', 22, 'bold'))
|
||||
item = app.bg_canvas.create_text(48, y, anchor='nw', text=text, fill='black', font=('SF Pro Text', 18, 'bold'))
|
||||
app._canvas_items.append(item)
|
||||
|
||||
|
||||
def render_paragraph(app, text: str, y: int, wrap: int = 860):
|
||||
def render_paragraph(app, text: str, y: int, wrap: int = 800):
|
||||
"""Render a paragraph on the main background canvas."""
|
||||
if getattr(app, 'bg_canvas', None) is None:
|
||||
return
|
||||
item = app.bg_canvas.create_text(40, y, anchor='nw', text=text, fill='#1d1d1f', font=('Helvetica', 12), width=wrap)
|
||||
item = app.bg_canvas.create_text(48, y, anchor='nw', text=text, fill='black', font=('SF Pro Text', 11), width=wrap)
|
||||
app._canvas_items.append(item)
|
||||
|
||||
|
||||
def canvas_text(app, x: int, y: int, text: str, *, fill: str = '#1d1d1f',
|
||||
font: tuple = ('Helvetica', 12), anchor: str = 'nw', width: int | None = None,
|
||||
def canvas_text(app, x: int, y: int, text: str, *, fill: str = 'black',
|
||||
font: tuple = ('SF Pro Text', 11), anchor: str = 'nw', width: int | None = None,
|
||||
justify: str | None = None, state: str | None = None) -> int:
|
||||
"""Create a text item on the app's main canvas and track it.
|
||||
|
||||
@ -73,7 +73,7 @@ def canvas_rectangle(app, x1: int, y1: int, x2: int, y2: int, *, outline: str =
|
||||
return item
|
||||
|
||||
|
||||
def canvas_line(app, x1: int, y1: int, x2: int, y2: int, *, fill: str = '#1d1d1f',
|
||||
def canvas_line(app, x1: int, y1: int, x2: int, y2: int, *, fill: str = 'black',
|
||||
width: int = 2, state: str | None = None) -> int:
|
||||
if getattr(app, 'bg_canvas', None) is None:
|
||||
return -1
|
||||
@ -145,49 +145,45 @@ def canvas_line_on(cnv: tk.Canvas, x1: int, y1: int, x2: int, y2: int, *, fill:
|
||||
|
||||
|
||||
def create_nav_footer(parent, buttons: list[tuple[int, str]], commands: dict[int, callable] | None = None,
|
||||
style_name: str = 'Nav.TButton') -> dict[int, ttk.Button]:
|
||||
style_name: str = 'Nav.TButton') -> dict[int, tk.Button]:
|
||||
"""Create a right-aligned navigation footer with uniform button styling.
|
||||
|
||||
Parameters:
|
||||
- parent: the container (typically the root installer container)
|
||||
- buttons: list of (button_id, title) in the order to display
|
||||
- commands: optional mapping of button_id -> callback function
|
||||
- style_name: ttk style to apply to all buttons
|
||||
|
||||
Returns: {button_id: ttk.Button}
|
||||
|
||||
Examples:
|
||||
create_nav_footer(footer, [(1, 'Next')])
|
||||
create_nav_footer(footer, [(1, 'Prev'), (2, 'Next')])
|
||||
create_nav_footer(footer, [(1, 'Exit')])
|
||||
Uses tk.Button instead of ttk.Button for better color control on macOS.
|
||||
"""
|
||||
# Ensure a consistent style for navigation buttons
|
||||
try:
|
||||
style = ttk.Style()
|
||||
# Keep padding modest to allow Aqua to size buttons naturally.
|
||||
# Avoid forcing a font so the native Aqua metrics are used.
|
||||
style.configure(style_name, padding=(10, 6))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
footer = ttk.Frame(parent)
|
||||
footer = tk.Frame(parent, bg='#F5F5DC', height=64)
|
||||
footer.pack(fill='x', side='bottom')
|
||||
# Let the footer naturally size to its contents to avoid clipping text on macOS
|
||||
footer.pack_propagate(False)
|
||||
|
||||
# Top divider line for the footer
|
||||
divider = tk.Frame(footer, bg='#CCCCCC', height=1)
|
||||
divider.pack(side='top', fill='x')
|
||||
|
||||
# Flexible spacer to push buttons to the right
|
||||
spacer = ttk.Frame(footer)
|
||||
spacer = tk.Frame(footer, bg='#F5F5DC')
|
||||
spacer.pack(side='left', expand=True, fill='x')
|
||||
|
||||
cmds = commands or {}
|
||||
btn_map: dict[int, ttk.Button] = {}
|
||||
btn_map: dict[int, tk.Button] = {}
|
||||
for btn_id, title in buttons:
|
||||
cmd = cmds.get(btn_id)
|
||||
b = ttk.Button(footer, text=title, style=style_name, command=cmd)
|
||||
# Use tk.Button for full control over background and borders on macOS
|
||||
b = tk.Button(footer,
|
||||
text=title,
|
||||
command=cmd,
|
||||
bg='#F5F5DC',
|
||||
fg='black',
|
||||
activebackground='#E5E5D5',
|
||||
activeforeground='black',
|
||||
highlightbackground='#F5F5DC', # Essential for macOS to avoid black boxes
|
||||
highlightthickness=0,
|
||||
relief='flat',
|
||||
font=('SF Pro Text', 11),
|
||||
padx=16,
|
||||
pady=8)
|
||||
|
||||
# Right-aligned order (pack to the right in the declared order)
|
||||
# Small extra right padding on the last (right-most) button
|
||||
pad = (0, 20) if title.lower() in ('finish', 'exit', 'done') else (0, 8)
|
||||
# Use modest vertical padding; let Aqua compute proper height.
|
||||
b.pack(side='right', padx=pad, pady=10)
|
||||
pad = (0, 20) if title.lower() in ('finish', 'exit', 'done', 'next') else (0, 8)
|
||||
b.pack(side='right', padx=pad, pady=12)
|
||||
btn_map[btn_id] = b
|
||||
|
||||
# Return both the frame and button map if needed later by callers
|
||||
@ -212,16 +208,25 @@ class TerminalConsole(ttk.Frame):
|
||||
|
||||
def write(self, content: str):
|
||||
"""Append text to the console and scroll to the bottom."""
|
||||
self.text.configure(state='normal')
|
||||
self.text.insert('end', content)
|
||||
self.text.see('end')
|
||||
self.text.configure(state='disabled')
|
||||
self.update_idletasks()
|
||||
|
||||
try:
|
||||
if not self.winfo_exists():
|
||||
return
|
||||
self.text.configure(state='normal')
|
||||
self.text.insert('end', content)
|
||||
self.text.see('end')
|
||||
self.text.configure(state='disabled')
|
||||
self.update_idletasks()
|
||||
except tk.TclError:
|
||||
pass
|
||||
|
||||
def clear(self):
|
||||
"""Clear all content from the console."""
|
||||
self.text.configure(state='normal')
|
||||
self.text.delete('1.0', 'end')
|
||||
self.text.configure(state='disabled')
|
||||
self.update_idletasks()
|
||||
try:
|
||||
if not self.winfo_exists():
|
||||
return
|
||||
self.text.configure(state='normal')
|
||||
self.text.delete('1.0', 'end')
|
||||
self.text.configure(state='disabled')
|
||||
self.update_idletasks()
|
||||
except tk.TclError:
|
||||
pass
|
||||
|
||||
@ -2,7 +2,7 @@ apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: prole-krb5-conf
|
||||
namespace: default
|
||||
namespace: prole
|
||||
data:
|
||||
krb5.conf: |
|
||||
[libdefaults]
|
||||
|
||||
@ -4,7 +4,7 @@ metadata:
|
||||
name: prole-db
|
||||
spec:
|
||||
instances: 3
|
||||
imageName: prole-db:17.7-031
|
||||
imageName: prole-db:17.7-033
|
||||
postgresUID: 100
|
||||
postgresGID: 101
|
||||
maxSyncReplicas: 1
|
||||
@ -24,6 +24,7 @@ spec:
|
||||
- host prole prole-db all scram-sha-256
|
||||
- host all all all scram-sha-256
|
||||
- hostssl prole prole-db all scram-sha-256
|
||||
- host all all all gss include_realm=1 krb_realm=EXAMPLE.COM
|
||||
|
||||
bootstrap:
|
||||
initdb:
|
||||
|
||||
@ -88,8 +88,8 @@ RUN set -eux; \
|
||||
apt-get update; \
|
||||
apt-cache search percona-postgresql-17 | grep -E "(pgvector|postgis|contrib)"; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
percona-pgvector-percona-postgresql-17 \
|
||||
percona-postgis-percona-postgresql-17 \
|
||||
percona-postgresql-17-pgvector \
|
||||
percona-postgresql-17-postgis-3 \
|
||||
percona-postgresql-contrib-17 \
|
||||
freetds-dev \
|
||||
; \
|
||||
|
||||
15
run_tests.sh
Executable file
15
run_tests.sh
Executable file
@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
# Run all unit tests and generate a coverage report
|
||||
|
||||
export PYTHONPATH=$PYTHONPATH:.
|
||||
|
||||
# Check if pytest-cov is installed
|
||||
if pytest --trace-config | grep -q "pytest_cov"; then
|
||||
echo "Running tests with coverage..."
|
||||
pytest --cov=installer --cov=install --cov-report=term-missing --cov-report=html tests/
|
||||
echo "Coverage report (HTML) generated in htmlcov/index.html"
|
||||
else
|
||||
echo "Warning: pytest-cov not found. Running tests without coverage."
|
||||
echo "To enable coverage, install it via: pip install pytest-cov"
|
||||
pytest tests/
|
||||
fi
|
||||
8
run_with_coverage.sh
Executable file
8
run_with_coverage.sh
Executable file
@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
# Run the Prole installer with execution instrumentation (coverage)
|
||||
# This helps identify unused code paths during manual testing/usage.
|
||||
export PYTHONPATH=$PYTHONPATH:.
|
||||
coverage run install.py "$@"
|
||||
coverage report -m
|
||||
coverage html
|
||||
echo "Execution instrumentation report generated in htmlcov/index.html"
|
||||
15
tests/installer/test_build.py
Normal file
15
tests/installer/test_build.py
Normal file
@ -0,0 +1,15 @@
|
||||
from pathlib import Path
|
||||
from installer.build import get_build_command
|
||||
|
||||
def test_get_build_command():
|
||||
root = Path("/fake/root")
|
||||
cmd = get_build_command(root, "Dev")
|
||||
assert 'cd "/fake/root"' in cmd
|
||||
assert "PROLE_VERBOSE=1" in cmd
|
||||
assert "# Dev" in cmd
|
||||
|
||||
cmd_prod = get_build_command(root, "Prod")
|
||||
assert "# Prod" in cmd_prod
|
||||
|
||||
cmd_none = get_build_command(root, None)
|
||||
assert "# Dev" in cmd_none
|
||||
33
tests/installer/test_config.py
Normal file
33
tests/installer/test_config.py
Normal file
@ -0,0 +1,33 @@
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from installer.config import normalize_version, is_apple_silicon, get_docker_build_platform_args
|
||||
|
||||
def test_normalize_version():
|
||||
assert normalize_version("1.2.3") == "01.02.03"
|
||||
assert normalize_version("v1.2") == "01.02.00"
|
||||
assert normalize_version("2") == "02.00.00"
|
||||
assert normalize_version("10.0.1-beta") == "10.00.01"
|
||||
assert normalize_version("") == ""
|
||||
assert normalize_version("no numbers") == "no numbers"
|
||||
|
||||
@patch("platform.machine")
|
||||
@patch("platform.system")
|
||||
def test_is_apple_silicon(mock_system, mock_machine):
|
||||
mock_machine.return_value = "arm64"
|
||||
mock_system.return_value = "Darwin"
|
||||
assert is_apple_silicon() is True
|
||||
|
||||
mock_machine.return_value = "x86_64"
|
||||
assert is_apple_silicon() is False
|
||||
|
||||
mock_machine.return_value = "arm64"
|
||||
mock_system.return_value = "Linux"
|
||||
assert is_apple_silicon() is False
|
||||
|
||||
@patch("installer.config.is_apple_silicon")
|
||||
def test_get_docker_build_platform_args(mock_is_apple_silicon):
|
||||
mock_is_apple_silicon.return_value = True
|
||||
assert get_docker_build_platform_args() == ["--platform", "linux/amd64"]
|
||||
|
||||
mock_is_apple_silicon.return_value = False
|
||||
assert get_docker_build_platform_args() == []
|
||||
48
tests/installer/test_screen.py
Normal file
48
tests/installer/test_screen.py
Normal file
@ -0,0 +1,48 @@
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
import tkinter as tk
|
||||
from installer.screen import render_title, render_paragraph, TerminalConsole
|
||||
|
||||
@pytest.fixture
|
||||
def mock_app():
|
||||
app = MagicMock()
|
||||
app.bg_canvas = MagicMock()
|
||||
app._canvas_items = []
|
||||
return app
|
||||
|
||||
def test_render_title(mock_app):
|
||||
render_title(mock_app, "Test Title", y=50)
|
||||
mock_app.bg_canvas.create_text.assert_called_once()
|
||||
args, kwargs = mock_app.bg_canvas.create_text.call_args
|
||||
assert args == (48, 50)
|
||||
assert kwargs['text'] == "Test Title"
|
||||
assert len(mock_app._canvas_items) == 1
|
||||
|
||||
def test_render_paragraph(mock_app):
|
||||
render_paragraph(mock_app, "Test Paragraph", y=100)
|
||||
mock_app.bg_canvas.create_text.assert_called_once()
|
||||
args, kwargs = mock_app.bg_canvas.create_text.call_args
|
||||
assert args == (48, 100)
|
||||
assert kwargs['text'] == "Test Paragraph"
|
||||
assert len(mock_app._canvas_items) == 1
|
||||
|
||||
def test_render_title_no_canvas():
|
||||
app = MagicMock()
|
||||
app.bg_canvas = None
|
||||
render_title(app, "Title")
|
||||
# Should not raise exception
|
||||
|
||||
@patch('platform.system', return_value='Darwin')
|
||||
def test_terminal_console(mock_platform):
|
||||
root = tk.Tk()
|
||||
try:
|
||||
console = TerminalConsole(root)
|
||||
console.write("Hello\n")
|
||||
# In mock or headless env, we might not be able to check text content easily
|
||||
# but we can verify it doesn't crash and state is managed
|
||||
assert console.text.cget('state') == 'disabled'
|
||||
|
||||
console.clear()
|
||||
assert console.text.cget('state') == 'disabled'
|
||||
finally:
|
||||
root.destroy()
|
||||
62
tests/test_install_logic.py
Normal file
62
tests/test_install_logic.py
Normal file
@ -0,0 +1,62 @@
|
||||
import sys
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
# Mock tkinter and other GUI/macOS specific imports before they are imported in install.py
|
||||
sys.modules['tkinter'] = MagicMock()
|
||||
sys.modules['tkinter.ttk'] = MagicMock()
|
||||
sys.modules['tkinter.scrolledtext'] = MagicMock()
|
||||
sys.modules['tkinter.messagebox'] = MagicMock()
|
||||
sys.modules['Foundation'] = MagicMock()
|
||||
sys.modules['objc'] = MagicMock()
|
||||
|
||||
import os
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
|
||||
# Now import install.py after mocking
|
||||
import install
|
||||
from install import ProleInstaller
|
||||
|
||||
@pytest.fixture
|
||||
def installer(tmp_path):
|
||||
# Mocking os.environ to avoid messing with real env
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
# We need to mock ProleInstaller.__init__ because it creates GUI elements
|
||||
with patch.object(ProleInstaller, '__init__', return_value=None):
|
||||
ins = ProleInstaller()
|
||||
return ins
|
||||
|
||||
def test_resolve_prole_home_default(installer):
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
# Path.home() might vary, so we just check it ends with .prole if no env var
|
||||
home = installer.resolve_prole_home()
|
||||
assert home.name == ".prole"
|
||||
|
||||
def test_resolve_prole_home_env(installer):
|
||||
custom_home = "/tmp/custom_prole"
|
||||
with patch.dict(os.environ, {'PROLE_HOME': custom_home}):
|
||||
home = installer.resolve_prole_home()
|
||||
assert str(home) == custom_home
|
||||
|
||||
def test_env_defaults(installer):
|
||||
defaults = installer._env_defaults()
|
||||
assert 'PROLE_HOME' in defaults
|
||||
assert defaults['PROLE_HOME'].endswith('.prole')
|
||||
assert defaults['PROLE_CONF'].endswith('.prole/conf')
|
||||
|
||||
def test_read_existing_env_no_file(installer, tmp_path):
|
||||
# Ensure neither PROLE_HOME nor the default location has an env.sh for this test
|
||||
with patch.dict(os.environ, {'PROLE_HOME': str(tmp_path)}):
|
||||
with patch('pathlib.Path.home', return_value=tmp_path / "fake_home"):
|
||||
env = installer._read_existing_env()
|
||||
assert env == {}
|
||||
|
||||
def test_read_existing_env_with_file(installer, tmp_path):
|
||||
env_sh = tmp_path / "env.sh"
|
||||
env_sh.write_text('export VAR1="val1"\nVAR2=val2\n# comment\n')
|
||||
|
||||
with patch.dict(os.environ, {'PROLE_HOME': str(tmp_path)}):
|
||||
with patch('pathlib.Path.home', return_value=tmp_path / "fake_home"):
|
||||
env = installer._read_existing_env()
|
||||
assert env.get('VAR1') == "val1"
|
||||
assert env.get('VAR2') == "val2"
|
||||
Loading…
Reference in New Issue
Block a user