Skip to content

TestHelper: load plugins via JUnit extension; cleanup#7580

Merged
karianna merged 18 commits into
PCGen:masterfrom
Vest:testhelper-static-init
Jun 4, 2026
Merged

TestHelper: load plugins via JUnit extension; cleanup#7580
karianna merged 18 commits into
PCGen:masterfrom
Vest:testhelper-static-init

Conversation

@Vest

@Vest Vest commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Replace the manual TestHelper.loadPlugins() boilerplate with a proper JUnit 5 lifecycle hook. Originally tried a static initializer (early commits on this branch); that turned out to load plugins at JVM class-load time, before test setup — which broke :datatest (settings-file race) and :test (TestFX tearDown trying deep reflection without --add-opens). Rewrote using a BeforeAllCallback extension instead. Also keeps the orthogonal cleanups from the original PR: dead-method removal and parsePCClassText deduplication.

The PR also finishes the long-pending JUnit 4 → 6 migration (last three commits) — the getOrComputeIfAbsent deprecation warning that surfaced while building this branch is what kicked it off.

OK to squash, but the commit history is split into logical groups if reviewers prefer to keep them.

1. JUnit 5 extension (PCGenTestEnvironment)

  • New code/src/testcommon/pcgen/test/PCGenTestEnvironment.java — a BeforeAllCallback that runs Main.createLoadPluginTask().run() exactly once per JVM, keyed via ExtensionContext.Store.GLOBAL.
  • Opt-in via @ExtendWith(PCGenTestEnvironment.class), NOT auto-discovered. Auto-discovery would force plugin loading on tests like SetSolverManagerTest.setUp that explicitly assume an empty PluginFunctionLibrary (its addFunction(getOther) collides with the entry that plugin loading puts in the singleton). The original loadPlugins() was opt-in too — each test that wanted plugins called it explicitly. The annotation preserves that boundary while removing the boilerplate.
  • Applied to AbstractCharacterTestCase and AbstractJunit5CharacterTestCase. The annotation propagates to ~114 subclasses automatically.
  • Applied directly to the 10 standalone test classes that previously called TestHelper.loadPlugins().
  • TestHelper.loadPlugins() and the static initializer that briefly replaced it are both deleted. No call sites remain.

2. Main.runBootstrapTasks()

The 4-step bootstrap sequence (createLoadPluginTaskGameModeFileLoaderCampaignFileLoader, run via PCGenTaskExecutor) was duplicated verbatim in startupWithGUI(), startupWithoutGUI(), and open-coded twice more in DataTest.loadGameModes() / DataLoadTest.loadGameModes(). Pulled into one Main.runBootstrapTasks(PCGenTaskListener…) helper. All four call sites now use it. Eliminates the risk of one of them drifting out of order — the bug we just hit when the static-initializer attempt loaded plugins before Main.loadProperties().

3. TestFX/JavaFX 25 fix

