Files
Stirling-PDF/testing/tool-io-cases.json
James Brunton af5f54274d Add defaults to calculations for ToolIO (#7289)
# Description of Changes
Currently when calculating the output file type for some tools, the
system will get it wrong because it doesn't know about what the default
parameters in tools are, so if it doesn't have a value for some key,
it'll just bail out and say "it might not be compatible". This PR adds
logic to `ToolIO` to read the default values set for the parameters if
the tool has `ToolIOCase`s and takes them into account when figuring out
the output type. I've built it with horrible Java reflection magic to
avoid having to specify the default for params twice, which will make it
impossible for the defaults to disagree with each other. This just runs
once at startup so there's negligible performance impact.

The change is easily tested with Change Parameters, which is just
`add-password` behind the scenes but with the password params omitted
(so Change Password is always PDF->PDF, never encrypted like Add
Password).

Also (somewhat hackily) fixes a bug I noticed where saving a Change
Permissions step then leaving and returning to the pipeline will cause
the step to be reloaded as Add Password. I've added a system to
disambiguate tools which share the same endpoint (which is only these
two currently).

## Currently

<img width="455" height="135" alt="image"
src="https://github.com/user-attachments/assets/c8867d6a-599b-4f21-a2db-4a1b6ac22d73"
/>

## Now

<img width="415" height="122" alt="image"
src="https://github.com/user-attachments/assets/bd8d1bab-f00a-421b-8c91-5af0d2ad5335"
/>
2026-08-10 11:00:16 +00:00

336 lines
12 KiB
JSON

{
"$comment": [
"Shared conformance cases for the tool I/O compatibility check.",
"Run by Java (ToolChainValidatorConformanceTest), the frontend (toolIOCompat.test.ts) and the",
"engine (test_tool_io_compat.py) against their own implementations, which must agree.",
"Specs are synthetic, so retyping a real endpoint never invalidates a case.",
"'expected' is matched on stepIndex/severity/code; messages are free text."
],
"specs": {
"pdfToPdf": { "accepts": ["PDF"], "produces": "PDF", "arity": "SISO", "cases": [] },
"pdfToPdfSplit": { "accepts": ["PDF"], "produces": "PDF", "arity": "SIMO", "cases": [] },
"pdfMerge": { "accepts": ["PDF"], "produces": "PDF", "arity": "MISO", "cases": [] },
"addPassword": { "accepts": ["PDF"], "produces": "PDF_ENCRYPTED", "arity": "SISO", "cases": [] },
"removePassword": { "accepts": ["PDF_ENCRYPTED"], "produces": "PDF", "arity": "SISO", "cases": [] },
"extractImages": { "accepts": ["PDF"], "produces": "IMAGE", "arity": "SIMO", "cases": [] },
"imageToPdf": { "accepts": ["IMAGE"], "produces": "PDF", "arity": "MISO", "cases": [] },
"getAttachments": { "accepts": ["PDF"], "produces": "ZIP", "arity": "SISO", "cases": [] },
"getInfo": { "accepts": ["PDF"], "produces": "JSON", "arity": "SISO", "cases": [] },
"validateOnly": { "accepts": ["PDF"], "produces": "NONE", "arity": "SISO", "cases": [] },
"anyToPdf": { "accepts": ["ANY"], "produces": "PDF", "arity": "SISO", "cases": [] },
"pdfToAny": { "accepts": ["PDF"], "produces": "ANY", "arity": "SISO", "cases": [] },
"pdfOrImage": { "accepts": ["PDF", "IMAGE"], "produces": "PDF", "arity": "SISO", "cases": [] },
"pdfToImageConfigurable": {
"accepts": ["PDF"],
"produces": "IMAGE",
"arity": "SIMO",
"cases": [
{
"when": [{ "param": "singleOrMultiple", "matches": ["single"] }],
"produces": "IMAGE",
"arity": "SISO"
}
]
},
"addPasswordConfigurable": {
"accepts": ["PDF"],
"produces": "PDF_ENCRYPTED",
"arity": "SISO",
"cases": [
{
"when": [{ "param": "password", "matches": [""] }],
"produces": "PDF",
"arity": "SISO"
}
]
},
"pdfToImageMixedCase": {
"accepts": ["PDF"],
"produces": "IMAGE",
"arity": "SIMO",
"cases": [
{
"when": [{ "param": "singleOrMultiple", "matches": [" SiNgLe "] }],
"produces": "IMAGE",
"arity": "SISO"
}
]
},
"addPasswordTwoKeys": {
"accepts": ["PDF"],
"produces": "PDF_ENCRYPTED",
"arity": "SISO",
"cases": [
{
"when": [
{ "param": "password", "matches": [""] },
{ "param": "ownerPassword", "matches": [""] }
],
"produces": "PDF",
"arity": "SISO"
}
]
},
"autoRotateShape": {
"accepts": ["PDF"],
"produces": "PDF",
"arity": "SISO",
"cases": [
{
"when": [{ "param": "dryRun", "matches": ["true"], "default": "false" }],
"produces": "JSON",
"arity": "SISO"
}
]
},
"changePermsShape": {
"accepts": ["PDF"],
"produces": "PDF_ENCRYPTED",
"arity": "SISO",
"cases": [
{
"when": [
{ "param": "password", "matches": [""], "default": "" },
{ "param": "ownerPassword", "matches": [""], "default": "" }
],
"produces": "PDF",
"arity": "SISO"
}
]
}
},
"cases": [
{
"name": "a plain pdf chain is clean",
"steps": [{ "spec": "pdfToPdf" }, { "spec": "pdfToPdf" }],
"expected": []
},
{
"name": "an empty chain is clean",
"steps": [],
"expected": []
},
{
"name": "a single step is never a transition",
"steps": [{ "spec": "extractImages" }],
"expected": []
},
{
"name": "encrypting then editing is rejected",
"steps": [{ "spec": "addPassword" }, { "spec": "pdfToPdf" }],
"expected": [{ "stepIndex": 1, "severity": "ERROR", "code": "format-mismatch" }]
},
{
"name": "encrypting then decrypting is allowed",
"steps": [{ "spec": "addPassword" }, { "spec": "removePassword" }],
"expected": []
},
{
"name": "decrypting then editing is allowed",
"steps": [{ "spec": "removePassword" }, { "spec": "pdfToPdf" }],
"expected": []
},
{
"name": "an ordinary tool cannot be handed an encrypted pdf from source",
"sourceFormat": "PDF_ENCRYPTED",
"steps": [{ "spec": "pdfToPdf" }],
"expected": [{ "stepIndex": 0, "severity": "ERROR", "code": "source-mismatch" }]
},
{
"name": "remove-password is the one thing an encrypted source can start with",
"sourceFormat": "PDF_ENCRYPTED",
"steps": [{ "spec": "removePassword" }, { "spec": "pdfToPdf" }],
"expected": []
},
{
"name": "extracted images cannot feed a pdf tool",
"steps": [{ "spec": "extractImages" }, { "spec": "pdfToPdf" }],
"expected": [{ "stepIndex": 1, "severity": "ERROR", "code": "format-mismatch" }]
},
{
"name": "extracted images can feed an image tool, and fan in",
"steps": [{ "spec": "extractImages" }, { "spec": "imageToPdf" }],
"expected": [{ "stepIndex": 1, "severity": "INFO", "code": "fan-in" }]
},
{
"name": "splitting then a single-file tool fans out",
"steps": [{ "spec": "pdfToPdfSplit" }, { "spec": "pdfToPdf" }],
"expected": [{ "stepIndex": 1, "severity": "INFO", "code": "fan-out" }]
},
{
"name": "splitting then merging fans in",
"steps": [{ "spec": "pdfToPdfSplit" }, { "spec": "pdfMerge" }],
"expected": [{ "stepIndex": 1, "severity": "INFO", "code": "fan-in" }]
},
{
"name": "an archive deliverable does not feed a pdf tool",
"steps": [{ "spec": "getAttachments" }, { "spec": "pdfToPdf" }],
"expected": [{ "stepIndex": 1, "severity": "ERROR", "code": "format-mismatch" }]
},
{
"name": "a json report does not feed a pdf tool",
"steps": [{ "spec": "getInfo" }, { "spec": "pdfToPdf" }],
"expected": [{ "stepIndex": 1, "severity": "ERROR", "code": "format-mismatch" }]
},
{
"name": "a step producing no file terminates the chain",
"steps": [{ "spec": "validateOnly" }, { "spec": "pdfToPdf" }],
"expected": [{ "stepIndex": 1, "severity": "ERROR", "code": "format-mismatch" }]
},
{
"name": "a tool accepting anything swallows any upstream output",
"steps": [{ "spec": "extractImages" }, { "spec": "anyToPdf" }],
"expected": [{ "stepIndex": 1, "severity": "INFO", "code": "fan-out" }]
},
{
"name": "a tool producing anything satisfies any downstream input",
"steps": [{ "spec": "pdfToAny" }, { "spec": "imageToPdf" }],
"expected": []
},
{
"name": "a source of unknown type is not checked against step one",
"steps": [{ "spec": "imageToPdf" }],
"expected": []
},
{
"name": "a multi-format tool accepts either branch",
"steps": [{ "spec": "extractImages" }, { "spec": "pdfOrImage" }],
"expected": [{ "stepIndex": 1, "severity": "INFO", "code": "fan-out" }]
},
{
"name": "an unconfigured parameter-dependent output only warns",
"steps": [{ "spec": "addPasswordConfigurable" }, { "spec": "pdfToPdf" }],
"expected": [{ "stepIndex": 1, "severity": "WARN", "code": "output-uncertain" }]
},
{
"name": "configuring the parameter to the encrypting branch makes it an error",
"steps": [
{ "spec": "addPasswordConfigurable", "parameters": { "password": "hunter2" } },
{ "spec": "pdfToPdf" }
],
"expected": [{ "stepIndex": 1, "severity": "ERROR", "code": "format-mismatch" }]
},
{
"name": "configuring the parameter to the non-encrypting branch clears it",
"steps": [
{ "spec": "addPasswordConfigurable", "parameters": { "password": "" } },
{ "spec": "pdfToPdf" }
],
"expected": []
},
{
"name": "a multi-condition case needs every condition to hold",
"steps": [
{
"spec": "addPasswordTwoKeys",
"parameters": { "password": "", "ownerPassword": "hunter2" }
},
{ "spec": "pdfToPdf" }
],
"expected": [{ "stepIndex": 1, "severity": "ERROR", "code": "format-mismatch" }]
},
{
"name": "a multi-condition case applies when every condition holds",
"steps": [
{ "spec": "addPasswordTwoKeys", "parameters": { "password": "", "ownerPassword": "" } },
{ "spec": "pdfToPdf" }
],
"expected": []
},
{
"name": "a multi-condition case is unresolved when one condition is unconfigured",
"steps": [
{ "spec": "addPasswordTwoKeys", "parameters": { "password": "" } },
{ "spec": "pdfToPdf" }
],
"expected": [{ "stepIndex": 1, "severity": "WARN", "code": "output-uncertain" }]
},
{
"name": "the single branch of an arity case produces one file, so no fan-out",
"steps": [
{ "spec": "pdfToImageConfigurable", "parameters": { "singleOrMultiple": "single" } },
{ "spec": "imageToPdf" }
],
"expected": []
},
{
"name": "the default branch of an arity case fans in",
"steps": [
{ "spec": "pdfToImageConfigurable", "parameters": { "singleOrMultiple": "multiple" } },
{ "spec": "imageToPdf" }
],
"expected": [{ "stepIndex": 1, "severity": "INFO", "code": "fan-in" }]
},
{
"name": "a condition matches regardless of the casing and padding it was declared with",
"steps": [
{ "spec": "pdfToImageMixedCase", "parameters": { "singleOrMultiple": "SINGLE" } },
{ "spec": "imageToPdf" }
],
"expected": []
},
{
"name": "a condition declared in mixed case still fails on a genuinely different value",
"steps": [
{ "spec": "pdfToImageMixedCase", "parameters": { "singleOrMultiple": "multiple" } },
{ "spec": "imageToPdf" }
],
"expected": [{ "stepIndex": 1, "severity": "INFO", "code": "fan-in" }]
},
{
"name": "an absent parameter takes its default, which here does not trigger the case",
"steps": [{ "spec": "autoRotateShape" }, { "spec": "pdfToPdf" }],
"expected": []
},
{
"name": "an explicit value overrides the default and triggers the case",
"steps": [
{ "spec": "autoRotateShape", "parameters": { "dryRun": "true" } },
{ "spec": "pdfToPdf" }
],
"expected": [{ "stepIndex": 1, "severity": "ERROR", "code": "format-mismatch" }]
},
{
"name": "an absent parameter whose default triggers the case resolves to that branch",
"steps": [{ "spec": "changePermsShape" }, { "spec": "pdfToPdf" }],
"expected": []
},
{
"name": "setting the branch parameter away from its default flips the outcome",
"steps": [
{
"spec": "changePermsShape",
"parameters": { "password": "hunter2", "ownerPassword": "hunter2" }
},
{ "spec": "pdfToPdf" }
],
"expected": [{ "stepIndex": 1, "severity": "ERROR", "code": "format-mismatch" }]
},
{
"name": "an undeclared step warns and stops the chain being checked past it",
"steps": [{ "spec": "addPassword" }, { "spec": null }, { "spec": "pdfToPdf" }],
"expected": [{ "stepIndex": 1, "severity": "WARN", "code": "undeclared-operation" }]
},
{
"name": "the source format is not re-applied to a step that follows an undeclared one",
"sourceFormat": "PDF_ENCRYPTED",
"steps": [{ "spec": "removePassword" }, { "spec": null }, { "spec": "pdfToPdf" }],
"expected": [{ "stepIndex": 1, "severity": "WARN", "code": "undeclared-operation" }]
},
{
"name": "an undeclared first step does not pass the source format on to the next step",
"sourceFormat": "PDF_ENCRYPTED",
"steps": [{ "spec": null }, { "spec": "pdfToPdf" }],
"expected": [{ "stepIndex": 0, "severity": "WARN", "code": "undeclared-operation" }]
},
{
"name": "every broken transition is reported, not just the first",
"steps": [{ "spec": "extractImages" }, { "spec": "pdfToPdf" }, { "spec": "imageToPdf" }],
"expected": [
{ "stepIndex": 1, "severity": "ERROR", "code": "format-mismatch" },
{ "stepIndex": 2, "severity": "ERROR", "code": "format-mismatch" }
]
}
]
}