From 9811979d6c78e9c6bd76566c3fb041f9a63326be Mon Sep 17 00:00:00 2001 From: James Maes Date: Sun, 1 Nov 2026 16:34:51 -0600 Subject: [PATCH 1/4] feat: add branded tests for short Zebra label stock Generate compact four by one and four by two PDF and ZPL labels, and carry explicit PDF paper dimensions per job without altering queue defaults. Co-Authored-By: Claude Sonnet 5.5 Signed-off-by: James Maes --- README.md | 2 +- .../qbits/printing/PrintingProcesses.java | 23 +- .../qbits/printing/PrintingService.java | 26 +- .../qbits/printing/PrintingTestDocuments.java | 265 +++++++++++++++++- .../qbits/printing/TestPrintProfile.java | 84 +++++- .../qbits/printing/PrintingProcessesTest.java | 79 +++++- .../printing/PrintingTestDocumentsTest.java | 156 ++++++++++- 7 files changed, 612 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index e72ba0a..f37b9bf 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ For durable QQQ jobs register `PrintEnqueueEffectTarget` as an `IDEMPOTENT_EXTER ## Deliberate first-version limits -The implemented path targets an explicit device/printer. Logical destination bindings, cancellation, reprint workflow, automatic reconciliation, clustered throughput optimization, retention and alerts are follow-ups. Documents are durably stored as base64 in the database for this first path; object-storage integration is deferred. Custom paper dimensions are rejected; configure the Windows queue. Received/submitting jobs are not automatically offered again. The queue records SPOOLED honestly and does not assert physical delivery. +The implemented path targets an explicit device/printer. Logical destination bindings, cancellation, reprint workflow, automatic reconciliation, clustered throughput optimization, retention and alerts are follow-ups. Documents are durably stored as base64 in the database for this first path; object-storage integration is deferred. Job paper (`paperWidthMm` and `paperHeightMm`, both positive, at most 2000, never with `mediaProfile`) is accepted; the built-in 4 x 1 and 4 x 2 in PDF test labels use it (101.6 x 25.4 / 50.8 mm) so Windows does not feed 4 x 6 stock. Only a client from the same unreleased 0.4 cohort applies it; older clients reject the job. Other jobs follow the Windows queue default. Received/submitting jobs are not automatically offered again. The queue records SPOOLED honestly and does not assert physical delivery. The protocol demo and synthetic native UI preview are separate local hosts. Attaching the QBit to a deployed application and its real authentication/facility policy remains a host integration task. PostgreSQL behavior, production deployment, upgrades, cancellation/reprint and broad printer-driver coverage are not certified by the initial H2/laser tests. diff --git a/src/main/java/com/kingsrook/qbits/printing/PrintingProcesses.java b/src/main/java/com/kingsrook/qbits/printing/PrintingProcesses.java index c35e8e4..111de13 100644 --- a/src/main/java/com/kingsrook/qbits/printing/PrintingProcesses.java +++ b/src/main/java/com/kingsrook/qbits/printing/PrintingProcesses.java @@ -102,7 +102,18 @@ public class PrintingProcesses implements BackendStep for(TestPrintProfile profile : TestPrintProfile.values()) { QPossibleValue option = new QPossibleValue<>(profile.name(), profile.getLabel()); - pdf.add(option); + if(profile.getDpi() > 0 && profile.getHeightInches() != 6) + { + // PDF is vector, so the printer resolution is irrelevant: one friendly choice per short stock. + if(profile.getDpi() == 300) + { + pdf.add(new QPossibleValue<>(profile.name(), "4 x " + profile.getHeightInches() + " in label")); + } + } + else + { + pdf.add(option); + } if(profile.getDpi() > 0) { zpl.add(option); @@ -349,8 +360,14 @@ public class PrintingProcesses implements BackendStep { TestPrintProfile profile = TestPrintProfile.resolve(pdf ? "PDF" : "ZPL", profileName); byte[] bytes = pdf ? PrintingTestDocuments.pdf(profile) : PrintingTestDocuments.zpl(profile); - JSONObject job = currentService().submit(new JSONObject().put("deviceId", printer.getValueString("device_id")).put("printerId", printer.getValueString("printer_id")) - .put("title", "QQQ print test").put("format", pdf ? "PDF" : "ZPL").put("contentBase64", Base64.getEncoder().encodeToString(bytes)).put("idempotencyKey", key)); + JSONObject request = new JSONObject().put("deviceId", printer.getValueString("device_id")).put("printerId", printer.getValueString("printer_id")) + .put("title", "QQQ print test").put("format", pdf ? "PDF" : "ZPL").put("contentBase64", Base64.getEncoder().encodeToString(bytes)).put("idempotencyKey", key); + JSONObject options = profile.jobOptions(pdf ? "PDF" : "ZPL"); + if(options != null) + { + request.put("options", options); + } + JSONObject job = currentService().submit(request); return (job.getString("jobId")); } catch(java.sql.SQLException | PrintingException exception) diff --git a/src/main/java/com/kingsrook/qbits/printing/PrintingService.java b/src/main/java/com/kingsrook/qbits/printing/PrintingService.java index 956a671..e3343bf 100644 --- a/src/main/java/com/kingsrook/qbits/printing/PrintingService.java +++ b/src/main/java/com/kingsrook/qbits/printing/PrintingService.java @@ -1184,7 +1184,11 @@ public class PrintingService } if(options.has("paperWidthMm") || options.has("paperHeightMm")) { - throw new PrintingException(400, "Custom paper dimensions are not supported yet; configure the Windows queue"); + // Explicit job paper is a complete, positive size and is never combined with a queue media profile. + if(!validPaperMillimetres(options.opt("paperWidthMm")) || !validPaperMillimetres(options.opt("paperHeightMm")) || options.has("mediaProfile")) + { + throw new PrintingException(400, "Paper dimensions require both a width and height in millimetres (up to 2000) and cannot be combined with a media profile"); + } } PrintingMediaProfile.selection(options); options.put("copies", count).put("scale", scale); @@ -1197,6 +1201,16 @@ public class PrintingService + /*************************************************************************** + ** A finite number within the protocol's physical paper bounds. + ***************************************************************************/ + private static boolean validPaperMillimetres(Object value) + { + return (value instanceof Number number && Double.isFinite(number.doubleValue()) && number.doubleValue() > 0 && number.doubleValue() <= 2000); + } + + + /*************************************************************************** ** Claim one job; replay the same assignment until the device advances it. ***************************************************************************/ @@ -1205,9 +1219,15 @@ public class PrintingService String format = required(request.optString("format"), "format"); TestPrintProfile profile = TestPrintProfile.resolve(format, request.has("testProfile") ? request.optString("testProfile") : null); byte[] bytes = "PDF".equals(format) ? PrintingTestDocuments.pdf(profile) : PrintingTestDocuments.zpl(profile); - return (submit(new JSONObject().put("deviceId", deviceId).put("printerId", required(request.optString("printerId"), "printerId")) + JSONObject job = new JSONObject().put("deviceId", deviceId).put("printerId", required(request.optString("printerId"), "printerId")) .put("title", "QQQ print test").put("format", format).put("contentBase64", Base64.getEncoder().encodeToString(bytes)) - .put("idempotencyKey", "device-test:" + deviceId + ":" + required(request.optString("requestId"), "requestId")))); + .put("idempotencyKey", "device-test:" + deviceId + ":" + required(request.optString("requestId"), "requestId")); + JSONObject options = profile.jobOptions(format); + if(options != null) + { + job.put("options", options); + } + return (submit(job)); } diff --git a/src/main/java/com/kingsrook/qbits/printing/PrintingTestDocuments.java b/src/main/java/com/kingsrook/qbits/printing/PrintingTestDocuments.java index e915ac3..75c1849 100644 --- a/src/main/java/com/kingsrook/qbits/printing/PrintingTestDocuments.java +++ b/src/main/java/com/kingsrook/qbits/printing/PrintingTestDocuments.java @@ -44,6 +44,7 @@ import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.font.PDType0Font; import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; /******************************************************************************* @@ -55,6 +56,10 @@ public final class PrintingTestDocuments static final String FONT_RESOURCE = "/com/kingsrook/qbits/printing/fonts/JetBrainsMono-Bold.ttf"; private static final String LOGO_RESOURCE = "/com/kingsrook/qbits/printing/images/query-mascot.png"; static final String BARCODE_VALUE = "QQQ-TEST-123456"; + private static final int LEGACY_LABEL_HEIGHT_INCHES = 6; + static final int COMPACT_BARCODE_X = 16; + static final int COMPACT_BARCODE_Y = 118; + static final int COMPACT_BARCODE_HEIGHT = 16; @@ -80,16 +85,21 @@ public final class PrintingTestDocuments identifiers.add(new COSString("QQQ-TEST-" + profile.name())); document.getDocument().getTrailer().setItem(COSName.ID, identifiers); PDType0Font font = PDType0Font.load(document, fontStream, true); - PDRectangle media = profile.getDpi() > 0 ? new PDRectangle(288, 432) : (profile == TestPrintProfile.A4 ? PDRectangle.A4 : PDRectangle.LETTER); + PDRectangle media = profile.getDpi() > 0 ? new PDRectangle(profile.getWidthPoints(), profile.getHeightPoints()) : (profile == TestPrintProfile.A4 ? PDRectangle.A4 : PDRectangle.LETTER); PDPage page = new PDPage(media); document.addPage(page); try(PDPageContentStream content = new PDPageContentStream(document, page)) { - if(profile.getDpi() > 0) + if(profile.getDpi() > 0 && profile.getHeightInches() == LEGACY_LABEL_HEIGHT_INCHES) { pdfLabel(content, font); content.drawImage(LosslessFactory.createFromImage(document, logo()), 210, 348, 54, 54); } + else if(profile.getDpi() > 0) + { + content.setLineWidth(1); + compactLabel(new PdfCanvas(content, font, LosslessFactory.createFromImage(document, logo()), profile.getHeightPoints()), profile.getHeightInches(), "PDF / ACTUAL SIZE"); + } else { pdfText(content, font, 56, 48, 690, "QQQ"); @@ -144,18 +154,30 @@ public final class PrintingTestDocuments ***************************************************************************/ public static byte[] zpl(TestPrintProfile profile) throws QException { - return (zpl(profile.getDpi())); + return (zpl(profile.getDpi(), profile.getHeightInches())); } /*************************************************************************** ** A four-by-six-inch label at the explicitly chosen printer resolution. - ** Text uses the bundled font rasterized to one bit; Code 128 stays native. ***************************************************************************/ public static byte[] zpl(int dpi) throws QException { - BufferedImage image = labelImage(dpi); + return (zpl(dpi, LEGACY_LABEL_HEIGHT_INCHES)); + } + + + + /*************************************************************************** + ** A four-inch-wide label of the explicit height at the chosen resolution. + ** Text uses the bundled font rasterized to one bit; Code 128 stays native. + ** No media-type, darkness or speed commands are sent, so the printer keeps + ** its own direct-thermal configuration and this never changes its defaults. + ***************************************************************************/ + static byte[] zpl(int dpi, int heightInches) throws QException + { + BufferedImage image = labelImage(dpi, heightInches); int rowBytes = (image.getWidth() + 7) / 8; byte[] bitmap = new byte[rowBytes * image.getHeight()]; for(int y = 0; y < image.getHeight(); y++) @@ -171,8 +193,17 @@ public final class PrintingTestDocuments int module = Math.max(2, dpi / 100); StringBuilder zpl = new StringBuilder("^XA^PW" + image.getWidth() + "^LL" + image.getHeight() + "^LH0,0"); appendGraphicStrips(zpl, bitmap, rowBytes, image.getHeight()); - zpl.append("^FO").append(dots(24, dpi)).append(',').append(dots(276, dpi)).append("^BY").append(module) - .append("^BCN,").append(dots(42, dpi)).append(",N,N,N^FD").append(BARCODE_VALUE).append("^FS^PQ1^XZ"); + if(heightInches == LEGACY_LABEL_HEIGHT_INCHES) + { + zpl.append("^FO").append(dots(24, dpi)).append(',').append(dots(276, dpi)).append("^BY").append(module) + .append("^BCN,").append(dots(42, dpi)).append(",N,N,N^FD").append(BARCODE_VALUE).append("^FS"); + } + else if(heightInches == 2) + { + zpl.append("^FO").append(dots(COMPACT_BARCODE_X, dpi)).append(',').append(dots(COMPACT_BARCODE_Y, dpi)).append("^BY").append(module) + .append("^BCN,").append(dots(COMPACT_BARCODE_HEIGHT, dpi)).append(",N,N,N^FD").append(BARCODE_VALUE).append("^FS"); + } + zpl.append("^PQ1^XZ"); return (zpl.toString().getBytes(StandardCharsets.US_ASCII)); } @@ -201,15 +232,29 @@ public final class PrintingTestDocuments ** Deterministic image also supports inspection without contacting a printer. ***************************************************************************/ static BufferedImage labelImage(int dpi) throws QException + { + return (labelImage(dpi, LEGACY_LABEL_HEIGHT_INCHES)); + } + + + + /*************************************************************************** + ** Four-inch-wide label raster of an explicit supported height. + ***************************************************************************/ + static BufferedImage labelImage(int dpi, int heightInches) throws QException { if(!Set.of(203, 300, 600).contains(dpi)) { throw new IllegalArgumentException("Label resolution must be 203, 300 or 600 dpi"); } + if(!Set.of(1, 2, LEGACY_LABEL_HEIGHT_INCHES).contains(heightInches)) + { + throw new IllegalArgumentException("Label height must be 1, 2 or 6 inches"); + } try(InputStream stream = fontStream()) { Font font = Font.createFont(Font.TRUETYPE_FONT, stream); - BufferedImage image = new BufferedImage(dpi * 4, dpi * 6, BufferedImage.TYPE_BYTE_BINARY); + BufferedImage image = new BufferedImage(dpi * 4, dpi * heightInches, BufferedImage.TYPE_BYTE_BINARY); Graphics2D graphics = image.createGraphics(); try { @@ -218,6 +263,12 @@ public final class PrintingTestDocuments graphics.scale(dpi / 72.0, dpi / 72.0); graphics.setColor(Color.BLACK); graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF); + if(heightInches != LEGACY_LABEL_HEIGHT_INCHES) + { + compactLabel(new ImageCanvas(graphics, font, logo()), heightInches, dpi + " dpi / 1 copy"); + scanText(graphics, font, heightInches); + return (image); + } graphics.drawRect(12, 12, 264, 408); graphics.drawImage(logo(), 210, 30, 54, 54, null); labelText(graphics, font, 43, 24, 58, "QQQ"); @@ -378,5 +429,203 @@ public final class PrintingTestDocuments content.addRect(12, 12, 264, 408); content.stroke(); } + + + + /*************************************************************************** + ** Top-left origin drawing surface in points, shared by the PDF and raster + ** paths so the compact 4 x 1 and 4 x 2 layouts cannot drift apart. + ***************************************************************************/ + private interface LabelCanvas + { + /************************************************************************ + ** Draw a text baseline at the given point size and position. + ************************************************************************/ + void text(int size, int x, int y, String text) throws IOException; + + + + /************************************************************************ + ** Stroke a one-point line between two points. + ************************************************************************/ + void line(int x1, int y1, int x2, int y2) throws IOException; + + + + /************************************************************************ + ** Stroke a rectangle outline from its top-left corner. + ************************************************************************/ + void rect(int x, int y, int width, int height) throws IOException; + + + + /************************************************************************ + ** Draw the square Query mascot from its top-left corner. + ************************************************************************/ + void logo(int x, int y, int size) throws IOException; + } + + + + /*************************************************************************** + ** Own layouts for the short stocks; the 4 x 6 layout is never scaled down. + ** Text starts at 7 pt (about 20 dots at 203 dpi); only a 4 x 2 label has + ** room for the native barcode, which the ZPL path places at COMPACT_BARCODE_*. + ***************************************************************************/ + private static void compactLabel(LabelCanvas canvas, int heightInches, String medium) throws IOException + { + String stock = "4 x " + heightInches + " in / " + medium; + if(heightInches == 2) + { + canvas.rect(5, 5, 278, 134); + canvas.logo(240, 11, 36); + canvas.text(26, 12, 38, "QQQ"); + canvas.text(9, 70, 26, "TEST PRINT - NOT POSTAGE"); + canvas.text(8, 70, 38, stock); + canvas.line(12, 54, 232, 54); + canvas.text(16, 12, 72, "0O 1Il 5S 8B 2Z"); + canvas.text(9, 12, 86, "BIN A01-I1 / SKU O0-5S8B"); + canvas.text(9, 12, 98, "ORDER QQQ-123456 / QTY 02"); + ruler(canvas, 108, 8, 92, 111); + } + else + { + canvas.rect(5, 5, 278, 62); + canvas.logo(248, 9, 28); + canvas.text(20, 12, 28, "QQQ"); + canvas.text(8, 56, 17, "TEST PRINT - NOT POSTAGE"); + canvas.text(7, 56, 27, stock); + canvas.line(12, 33, 240, 33); + canvas.text(12, 12, 46, "0O 1Il 5S 8B 2Z"); + canvas.text(7, 158, 44, "BIN A01-I1 / SKU O0-5S8B"); + ruler(canvas, 57, 7, 92, 60); + canvas.text(7, 165, 60, "ORDER QQQ-123456 / QTY 02"); + } + } + + + + /*************************************************************************** + ** One-inch (25.4 mm) measuring line with end ticks and its caption. + ***************************************************************************/ + private static void ruler(LabelCanvas canvas, int y, int size, int captionX, int captionY) throws IOException + { + canvas.line(12, y, 84, y); + canvas.line(12, y - 3, 12, y + 3); + canvas.line(84, y - 3, 84, y + 3); + canvas.text(size, captionX, captionY, "25.4 mm / 1 inch"); + } + + + + /*************************************************************************** + ** Caption beside the native 4 x 2 barcode; it exists only in the ZPL raster. + ***************************************************************************/ + private static void scanText(Graphics2D graphics, Font font, int heightInches) + { + if(heightInches == 2) + { + labelText(graphics, font, 8, 170, 130, "SCAN: " + BARCODE_VALUE); + } + } + + + + /*************************************************************************** + ** Vector PDF surface; converts the top-left layout to PDF's bottom-left origin. + ***************************************************************************/ + private static final class PdfCanvas implements LabelCanvas + { + private final PDPageContentStream content; + private final PDType0Font font; + private final PDImageXObject mascot; + private final int height; + + + + /************************************************************************ + ** Draw onto one page whose height converts top-left layout coordinates. + ************************************************************************/ + PdfCanvas(PDPageContentStream content, PDType0Font font, PDImageXObject mascot, int height) + { + this.content = content; + this.font = font; + this.mascot = mascot; + this.height = height; + } + + + + @Override + public void text(int size, int x, int y, String text) throws IOException + { + pdfText(content, font, size, x, height - y, text); + } + + + + @Override + public void line(int x1, int y1, int x2, int y2) throws IOException + { + content.moveTo(x1, height - y1); + content.lineTo(x2, height - y2); + content.stroke(); + } + + + + @Override + public void rect(int x, int y, int width, int rectHeight) throws IOException + { + content.addRect(x, height - y - rectHeight, width, rectHeight); + content.stroke(); + } + + + + @Override + public void logo(int x, int y, int size) throws IOException + { + content.drawImage(mascot, x, height - y - size, size, size); + } + } + + + + /*************************************************************************** + ** One-bit raster surface drawing the same layout with the same bundled font. + ***************************************************************************/ + private record ImageCanvas(Graphics2D graphics, Font font, BufferedImage mascot) implements LabelCanvas + { + @Override + public void text(int size, int x, int y, String text) + { + labelText(graphics, font, size, x, y, text); + } + + + + @Override + public void line(int x1, int y1, int x2, int y2) + { + graphics.drawLine(x1, y1, x2, y2); + } + + + + @Override + public void rect(int x, int y, int width, int height) + { + graphics.drawRect(x, y, width, height); + } + + + + @Override + public void logo(int x, int y, int size) + { + graphics.drawImage(mascot, x, y, size, size, null); + } + } } diff --git a/src/main/java/com/kingsrook/qbits/printing/TestPrintProfile.java b/src/main/java/com/kingsrook/qbits/printing/TestPrintProfile.java index ae5d79d..eacb83a 100644 --- a/src/main/java/com/kingsrook/qbits/printing/TestPrintProfile.java +++ b/src/main/java/com/kingsrook/qbits/printing/TestPrintProfile.java @@ -21,6 +21,9 @@ package com.kingsrook.qbits.printing; +import org.json.JSONObject; + + /******************************************************************************* ** Explicit stock and resolution choices for built-in diagnostic documents. *******************************************************************************/ @@ -30,10 +33,20 @@ public enum TestPrintProfile A4("A4 - 210 x 297 mm", 0), LABEL_4X6_203("4 x 6 in label - 203 dpi", 203), LABEL_4X6_300("4 x 6 in label - 300 dpi", 300), - LABEL_4X6_600("4 x 6 in label - 600 dpi", 600); + LABEL_4X6_600("4 x 6 in label - 600 dpi", 600), + LABEL_4X2_203("4 x 2 in label - 203 dpi", 203, 2), + LABEL_4X2_300("4 x 2 in label - 300 dpi", 300, 2), + LABEL_4X2_600("4 x 2 in label - 600 dpi", 600, 2), + LABEL_4X1_203("4 x 1 in label - 203 dpi", 203, 1), + LABEL_4X1_300("4 x 1 in label - 300 dpi", 300, 1), + LABEL_4X1_600("4 x 1 in label - 600 dpi", 600, 1); + + private static final int LEGACY_LABEL_HEIGHT_INCHES = 6; + private static final double MM_PER_INCH = 25.4; private final String label; private final int dpi; + private final int heightInches; @@ -41,9 +54,20 @@ public enum TestPrintProfile ** Store a human label and printer density without guessing from a name. ***************************************************************************/ TestPrintProfile(String label, int dpi) + { + this(label, dpi, dpi > 0 ? LEGACY_LABEL_HEIGHT_INCHES : 0); + } + + + + /*************************************************************************** + ** Labels are always four inches wide; the height is explicit stock data. + ***************************************************************************/ + TestPrintProfile(String label, int dpi, int heightInches) { this.label = label; this.dpi = dpi; + this.heightInches = heightInches; } @@ -68,7 +92,7 @@ public enum TestPrintProfile } if("ZPL".equals(format) && profile.dpi == 0) { - throw new PrintingException(400, "ZPL test prints require a 4 x 6 label profile with the matching printer dpi"); + throw new PrintingException(400, "ZPL test prints require a label profile with the matching printer dpi"); } return (profile); } @@ -92,5 +116,61 @@ public enum TestPrintProfile { return (dpi); } + + + + /*************************************************************************** + ** Label height in inches; zero for page profiles. + ***************************************************************************/ + public int getHeightInches() + { + return (heightInches); + } + + + + /*************************************************************************** + ** Exact PDF media width in points (72 per inch); zero for page profiles. + ***************************************************************************/ + public int getWidthPoints() + { + return (dpi > 0 ? 4 * 72 : 0); + } + + + + /*************************************************************************** + ** Exact PDF media height in points (72 per inch); zero for page profiles. + ***************************************************************************/ + public int getHeightPoints() + { + return (heightInches * 72); + } + + + + /*************************************************************************** + ** Only labels shorter than the legacy 4 x 6 stock need job-specific paper; + ** 4 x 6 and page profiles keep using the Windows queue default untouched. + ***************************************************************************/ + public boolean needsJobPaper(String format) + { + return ("PDF".equals(format) && dpi > 0 && heightInches != LEGACY_LABEL_HEIGHT_INCHES); + } + + + + /*************************************************************************** + ** Actual-size paper for this job (never a media profile), or null when the + ** configured queue default applies. Millimetres are exact: 101.6 x 25.4/50.8. + ***************************************************************************/ + public JSONObject jobOptions(String format) + { + if(!needsJobPaper(format)) + { + return (null); + } + return (new JSONObject().put("paperWidthMm", 4 * MM_PER_INCH).put("paperHeightMm", heightInches * MM_PER_INCH)); + } } diff --git a/src/test/java/com/kingsrook/qbits/printing/PrintingProcessesTest.java b/src/test/java/com/kingsrook/qbits/printing/PrintingProcessesTest.java index d51e349..6e2dcaa 100644 --- a/src/test/java/com/kingsrook/qbits/printing/PrintingProcessesTest.java +++ b/src/test/java/com/kingsrook/qbits/printing/PrintingProcessesTest.java @@ -268,8 +268,83 @@ class PrintingProcessesTest assertEquals("testProfile", selected.getStepList().get(1).getName()); QFrontendStepMetaData form = (QFrontendStepMetaData) selected.getStep("testProfile"); assertEquals("printing_testProfilePdf", form.getFormField("testProfile").getPossibleValueSourceName()); - assertEquals(3, fixture.instance().getPossibleValueSource("printing_testProfileZpl").getEnumValues().size()); - assertEquals(5, fixture.instance().getPossibleValueSource("printing_testProfilePdf").getEnumValues().size()); + // 4 x 6 choices are unchanged; each short stock adds friendly choices without raw IDs or typed sizes. + assertEquals(9, fixture.instance().getPossibleValueSource("printing_testProfileZpl").getEnumValues().size()); + var pdfChoices = fixture.instance().getPossibleValueSource("printing_testProfilePdf").getEnumValues(); + assertEquals(7, pdfChoices.size()); + assertEquals(List.of("LETTER", "A4", "LABEL_4X6_203", "LABEL_4X6_300", "LABEL_4X6_600", "LABEL_4X2_300", "LABEL_4X1_300"), + pdfChoices.stream().map(choice -> String.valueOf(choice.getId())).toList()); + assertEquals("4 x 1 in label", pdfChoices.getLast().getLabel()); + assertEquals(TestPrintProfile.LABEL_4X6_203.name(), TestPrintProfile.resolve("ZPL", null).name()); + } + + + + /*************************************************************************** + ** Job paper is one complete positive size and never mixes with a profile. + ***************************************************************************/ + @Test + void jobPaperRequiresACompleteBoundedPairWithoutMediaProfile() throws Exception + { + Fixture fixture = fixture(); + fixture.service().inventory(fixture.device(), new JSONArray().put(new JSONObject().put("printerId", "laser") + .put("name", "Packing test printer").put("formats", new JSONArray(List.of("PDF", "ZPL"))))); + String content = java.util.Base64.getEncoder().encodeToString(PrintingTestDocuments.pdf(TestPrintProfile.LABEL_4X1_300)); + JSONObject selection = new JSONObject().put("id", "default").put("revision", "a".repeat(64)); + List rejected = List.of( + new JSONObject().put("paperWidthMm", 101.6), + new JSONObject().put("paperHeightMm", 25.4), + new JSONObject().put("paperWidthMm", 0).put("paperHeightMm", 25.4), + new JSONObject().put("paperWidthMm", 101.6).put("paperHeightMm", -25.4), + new JSONObject().put("paperWidthMm", "101.6").put("paperHeightMm", 25.4), + new JSONObject().put("paperWidthMm", 101.6).put("paperHeightMm", 2000.1), + new JSONObject().put("paperWidthMm", 101.6).put("paperHeightMm", 25.4).put("mediaProfile", selection)); + for(int index = 0; index < rejected.size(); index++) + { + JSONObject request = new JSONObject().put("deviceId", fixture.device()).put("printerId", "laser").put("title", "paper").put("format", "PDF") + .put("contentBase64", content).put("idempotencyKey", "job-paper-" + index).put("options", rejected.get(index)); + assertEquals(400, assertThrows(PrintingException.class, () -> fixture.service().submit(request)).getStatus(), rejected.get(index).toString()); + } + assertEquals(0, fixture.service().queuedJobs(fixture.device()).length()); + JSONObject accepted = new JSONObject().put("deviceId", fixture.device()).put("printerId", "laser").put("title", "paper").put("format", "PDF") + .put("contentBase64", content).put("idempotencyKey", "job-paper-valid").put("options", new JSONObject().put("paperWidthMm", 101.6).put("paperHeightMm", 25.4)); + JSONObject options = fixture.service().getJob(fixture.service().submit(accepted).getString("jobId")).getJSONObject("options"); + assertEquals(25.4, options.getDouble("paperHeightMm"), 0.0001); + } + + + + /*************************************************************************** + ** Short PDF stock sends exact actual-size job paper and no media profile; + ** legacy 4 x 6 and every ZPL label keep the queue/payload-defined path. + ***************************************************************************/ + @Test + void shortLabelPdfJobsCarryExactJobPaperOnly() throws Exception + { + Fixture fixture = fixture(); + fixture.service().inventory(fixture.device(), new JSONArray().put(new JSONObject().put("printerId", "laser") + .put("name", "Packing test printer").put("formats", new JSONArray(List.of("PDF", "ZPL"))))); + QRecord printer = PrintingProcesses.resolvePrinter(fixture.printerId(), fixture.device()); + String[][] stocks = {{"LABEL_4X1_300", "25.4"}, {"LABEL_4X2_300", "50.8"}}; + for(String[] stock : stocks) + { + JSONObject options = fixture.service().getJob(PrintingProcesses.submitTest(printer, true, stock[0])).getJSONObject("options"); + assertEquals(101.6, options.getDouble("paperWidthMm"), 0.0001); + assertEquals(Double.parseDouble(stock[1]), options.getDouble("paperHeightMm"), 0.0001); + assertFalse(options.has("mediaProfile")); + assertEquals(1, options.getInt("copies")); + assertEquals("ACTUAL_SIZE", options.getString("scale")); + } + for(boolean pdf : new boolean[] {true, false}) + { + for(String profile : pdf ? new String[] {"LABEL_4X6_300"} : new String[] {"LABEL_4X6_300", "LABEL_4X2_203", "LABEL_4X1_600"}) + { + JSONObject options = fixture.service().getJob(PrintingProcesses.submitTest(printer, pdf, profile)).getJSONObject("options"); + assertFalse(options.has("paperWidthMm")); + assertFalse(options.has("paperHeightMm")); + assertFalse(options.has("mediaProfile")); + } + } } diff --git a/src/test/java/com/kingsrook/qbits/printing/PrintingTestDocumentsTest.java b/src/test/java/com/kingsrook/qbits/printing/PrintingTestDocumentsTest.java index 59c1d23..c161fbe 100644 --- a/src/test/java/com/kingsrook/qbits/printing/PrintingTestDocumentsTest.java +++ b/src/test/java/com/kingsrook/qbits/printing/PrintingTestDocumentsTest.java @@ -31,11 +31,14 @@ import java.util.regex.Pattern; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; +import org.apache.pdfbox.rendering.PDFRenderer; import org.apache.pdfbox.text.PDFTextStripper; +import org.json.JSONObject; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -82,8 +85,14 @@ class PrintingTestDocumentsTest if(profile.getDpi() > 0) { double scale = profile.getDpi() / 72.0; - BufferedImage raster = PrintingTestDocuments.labelImage(profile.getDpi()); - BufferedImage mascot = raster.getSubimage((int) Math.floor(210 * scale), (int) Math.floor(30 * scale), (int) Math.ceil(54 * scale), (int) Math.ceil(54 * scale)); + BufferedImage raster = PrintingTestDocuments.labelImage(profile.getDpi(), profile.getHeightInches()); + int[] box = switch(profile.getHeightInches()) + { + case 1 -> new int[] {248, 9, 28}; + case 2 -> new int[] {240, 11, 36}; + default -> new int[] {210, 30, 54}; + }; + BufferedImage mascot = raster.getSubimage((int) Math.floor(box[0] * scale), (int) Math.floor(box[1] * scale), (int) Math.ceil(box[2] * scale), (int) Math.ceil(box[2] * scale)); assertEquals(expectedRatio, visibleAspectRatio(mascot), 0.03); } } @@ -134,7 +143,7 @@ class PrintingTestDocumentsTest { assertEquals(1, document.getNumberOfPages()); PDRectangle size = document.getPage(0).getMediaBox(); - PDRectangle expected = profile.getDpi() > 0 ? new PDRectangle(288, 432) : (profile == TestPrintProfile.A4 ? PDRectangle.A4 : PDRectangle.LETTER); + PDRectangle expected = profile.getDpi() > 0 ? new PDRectangle(288, profile.getHeightInches() * 72) : (profile == TestPrintProfile.A4 ? PDRectangle.A4 : PDRectangle.LETTER); assertEquals(expected.getWidth(), size.getWidth(), 0.01); assertEquals(expected.getHeight(), size.getHeight(), 0.01); String text = new PDFTextStripper().getText(document); @@ -211,6 +220,16 @@ class PrintingTestDocumentsTest ** strip before comparing the complete bitmap with the rendered source. ***************************************************************************/ private static byte[] reconstructGraphicStrips(String zpl, int rowBytes, int height) + { + return (reconstructGraphicStrips(zpl, rowBytes, height, 2)); + } + + + + /*************************************************************************** + ** Short labels fit one strip, so callers state how many strips they require. + ***************************************************************************/ + private static byte[] reconstructGraphicStrips(String zpl, int rowBytes, int height, int minimumStrips) { Matcher matcher = Pattern.compile("\\^FO0,(\\d+)\\^GFA,(\\d+),(\\d+),(\\d+),([0-9A-F]+)\\^FS").matcher(zpl); byte[] bitmap = new byte[rowBytes * height]; @@ -233,7 +252,7 @@ class PrintingTestDocumentsTest nextRow += length / rowBytes; } assertEquals(height, nextRow); - assertTrue(strips > 1); + assertTrue(strips >= minimumStrips); assertEquals(strips, zpl.split("\\^GFA,", -1).length - 1); return (bitmap); } @@ -268,6 +287,135 @@ class PrintingTestDocumentsTest + /*************************************************************************** + ** Short stock has exact media boxes and job paper; 4 x 6 is unchanged and + ** never gets job paper. Drawn pixels stay inside the physical label. + ***************************************************************************/ + @Test + void shortLabelPdfsUseExactMediaAndJobPaperWhileFourBySixIsPreserved() throws Exception + { + Object[][] stocks = { + {TestPrintProfile.LABEL_4X1_300, 72f, 25.4}, + {TestPrintProfile.LABEL_4X2_300, 144f, 50.8}, + {TestPrintProfile.LABEL_4X6_300, 432f, null}}; + for(Object[] stock : stocks) + { + TestPrintProfile profile = (TestPrintProfile) stock[0]; + float height = (Float) stock[1]; + try(PDDocument document = PDDocument.load(PrintingTestDocuments.pdf(profile))) + { + PDRectangle media = document.getPage(0).getMediaBox(); + assertEquals(0, media.getLowerLeftX(), 0.001); + assertEquals(0, media.getLowerLeftY(), 0.001); + assertEquals(288, media.getWidth(), 0.001); + assertEquals(height, media.getHeight(), 0.001); + BufferedImage page = new PDFRenderer(document).renderImageWithDPI(0, 300); + // PDFBox truncates float scale * points (e.g. 432 * 4.1666665 = 1799.9999), so the raster may be one pixel short. + // The exact physical size is asserted on the media box above. + assertEquals(1200, page.getWidth(), 1); + assertEquals(Math.round(height / 72 * 300), page.getHeight(), 1); + int margin = profile.getHeightInches() == 6 ? 11 * 300 / 72 : 4 * 300 / 72; + int dark = 0; + for(int y = 0; y < page.getHeight(); y++) + { + for(int x = 0; x < page.getWidth(); x++) + { + if((page.getRGB(x, y) & 0xff) < 128) + { + dark++; + assertTrue(x >= margin - 1 && x < page.getWidth() - margin + 1 && y >= margin - 1 && y < page.getHeight() - margin + 1, "Ink outside quiet margin at " + x + "," + y); + } + } + } + assertTrue(dark > 1000); + } + JSONObject options = profile.jobOptions("PDF"); + if(stock[2] == null) + { + assertNull(options); + } + else + { + assertEquals(101.6, options.getDouble("paperWidthMm"), 0.0001); + assertEquals((Double) stock[2], options.getDouble("paperHeightMm"), 0.0001); + assertEquals(2, options.length()); + } + assertNull(profile.jobOptions("ZPL")); + } + } + + + + /*************************************************************************** + ** Every short-label density emits proper PW/LL, one copy, no printer-wide + ** settings, an exact raster, and native barcode only where it fits. + ***************************************************************************/ + @Test + void shortLabelZplCarriesExactDimensionsAndBoundedBarcode() throws Exception + { + for(TestPrintProfile profile : TestPrintProfile.values()) + { + if(profile.getDpi() == 0 || profile.getHeightInches() == 6) + { + continue; + } + int dpi = profile.getDpi(); + int width = dpi * 4; + int height = dpi * profile.getHeightInches(); + String zpl = new String(PrintingTestDocuments.zpl(profile), StandardCharsets.US_ASCII); + assertTrue(zpl.startsWith("^XA^PW" + width + "^LL" + height + "^LH0,0"), profile.name()); + assertTrue(zpl.endsWith("^PQ1^XZ")); + assertEquals(1, zpl.split("\\^PQ", -1).length - 1); + assertEquals(1, zpl.split("\\^XA", -1).length - 1); + for(String forbidden : new String[] {"~D", "^A0", "^MT", "^MD", "^PR", "^JU", "^CF", "^PO"}) + { + assertFalse(zpl.contains(forbidden), forbidden); + } + int rowBytes = (width + 7) / 8; + byte[] bitmap = reconstructGraphicStrips(zpl, rowBytes, height, 1); + BufferedImage expected = PrintingTestDocuments.labelImage(dpi, profile.getHeightInches()); + assertEquals(width, expected.getWidth()); + assertEquals(height, expected.getHeight()); + int margin = dpi / 18; + int black = 0; + for(int y = 0; y < height; y++) + { + for(int x = 0; x < rowBytes * 8; x++) + { + boolean set = (bitmap[y * rowBytes + x / 8] & (0x80 >>> (x % 8))) != 0; + boolean expectedBlack = x < width && (expected.getRGB(x, y) & 0xffffff) == 0; + assertEquals(expectedBlack, set, "Reconstructed raster and row padding must match"); + if(expectedBlack) + { + black++; + assertTrue(x >= margin && x < width - margin && y >= margin && y < height - margin, "Ink outside margin at " + x + "," + y); + } + } + } + assertTrue(black > dpi * dpi / 8); + Matcher barcode = Pattern.compile("\\^FO(\\d+),(\\d+)\\^BY(\\d+)\\^BCN,(\\d+),N,N,N\\^FD" + PrintingTestDocuments.BARCODE_VALUE + "\\^FS").matcher(zpl); + if(profile.getHeightInches() == 1) + { + assertFalse(zpl.contains("^BC"), "A 4 x 1 label has no room for a scannable barcode"); + continue; + } + assertTrue(barcode.find()); + int x = Integer.parseInt(barcode.group(1)); + int y = Integer.parseInt(barcode.group(2)); + int module = Integer.parseInt(barcode.group(3)); + int barHeight = Integer.parseInt(barcode.group(4)); + assertFalse(barcode.find(), "Exactly one native barcode"); + int modules = 11 * (PrintingTestDocuments.BARCODE_VALUE.length() + 2) + 13; + assertTrue(x >= 10 * module + margin, "Quiet zone"); + assertTrue(x + modules * module <= 170 * dpi / 72, "Barcode must end before its caption"); + assertTrue(y > 111 * dpi / 72, "Barcode must sit below the ruler"); + assertTrue(y + barHeight < height - margin); + assertTrue(barHeight >= dpi / 8); + } + } + + + /*************************************************************************** ** Invalid combinations fail before a job can be created. ***************************************************************************/ -- 2.54.0 From 38fd06229e1f40d90538bb3c0c69b5caae26cc7e Mon Sep 17 00:00:00 2001 From: James Maes Date: Sun, 1 Nov 2026 17:30:18 -0600 Subject: [PATCH 2/4] fix: complete printing cohort verification and pin its contract Keep the compiler-clean label test helper and record the passed 193-case verification and matching protocol fixture pin without changing runtime behavior. Co-Authored-By: Claude Sonnet 5.5 Signed-off-by: James Maes --- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 8 ++++++++ README.md | 2 +- docs/SESSION-STATE.md | 12 ++++++++---- docs/TODO.md | 7 ++++--- .../qbits/printing/PrintingTestDocumentsTest.java | 2 +- 6 files changed, 23 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f6e991..052c164 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: QRun-IO/qqq-print-protocol - ref: a26c18c90c7d6102bbe26eb50cbb90fcd1bd46e3 + ref: 42c0d249f81395f00408ec0f92638dcbbf5deb03 path: qqq-print-protocol persist-credentials: false - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index ecc0deb..6f93c81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,14 @@ metadata, not by comparing versions. An agent that does not advertise `printer-media-v1` stays a three-capability peer and cannot claim a job carrying a media selection, whatever extension version it discovers. +- Built-in test prints add 4 x 1 and 4 x 2 inch labels (203/300/600 dpi) beside the unchanged + 4 x 6 path: compact PDF and ZPL layouts, and for short PDF labels an explicit actual-size + job paper (`paperWidthMm`/`paperHeightMm`, a complete positive pair up to 2000 mm, never + combined with a media profile). 4 x 6 and page profiles keep using the queue default. +- Strict source standards: Error Prone 2.51.0 and NullAway run as errors with `-Werror` on + main and test sources, with honest `@Nullable` contracts, strict source headers and + development-fixture style gates. No authorization, retained-source, replay or queue + behavior was changed by this work. - Realtime shutdown now waits, with a bound, for channel workers that are already running before the host's durable state is released. Previously the executor was only told to stop, so a late drain could query a stopped file database and diff --git a/README.md b/README.md index f37b9bf..112f0ad 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Java/.NET WebSocket jobs and S3 checks without an installed connector or printer ## Build and verify -Install Java 21. Clone this repository and `qqq-print-protocol` as siblings. This source pins protocol commit `f3cbc02e9b0fb4fdfefaa59d976c04df747cc589`; use that commit for reproducible compatibility tests. Use the checked-in wrapper (Maven 3.9.11). +Install Java 21. Clone this repository and `qqq-print-protocol` as siblings. This source pins protocol commit `42c0d249f81395f00408ec0f92638dcbbf5deb03` (the integrated contract under Gitea PR 2; the same ref CI checks out); use that commit for reproducible compatibility tests. Use the checked-in wrapper (Maven 3.9.11). ```powershell .\mvnw.cmd verify diff --git a/docs/SESSION-STATE.md b/docs/SESSION-STATE.md index 33b7a39..9b7cc6d 100644 --- a/docs/SESSION-STATE.md +++ b/docs/SESSION-STATE.md @@ -14,10 +14,14 @@ Its four conflicts (`PrintingProcesses`, `PrintingService`, `PrintingTestDocumen resolved by keeping all label behavior and tests and layering the strict contracts: `jobOptions` is `@Nullable` (null means the queue default applies), `validPaperMillimetres` accepts a missing value, redundant return wrappers are removed, and no suppression or rule change was added. Submission, authority, retained -binding, replay and queue code paths were not edited. **Pending, not observed:** the release cohort compile and -full verify, hosted CI, the final protocol fixture pin (the CI fixture ref still names the earlier reviewed -commit until the merged protocol sha is pinned), publication and any physical print. The installed printer -default is untouched; no printing was performed. +binding, replay and queue code paths were not edited. + +**Local gate passed (observed):** on the merged cohort, main and test compile clean under the strict compiler and +the full `-Pui-demo verify` passed with 193 tests, 0 failures, 0 errors, one intentional opt-in 10,000-job skip and +0 Checkstyle violations. CI and the README pin the final protocol commit +`42c0d249f81395f00408ec0f92638dcbbf5deb03` (merged Gitea protocol PR 2); earlier pins elsewhere in this file are +historical. **Still pending, not observed:** hosted Gitea CI, publication, installation and any physical print. The +installed printer default is untouched; no printing was performed. ## Echo operator/media and retained-source candidate — 2026-11-01 diff --git a/docs/TODO.md b/docs/TODO.md index 5cebb38..9d675de 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -40,9 +40,10 @@ - [x] Merge the 4 x 1 / 4 x 2 label profiles, compact PDF/ZPL layouts, explicit PDF job paper and their tests onto the strict standards tree, keeping the strict headers, null contracts and Error Prone fixes. -- [ ] Run the single cohort gate (compile, full verify, compiler guard) on the merged tree; no result is claimed yet. -- [ ] Pin the final merged protocol fixture sha in CI and confirm version/protocol references agree across the cohort. -- [ ] Observe hosted CI, then publish; physical confirmation of the short labels stays an owner step (4 x 6 stock is loaded). +- [x] Cohort gate passed locally on the merged tree: strict main/test compile and full `-Pui-demo verify`, 193 tests, + 0 failures, 0 errors, one intentional opt-in skip, 0 Checkstyle violations. +- [x] CI and the README pin the final protocol commit `42c0d249f81395f00408ec0f92638dcbbf5deb03` (merged Gitea PR 2). +- [ ] Observe hosted Gitea CI, then publish and install; physical confirmation of the short labels stays an owner step (4 x 6 stock is loaded). ## Current local development qualification diff --git a/src/test/java/com/kingsrook/qbits/printing/PrintingTestDocumentsTest.java b/src/test/java/com/kingsrook/qbits/printing/PrintingTestDocumentsTest.java index de25769..17a2fb3 100644 --- a/src/test/java/com/kingsrook/qbits/printing/PrintingTestDocumentsTest.java +++ b/src/test/java/com/kingsrook/qbits/printing/PrintingTestDocumentsTest.java @@ -221,7 +221,7 @@ class PrintingTestDocumentsTest ***************************************************************************/ private static byte[] reconstructGraphicStrips(String zpl, int rowBytes, int height) { - return (reconstructGraphicStrips(zpl, rowBytes, height, 2)); + return reconstructGraphicStrips(zpl, rowBytes, height, 2); } -- 2.54.0 From aaa4a9ff03c177d4bd0dca81ac011e6ccbf62871 Mon Sep 17 00:00:00 2001 From: James Maes Date: Sun, 1 Nov 2026 18:17:10 -0600 Subject: [PATCH 3/4] style: normalize development project copyright headers Co-Authored-By: Claude Sonnet 5.5 Co-Authored-By: Codex --- dev/client/Workload.csproj | 5 ++++- dev/native-auth/client/NativeAuth.csproj | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/dev/client/Workload.csproj b/dev/client/Workload.csproj index 74bb3c4..73fd9f5 100644 --- a/dev/client/Workload.csproj +++ b/dev/client/Workload.csproj @@ -1,4 +1,7 @@ - + Exe diff --git a/dev/native-auth/client/NativeAuth.csproj b/dev/native-auth/client/NativeAuth.csproj index 6dc7098..93aa3ae 100644 --- a/dev/native-auth/client/NativeAuth.csproj +++ b/dev/native-auth/client/NativeAuth.csproj @@ -1,4 +1,7 @@ - + Exe -- 2.54.0 From fc99caf85e5f232cd2383fc450fc167c9dc96c12 Mon Sep 17 00:00:00 2001 From: James Maes Date: Sun, 1 Nov 2026 20:52:57 -0600 Subject: [PATCH 4/4] test: make pinned printing contract fixtures self-contained Keep the exact existing protocol fixture bytes and the explicit cross-repository override. Default Maven verification no longer depends on a sibling checkout. Co-Authored-By: Claude Sonnet 5.5 Co-Authored-By: Codex --- .../qbits/printing/PrintingContractTest.java | 2 +- .../resources/protocol-fixtures/README.md | 30 +++++++++++++++++++ .../action-refresh-inventory.request.json | 5 ++++ .../action-revoke.request.json | 5 ++++ .../protocol-fixtures/approval.request.json | 3 ++ .../control-inventory-refreshed.receipt.json | 5 ++++ .../control-poll.request.json | 9 ++++++ .../create-enrollment.request.json | 4 +++ .../device-retry.request.json | 1 + .../device-test.request.json | 1 + .../protocol-fixtures/enroll.request.json | 5 ++++ .../protocol-fixtures/heartbeat.request.json | 3 ++ .../protocol-fixtures/printers.request.json | 25 ++++++++++++++++ .../protocol-fixtures/received.event.json | 4 +++ .../protocol-fixtures/spooled.event.json | 6 ++++ .../protocol-fixtures/submit.request.json | 12 ++++++++ .../protocol-fixtures/submitting.event.json | 4 +++ .../protocol-fixtures/test-label.zpl | 4 +++ 18 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 src/test/resources/protocol-fixtures/README.md create mode 100644 src/test/resources/protocol-fixtures/action-refresh-inventory.request.json create mode 100644 src/test/resources/protocol-fixtures/action-revoke.request.json create mode 100644 src/test/resources/protocol-fixtures/approval.request.json create mode 100644 src/test/resources/protocol-fixtures/control-inventory-refreshed.receipt.json create mode 100644 src/test/resources/protocol-fixtures/control-poll.request.json create mode 100644 src/test/resources/protocol-fixtures/create-enrollment.request.json create mode 100644 src/test/resources/protocol-fixtures/device-retry.request.json create mode 100644 src/test/resources/protocol-fixtures/device-test.request.json create mode 100644 src/test/resources/protocol-fixtures/enroll.request.json create mode 100644 src/test/resources/protocol-fixtures/heartbeat.request.json create mode 100644 src/test/resources/protocol-fixtures/printers.request.json create mode 100644 src/test/resources/protocol-fixtures/received.event.json create mode 100644 src/test/resources/protocol-fixtures/spooled.event.json create mode 100644 src/test/resources/protocol-fixtures/submit.request.json create mode 100644 src/test/resources/protocol-fixtures/submitting.event.json create mode 100644 src/test/resources/protocol-fixtures/test-label.zpl diff --git a/src/test/java/com/kingsrook/qbits/printing/PrintingContractTest.java b/src/test/java/com/kingsrook/qbits/printing/PrintingContractTest.java index 482aee8..fcd5e0c 100644 --- a/src/test/java/com/kingsrook/qbits/printing/PrintingContractTest.java +++ b/src/test/java/com/kingsrook/qbits/printing/PrintingContractTest.java @@ -50,7 +50,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; class PrintingContractTest { private static final String ADMIN = "contract-test-administrator-token"; - private static final Path FIXTURES = Path.of(System.getProperty("printing.protocol.fixtures", "../qqq-print-protocol/fixtures")); + private static final Path FIXTURES = Path.of(System.getProperty("printing.protocol.fixtures", "src/test/resources/protocol-fixtures")); /** Default hosts cannot consume enrollment codes through the legacy bearer transport. */ diff --git a/src/test/resources/protocol-fixtures/README.md b/src/test/resources/protocol-fixtures/README.md new file mode 100644 index 0000000..7f28bdc --- /dev/null +++ b/src/test/resources/protocol-fixtures/README.md @@ -0,0 +1,30 @@ + +# Protocol fixtures used by PrintingContractTest + +Unaltered copies of files from the `fixtures/` directory of the canonical `qqq-print-protocol` repository +at commit `42c0d249f81395f00408ec0f92638dcbbf5deb03` (the commit this source pins), so the contract tests run +without a sibling checkout. Only the files the test reads are copied: + +- `action-refresh-inventory.request.json` +- `action-revoke.request.json` +- `approval.request.json` +- `control-inventory-refreshed.receipt.json` +- `control-poll.request.json` +- `create-enrollment.request.json` +- `device-retry.request.json` +- `device-test.request.json` +- `enroll.request.json` +- `heartbeat.request.json` +- `printers.request.json` +- `received.event.json` +- `spooled.event.json` +- `submit.request.json` +- `submitting.event.json` +- `test-label.zpl` + +To check these against another protocol revision, run the tests with +`-Dprinting.protocol.fixtures=`. When the pinned commit changes, replace these +files from that commit's `fixtures/` directory. diff --git a/src/test/resources/protocol-fixtures/action-refresh-inventory.request.json b/src/test/resources/protocol-fixtures/action-refresh-inventory.request.json new file mode 100644 index 0000000..ddd9a26 --- /dev/null +++ b/src/test/resources/protocol-fixtures/action-refresh-inventory.request.json @@ -0,0 +1,5 @@ +{ + "requestId": "action-refresh-inventory-1", + "issuedAt": "2026-10-31T16:00:00Z", + "action": "REFRESH_INVENTORY" +} diff --git a/src/test/resources/protocol-fixtures/action-revoke.request.json b/src/test/resources/protocol-fixtures/action-revoke.request.json new file mode 100644 index 0000000..66ebe80 --- /dev/null +++ b/src/test/resources/protocol-fixtures/action-revoke.request.json @@ -0,0 +1,5 @@ +{ + "requestId": "action-revoke-1", + "issuedAt": "2026-10-31T16:00:00Z", + "action": "REVOKE" +} diff --git a/src/test/resources/protocol-fixtures/approval.request.json b/src/test/resources/protocol-fixtures/approval.request.json new file mode 100644 index 0000000..4e609c7 --- /dev/null +++ b/src/test/resources/protocol-fixtures/approval.request.json @@ -0,0 +1,3 @@ +{ + "enabled": true +} diff --git a/src/test/resources/protocol-fixtures/control-inventory-refreshed.receipt.json b/src/test/resources/protocol-fixtures/control-inventory-refreshed.receipt.json new file mode 100644 index 0000000..49905b2 --- /dev/null +++ b/src/test/resources/protocol-fixtures/control-inventory-refreshed.receipt.json @@ -0,0 +1,5 @@ +{ + "receiptId": "receipt-inventory-refreshed-1", + "outcome": "INVENTORY_REFRESHED", + "completedAt": "2026-10-31T16:00:05Z" +} diff --git a/src/test/resources/protocol-fixtures/control-poll.request.json b/src/test/resources/protocol-fixtures/control-poll.request.json new file mode 100644 index 0000000..cf22406 --- /dev/null +++ b/src/test/resources/protocol-fixtures/control-poll.request.json @@ -0,0 +1,9 @@ +{ + "controlVersion": 1, + "agentVersion": "0.2.0-rc.1", + "capabilities": [ + "REFRESH_INVENTORY", + "CANCEL_SPOOL" + ], + "lastAppliedRevision": 0 +} diff --git a/src/test/resources/protocol-fixtures/create-enrollment.request.json b/src/test/resources/protocol-fixtures/create-enrollment.request.json new file mode 100644 index 0000000..813d5f7 --- /dev/null +++ b/src/test/resources/protocol-fixtures/create-enrollment.request.json @@ -0,0 +1,4 @@ +{ + "facilityId": "test-facility", + "deviceName": "Packing station" +} diff --git a/src/test/resources/protocol-fixtures/device-retry.request.json b/src/test/resources/protocol-fixtures/device-retry.request.json new file mode 100644 index 0000000..74aa6bf --- /dev/null +++ b/src/test/resources/protocol-fixtures/device-retry.request.json @@ -0,0 +1 @@ +{"requestId":"operator-retry-1","confirmDuplicateRisk":true} diff --git a/src/test/resources/protocol-fixtures/device-test.request.json b/src/test/resources/protocol-fixtures/device-test.request.json new file mode 100644 index 0000000..6a79c4c --- /dev/null +++ b/src/test/resources/protocol-fixtures/device-test.request.json @@ -0,0 +1 @@ +{"printerId":"printer-zebra","format":"ZPL","requestId":"operator-test-1"} diff --git a/src/test/resources/protocol-fixtures/enroll.request.json b/src/test/resources/protocol-fixtures/enroll.request.json new file mode 100644 index 0000000..8cd692a --- /dev/null +++ b/src/test/resources/protocol-fixtures/enroll.request.json @@ -0,0 +1,5 @@ +{ + "enrollmentCode": "EXAMPLE-NOT-A-REAL-CODE", + "deviceName": "Packing station", + "agentVersion": "0.1.0" +} diff --git a/src/test/resources/protocol-fixtures/heartbeat.request.json b/src/test/resources/protocol-fixtures/heartbeat.request.json new file mode 100644 index 0000000..d9a3870 --- /dev/null +++ b/src/test/resources/protocol-fixtures/heartbeat.request.json @@ -0,0 +1,3 @@ +{ + "agentVersion": "0.1.0" +} diff --git a/src/test/resources/protocol-fixtures/printers.request.json b/src/test/resources/protocol-fixtures/printers.request.json new file mode 100644 index 0000000..0364595 --- /dev/null +++ b/src/test/resources/protocol-fixtures/printers.request.json @@ -0,0 +1,25 @@ +{ + "printers": [ + { + "printerId": "laser-example", + "name": "Local Laser", + "driverName": "Example Laser Driver", + "portName": "USB001", + "status": "READY", + "formats": [ + "PDF" + ] + }, + { + "printerId": "zebra-example", + "name": "Local Zebra", + "driverName": "Example Zebra Driver", + "portName": "USB002", + "status": "READY", + "formats": [ + "PDF", + "ZPL" + ] + } + ] +} diff --git a/src/test/resources/protocol-fixtures/received.event.json b/src/test/resources/protocol-fixtures/received.event.json new file mode 100644 index 0000000..e558a4a --- /dev/null +++ b/src/test/resources/protocol-fixtures/received.event.json @@ -0,0 +1,4 @@ +{ + "eventId": "event-received-example", + "status": "RECEIVED" +} diff --git a/src/test/resources/protocol-fixtures/spooled.event.json b/src/test/resources/protocol-fixtures/spooled.event.json new file mode 100644 index 0000000..f07228e --- /dev/null +++ b/src/test/resources/protocol-fixtures/spooled.event.json @@ -0,0 +1,6 @@ +{ + "eventId": "event-spooled-example", + "status": "SPOOLED", + "spoolerJobId": "42", + "message": "Accepted by the Windows spooler." +} diff --git a/src/test/resources/protocol-fixtures/submit.request.json b/src/test/resources/protocol-fixtures/submit.request.json new file mode 100644 index 0000000..8ce5386 --- /dev/null +++ b/src/test/resources/protocol-fixtures/submit.request.json @@ -0,0 +1,12 @@ +{ + "deviceId": "device-example", + "printerId": "zebra-example", + "title": "QQQ cloud print test", + "format": "ZPL", + "contentBase64": "XlhBCl5GTzQwLDQwXkEwTiwzNiwzNl5GRFFRUSBjbG91ZCBwcmludCB0ZXN0XkZTCl5GTzQwLDkwXkEwTiwyNCwyNF5GRFByb3RvY29sIHYxIC0gb25lIHRlc3QgbGFiZWxeRlMKXlhaCg==", + "idempotencyKey": "protocol-fixture-label-v1", + "options": { + "copies": 1, + "scale": "ACTUAL_SIZE" + } +} diff --git a/src/test/resources/protocol-fixtures/submitting.event.json b/src/test/resources/protocol-fixtures/submitting.event.json new file mode 100644 index 0000000..d4ee0de --- /dev/null +++ b/src/test/resources/protocol-fixtures/submitting.event.json @@ -0,0 +1,4 @@ +{ + "eventId": "event-submitting-example", + "status": "SUBMITTING" +} diff --git a/src/test/resources/protocol-fixtures/test-label.zpl b/src/test/resources/protocol-fixtures/test-label.zpl new file mode 100644 index 0000000..95d2cbf --- /dev/null +++ b/src/test/resources/protocol-fixtures/test-label.zpl @@ -0,0 +1,4 @@ +^XA +^FO40,40^A0N,36,36^FDQQQ cloud print test^FS +^FO40,90^A0N,24,24^FDProtocol v1 - one test label^FS +^XZ -- 2.54.0