Separately, :test was crashing with InaccessibleObjectException: module javafx.graphics does not "opens" com.sun.javafx.application from TestFX's cleanupParameters (which uses setAccessible(true) on a private static field). The existing --add-exports flag in build.gradle:684 wasn't enough — setAccessible needs --add-opens. Changed --add-exports--add-opens for com.sun.javafx.application. The other three --add-exports flags in the JVM-arg block stay as-is (they're for Monocle and JavaFX logging, which only need export-level access).

4. --stacktrace on every CI gradle invocation

Costs nothing on green builds (only fires on failure). This session needed two separate diagnostic commits — one for the TestFX InaccessibleObjectException, one for the static-initializer-induced JVM exit — purely because Gradle's default output for a crashed test JVM is non-zero exit value 1 with no further detail, plus a hint to use --stacktrace. Make that the next step happen automatically.

5. Original cleanup work (kept from earlier in the PR)

These were done before the static-initializer attempt and are unrelated to it. Worth keeping:

  • Dead-method removal in TestHelper: makeAbilityFromString, makeWeaponProf, loadGameModes(String) — zero callers anywhere in the tree, last real users removed years ago (2014, 2009, 2018 respectively). Recoverable from git via git log -S if anyone needs them.
  • parsePCClassText dedup: PCClassTest and AddClassSkillsTest carried near-identical 17-line copies. Both now route through the canonical TestHelper.parsePCClassText. -62/+5 lines net.
  • IDE-flagged warnings: 4 unused imports, an unthrown throws, raw-type → parameterized Class<?>, string-based class-walk replaced by identity compare, redundant local inlined, parameterized log calls, missing Javadoc filled in.

6. JUnit 4 → 6 migration (final three commits)

The BOM was already on org.junit:junit-bom:6.1.0, but several pieces of test scaffolding still referenced JUnit 3/4 APIs. The build comment claiming "~870 legacy tests" was stale by years — actual count of pure-JUnit-4 files at the start of this work was 1, plus 86 hybrid files using org.junit.Assert.* static imports.

  • Deprecated APIs in test scaffolding (aca6bc17a5): ExtensionContext.Store#getOrComputeIfAbsent was deprecated in JUnit 6.0 in favor of #computeIfAbsent (mirrors java.util.Map) — PCGenTestEnvironment swapped over. AbstractFormulaTestCase and TestUtilities dropped junit.framework.TestCase.fail for Jupiter Assertions.fail. RecursiveFileFinderTest swapped org.junit.Test for the Jupiter @Test.

  • Bulk AssertAssertions rewrite (efeb5e11a5): 86 files, 1054 message-arg position swaps. JUnit 4's assertEquals(message, expected, actual) becomes Jupiter's assertEquals(expected, actual, message). The trap is that mass-rewriting the imports alone would compile cleanly because Jupiter's assertEquals(Object, Object, String) overload silently matches the old shape with message interpreted as expected and vice versa — every failure message would be wrong, but the test class would still pass when assertions hold. The script (paren-balanced parser, top-level comma split) only swaps when the first arg is a string literal and the arity matches the JUnit-4 leading-message shape (3+ for assertEquals/assertSame/assertNotSame/assertArrayEquals, 2 for the rest). Nine cases where the leading message was a +-concatenation expression rather than a literal were caught by the compiler (the assertTrue(BooleanSupplier, ...) overload doesn't accept a String first arg) and fixed by hand in PreRaceTest and EquipmentSetFacadeImplTest.

  • Drop the dead deps (ac6034694c): junit:junit:4.13.2 and junit-vintage-engine come out of both build.gradle files. Stale comment removed.

Test plan

  • ./gradlew :test :itest :datatest :slowtest — all four green locally before the JUnit 6 commits (:slowtest ~11 min, others a couple minutes total).
  • Reviewed the test-result XMLs under build/test-results/{test,itest,datatest,slowtest}/ — no failures, no errors, no skipped tests beyond the existing baseline.
  • After the JUnit 6 commits: compileTestJava, compileSlowtestJava, compileItestJava all clean; :test, :itest, and a representative :slowtest set (StatListTest, PrereqHandlerTest, PObjectTest, PreRaceTest) green.
  • Full :slowtest and :datatest re-run after the JUnit 6 commits — the migration is mechanical but worth confirming end-to-end.
  • CI green on this PR's Build PCGen with Gradle workflow.

Vest added 4 commits June 3, 2026 07:13
`TestHelper.loadPlugins()` was already idempotent — a `boolean loaded`
flag guarded the expensive `Main.createLoadPluginTask().run()` call so
only the first invocation did real work. The remaining cleanup turns
that runtime guard into a structural one:

* Replace the mutable `boolean loaded` field + `if (!loaded)` branch
  with a private static initializer. Plugin loading now runs exactly
  once when `TestHelper` is first class-loaded, enforced by the JVM
  rather than by a flag check that future maintainers could bypass.
* Drop the two internal `loadPlugins()` calls in `makeEquipment` and
  `makeAbilityFromString` — they're inside the same class that defines
  the no-op, so by the time the JVM is executing those methods the
  static initializer has already run.
* Keep the no-op `loadPlugins()` method and the 12 external call sites.
  In most callers `loadPlugins()` is the only `TestHelper` reference, so
  removing it would let unused-import cleanup strip `import TestHelper`,
  which would in turn prevent class-loading and skip the static
  initializer. The "redundant" calls are load-bearing: they keep
  `TestHelper` on each test's class-load graph.
