The black bars above and below a printed picture were neither in the picture nor in the layout. A PNG or JPEG in a tab of its own is shown by the engine on a viewer page whose background is rgb(14, 14, 14), and Qt prints page backgrounds by default; 0.2.6.1 printed that page. A picture is now taken as itself — drawn onto white, capped at 300 dpi across Letter — and laid out on the printer's paper by the preview, with Size (as large as fits down to 25%), Position (nine squares), Paper (automatic, upright, sideways) and Fill the whole page, which says it cuts the edges off and is never the default. The preview is the PDF that prints, laid out again after each change. Worksheet PDFs and lesson pages get no picture controls and are unchanged. Page backgrounds are no longer printed for lesson pages either, as in Chrome; Blink darkens light text when it does that, so a white-on-black page prints readable grey on white. "Colour" is "Color" in the print interface and the manual. Lab: 135 print checks, now including eight picture layouts measured on the job file CUPS kept — byte-identical to the preview, within 1 point of where asked, no ink anywhere else. Printed from the real session with the pointer: 50% top left, Elmer-66, the same. Not yet on paper. Co-Authored-By: Claude Opus 5.5 <[email protected]>
240 lines
12 KiB
Bash
Executable File
240 lines
12 KiB
Bash
Executable File
#!/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 ]]
|