#!/bin/bash
# Does Wonder Ink print — and does a job really reach CUPS?
#
# The lab's CUPS gets an "Elmer" built from Elmer's own PPD and options,
# writing to a file instead of socket://10.10.0.155:9100, so every job
# goes through the same filters the real printer's jobs do and none can
# reach a printer. A second queue refuses jobs, to show a teacher is told.
#
# Each scenario runs the INSTALLED classroom-wonderink under a real
# QtWebEngine (offscreen), presses the preview's buttons, and is then
# judged by CUPS's own record of what it was given.
#
#   tools/lab/wonderink-print-check [SCENARIO...]
set -uo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
NAME="${LAB_NAME:-classroom-v2-lab}"
COMMAND="${WONDERINK:-/usr/local/bin/classroom-wonderink}"

lab() { docker exec -i "$NAME" bash -c "$1"; }

# Not /tmp: in the Plasma lab /tmp is a tmpfs, and docker cp writes into
# the layer underneath it, where nothing can see the file. Measured — the
# first run there "failed" every scenario for exactly that reason.
RIG=/opt/wonderink-check/wonderink-print-rig.py
lab 'mkdir -p /opt/wonderink-check'
docker cp "$ROOT/tools/lab/wonderink-print-rig.py" "$NAME:$RIG" >/dev/null
MEASURE=/opt/wonderink-check/wonderink-picture-measure.py
docker cp "$ROOT/tools/lab/wonderink-picture-measure.py" "$NAME:$MEASURE" >/dev/null
lab "chmod -R a+rX /opt/wonderink-check && test -r $RIG" || {
    echo "could not put the rig in the lab" >&2
    exit 1
}

# The church's printer network is fenced off inside the lab. Checked, not
# assumed: a lab job must never be able to reach the real Elmer.
lab 'ip route replace blackhole 10.10.0.0/16 2>/dev/null
     ! timeout 3 bash -c "exec 3<>/dev/tcp/10.10.0.155/9100" 2>/dev/null' || {
    echo "REFUSING TO CONTINUE: the real Elmer is reachable from the lab" >&2
    exit 1
}

lab '
set -e
export DEBIAN_FRONTEND=noninteractive
command -v lpadmin >/dev/null && command -v pdftoppm >/dev/null || \
    apt-get install -y -qq cups cups-client poppler-utils >/dev/null 2>&1
dpkg -s foomatic-db-compressed-ppds >/dev/null 2>&1 || \
    apt-get install -y -qq foomatic-db-compressed-ppds cups-filters ghostscript >/dev/null 2>&1
grep -q "^FileDevice Yes" /etc/cups/cups-files.conf || echo "FileDevice Yes" >> /etc/cups/cups-files.conf
pkill -x cupsd || true; sleep 1; /usr/sbin/cupsd; sleep 1
cupsctl --debug-logging
: > /var/log/cups/error_log
mkdir -p /var/lab-print && chown lp /var/lab-print
driver="foomatic-db-compressed-ppds:0/ppd/foomatic-ppd/Generic-PCL_6_PCL_XL_Printer-pxlcolor.ppd"
for q in $(lpstat -e 2>/dev/null); do lpadmin -x "$q"; done
lpadmin -p Elmer -E -v file:///var/lab-print/elmer.pxl -m "$driver" \
    -D "LAB STAND-IN for Elmer: its PPD, a file for a printer" \
    -o PrintoutMode=Normal -o ColorModel=Color 2>/dev/null
lpadmin -d Elmer
lpadmin -p Refusing -E -v file:///dev/null -m "$driver" -D "LAB: refuses jobs" 2>/dev/null
cupsreject Refusing
id teacher >/dev/null 2>&1 || useradd -m teacher
install -d -o teacher -m 700 /tmp/teacher-rt
# An inline PDF, served the way a site shows one rather than sends it. Its
# own port and directory: a lab often has another test server on 8088.
mkdir -p /opt/wonderink-check/www
# CUPS keeps what it was given, so a check can compare the submitted
# file with the one the preview showed, byte for byte.
cupsctl PreserveJobFiles=Yes
' || { echo "could not prepare the lab's CUPS" >&2; exit 1; }

lab "test -f /opt/wonderink-check/www/ark-clear.png || \
     QT_QPA_PLATFORM=offscreen python3 $MEASURE make /opt/wonderink-check/www; \
     chmod -R a+r /opt/wonderink-check/www; test -f /opt/wonderink-check/www/ark-clear.png" || {
    echo "could not make the test pictures" >&2
    exit 1
}

# Started detached. A background process started inside a docker exec
# dies with it in the Plasma lab, whose PID 1 is systemd.
lab 'pgrep -f "[h]ttp.server 8089" >/dev/null' || \
    docker exec -d -w /opt/wonderink-check/www "$NAME" python3 -m http.server 8089