* Add an inline comment to silence SonarQube S1186 ("Methods should not
  be empty") and to record the load-bearing rationale in-place.
- Remove unused imports (BufferedReader, FileInputStream,
  InputStreamReader, PersistenceLayerException)
- Drop unthrown 'throws PersistenceLayerException' from parsePCClassText
- Parameterize Class<?> in createSource
- Replace string-based "Object".equals(clazz.getName()) walk-stop with
  identity compare clazz != Object.class
- Inline redundant pccFilesPath local in findDataFolder
- Switch three concatenated LOG.info calls to parameterized LOG.log
- Fill in missing @param entries on hasWeaponProfKeyed
PCClassTest and AddClassSkillsTest each carried a private duplicate of
the same parsePCClassText helper, declared with an unthrown
PersistenceLayerException. Now that the canonical helper in TestHelper
has had its stale throws removed, the local copies are pure dead code
and the AddClassSkillsTest try/catch was already unreachable.
…loadGameModes

None of the three are referenced anywhere in the repo (no callers, no
reflective lookups). Removing them also lets us drop the unused abLoader
and source fields plus six imports they pulled in (AbilityLoader,
CampaignFileLoader, ConfigurationSettings, PCGenTask,
PropertyContextFactory, SystemUtils).
@Vest Vest marked this pull request as draft June 3, 2026 05:41
Vest added 2 commits June 3, 2026 07:47
TestFX's ApplicationExtension.afterEach calls cleanupParameters, which
does setAccessible(true) on the private static ParametersImpl.params
field — deep reflection that --add-exports doesn't grant. Pre-Java 25
this slipped through; on JavaFX 25 it throws InaccessibleObjectException
("module javafx.graphics does not opens com.sun.javafx.application to
unnamed module") and crashes the test JVM, which surfaces downstream as
"non-zero exit value 1" with no test failures reported.

Replace --add-exports with --add-opens for that single package; the rest
of the JVM-arg block is correct.

Also revert the temporary --stacktrace --info debug flags from the test
workflow (they were a one-shot diagnostic, not a permanent setting; they
balloon the CI log to ~100k lines).
@Vest

Vest commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

Diagnosed the failure — it isn't actually caused by this PR's static-initializer change. The :datatest step's non-zero exit value 1 with zero reported test failures is a downstream symptom; the real crash is in :test (which runs as part of ./gradlew build):

java.lang.reflect.InaccessibleObjectException: Unable to make field
private static java.util.Map com.sun.javafx.application.ParametersImpl.params accessible:
module javafx.graphics does not "opens com.sun.javafx.application" to unnamed module @609e8838

  at org.testfx.toolkit.impl.ToolkitServiceImpl.cleanupParameters(ToolkitServiceImpl.java:177)
  at org.testfx.toolkit.impl.ToolkitServiceImpl.cleanupApplication(ToolkitServiceImpl.java:138)
  at org.testfx.api.FxToolkit.cleanupAfterTest(FxToolkit.java:178)
  at org.testfx.framework.junit5.ApplicationExtension.afterEach(ApplicationExtension.java:87)

Full stack in run 26866284491. The :datatest JVM that runs after this inherits the broken classpath/JVM-args setup and dies at startup, hence the orphan-java-pid messages and zero JUnit XMLs.

What's wrong

build.gradle line 684 had --add-exports for javafx.graphics/com.sun.javafx.application to work around TestFX/TestFX#638. But TestFX's ApplicationExtension.afterEachToolkitServiceImpl.cleanupParameters does setAccessible(true) on a private static field — that's deep reflection, requires --add-opens, not just --add-exports. --add-exports only exposes public types to compilation. JavaFX 25 GA tightened up the difference; older JavaFX versions silently accepted the weaker flag.

Why this PR triggered a latent bug

The pre-existing --add-exports was load-bearing only for tests that touched JavaFX during their @AfterEach. Moving plugin loading into TestHelper.<clinit> means a much broader set of tests now bootstraps JavaFX via Main.createLoadPluginTask, which lands in TestFX's lifecycle, which then trips the cleanup hook.

Fix

Pushed d487ffc6d1 which:

  • Changes the single --add-exports for com.sun.javafx.application--add-opens (the rest of the JVM-arg block is correct as-is — com.sun.javafx.util and com.sun.javafx.logging only need --add-exports).
  • Reverts the temporary --stacktrace --info flags I added to gradle-test.yml for diagnosis (they were a one-shot, not a permanent setting — they balloon the log to ~100k lines).

Should be green now. Worth a re-run.

@Vest

Vest commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

The TestFX --add-opens fix worked — the earlier InaccessibleObjectException is gone, :test and :itest now pass. But :datatest is still crashing at JVM startup (~4s after fork, before any test code runs).

Looking at the diff again, this is almost certainly the original concern about the static initializer:

  • :datatest's test classes (DataTest, DataLoadTest) both import pcgen.util.TestHelper. JUnit's class loader touches TestHelper during test discovery → triggers TestHelper.<clinit>Main.createLoadPluginTask().run().
  • Unlike :test (which sets up TestFX/Monocle/prism.order=sw/etc. in its jvmArgs), :datatest runs with bare-bones JVM args. So Main.createLoadPluginTask() is running in a JVM environment where its expectations about working directory, ConfigurationSettings, plugin path layout, etc. may not hold.
  • In the OLD code, loadPlugins() was called from @BeforeEach setup hooks AFTER tests had configured their environment. Moving it into <clinit> means it runs at a much earlier (and less prepared) lifecycle point.

Just pushed c4f9d849 which adds --stacktrace --info to the :datatest invocation so the actual exception will show in the next failed log. That'll confirm whether it's an ExceptionInInitializerError from Main.createLoadPluginTask, or something else entirely. Will revert the diagnostic flag once we have the cause.

@Vest

Vest commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

The diagnostic flags exposed the cause. From the :datatest log:

```
DataLoadTest > testLoadSources STANDARD_ERROR
INFO: Using PCC Location of 'data'.
SEVERE Test worker Main:263 No settingsDir specified via -s in batch mode and no default exists.
```

Then the JVM exits — that SEVERE message is from Main.java:263 calling GracefulExit.exit(1). No exception, no stack trace; the JVM just dies.

Why

Look at the proper init order inside Main.startupWithGUI():

```java
loadProperties(true); // line 185 — load properties FIRST
PCGenTaskExecutor executor = new PCGenTaskExecutor();
executor.addPCGenTask(createLoadPluginTask()); // line 192 — THEN load plugins
```

DataLoadTest.loadGameModes() follows the same order:

```java
String pccLoc = TestHelper.findDataFolder(); // [1]
TestHelper.createDummySettingsFile(TEST_CONFIG_FILE, ...); // [2]
configFactory.registerAndLoadPropertyContext(...); // [3] sets SETTINGS_FILES_PATH
Main.loadProperties(false); // [4]
PCGenTask loadPluginTask = Main.createLoadPluginTask(); // [5]
loadPluginTask.run();
```

In the OLD code, step [1] just returned the data folder path — loadPlugins() was a separate method that the test never called itself (Main.loadProperties handled it). Order was preserved.

In the NEW code, step [1] triggers TestHelper.<clinit> which immediately runs Main.createLoadPluginTask().run() — that's plugin loading happening BEFORE step [4]'s loadProperties. By the time step [4] runs, something in the plugin-loading path has perturbed system properties / ConfigurationSettings state enough that loadProperties no longer sees SETTINGS_FILES_PATH, and Main exits the JVM.

Recommendation

Revert the static-initializer approach. The stated goal — "plugins load once automatically without each test having to call loadPlugins()" — is fine in principle, but \<clinit\> is the wrong mechanism: it runs whenever any classloader touches TestHelper.class, including at JUnit discovery time, which is well before any test's @BeforeAll/setup has run. Better alternatives:

  1. Keep the original loaded guard. Old code was three lines and worked. The "I keep forgetting to call loadPlugins" annoyance is real but doesn't justify breaking the init order.
  2. JUnit 5 extension. Write a BeforeAllCallback registered via @ExtendWith(LoadPluginsExtension.class) on test classes (or globally via @AutoEnable). That way plugin loading happens AFTER the JUnit lifecycle has set up its state and run user @BeforeAll/@BeforeEach, but before the test method.
  3. Move the call inside findDataFolder()/createSource(). They're the methods that actually depend on plugins being loaded. Idempotent guard inside the method body keeps it lazy and ordered correctly.

Option 1 is the smallest change and gets the build green immediately. Options 2 and 3 are nicer if you want to address the original ergonomic concern.

Want me to push option 1 as a "revert + targeted improvements" commit on this branch, or do you want to take it from here?

Vest added 6 commits June 3, 2026 13:59
The plugin/game-mode/campaign loader sequence was duplicated verbatim
in startupWithGUI() and startupWithoutGUI(). Pull it into a single
package-private helper so test code can run the same sequence and
the order can never drift between the two callers.

Behaviour-preserving refactor; no production changes.
Loads PCGen plugins exactly once per JVM via a BeforeAllCallback,
keyed on ExtensionContext.Store.GLOBAL so the work happens at most
once across the whole test JVM. Auto-discovered via
META-INF/services and the autodetection.enabled property, so test
classes don't need any annotation to opt in.

This is the proper replacement for the static initializer the
previous commit introduced: BeforeAllCallback fires after JUnit's
lifecycle is set up but before any user @BeforeAll runs, so it
no longer perturbs the init order that DataTest/DataLoadTest
require.
The static initializer ran Main.createLoadPluginTask() at class-load
time — earlier than DataTest/DataLoadTest's expected init order, which
caused Main.loadProperties() to find no settings file and call
GracefulExit.exit(1), tearing down the test JVM with no stack trace.

PCGenTestEnvironment (added in the previous commit) now owns plugin
loading, runs at the correct point in the JUnit lifecycle, and is
auto-discovered for every test class.

Keep loadPlugins() as a deprecated no-op so existing call sites keep
compiling — they can be cleaned up incrementally rather than all in
one go.
The two loadGameModes() helpers each open-coded the same 4-step
sequence (createLoadPluginTask + GameModeFileLoader + CampaignFileLoader,
each constructed and run individually). Replace with the runBootstrapTasks()
helper extracted in the earlier commit, so all three call sites
(production GUI, production headless, test) share one definition of
the canonical bootstrap order.

Make runBootstrapTasks public — it's the same visibility the existing
loadProperties() and createLoadPluginTask() helpers already have.
Auto-discovery via META-INF/services would force plugin loading on
EVERY test class in the JVM, including ones that explicitly assume
plugins are NOT loaded. Concrete example: SetSolverManagerTest.setUp
constructs a fresh VariableContext and calls addFunction(getOther),
which fails with 'Cannot load two functions of name: getOther' if
PluginFunctionLibrary already has it (plugin loading populates that
singleton, and VariableContext's constructor copies all entries from
it).

Make plugin loading opt-in: @ExtendWith(PCGenTestEnvironment.class)
on classes that need it. Apply to AbstractCharacterTestCase and
AbstractJunit5CharacterTestCase (lifts ~114 subclasses) plus the
10 standalone test classes that previously called loadPlugins()
explicitly. Drop the META-INF/services + junit-platform.properties
files.
@Vest

Vest commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

Rewrote the static-initializer approach as five logical commits (572dde5..d02509c). All four test source sets (`:test`, `:itest`, `:datatest`, `:slowtest`) pass locally.

Summary

# Commit Change
1 572dde5 `Main.runBootstrapTasks()` extracted from `startupWithGUI`/`startupWithoutGUI`. One canonical definition of the loader sequence; production behaviour-preserving.
2 05f9bde New `PCGenTestEnvironment` JUnit 5 `BeforeAllCallback` in `code/src/testcommon/pcgen/test/`. Loads plugins once per JVM.
3 08c8d56 Static initializer removed from `TestHelper`; `loadPlugins()` kept as a deprecated no-op.
4 73a4d7b `DataTest`/`DataLoadTest`'s open-coded loader dance replaced with `Main.runBootstrapTasks()`.
5 d02509c Plugin loading is opt-in via `@ExtendWith(PCGenTestEnvironment.class)`, not auto-discovered.

Why opt-in instead of auto-discovery

I initially wired auto-discovery via `META-INF/services` so every test class would get plugins for free. That broke `SetSolverManagerTest`:

```
java.lang.IllegalArgumentException: Cannot load two functions of name: getOther
at pcgen.base.formula.inst.SimpleFunctionLibrary.addFunction
at pcgen.rules.context.VariableContext.addFunction
at pcgen.base.solver.SetSolverManagerTest.setUp(SetSolverManagerTest.java:78)
```

`VariableContext`'s constructor copies all functions from the global `PluginFunctionLibrary` singleton (`code/src/java/pcgen/rules/context/VariableContext.java:147`). `SetSolverManagerTest` builds a fresh `VariableContext` and adds `GetOtherFunction` itself — assuming the singleton was empty. The test was relying on plugins NOT being loaded. Auto-discovery breaks that assumption.

The OLD code already had this property: `loadPlugins()` was opt-in (each test that wanted plugins called it from `@BeforeEach`/`@BeforeAll`). Tests that didn't call it ran without plugins, and `SetSolverManagerTest` was one of them. The `@ExtendWith` annotation preserves that boundary while removing the boilerplate.

Where the annotation is applied

  • `AbstractCharacterTestCase` and `AbstractJunit5CharacterTestCase` — the two abstract bases that previously called `loadPlugins()` from their setup. The annotation propagates to ~114 subclasses automatically.
  • 10 standalone test classes that previously called `loadPlugins()` directly.
  • All call sites of `TestHelper.loadPlugins()` are gone (the method is kept as a deprecated no-op for any external callers).

Verification

```
$ ./gradlew :test :itest :datatest :slowtest
BUILD SUCCESSFUL
```

(`:slowtest` took 11 min; the other three a couple minutes total.)

Vest added 2 commits June 3, 2026 15:04
No call sites remain in the tree. The previous commit kept it as a
deprecated shim only to avoid touching every test class in one go;
that's now done. Drop it.
Costs nothing on green builds (only fires on failure), saves a
diagnostic round-trip when CI fails for a non-obvious reason. This
session needed two separate diagnostic commits (one for the
TestFX/JavaFX 25 InaccessibleObjectException and one for the
TestHelper static-initializer-induced JVM exit) precisely because
neither failure showed a stack trace by default — Gradle just said
'non-zero exit value 1' and pointed at --stacktrace as the next
step. Make that the next step happen automatically.
@Vest Vest changed the title TestHelper: load plugins via static initializer TestHelper: load plugins via JUnit extension; cleanup Jun 3, 2026
@Vest Vest marked this pull request as ready for review June 3, 2026 13:09
Vest added 3 commits June 3, 2026 18:11
ExtensionContext.Store#getOrComputeIfAbsent was deprecated in JUnit 6.0
in favor of #computeIfAbsent (mirrors java.util.Map). Three other files
still imported junit.framework.TestCase / org.junit.Test from JUnit 3/4
even though the rest of their bodies were already on Jupiter.

PCGenTestEnvironment switches to computeIfAbsent. AbstractFormulaTestCase
and TestUtilities drop junit.framework.TestCase.fail in favor of Jupiter
Assertions.fail. RecursiveFileFinderTest swaps org.junit.Test for the
Jupiter @test.
JUnit 6 drops junit:junit from the classpath, so the org.junit.Assert
static helpers used by 86 test files no longer resolve. Each file now
imports the equivalent from org.junit.jupiter.api.Assertions.

Jupiter reverses the message argument convention: JUnit 4's
assertEquals(message, expected, actual) becomes
assertEquals(expected, actual, message). Mass-applying the import swap
alone would silently match Jupiter's assertEquals(Object, Object, String)
overload with `message` read as `expected` and vice versa - the calls
would compile but every failure message would be wrong. The 1054
swap-able call sites (assertEquals/assertTrue/assertFalse/assertNull/
assertNotNull/assertSame/assertArrayEquals with leading String literal)
are reordered to match Jupiter's convention. Nine sites where the
leading message was a String concatenation expression rather than a
literal were caught by the compiler and fixed by hand.
With every test file now on Jupiter, neither the junit:junit jar nor the
junit-vintage-engine runner is needed. The stale comment claiming "~870
legacy tests" is removed - the actual count was zero by the time the
migration finished.
@karianna karianna merged commit eeebb35 into PCGen:master Jun 4, 2026
3 checks passed
@Vest Vest deleted the testhelper-static-init branch June 6, 2026 12:11
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.

2 participants