Skip to content

Fix URIFactoryTest failing on Windows#7527

Merged
karianna merged 6 commits into
PCGen:masterfrom
karianna:fix-uri-factory-test-windows
Feb 26, 2026
Merged

Fix URIFactoryTest failing on Windows#7527
karianna merged 6 commits into
PCGen:masterfrom
karianna:fix-uri-factory-test-windows

Conversation

@karianna
Copy link
Copy Markdown
Contributor

Problem

URIFactoryTest.testGetURI_WithValidRootAndOffset() fails on Windows CI runners with:

java.net.URISyntaxException: Illegal character in opaque part at index 7: file:D:\a\pcgen\pcgen/data/file.txt

The test constructed the expected URI via new URI("file:" + System.getProperty("user.dir") + "/data/file.txt"). On Windows, user.dir contains backslashes (e.g. D:\a\pcgen\pcgen) which are illegal in URIs.

Fix

Use new File(System.getProperty("user.dir"), "data/file.txt").toURI() which produces a properly formatted file:/// URI with forward slashes on all platforms.

karianna and others added 6 commits February 26, 2026 22:11
Use File.toURI() instead of manually constructing a URI string with
new URI("file:" + path). On Windows, System.getProperty("user.dir")
returns backslash-separated paths (e.g. D:\a\pcgen\pcgen) which are
illegal characters in a URI, causing URISyntaxException.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@karianna karianna merged commit 737ac4a into PCGen:master Feb 26, 2026
0 of 2 checks passed
Vest pushed a commit to Vest/pcgen that referenced this pull request May 20, 2026
* Create separate action

* get the tag you created earlier

* upload zip artifacts for all platforms

* upload zip artifacts for all platforms

* Fix URIFactoryTest failing on Windows

Use File.toURI() instead of manually constructing a URI string with
new URI("file:" + path). On Windows, System.getProperty("user.dir")
returns backslash-separated paths (e.g. D:\a\pcgen\pcgen) which are
illegal characters in a URI, causing URISyntaxException.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
karianna added a commit that referenced this pull request May 22, 2026
* Replace commons-io FilenameUtils with JDK equivalents

Step 1 of removing the commons-io dependency. Adds private static
helpers in BatchExporter for getExtension/removeExtension semantics
(last-dot-in-final-segment), and uses java.io.File.getName() with a
null guard in SpellsKnownTab.

* Replace commons-io FileUtils.writeLines with Files.writeString

Step 2 of removing commons-io. The behavior is preserved: UTF-8
encoding, System.lineSeparator() between lines, and a trailing
separator after the last line — matching FileUtils.writeLines.

* Add unit tests for PCGIOHandler.write party-file format

Covers the structural contract of the party file: two lines, version
prefix, comma-separated character paths, trailing line separator,
UTF-8 encoding. Verified to pass against both the old FileUtils.writeLines
implementation and the new Files.writeString one.

* Refactor test method visibility in PCGIOHandlerWriteTest

Signed-off-by: Vest <Vest@users.noreply.github.com>

* Replace commons-io FileUtils.listFiles with Files.walk in DataTest

Step 3 of removing commons-io. Adds a small private helper that
mirrors FileUtils.listFiles(File, String[], boolean) semantics:
recursive, regular files only, case-sensitive extension match.

* Drop commons-io TeeOutputStream from BatchExporter PDF export

The original code wrapped a ByteArrayOutputStream in a TeeOutputStream so
the export simultaneously fed an in-memory buffer (consumed by FOP) and
an optional debug temp file. Since the bytes are fully materialised in
memory before FOP runs, the tee was unnecessary: write the temp file
sequentially after the export completes instead. This removes the last
commons-io dependency in this file and lets us delete the internal
TeeOutputStream helper.

* Report FOP errors from exportPartyToPDF

exportCharacterToPDF inspects task.getErrorMessages() after running FOP
and returns false (with a logged message) when FOP reports problems.
exportPartyToPDF was missing the same check, so a party PDF that FOP
failed to render would silently return true and leave the user thinking
the export succeeded. Mirror the character path: log and return false
when FOP produces a non-blank error message.