passed=0
failed=0

judge() {
    if eval "$2"; then
        printf '  \033[32mPASS\033[0m  %s\n' "$1"
        passed=$((passed + 1))
    else
        printf '  \033[31mFAIL\033[0m  %s\n' "$1"
        failed=$((failed + 1))
    fi
}

# Runs one scenario; leaves its output in $out and the jobs it caused in $jobs.
run() {
    local before
    before=$(lab 'lpstat -W all -o 2>/dev/null | wc -l')
    out=$(lab "su teacher -c 'cd /tmp; XDG_RUNTIME_DIR=/tmp/teacher-rt QT_QPA_PLATFORM=offscreen \
        QTWEBENGINE_CHROMIUM_FLAGS=\"--no-sandbox --disable-gpu\" \
        timeout 120 python3 $RIG $COMMAND $1 2>&1'; echo \"exit=\$?\"")
    sleep 3
    jobs=$(lab "lpstat -W all -o | tail -n +$((before + 1)) | awk '{print \$1}' | tr '\n' ' '")
    job_options() { lab "grep -E '\\[Job ${1#*-}\\] argv\\[(4|5)\\]' /var/log/cups/error_log"; }
    job_device() { lab "grep -E '\\[Job ${1#*-}\\]' /var/log/cups/error_log | grep -oE 'sDEVICE=[a-z]+' | sort -u"; }
}

count() { wc -w <<< "$jobs"; }
# The file CUPS was handed for a job, kept by PreserveJobFiles.
spool_file() { local id="${1%% *}"; printf '/var/spool/cups/d%05d-001' "${id##*-}"; }
said() { grep -q -- "$1" <<< "$out"; }
exited_cleanly() { said 'exit=0' && ! said 'Traceback' && ! said 'Segmentation' && ! said 'Aborted'; }

# Where a picture goes, measured on the page CUPS was given:
# picture:FILE:SIZE:POSITION:PAPER:FILL[:gray]
PICTURES=(
    picture:ark-wide.png:100:centre:auto:fit
    picture:ark-wide.png:100:centre:portrait:fit
    picture:ark-wide.png:50:top-left:auto:fit
    picture:ark-tall.png:100:centre:auto:fit
    picture:ark-tall.png:33:bottom-right:portrait:fit
    picture:ark-wide.png:100:centre:portrait:fill
    picture:ark-wide.jpg:75:right:auto:fit:gray
    picture:ark-clear.png:50:top:auto:fit
)

