mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 11:03:59 +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
|
echo "ERROR: CNPG manifest not found at $CNPG_MANIFEST" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "Starting CloudNative-PG cluster from $CNPG_MANIFEST ..."
|
echo "Starting CloudNative-PG cluster from $CNPG_MANIFEST in namespace $NAMESPACE..."
|
||||||
kubectl apply -f "$CNPG_MANIFEST"
|
kubectl apply -n "$NAMESPACE" -f "$CNPG_MANIFEST"
|
||||||
;;
|
;;
|
||||||
stop)
|
stop)
|
||||||
ensure_tools
|
ensure_tools
|
||||||
|
|||||||
@ -33,9 +33,8 @@ fi
|
|||||||
ACTION=${1:-}
|
ACTION=${1:-}
|
||||||
|
|
||||||
# Defaults
|
# Defaults
|
||||||
NAMESPACE=${NAMESPACE:-default}
|
NAMESPACE=${NAMESPACE:-prole}
|
||||||
OPENBAO_NAME=${OPENBAO_NAME:-openbao}
|
OPENBAO_NAME=${OPENBAO_NAME:-openbao}
|
||||||
OPENBAO_IMAGE=${OPENBAO_IMAGE:-ghcr.io/openbao/openbao:latest}
|
|
||||||
OPENBAO_MANIFEST_DIR="$SCRIPT_DIR/../k8s/openbao"
|
OPENBAO_MANIFEST_DIR="$SCRIPT_DIR/../k8s/openbao"
|
||||||
|
|
||||||
# Kerberos realm defaults
|
# Kerberos realm defaults
|
||||||
@ -167,14 +166,23 @@ EOF
|
|||||||
|
|
||||||
apply_k8s() {
|
apply_k8s() {
|
||||||
echo "Applying OpenBao manifest to namespace '$NAMESPACE' ..."
|
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' ..."
|
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() {
|
wait_for_openbao() {
|
||||||
echo "Waiting for OpenBao to become ready ..."
|
echo "Waiting for OpenBao to become ready ..."
|
||||||
|
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
|
kubectl rollout status deploy/$OPENBAO_NAME -n "$NAMESPACE" --timeout=120s
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
init_openbao_kv_and_store_admin_key() {
|
init_openbao_kv_and_store_admin_key() {
|
||||||
@ -282,7 +290,19 @@ cmd_status() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# K8s resources
|
# 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
|
# Get readiness
|
||||||
local ready desired
|
local ready desired
|
||||||
ready=$(kubectl -n "$NAMESPACE" get deploy "$OPENBAO_NAME" -o jsonpath='{.status.readyReplicas}' 2>/dev/null || echo "0")
|
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
|
ok=1
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo "[MISSING] OpenBao Deployment '$OPENBAO_NAME' in namespace '$NAMESPACE'"
|
echo "[MISSING] OpenBao Deployment or StatefulSet '$OPENBAO_NAME' in namespace '$NAMESPACE'"
|
||||||
ok=1
|
ok=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@ -416,13 +436,13 @@ case "$ACTION" in
|
|||||||
kubectl create secret generic prole-db-user -n "$NAMESPACE" \
|
kubectl create secret generic prole-db-user -n "$NAMESPACE" \
|
||||||
--from-literal=username=prole \
|
--from-literal=username=prole \
|
||||||
--from-literal=password="$db_pass" \
|
--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' ..."
|
echo "Creating database superuser secret 'prole-db-superuser' ..."
|
||||||
kubectl create secret generic prole-db-superuser -n "$NAMESPACE" \
|
kubectl create secret generic prole-db-superuser -n "$NAMESPACE" \
|
||||||
--from-literal=username=postgres \
|
--from-literal=username=postgres \
|
||||||
--from-literal=password="$db_pass" \
|
--from-literal=password="$db_pass" \
|
||||||
--dry-run=client -o yaml | kubectl apply -f -
|
--dry-run=client -o yaml | kubectl apply -n "$NAMESPACE" -f -
|
||||||
fi
|
fi
|
||||||
|
|
||||||
wait_for_openbao
|
wait_for_openbao
|
||||||
@ -442,8 +462,7 @@ case "$ACTION" in
|
|||||||
update|reload)
|
update|reload)
|
||||||
generate_openbao_manifests
|
generate_openbao_manifests
|
||||||
generate_kerberos_configmap
|
generate_kerberos_configmap
|
||||||
kubectl apply -f "$OPENBAO_MANIFEST_DIR/deployment.yaml"
|
apply_k8s
|
||||||
kubectl apply -f "$OPENBAO_MANIFEST_DIR/kerberos-configmap.yaml"
|
|
||||||
echo "Re-applied manifests."
|
echo "Re-applied manifests."
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
|
|||||||
@ -6,7 +6,7 @@ set -u
|
|||||||
# Manages kubectl port-forward daemons defined in an XML file.
|
# Manages kubectl port-forward daemons defined in an XML file.
|
||||||
|
|
||||||
PROG="init_port_forwards"
|
PROG="init_port_forwards"
|
||||||
PROLE_HOME="/Users/chrisfu/dev/prole"
|
PROLE_HOME="${PROLE_HOME:-/Users/chrisfu/dev/prole}"
|
||||||
VERBOSE=0
|
VERBOSE=0
|
||||||
CONFIG_FILE="$PROLE_HOME/conf/port-mappings.properties"
|
CONFIG_FILE="$PROLE_HOME/conf/port-mappings.properties"
|
||||||
|
|
||||||
|
|||||||
1352
install.py
1352
install.py
File diff suppressed because it is too large
Load Diff
@ -9,6 +9,7 @@ from __future__ import annotations
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import platform
|
import platform
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
from typing import Tuple, Optional
|
from typing import Tuple, Optional
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@ -171,6 +172,24 @@ DEPENDENCIES = [
|
|||||||
"check_cmd": "ollama --version",
|
"check_cmd": "ollama --version",
|
||||||
"bin": "ollama",
|
"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)
|
res2 = subprocess.run(["bash", "-lc", check_cmd], capture_output=True, text=True)
|
||||||
if res2.returncode == 0:
|
if res2.returncode == 0:
|
||||||
version = " ".join(res2.stdout.strip().splitlines()[:1])
|
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:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return installed, location, version
|
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."""
|
"""Render a section title on the main background canvas."""
|
||||||
if getattr(app, 'bg_canvas', None) is None:
|
if getattr(app, 'bg_canvas', None) is None:
|
||||||
return
|
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)
|
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."""
|
"""Render a paragraph on the main background canvas."""
|
||||||
if getattr(app, 'bg_canvas', None) is None:
|
if getattr(app, 'bg_canvas', None) is None:
|
||||||
return
|
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)
|
app._canvas_items.append(item)
|
||||||
|
|
||||||
|
|
||||||
def canvas_text(app, x: int, y: int, text: str, *, fill: str = '#1d1d1f',
|
def canvas_text(app, x: int, y: int, text: str, *, fill: str = 'black',
|
||||||
font: tuple = ('Helvetica', 12), anchor: str = 'nw', width: int | None = None,
|
font: tuple = ('SF Pro Text', 11), anchor: str = 'nw', width: int | None = None,
|
||||||
justify: str | None = None, state: str | None = None) -> int:
|
justify: str | None = None, state: str | None = None) -> int:
|
||||||
"""Create a text item on the app's main canvas and track it.
|
"""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
|
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:
|
width: int = 2, state: str | None = None) -> int:
|
||||||
if getattr(app, 'bg_canvas', None) is None:
|
if getattr(app, 'bg_canvas', None) is None:
|
||||||
return -1
|
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,
|
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.
|
"""Create a right-aligned navigation footer with uniform button styling.
|
||||||
|
|
||||||
Parameters:
|
Uses tk.Button instead of ttk.Button for better color control on macOS.
|
||||||
- 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')])
|
|
||||||
"""
|
"""
|
||||||
# Ensure a consistent style for navigation buttons
|
footer = tk.Frame(parent, bg='#F5F5DC', height=64)
|
||||||
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.pack(fill='x', side='bottom')
|
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
|
# 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')
|
spacer.pack(side='left', expand=True, fill='x')
|
||||||
|
|
||||||
cmds = commands or {}
|
cmds = commands or {}
|
||||||
btn_map: dict[int, ttk.Button] = {}
|
btn_map: dict[int, tk.Button] = {}
|
||||||
for btn_id, title in buttons:
|
for btn_id, title in buttons:
|
||||||
cmd = cmds.get(btn_id)
|
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)
|
# 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', 'next') else (0, 8)
|
||||||
pad = (0, 20) if title.lower() in ('finish', 'exit', 'done') else (0, 8)
|
b.pack(side='right', padx=pad, pady=12)
|
||||||
# Use modest vertical padding; let Aqua compute proper height.
|
|
||||||
b.pack(side='right', padx=pad, pady=10)
|
|
||||||
btn_map[btn_id] = b
|
btn_map[btn_id] = b
|
||||||
|
|
||||||
# Return both the frame and button map if needed later by callers
|
# 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):
|
def write(self, content: str):
|
||||||
"""Append text to the console and scroll to the bottom."""
|
"""Append text to the console and scroll to the bottom."""
|
||||||
|
try:
|
||||||
|
if not self.winfo_exists():
|
||||||
|
return
|
||||||
self.text.configure(state='normal')
|
self.text.configure(state='normal')
|
||||||
self.text.insert('end', content)
|
self.text.insert('end', content)
|
||||||
self.text.see('end')
|
self.text.see('end')
|
||||||
self.text.configure(state='disabled')
|
self.text.configure(state='disabled')
|
||||||
self.update_idletasks()
|
self.update_idletasks()
|
||||||
|
except tk.TclError:
|
||||||
|
pass
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
"""Clear all content from the console."""
|
"""Clear all content from the console."""
|
||||||
|
try:
|
||||||
|
if not self.winfo_exists():
|
||||||
|
return
|
||||||
self.text.configure(state='normal')
|
self.text.configure(state='normal')
|
||||||
self.text.delete('1.0', 'end')
|
self.text.delete('1.0', 'end')
|
||||||
self.text.configure(state='disabled')
|
self.text.configure(state='disabled')
|
||||||
self.update_idletasks()
|
self.update_idletasks()
|
||||||
|
except tk.TclError:
|
||||||
|
pass
|
||||||
|
|||||||
@ -2,7 +2,7 @@ apiVersion: v1
|
|||||||
kind: ConfigMap
|
kind: ConfigMap
|
||||||
metadata:
|
metadata:
|
||||||
name: prole-krb5-conf
|
name: prole-krb5-conf
|
||||||
namespace: default
|
namespace: prole
|
||||||
data:
|
data:
|
||||||
krb5.conf: |
|
krb5.conf: |
|
||||||
[libdefaults]
|
[libdefaults]
|
||||||
|
|||||||
@ -4,7 +4,7 @@ metadata:
|
|||||||
name: prole-db
|
name: prole-db
|
||||||
spec:
|
spec:
|
||||||
instances: 3
|
instances: 3
|
||||||
imageName: prole-db:17.7-031
|
imageName: prole-db:17.7-033
|
||||||
postgresUID: 100
|
postgresUID: 100
|
||||||
postgresGID: 101
|
postgresGID: 101
|
||||||
maxSyncReplicas: 1
|
maxSyncReplicas: 1
|
||||||
@ -24,6 +24,7 @@ spec:
|
|||||||
- host prole prole-db all scram-sha-256
|
- host prole prole-db all scram-sha-256
|
||||||
- host all all all scram-sha-256
|
- host all all all scram-sha-256
|
||||||
- hostssl prole prole-db 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:
|
bootstrap:
|
||||||
initdb:
|
initdb:
|
||||||
|
|||||||
@ -88,8 +88,8 @@ RUN set -eux; \
|
|||||||
apt-get update; \
|
apt-get update; \
|
||||||
apt-cache search percona-postgresql-17 | grep -E "(pgvector|postgis|contrib)"; \
|
apt-cache search percona-postgresql-17 | grep -E "(pgvector|postgis|contrib)"; \
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
percona-pgvector-percona-postgresql-17 \
|
percona-postgresql-17-pgvector \
|
||||||
percona-postgis-percona-postgresql-17 \
|
percona-postgresql-17-postgis-3 \
|
||||||
percona-postgresql-contrib-17 \
|
percona-postgresql-contrib-17 \
|
||||||
freetds-dev \
|
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