* Rewrite removeTemporaryFiles to use listFiles for what it is

The previous implementation called File.list(FilenameFilter) and used
the filter lambda for its side effect — deleting matching files inside
the predicate and always returning false so list() would return an
empty array the caller then ignored. That misuse hid three real
problems:

  - tf.delete() returned false silently; failures to delete were
    invisible.
  - The defensive try/catch around the filter caught exceptions a
    FilenameFilter is not allowed to throw — pure cargo cult.
  - File.list returns null when the directory does not exist; the
    accidental side-effect form happened to tolerate that, but only
    by coincidence.

Use listFiles to actually list the temp files matching the prefix,
guard against the null result, then iterate and delete with a logged
warning when delete fails. Drop the manual File.separator concatenation
in favour of SettingsHandler.getTempPath() (already a File).

* Use StandardCharsets.UTF_8 consistently in BatchExporter

exportCharacterToNonPDF already used StandardCharsets.UTF_8 but four
sibling methods (exportPartyToNonPDF, both exportParty overloads, and
the private exportCharacter overload) still passed the literal string
"UTF-8" to OutputStreamWriter. The two forms behave identically but
the string form forces callers to deal with UnsupportedEncodingException
and lets a typo go undetected until runtime. Switch all four sites to
the constant so the file is internally consistent and the encoding is
checked at compile time.

* Use Files.delete for diagnosable temp-file cleanup failures

removeTemporaryFiles previously used File.delete, which returns a bare
boolean on failure with no indication of why the OS refused the
operation — permission denied, file locked by another process,
filesystem read-only, path no longer present, etc. The log entry could
only say "Could not delete X", which is unactionable.

Switch to Files.delete(Path), which throws an IOException carrying the
underlying cause (NoSuchFileException, AccessDeniedException, or a
generic IOException with the platform message). Log the exception
along with the path so the operator has something to act on. Sonar
S4042 flags exactly this pattern.

* Use unnamed pattern for ignored ExportException in catch block

exportCharacterToNonPDF catches ExportException only to short-circuit
to a false return; the comment "Error will already be reported to the
log" makes the intent explicit. Naming the variable "e" suggested it
might be used and forced a reader to scan the catch body to confirm it
was not.

Use the unnamed pattern (_) introduced in JEP 456 / Java 21 to declare
the variable as deliberately unused. The compiler now enforces the
intent and the discard is visible at the declaration site. Sonar S7467
flags exactly this case.

* Convert Path to File directly in getXMLTemplate

The original code converted the Path to a URI and then constructed a
File from that URI. The intermediate URI step served no purpose: it
introduced URI parsing and a second File constructor invocation while
producing an equivalent File. Use Path.toFile() instead, which is the
direct API for this conversion.

* Guard generateOutputFilename against dotless filenames

generateOutputFilename built the output name as

    charname.substring(0, charname.lastIndexOf('.')) + '.' + extension

If charname has no dot, lastIndexOf returns -1 and substring(0, -1)
throws StringIndexOutOfBoundsException. Today this cannot happen in
practice because the only caller (exportCharacter / exportParty)
gates on PCGFile.isPCGenCharacterFile / isPCGenPartyFile, which
require .pcg or .pcp suffixes. But the guard is implicit and the
crash is a latent landmine if validation ever changes upstream.

Reuse removeFileExtension, which already handles the dotless case by
returning the input unchanged. Behaviour is identical for every name
that reaches this method today and safe for any name that might in
the future.

* Replace commons-io filters in ExportUtilities with JDK Predicate

getValidFiles built its template filter as a chain of commons-io
IOFileFilter wrappers: FileFilterUtils.asFileFilter(sheetFilter) +
prefixFileFilter + and() + filterList. The SheetFilter enum already
implements java.io.FilenameFilter, and the prefix check is a one-line
String.startsWith — there is nothing here that needs the IOFileFilter
abstraction.