scenarios=("$@")
[[ ${#scenarios[@]} -gt 0 ]] || scenarios=(web pdf pdf-options pdf-viewer-button inline
    options bad-range broken cancel page-closes-itself close-window page-prints-again repeat
    dark-page "${PICTURES[@]}")

# The inline scenario needs a PDF to serve; the worksheet scenario writes one.
lab 'test -f /opt/wonderink-check/www/ark.pdf' || { run pdf >/dev/null; lab 'cp "/home/teacher/Downloads/Wonder Ink/Colour the Ark.pdf" /opt/wonderink-check/www/ark.pdf; chmod a+r /opt/wonderink-check/www/ark.pdf'; }

for scenario in "${scenarios[@]}"; do
    echo "$scenario"
    run "$scenario"

    judge "Wonder Ink did not crash, and exited by itself" exited_cleanly

    # The preview has to SHOW the page, not merely count it. A preview
    # that printed the right thing while showing a blank was what the
    # first version of this check let through.
    if said 'PREVIEW '; then
        judge "the preview drew the page it will print" "! grep 'PREVIEW ' <<< \"\$out\" | grep -q 'shown=False'"
    fi

    case "$scenario" in
    web)
        judge "the preview showed the lesson, 3 Letter pages, Elmer chosen" \
            "said \"pages=3 printers=\\['Elmer', 'Refusing'\\] paper=Letter\""
        judge "one job reached CUPS" '[[ $(count) -eq 1 ]]'
        judge "Elmer's filters printed it in colour" '[[ "$(job_device $jobs)" == *pxlcolor* ]]'
        judge "and nothing was left behind" "said 'scratch_left=\\[\\]'" ;;
    pdf|pdf-viewer-button)
        judge "the preview showed the worksheet's own 2 pages" "said 'pages=2'"
        judge "one job reached CUPS" '[[ $(count) -eq 1 ]]' ;;
    pdf-options)
        judge "one job" '[[ $(count) -eq 1 ]]'
        judge "page 2 only, three copies" \
            '[[ "$(job_options $jobs)" == *"argv[4]=\"3\""* && "$(job_options $jobs)" == *page-ranges=2* ]]'
        judge "in black and white, by the driver's own option" \
            '[[ "$(job_options $jobs)" == *ColorModel=Grayscale* && "$(job_device $jobs)" == *pxlmono* ]]' ;;
    inline)
        judge "the inline PDF was recognised" "said \"mime='application/pdf'\""
        judge "and opened as a worksheet tab" "said 'current_is_pdf=True'"
        judge "one job reached CUPS" '[[ $(count) -eq 1 ]]' ;;
    options)
        judge "pages 2-3, two copies, black and white" \
            '[[ "$(job_options $jobs)" == *page-ranges=2-3* && "$(job_options $jobs)" == *"argv[4]=\"2\""* && "$(job_device $jobs)" == *pxlmono* ]]' ;;
    bad-range)
        judge "a page that does not exist is explained, not printed" "said 'This document has 3 pages'"
        judge "and once corrected, one job" '[[ $(count) -eq 1 ]]' ;;
    broken)
        judge "a refusing printer is reported in CUPS's words" "said 'Refusing did not take the job'"
        judge "and no job is claimed" '[[ $(count) -eq 0 ]] && ! said "NOTIFY Sent"' ;;
    cancel)
        judge "Cancel sends nothing" '[[ $(count) -eq 0 ]]' ;;
    page-closes-itself)
        judge "the preview says the page closed, and cannot print" \
            "said \"status='The page was closed.' print_enabled=False\""
        judge "and sends nothing" '[[ $(count) -eq 0 ]]' ;;
    close-window)
        judge "closing Wonder Ink with the preview open sends nothing" '[[ $(count) -eq 0 ]]' ;;
    page-prints-again)
        judge "a second print request does not open a second preview" "said 'still one preview=True'"
        judge "one job" '[[ $(count) -eq 1 ]]' ;;
    repeat)
        judge "five prints in a row, five jobs" '[[ $(count) -eq 5 ]]' ;;
    dark-page)
        judge "one job" '[[ $(count) -eq 1 ]]'
        spool=$(spool_file "$jobs")
        answer=$(lab "python3 $MEASURE darkness $spool")
        dark=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["dark_fraction"])' "$answer")
        ink=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["ink_fraction"])' "$answer")
        judge "a black page background does not print (dark: $dark of the page)" \
            "python3 -c 'import sys; sys.exit(0 if $dark < 0.05 else 1)'"
        judge "and its white text still does, darkened ($ink of the page)" \
            "python3 -c 'import sys; sys.exit(0 if $ink > 0.005 else 1)'" ;;
    picture:*)
        IFS=: read -r _ file size position paper fill color <<< "$scenario"
        judge "the picture controls appeared, and none for a page" "said 'PICTURE picture_box=True'"
        judge "the print interface says Color" "said \"labels='Color','Color'\""
        judge "one job reached CUPS" '[[ $(count) -eq 1 ]]'
        spool=$(spool_file "$jobs")
        judge "CUPS was given exactly the page the preview showed" \
            "[[ -n '$spool' ]] && lab 'cmp -s /tmp/rig-out/preview.pdf $spool'"
        dims=$(lab "python3 -c 'from PyQt6.QtGui import QImage; i=QImage(\"/opt/wonderink-check/www/$file\"); print(f\"{i.width()}x{i.height()}\")'" 2>/dev/null)
        result=$(lab "python3 $MEASURE measure $spool $dims $size $position $paper $fill")
        field() { python3 -c "import json,sys; print(json.loads(sys.argv[1])[sys.argv[2]])" "$result" "$1"; }
        judge "the paper is turned the way asked ($paper): $(field page)" \
            '[[ "$(field page_landscape)" == "$(field wanted_landscape)" ]]'
        judge "the picture is where and as large as asked, within 3 points (off by $(field off_by_points))" \
            '[[ "$(field off_by_points)" != None ]] && (( $(field off_by_points) <= 3 ))'
        judge "no ink anywhere else on the page — no bars ($(field ink_outside_picture) pixels)" \
            '[[ "$(field ink_outside_picture)" == 0 ]]'
        if [[ "$fill" == fill ]]; then
            # The mark is in the picture's left fifth, which filling a
            # portrait page with a wide picture cuts off. That is what the
            # control says it does; the fit scenarios prove it happens only
            # when asked.
            judge "Fill cut the edges off, as it says it will" '(( $(field pink_pixels) == 0 ))'
        else
            judge "the pink corner mark is on the paper — nothing cut off" '(( $(field pink_pixels) > 0 ))'
        fi
        if [[ "$color" == gray ]]; then
            judge "and it went in black and white" '[[ "$(job_device $jobs)" == *pxlmono* ]]'
        fi ;;
    esac

    [[ "$failed" -eq 0 ]] || { echo "$out" | tail -15 | sed 's/^/      /'; }
done

echo
echo "$passed passed, $failed failed"
[[ "$failed" -eq 0 ]]
