Skip to content

[#159] Refuse a duplicate name and return the new uid in an XML update - #197

Merged
maximthomas merged 5 commits into
OpenIdentityPlatform:masterfrom
maximthomas:issues/159-xml-update-name
Oct 20, 2026
Merged

maximthomas merged 5 commits into
OpenIdentityPlatform:masterfrom
maximthomas:issues/159-xml-update-name

Conversation

@maximthomas

@maximthomas maximthomas commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

XMLHandlerImpl.update now refuses to rename an entry to a __NAME__ that another entry of the same class has, and it refuses a blank __NAME__ even where the XSD makes __NAME__ optional. When the class has no __UID__, so the uid is the name, it returns the new uid.

Before, create refused a name that already existed or was blank, but update accepted any new __NAME__, a blank one too when the XSD made __NAME__ optional, and it always returned the uid it was given.

call before after
update(ACCOUNT, <uid of n2>, {__NAME__=n1}) while another account is n1 accepted; a search for n1 returns two objects, and authenticate as n1 with n2's password fails with InvalidPasswordException (#159) AlreadyExistsException: Could not update entry. An entry with the __NAME__ of n1 already exists.; entry and file unchanged
update(GROUP, Rebels, {__NAME__=Alliance}) (__GROUP__ has no __UID__) returns Rebels; getObject(GROUP, Rebels) is null returns Alliance
update(ACCOUNT, <uid>, {__NAME__=""}) where a user XSD makes __ACCOUNT__'s __NAME__ optional accepted; a search of the whole class, or by that entry's __UID__, then fails with IllegalArgumentException: The Attribute set must contain a 'Name'. " " is accepted as well and fails the same way once the file is saved and loaded again IllegalArgumentException: Parameter '__NAME__' must not be blank.; entry unchanged
update(GROUP, dup, {__NAME__=""}) where a user XSD makes __GROUP__'s __NAME__ optional accepted; a search of the whole class then fails with IllegalArgumentException: Uid value must not be blank! IllegalArgumentException: Parameter '__NAME__' must not be blank.; entry unchanged
  • The check runs in update's check phase (XML connector: a failed update is saved half-applied #158), after the attribute's own checks and before the entry changes. A refused rename leaves the document and the file as they were. A blank __NAME__ is still reported as Parameter '__NAME__' must not be blank.
  • It uses the same index lookup as create's duplicate check: names are looked up within the entry's object class, so an account may still have a group's name, and values are compared as they are (" padded " is not "padded").
  • An entry may send the name it already has, even when another entry has it too (duplicates already in a file). update(u2, {__NAME__=dup, lastname=L2b}) still succeeds when u1 is dup as well.
  • __NAME__ is checked as a required attribute in every class, even where a user XSD makes it optional: create requires a name whatever the XSD says, and the framework builds no object without a Name. One cleared entry made every search of its class fail, reconciliation included, and it could not be read by its __UID__ to be repaired. The blank check runs before the duplicate check, so a blank name is reported as blank, not as taken.
  • The message names __NAME__. create's duplicate message still names __UID__; it is not changed here.
  • For a class with __UID__, the returned uid is unchanged.

Two existing tests renamed an entry into a name another entry had, and they now use other names. XMLHandlerIndexTests#renamedEntryIsFoundByItsNewNameOnly renames u1 to renamed-again instead of back to dup, which u2 has. XMLHandlerIndexTests#indexAgreesWithXQueryAfterChanges no longer renames u2 to renamed and back to dup; instead u1 re-sends dup, its own name. Re-linking u1 in front of u2 also pins the ordered insert of EntryIndex.link (EntryIndex.java:225). The old step re-linked u2 at the end of the list, which is also its place in document order, so it could not catch an insert at the end.

Not fixed here:

  • A hand-edited file can nest an entry inside another entry's __NAME__ element (XML connector: update, delete and authenticate break on an entry below another element #182). Changing the inner entry leaves the outer entry's index key stale until the next reload (EntryIndex.java:49-51). Until then, the check can accept a rename into the outer entry's current name, and refuse the outer entry re-sending its current name when another entry has it too. The connector never writes that shape.
  • A DOM failure after the rename is applied (XML connector: update, delete and authenticate break on an entry below another element #182) still leaves the entry renamed. For a class without __UID__, the caller gets the exception and keeps the old uid.
  • An entry whose __NAME__ is already blank in the file (hand-edited, or cleared by update before this change) still makes every search of its class fail. The read side is not changed.

Test strength

arm case mutant killed red at
XMLHandlerImpl.java:357-362 refuse a __NAME__ another entry has XMLHandlerIndexTests#renameIntoANameAnotherEntryHasIsRefused step 1 (the message) no check; __UID__ in the message 1eeff90
same, before the entry changes XMLHandlerReloadTests#renameIntoATakenNameLeavesTheEntryAndTheFileAlone (firstname first, then the taken name: the message, the file's mtime and bytes, then both entries) check moved into the apply loop 1eeff90
:756-757 looked up within the entry's class renameIntoANameAnotherEntryHasIsRefused step 2 (group dup to The Empire, a group's name); keepingANameAnotherEntryAlsoHasIsAllowed step 3 (group dup to no-uid, an account's name) ObjectClass.ACCOUNT constant; both classes looked up those mutants only
same renameIntoATakenNameLeavesTheEntryAndTheFileAlone (accounts only), keepingANameAnotherEntryAlsoHasIsAllowed step 1, indexAgreesWithXQueryAfterChanges ObjectClass.GROUP constant (step 1 of renameIntoANameAnotherEntryHasIsRefused does not kill it: a group is named dup too) that mutant only
:758 an entry that has the name may keep it keepingANameAnotherEntryAlsoHasIsAllowed step 1 (u2 re-sends dup, which u1 has first); indexAgreesWithXQueryAfterChanges ("first duplicate re-keyed": u1 re-sends dup) refused whenever the name has a holder; refused unless the entry is the only holder; refused unless it is the first holder those mutants only
:357 only __NAME__ is checked keepingANameAnotherEntryAlsoHasIsAllowed step 2 (lastname = dup) every attribute checked that mutant only
:328 __NAME__ checked as required in every class, so a blank one is refused clearingAnOptionalNameIsRefused (a copy of the test XSD with an optional account __NAME__; "" and " " refused with the message, then a search of the whole class returns both accounts); clearingTheOptionalNameOfAnEntryWithoutUidIsRefused (a copy with an optional group __NAME__; the same refusals, then the group is still found as dup) __NAME__ checked only when required (the account's clear accepted; the group's fails on the framework's Uid value must not be blank!) 1eeff90
:334 the blank check before the duplicate check renameIntoANameAnotherEntryHasIsRefused step 3 (u1 to "" while u10's name is "" in the index: the blank message, not the duplicate one) duplicate check first that mutant only
EntryIndex.java:225 a re-keyed entry re-linked in document order indexAgreesWithXQueryAfterChanges ("first duplicate re-keyed") appended at the end that mutant only (green at 1eeff90)
:364, :365 the new name as uid when the class has no __UID__ keepingANameAnotherEntryAlsoHasIsAllowed step 3 (dup to no-uid returns no-uid); XMLConnectorTests#renamingAGroupReturnsItsNewNameAsUid (through the facade: Rebels to Alliance returns Alliance, getObject(Rebels) is null, a rename of Alliance to The Empire is refused) input uid returned 1eeff90
same, only then renamedEntryIsFoundByItsNewNameOnly (a renamed account keeps u1) isSupportUid ignored; inverted those mutants only (green at 1eeff90)
:401 the input uid otherwise keepingANameAnotherEntryAlsoHasIsAllowed step 3 (a __DESCRIPTION__ update of no-uid returns no-uid) newUid returned without the fallback that mutant only

mvn -pl OpenICF-xml-connector verify: 248 unit tests (242 + 6) and 1 IT, green on JDK 26 and JDK 11.

Fixes #159

… to a name another entry has

update() now checks a new __NAME__ against the entry index in its check phase, before the entry changes, and refuses it with AlreadyExistsException when another entry of the same class has it. An entry that already has the name may send it again, even when another entry has it too.
…mes an entry whose uid is its name

For an object class without a __UID__ element, create() makes the uid the name, but update() returned the uid it was given after a rename, so the caller kept a uid that no longer finds the entry. update() now returns the new name as the uid in that case.
…__ on more than one entry

create() refuses a blank name, so a blank name identifies no entry. The new uniqueness check treated a cleared optional __NAME__ as taken once another entry had cleared its name too; it now skips blank values.
@maximthomas maximthomas added bug Something isn't working java Pull requests that update java code tests Test additions or fixes connector:xml XML connector labels Oct 16, 2026
…f a class without __UID__

Without a __UID__ element the uid is the name, so a cleared name leaves an entry that no search of the class can read back. With an optional __NAME__ the required-name blank check is skipped, and update failed on new Uid("") with the framework's "Uid value must not be blank!"; it now refuses the blank name first with "Parameter '__NAME__' must not be blank.", as for a required name.
@maximthomas
maximthomas marked this pull request as ready for review October 16, 2026 08:09
@maximthomas
maximthomas requested a review from vharseko October 16, 2026 08:10

@vharseko vharseko left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked at 9e7d4dc. The duplicate check and the returned uid work as described: a refused rename leaves the entry and the file alone, an entry may re-send its own name, and a class without __UID__ gets its new uid back. One point, about the blank-name branch.

A cleared __NAME__ in a class with __UID__ leaves an entry that can no longer be read (XMLHandlerImpl.java:757-761, clearingAnOptionalNameOfTwoEntriesIsAllowed)

The description refuses a blank name in a class without __UID__ because "a search of the whole class then fails". The same happens in a class with __UID__, only with a different message. I ran it on this branch with the optional-name XSD from clearingAnOptionalNameOfTwoEntriesIsAllowed, accounts alice and bob:

step result
update(ACCOUNT, uid-alice, {__NAME__=""}), then the same for uid-bob both accepted, as the test asserts
search of the whole __ACCOUNT__ class IllegalArgumentException: The Attribute set must contain a 'Name'.
search by __UID__ = uid-alice the same
update(ACCOUNT, uid-carol, {__NAME__=" "}), then a search uid-carol| : readable until the save
dispose(), a new handler on the file, a search The Attribute set must contain a 'Name'.

setTextContent("") leaves the __NAME__ element without a text node (normalizeText does the same to " " on save or load). ConnectorObjectCreator.addAllAttributesToBuilder skips such an element, so the object has no Name, and ConnectorObject's constructor refuses it. One cleared entry is enough to make every search of the class fail, reconciliation included. getObject on that entry fails too, so it cannot even be read in order to repair it.

This is not new: on master, update accepted a blank optional __NAME__ in any class. But this PR now decides that case on purpose, and the test pins it as allowed. Two things suggest refusing it in every class, as the PR already does without __UID__:

  • create never makes a nameless entry. It requires __NAME__ whatever the XSD says (__NAME__ must be defined.), and new Uid(name) refuses a blank one.
  • The framework requires a Name on every object.

Doing so would make the blank branch in nameTakenByAnother unnecessary, and clearingAnOptionalNameOfTwoEntriesIsAllowed would turn into a refusal test. Its current form only checks that the two updates do not throw, which is why it stays green. If clearing has to stay, the read side has to accept a nameless entry instead, and that is a larger change than this PR.

(The fixture's u10, with __NAME__ = " ", is equally unreadable: a search by __UID__ = u10 fails the same way. That is a hand-written file and outside this PR; I mention it only because it shows the read side does not tolerate such entries.)

…f any class

create() requires __NAME__ whatever the XSD says, and the framework builds no ConnectorObject without a Name. With an optional __NAME__ in a class with __UID__, update accepted "" and left an entry that made every search of its class fail with "The Attribute set must contain a 'Name'.", the search by the entry's own __UID__ included; "   " did the same once the file was saved and loaded again. update now checks __NAME__ as a required attribute in every class, so a blank one is refused with "Parameter '__NAME__' must not be blank." before the entry changes. The blank branch in nameTakenByAnother and the separate check for a class without __UID__ are no longer needed.
@maximthomas

Copy link
Copy Markdown
Contributor Author

Fixed in 36def07: update now refuses a blank __NAME__ in every class.

  • XMLHandlerImpl.java:328 checks __NAME__ as a required attribute even where the XSD makes it optional. "" and " " get Parameter '__NAME__' must not be blank. in the check phase, before the entry changes. The blank branch in nameTakenByAnother and the separate check for a class without __UID__ are gone.
  • clearingAnOptionalNameOfTwoEntriesIsAllowed is now clearingAnOptionalNameIsRefused. It sends "" and " " to uid-alice, expects the refusal for both, and then expects a search of the whole class to return both accounts. It is red at 1eeff90 and at 9e7d4dc. It kills the mutant that checks __NAME__ only when it is required, and so does clearingTheOptionalNameOfAnEntryWithoutUidIsRefused (it then fails on the framework's Uid value must not be blank!).
  • With the blank branch gone, the order of the two checks can be observed: if the duplicate check runs first, renameIntoANameAnotherEntryHasIsRefused step 3 (u1 to "" while u10 is "" in the index) gets AlreadyExistsException. The description's "Not pinned" note is now a row of the test-strength table.
  • Your table reproduces on master 1eeff90 too: the whole-class search, the search by uid-alice, and the search for uid-carol after a reload all fail with The Attribute set must contain a 'Name'. The before/after table has a new __ACCOUNT__ row for it.
  • An entry whose name is already blank in the file, like the fixture's u10, still breaks a search of its class. The read side is unchanged; this is listed under "Not fixed here".

The earlier mutants were re-run on the new code and are still killed. mvn -pl OpenICF-xml-connector verify: 248 unit tests and 1 IT, green on JDK 26 and JDK 11.

@maximthomas
maximthomas requested a review from vharseko October 19, 2026 12:33

@vharseko vharseko left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked at 36def07. The blank-name point is fixed: update now checks __NAME__ as required in every class (XMLHandlerImpl.java:328), so "" and " " are refused with Parameter '__NAME__' must not be blank. before the entry changes, and a search of the class still returns both accounts.

Two mutants on this head: checking __NAME__ only when the XSD requires it fails clearingAnOptionalNameIsRefused and clearingTheOptionalNameOfAnEntryWithoutUidIsRefused; running the duplicate check before the blank check fails step 3 of renameIntoANameAnotherEntryHasIsRefused. mvn -pl OpenICF-xml-connector -am verify on JDK 21: 248 unit tests and 1 IT, green.

Nothing further from me.

@maximthomas
maximthomas merged commit 4648e38 into OpenIdentityPlatform:master Oct 20, 2026
15 checks passed
@maximthomas
maximthomas deleted the issues/159-xml-update-name branch October 20, 2026 08:51
maximthomas added a commit that referenced this pull request Oct 20, 2026
…et evicted scripts unload (#196)

Fixes #165.

`runScriptOnConnector` no longer compiles a new class into the script
engine's loader on every call. Each distinct script text is compiled
once per engine and kept in a bounded LRU cache. Classes that leave the
cache, and every cached class on `release()`, are unregistered so that
they can be unloaded. With the default size an engine keeps at most 64
runScriptOnConnector scripts, each with its closures and declared
classes, however many calls it serves.

- **One class per text, in its own loader.**
`ScriptedConfiguration.getScriptOnConnectorClass` compiles a text with a
`ScriptOnConnectorLoader`, a child of the engine's loader, which keeps
the new classes out of the engine's class cache. Before, every call went
through
`getGroovyScriptEngine().getGroovyClassLoader().parseClass(...)`. Each
class then stayed in the engine loader, and in `GroovyScriptEngine`'s
script cache, until `release()`.
- **Script-root classes are the engine's.** A class whose source is in
the script roots, such as `Helper`, resolves through
`GroovyScriptEngine.loadScriptByName`, as it does in the engine's own
compilations. A text therefore uses the `Helper` that the operation
scripts use. The engine compiles it when it is first needed or after its
source changes, and a script-root class still shadows a class of the
connector's class loader. Other classes resolve through the engine's
loader.
- **A cached text runs against the classes it was compiled against.**
Its code links a class by name the first time each reference runs.
`ScriptOnConnectorLoader` keeps every class the compiler resolved for
the text, and the loaders of the script-root classes among them. Its
`loadClass` returns a resolved class, or else a class that one of those
loaders defined: a nested class, a closure or another class compiled
from the same source. Without that, code that first ran after an
operation script had recompiled an edited `Helper` linked the new
classes. `@CompileStatic` code then failed with `NoSuchMethodError`
where it was compiled against an old signature, and with a
`LinkageError` (loader constraint violation) where it reached a class
such as `Helper.Greeting` only through the return type of a `Helper`
method.
- **The engine's configuration and compiler lock.** A text compiles with
the engine's compiler configuration, the one
`getRoots(CompilerConfiguration, GroovyClassLoader)` receives, so with
the default imports and with whatever a subclass adds there, a
`SecureASTCustomizer` for example. It compiles under the lock that the
engine's loader holds while it compiles an operation script or a
script-root class, as texts compiled on master: the monitor of the
loader's `GroovyClassLoader.sourceCache`. The engine therefore cannot
recompile a script-root class while a text resolves the classes it uses,
and the customizers of the configuration are not used by two
compilations at once. The compiler resolves the class nodes of an
`ImportCustomizer` in place. `sourceCache` is protected and the loader
is a private class of `GroovyScriptEngine`, so the field is read by
reflection; Groovy 2.2.1, 2.4.21, 4.0.22 and 5.0.3 have it and lock it
in `GroovyScriptEngine`'s loader.
`getImportCustomizer(ImportCustomizer)` and `getImports()` are called
once per engine, as on master.
- **One compilation at a time, no lock for a cached text.** The
configuration monitor is held only to read the engine and the cache, and
the cache's monitor only around map operations. Texts compile one at a
time, and not while the engine compiles, as on master. A cached text
takes no lock and does not wait for a compilation. If two threads
compile the same new text, the first one cached wins and the other's
classes are unregistered.
- **What a text adds or compiles through its loader goes to the
engine.** A text's class loader delegates `addURL`, `addClasspath` and
`parseClass` to the loader that compiled the text, now its
`ScriptOnConnectorLoader`. That loader passes them on to the engine's
loader, as on master. The operation scripts and the other texts load
classes from a URL that a text adds, and see a class that a text
compiles at run time. That compilation takes the engine's compiler lock,
and the class resolves `Helper` as the operation scripts do. The text
itself compiles through a separate method, into its own loader.
- **Bounded, least recently used first.** An engine keeps 64 scripts by
default. The system property
`org.identityconnectors.framework.groovy.scriptOnConnectorCacheSize`
changes the size. It is read when an engine is created. A value that is
not a positive integer logs a WARN and falls back to 64.
- **Evicted and released classes can unload.** Eviction and `release()`
call `InvokerHelper.removeClass` on every class of the script's child
loader, closures and declared classes included, outside the cache
monitor. The cache is created with the engine once its customizer has
succeeded. `release()` clears it last in its `finally`.
- **Run from Java.** `ScriptedConnectorBase.runScriptOnConnector` now
calls `evaluateScriptOnConnector`, which runs
`InvokerHelper.createScript(...).run()` in Java. A Groovy call site
caches the metaclass of the last script it ran, and so keeps that class
alive. The arguments, the `log` binding and the `catch` blocks are
unchanged.
- **Without a cache, as on master.** While the customizer runs, the
engine is not published and there is no cache. A subclass that overrides
`getGroovyScriptEngine()` without calling super has no cache either. A
text then compiles through the engine's loader, with the engine's
configuration, as on master, and is not cached. The configuration of an
engine that a subclass builds, a `SecureASTCustomizer` for example,
therefore still applies to its texts. If `GroovyClassLoader.sourceCache`
cannot be read, there is no cache either, and a WARN is logged when
`ScriptedConfiguration` is loaded.
- **A broken script-root class still fails as a compilation error.** The
engine compiles it, and its `CompilationFailedException` reaches the
caller, as on master. A `ScriptException` from `loadScriptByName`, which
a `GroovyScriptEngine` subclass may throw, also fails the text's
compilation with a `CompilationFailedException`, not a `GroovyBugError`.

Behaviour changes:
- a script's class name is stable while its text stays cached;
- static fields, metaclass changes and classes declared in a script
survive between calls with the same text;
- classes declared in one text are no longer visible to other texts or
to the operation scripts, and a text can no longer replace a script-root
class for everyone else;
- a text's class loader answers `getURLs`, `findResource`,
`findResources`, `getResourceLoader`, `setResourceLoader`,
`getLoadedClasses` and `clearCache` for its `ScriptOnConnectorLoader`,
not for the engine's loader. `getResource` and `getResourceAsStream`
give the same answers as on master;
- a cached script runs against the classes it was compiled against,
script-root classes such as `Helper` and the classes compiled with them
included, until it is evicted or the configuration is released. This
holds even after their source changes on disk and the operation scripts
move to the new classes. Master recompiled the text on every call and so
picked up the change; a newly compiled text still does;
- a class evicted while another thread runs it loses any metaclass
changes it made. Its metaclass is rebuilt on its next dynamic call,
after which the class is only softly held;
- a compilation that finishes after `release()` goes into the orphaned
cache, so its class is only softly held.

Not changed:
- operation scripts (`loadScript`, `evaluate`) are still compiled and
cached by the engine;
- the customizer lifecycle and the exceptions `runScriptOnConnector`
maps.

## Test strength

All in `ScriptOnConnectorCacheTest`. The ed25f73, ce3a86f, 37b50d1
and 21f99b4 cells are runs of the new tests against the production code
of those earlier heads of this PR.

| arm | case | evidence |
|---|---|---|
| one class per text | `#testSameTextReusesTheCompiledClass` |
compile-red at c328553 |
| classes stay out of the engine loader |
`#testScriptClassStaysOutOfTheEngineLoader` | compile-red at c328553;
mutants "child loader's parent is `getParentLoader()`, not the engine's
loader" and "the text compiles through the `parseClass` it passes on to
the engine" killed |
| the text's configuration has the default imports |
`#testScriptSeesTheDefaultImports` | mutant "text configuration without
the import customizer" killed |
| script-root classes resolve |
`#testScriptSeesTheClassesInTheScriptRoots` | kept behaviour |
| a text shares script-root classes with the operation scripts |
`#testScriptSharesScriptRootClassesWithOperationScripts` | red at
ed25f73; mutants "no engine resolver" and "default resolver first"
killed |
| … also after a source edit, the text compiled first |
`#testNewScriptSharesTheEditedScriptRootClassWithOperationScripts` | red
at ed25f73; both resolver mutants killed |
| script roots shadow the connector's classes |
`#testScriptRootClassShadowsAClassOfTheConnectorLoader` | red at
ed25f73; both resolver mutants killed |
| a cached text keeps the class it was compiled against |
`#testCachedScriptKeepsTheScriptRootClassItWasCompiledAgainst` | kept
behaviour, green at ed25f73 |
| … also where its code first runs after the engine recompiled the
class, nested classes included |
`#testCachedScriptLinksTheScriptRootClassesItWasCompiledAgainst` | red
at ce3a86f (`hi hi`); mutant "no `loadClass` override" killed |
| … a class of a script-root source that the text names without that
source's own class |
`#testCachedScriptLinksTheEngineClassesItWasCompiledAgainst` | kept
behaviour, green at 37b50d1; mutants "no lookup of the resolved
classes" and "keep only the script-root classes" killed |
| … statically compiled code, with classes reached only through the
methods of a script-root class |
`#testCachedStaticScriptLinksTheClassesCompiledWithAScriptRootClass` |
red at 37b50d1 (`LinkageError`, loader constraint violation); mutants
"no lookup in the script-root loaders" and "nested classes only" killed
|
| texts compiled together share a script-root class |
`#testScriptsCompiledTogetherShareTheScriptRootClasses` | red at
ed25f73; mutant "no engine compiler lock" killed (five rounds of four
threads: a race, so a likely kill, not a certain one) |
| a text and an operation script that load a script-root class together
share it |
`#testTextAndOperationScriptThatLoadAScriptRootClassTogetherShareIt` |
red at 37b50d1 (two copies of `Helper`); mutant "no engine compiler
lock" killed |
| texts compile together on a new engine |
`#testScriptsCompileTogetherOnANewEngine` | red at ed25f73 (`unable to
resolve class
org$forgerock$openicf$misc$scriptedcommon$ICFObjectBuilder`); mutant "no
engine compiler lock" killed, with the engine's configuration shared |
| what a subclass adds to the engine's configuration in `getRoots`
applies to texts |
`#testScriptCompilesWithWhatASubclassAddsToTheEngineConfiguration` | red
at 37b50d1 (`unable to resolve class RootsList`); mutant "a new
configuration built as the engine's" killed |
| binding reaches the script | `#testScriptSeesItsBinding` | compile-red
at c328553 |
| a URL that a text adds reaches the operation scripts |
`#testUrlThatATextAddsToItsLoaderReachesTheOperationScripts` | red at
37b50d1 (`ClassNotFoundException: drv.Driver`); mutant "no `addURL`
override" killed |
| a class that a text compiles at run time reaches the operation scripts
| `#testClassThatATextParsesReachesTheOperationScripts` | red at
21f99b4 (`MissingPropertyException: No such property: Parsed`); mutant
"the `parseClass` override takes the engine's lock but compiles into the
text's loader" killed |
| … and waits for the engine's compilation |
`#testClassThatATextParsesWaitsForTheEngineCompilation` | red at
21f99b4 (`parseClass` returns while the engine compiles an operation
script) |
| `release()` unregisters and recompiles |
`#testReleaseUnregistersTheCachedClasses` | compile-red at c328553 |
| … also when the release closure throws |
`#testReleaseUnregistersTheCachedClassesWhenTheReleaseClosureThrows` |
mutant "clear the cache after the `try`/`finally`" killed |
| customizer before publish | `#testCustomizerCanRunAScriptOnConnector`
| mutant without the null-cache guard killed (`NullPointerException`) |
| … and its script is not cached |
`#testScriptRunByTheCustomizerIsNotCached` | mutant "cache created
before the customizer runs" killed |
| an engine built by a subclass compiles with its own configuration |
`#testEngineBuiltByASubclassCompilesTheScriptWithItsOwnConfiguration` |
red at ce3a86f (`unable to resolve class EngineList`); mutants "no
cache: compile with the child loader anyway" and "no null-cache guard"
killed |
| a hit does not wait for a compile |
`#testCachedScriptDoesNotWaitForAnotherCompilation` | mutants
`synchronized getScriptOnConnectorClass` and "cache lookup under the
engine's compiler lock" killed (`TimeoutException`) |
| size property: name, default, `5`, ` 7 `, `0`, `-3`, `many` |
`#testCacheSizeComesFromTheSystemProperty` | compile-red at f4b1286 |
| LRU, not insertion order | `#testLeastRecentlyUsedScriptIsEvicted` |
mutant `accessOrder` `false` killed |
| eviction unregisters closures too |
`#testEvictedScriptClassesAreUnregistered` | mutants "unregister only
the main class" and "no unregister in `put`" killed |
| an evicted class unloads | `#testEvictedScriptClassCanBeUnloaded` |
same two mutants killed |
| the connector reuses the class |
`#testConnectorReusesTheCompiledScript` | red at a40030c |
| arguments and `log` binding |
`#testConnectorScriptSeesItsArgumentsAndTheLogger` | kept behaviour,
green at a40030c; mutants "`log` is `Log.getLog(Object.class)`" and
"`evaluateScriptOnConnector` rebinds `log` to the script's class" killed
|
| a connector-run class unloads once evicted |
`#testScriptRunByTheConnectorCanBeUnloadedOnceEvicted` | red at
a40030c; mutant Groovy-side `createScript(...).run()` killed (the call
site keeps the class) |
| broken script-root class → compile error |
`#testBrokenScriptRootClassFailsAsACompilationError` | red at a872c5b
(`GroovyBugError`); both resolver mutants killed |
| … also from the connector |
`#testConnectorReportsABrokenScriptRootClassAsACompilationError` | red
at a872c5b (`GroovyBugError`); both resolver mutants killed |
| a `ScriptException` from the engine → compile error |
`#testScriptExceptionFromTheEngineFailsAsACompilationError` | red at
37b50d1 (`GroovyBugError`); mutants "`ConnectorException.wrap`" and "no
cause" killed |

The two unload tests run `System.gc()` for up to about 5 s and return as
soon as the class is gone. They rely on the JVM's default soft-reference
policy, which surefire leaves unchanged.

Not pinned by a test:
- the first-wins rule when two threads compile the same new text;
- the WARN for an invalid size, and reading the size once per engine;
- unregistering closure classes on `release()` (eviction pins the shared
`unregister()`);
- the fallback without a cache, and its WARN, when
`GroovyClassLoader.sourceCache` cannot be read;
- that `loadClass` takes from the loader of a script-root class only the
classes that loader defined. A mutant that takes any class the loader
returns passes: the two differ only for a class that the script-root
class linked before the engine replaced it.

`mvn -pl OpenICF-groovy-connector,OpenICF-ssh-connector verify` on JDK
26:
- groovy: `Tests run: 179, Failures: 0, Errors: 0, Skipped: 42`. The 42
skipped tests are the five `*SampleTest` classes, which need a private
configuration;
- ssh: `Tests run: 135, Failures: 0, Errors: 0, Skipped: 120`. The
skipped sample tests need a live host.

Master is at 4648e38 now. #188, #191, #194 and #197 touch none of these
files. The branch merges into it cleanly, and the groovy module then
runs `Tests run: 210, Failures: 0, Errors: 0, Skipped: 42`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working connector:xml XML connector java Pull requests that update java code tests Test additions or fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

XML connector: update of __NAME__ allows duplicate names and returns a stale uid

2 participants