Combine the two checks into a single Predicate<File> lambda and run
it through Stream.filter().sorted().collect(toList()) so the result
matches the prior FileFilterUtils.filterList + Collections.sort
semantics. This drops both commons-io.filefilter imports from the
file and removes one more user of the dependency we are stripping.

* Replace commons-io filters in PrintPreviewDialog with Files.walk

SheetLoader located printable templates with FileUtils.listFiles plus
a FileFilterUtils.and() chain of pdfFilter / suffixFilter /
sheetFilter and a TrueFileFilter for directory descent. To do this it
also implemented FilenameFilter purely so its accept() could be
wrapped via FileFilterUtils.asFileFilter(this) — the class was
serving as both worker and filter without need.

Replace the listing with Files.walk(...) filtered by Files::isRegularFile
and a single Predicate<File> that combines all three checks (parent
dir name == "pdf", name does not end with .fo, name starts with the
character template prefix). Drop the FilenameFilter implementation
and the @NotNull import that came with it; the directory walk is
recursive by default so no separate dir filter is needed.

Failure mode is now explicit: an IOException from Files.walk is
logged with the offending directory rather than swallowed inside
commons-io. Removes the only remaining commons-io.* usage in this
file.

* Drop commons-io as a direct dependency

With ExportUtilities, PrintPreviewDialog, BatchExporter and the rest
of code/src/java migrated off org.apache.commons.io there are no
remaining direct call sites — verified by grep across all source
sets and by compiling main, test, itest and slowtest with the
declarations removed.

Drop the implementation declaration from build.gradle, the
forceMerge entry under the jlink block, and the corresponding
requires org.apache.commons.io from module-info.java. Commons-io
may still be pulled transitively by FOP / batik on the runtime
classpath, but our own module no longer reads it.

* Refactor BatchExporter.removeTemporaryFiles to NIO and drop commons-io

Rewrite the cleanup loop using java.nio.file: Files.list returns a
Stream<Path>, so the temp-file scan, filter, and delete collapse into
a try-with-resources block with two small helpers
(isTemporaryExportFile, deleteQuietly). Behavioural improvements:

- An unreadable temp directory is now logged, not silently treated as
  empty (the legacy listFiles returned null for both "not a directory"
  and "I/O error").
- A missing temp directory is handled explicitly via Files.isDirectory
  before listing, instead of relying on listFiles' implicit null.
- Stream is closed deterministically by try-with-resources (matters on
  Windows file locking).

This was the last in-tree usage of commons-io, so drop the dependency
from build.gradle and the requires directive from module-info.java.

* Fix XTSE0710 xsl:use-attribute-sets for equipment.title across all game modes

Replace xsl:use-attribute-sets="equipment.title" with an equivalent
xsl:call-template name="attrib" call in fantasy_common.xsl (fantasy,
5e, starfinder, pathfinder_2, 4e, sagaborn) and killshot_common.xsl.
Saxon-HE rejects references to undeclared attribute sets at compile time
(XTSE0710); the attrib template is the correct runtime skinning mechanism.

Add XsltPdfTemplateCompilationTest to guard against future regressions:
compiles each fixed file directly under Saxon and fails on any
XTSE0710/XTSE0720 error, ignoring expected fragment-level errors.

* Fix URIFactoryTest failing on Windows (#7527)

* Create separate action

* get the tag you created earlier

* upload zip artifacts for all platforms

* upload zip artifacts for all platforms

* Fix URIFactoryTest failing on Windows

Use File.toURI() instead of manually constructing a URI string with
new URI("file:" + path). On Windows, System.getProperty("user.dir")
returns backslash-separated paths (e.g. D:\a\pcgen\pcgen) which are
illegal characters in a URI, causing URISyntaxException.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Signed-off-by: Vest <Vest@users.noreply.github.com>
Co-authored-by: Martijn Verburg <martijnverburg@gmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant