diff --git a/.github/actions/align-serializer-version/action.yml b/.github/actions/align-serializer-version/action.yml
new file mode 100644
index 000000000..69abea3c1
--- /dev/null
+++ b/.github/actions/align-serializer-version/action.yml
@@ -0,0 +1,45 @@
+name: Align serializer version
+description: >-
+ Computes the branch-suffixed snapshot version for the current branch and, when a
+ branch of the same name exists in eclipse-serializer/serializer, rewrites the
+ eclipse.serializer.version POM property to that serializer branch snapshot. This is
+ the cross-repo coupling used so a store branch builds against its matching serializer
+ branch. No-op on main / release branches. Requires Maven/JDK to be set up beforehand.
+
+inputs:
+ github-token:
+ description: Token used to query branches of the serializer repository.
+ required: true
+
+outputs:
+ version:
+ description: The branch-suffixed snapshot version (e.g. 5.0.0-foo_bar-1a2b3c4d5e-SNAPSHOT).
+ value: ${{ steps.compute.outputs.version }}
+ aligned:
+ description: "'true' if eclipse.serializer.version was rewritten to the serializer branch snapshot."
+ value: ${{ steps.compute.outputs.aligned }}
+
+runs:
+ using: composite
+ steps:
+ - id: compute
+ shell: bash
+ run: |
+ BRANCH=${GITHUB_REF#refs/heads/}
+ currentVersion=$(mvn -q -Dexec.executable=echo -Dexec.args='${project.version}' --non-recursive exec:exec)
+ suffix=$(echo -n "$BRANCH" | tr '/' '_' | cut -c1-10)-$(echo -n "$BRANCH" | md5sum | cut -c1-10)
+ newVersion="${currentVersion%-SNAPSHOT}-${suffix}-SNAPSHOT"
+ echo "version=$newVersion" >> "$GITHUB_OUTPUT"
+
+ aligned=false
+ if [ "$BRANCH" != "main" ] && [[ "$BRANCH" != release/* ]]; then
+ RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token ${{ inputs.github-token }}" https://api.github.com/repos/eclipse-serializer/serializer/branches/$BRANCH)
+ if [ "$RESPONSE" -eq 200 ]; then
+ echo "Matching serializer branch found; aligning eclipse.serializer.version to $newVersion"
+ mvn versions:set-property -Dproperty=eclipse.serializer.version -DnewVersion=$newVersion --batch-mode
+ aligned=true
+ else
+ echo "No matching serializer branch; keeping default eclipse.serializer.version"
+ fi
+ fi
+ echo "aligned=$aligned" >> "$GITHUB_OUTPUT"
diff --git a/.github/workflows/maven_build.yml b/.github/workflows/maven_build.yml
index 02aeb7456..b89e085df 100644
--- a/.github/workflows/maven_build.yml
+++ b/.github/workflows/maven_build.yml
@@ -22,6 +22,10 @@ jobs:
java-version: '17'
distribution: 'temurin'
cache: 'maven'
+ - name: Align serializer version to matching branch snapshot
+ uses: ./.github/actions/align-serializer-version
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Build with Maven
run: mvn -T 1C -P examples -B clean package --file pom.xml -U
diff --git a/.github/workflows/maven_build_win.yml b/.github/workflows/maven_build_win.yml
index eba7b1818..5297af086 100644
--- a/.github/workflows/maven_build_win.yml
+++ b/.github/workflows/maven_build_win.yml
@@ -22,5 +22,9 @@ jobs:
java-version: '17'
distribution: 'temurin'
cache: 'maven'
+ - name: Align serializer version to matching branch snapshot
+ uses: ./.github/actions/align-serializer-version
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Build with Maven
run: mvn -T 1C -P examples -B clean package --file pom.xml -U
diff --git a/.github/workflows/maven_converter.yml b/.github/workflows/maven_converter.yml
index 94d26244e..da311cb7b 100644
--- a/.github/workflows/maven_converter.yml
+++ b/.github/workflows/maven_converter.yml
@@ -21,5 +21,9 @@ jobs:
with:
java-version: '17'
distribution: 'temurin'
+ - name: Align serializer version to matching branch snapshot
+ uses: ./.github/actions/align-serializer-version
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Build with Maven
run: mvn -B -Pconverter-standalone -pl storage/embedded-tools/storage-converter -am clean package --file pom.xml -U
diff --git a/.github/workflows/maven_deploy_snapshot_dev.yml b/.github/workflows/maven_deploy_snapshot_dev.yml
index d38fd9262..cce123677 100644
--- a/.github/workflows/maven_deploy_snapshot_dev.yml
+++ b/.github/workflows/maven_deploy_snapshot_dev.yml
@@ -24,26 +24,13 @@ jobs:
server-id: ossrh
server-username: MAVEN_USERNAME
server-password: MAVEN_PASSWORD
- - name: Prepare suffix
- run: |
- suffix=$(echo -n "${GITHUB_REF#refs/heads/}" | tr '/' '_' | cut -c1-10)-$(echo -n "${GITHUB_REF#refs/heads/}" | md5sum | cut -c1-10)
- echo "Suffix: $suffix"
- echo "SUFFIX=$suffix" >> $GITHUB_ENV
+ - name: Align serializer version
+ id: align
+ uses: ./.github/actions/align-serializer-version
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Update project version
- run: |
- currentVersion=$(mvn -q -Dexec.executable=echo -Dexec.args='${project.version}' --non-recursive exec:exec)
- currentVersionWithoutSnapshot=${currentVersion%-SNAPSHOT}
- newVersion="${currentVersionWithoutSnapshot}-$SUFFIX-SNAPSHOT"
- mvn versions:set -DnewVersion=$newVersion --batch-mode
- BRANCH_NAME=${{ github.ref }}
- REPO_OWNER="eclipse-serializer"
- REPO_NAME="serializer"
- RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/repos/$REPO_OWNER/$REPO_NAME/branches/$BRANCH_NAME)
- if [ $RESPONSE -eq 200 ]; then
- mvn versions:set-property -Dproperty=eclipse.serializer.version -DnewVersion=$newVersion
- else
- echo "Branch does not exist in serializer repository, skipping serializer version change"
- fi
+ run: mvn versions:set -DnewVersion=${{ steps.align.outputs.version }} --batch-mode
- name: Make a snapshot
run: mvn -T 1C -Pdeploy -Pproduction --no-transfer-progress --batch-mode clean deploy -U -Dmaven.javadoc.skip=true -Dgpg.skip=true -Dlicense.skip=true -Denforcer.skip=true
env:
diff --git a/.github/workflows/maven_it_test.yml b/.github/workflows/maven_it_test.yml
new file mode 100644
index 000000000..77312784a
--- /dev/null
+++ b/.github/workflows/maven_it_test.yml
@@ -0,0 +1,45 @@
+# This workflow will build a Java project with Maven
+# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
+
+name: Integration tests (vmt)
+
+permissions:
+ contents: read
+ checks: write
+
+on:
+ push:
+ branches:
+ - '**'
+ pull_request:
+ branches: [ main ]
+
+jobs:
+ integration-tests:
+ runs-on: ubuntu-latest
+ steps:
+ #Build with java 17
+ - uses: actions/checkout@v6
+ - name: Set up JDK 17
+ uses: actions/setup-java@v5
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+ cache: 'maven'
+ - name: Align serializer version to matching branch snapshot
+ uses: ./.github/actions/align-serializer-version
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ - name: Build reactor dependencies (no tests)
+ run: mvn -T 1C -P IT -B clean install -DskipTests -pl integration-tests -am --no-transfer-progress --file pom.xml -U -Dmaven.javadoc.skip=true -Dgpg.skip=true -Dlicense.skip=true -Denforcer.skip=true
+ - name: Run integration tests
+ run: mvn -P IT -B test -pl integration-tests --no-transfer-progress --file pom.xml -Dgpg.skip=true -Dlicense.skip=true -Denforcer.skip=true
+
+ - name: Report JUnit results (dorny - surefire)
+ if: always()
+ uses: dorny/test-reporter@v2
+ with:
+ name: "Integration tests (vmt) Results"
+ path: 'integration-tests/target/surefire-reports/*.xml'
+ reporter: java-junit
+ fail-on-error: false
diff --git a/.github/workflows/maven_slow_test.yml b/.github/workflows/maven_slow_test.yml
index d933e15f5..b21defde9 100644
--- a/.github/workflows/maven_slow_test.yml
+++ b/.github/workflows/maven_slow_test.yml
@@ -26,6 +26,10 @@ jobs:
java-version: '17'
distribution: 'temurin'
cache: 'maven'
+ - name: Align serializer version to matching branch snapshot
+ uses: ./.github/actions/align-serializer-version
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Run slow tests
run: mvn -T 1C -P examples -P slow-tests -B clean test --no-transfer-progress --file pom.xml -U -Dmaven.javadoc.skip=true -Dgpg.skip=true -Dlicense.skip=true -Denforcer.skip=true
diff --git a/integration-tests/LICENSE b/integration-tests/LICENSE
new file mode 100644
index 000000000..d3087e4c5
--- /dev/null
+++ b/integration-tests/LICENSE
@@ -0,0 +1,277 @@
+Eclipse Public License - v 2.0
+
+ THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
+ PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION
+ OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
+
+1. DEFINITIONS
+
+"Contribution" means:
+
+ a) in the case of the initial Contributor, the initial content
+ Distributed under this Agreement, and
+
+ b) in the case of each subsequent Contributor:
+ i) changes to the Program, and
+ ii) additions to the Program;
+ where such changes and/or additions to the Program originate from
+ and are Distributed by that particular Contributor. A Contribution
+ "originates" from a Contributor if it was added to the Program by
+ such Contributor itself or anyone acting on such Contributor's behalf.
+ Contributions do not include changes or additions to the Program that
+ are not Modified Works.
+
+"Contributor" means any person or entity that Distributes the Program.
+
+"Licensed Patents" mean patent claims licensable by a Contributor which
+are necessarily infringed by the use or sale of its Contribution alone
+or when combined with the Program.
+
+"Program" means the Contributions Distributed in accordance with this
+Agreement.
+
+"Recipient" means anyone who receives the Program under this Agreement
+or any Secondary License (as applicable), including Contributors.
+
+"Derivative Works" shall mean any work, whether in Source Code or other
+form, that is based on (or derived from) the Program and for which the
+editorial revisions, annotations, elaborations, or other modifications
+represent, as a whole, an original work of authorship.
+
+"Modified Works" shall mean any work in Source Code or other form that
+results from an addition to, deletion from, or modification of the
+contents of the Program, including, for purposes of clarity any new file
+in Source Code form that contains any contents of the Program. Modified
+Works shall not include works that contain only declarations,
+interfaces, types, classes, structures, or files of the Program solely
+in each case in order to link to, bind by name, or subclass the Program
+or Modified Works thereof.
+
+"Distribute" means the acts of a) distributing or b) making available
+in any manner that enables the transfer of a copy.
+
+"Source Code" means the form of a Program preferred for making
+modifications, including but not limited to software source code,
+documentation source, and configuration files.
+
+"Secondary License" means either the GNU General Public License,
+Version 2.0, or any later versions of that license, including any
+exceptions or additional permissions as identified by the initial
+Contributor.
+
+2. GRANT OF RIGHTS
+
+ a) Subject to the terms of this Agreement, each Contributor hereby
+ grants Recipient a non-exclusive, worldwide, royalty-free copyright
+ license to reproduce, prepare Derivative Works of, publicly display,
+ publicly perform, Distribute and sublicense the Contribution of such
+ Contributor, if any, and such Derivative Works.
+
+ b) Subject to the terms of this Agreement, each Contributor hereby
+ grants Recipient a non-exclusive, worldwide, royalty-free patent
+ license under Licensed Patents to make, use, sell, offer to sell,
+ import and otherwise transfer the Contribution of such Contributor,
+ if any, in Source Code or other form. This patent license shall
+ apply to the combination of the Contribution and the Program if, at
+ the time the Contribution is added by the Contributor, such addition
+ of the Contribution causes such combination to be covered by the
+ Licensed Patents. The patent license shall not apply to any other
+ combinations which include the Contribution. No hardware per se is
+ licensed hereunder.
+
+ c) Recipient understands that although each Contributor grants the
+ licenses to its Contributions set forth herein, no assurances are
+ provided by any Contributor that the Program does not infringe the
+ patent or other intellectual property rights of any other entity.
+ Each Contributor disclaims any liability to Recipient for claims
+ brought by any other entity based on infringement of intellectual
+ property rights or otherwise. As a condition to exercising the
+ rights and licenses granted hereunder, each Recipient hereby
+ assumes sole responsibility to secure any other intellectual
+ property rights needed, if any. For example, if a third party
+ patent license is required to allow Recipient to Distribute the
+ Program, it is Recipient's responsibility to acquire that license
+ before distributing the Program.
+
+ d) Each Contributor represents that to its knowledge it has
+ sufficient copyright rights in its Contribution, if any, to grant
+ the copyright license set forth in this Agreement.
+
+ e) Notwithstanding the terms of any Secondary License, no
+ Contributor makes additional grants to any Recipient (other than
+ those set forth in this Agreement) as a result of such Recipient's
+ receipt of the Program under the terms of a Secondary License
+ (if permitted under the terms of Section 3).
+
+3. REQUIREMENTS
+
+3.1 If a Contributor Distributes the Program in any form, then:
+
+ a) the Program must also be made available as Source Code, in
+ accordance with section 3.2, and the Contributor must accompany
+ the Program with a statement that the Source Code for the Program
+ is available under this Agreement, and informs Recipients how to
+ obtain it in a reasonable manner on or through a medium customarily
+ used for software exchange; and
+
+ b) the Contributor may Distribute the Program under a license
+ different than this Agreement, provided that such license:
+ i) effectively disclaims on behalf of all other Contributors all
+ warranties and conditions, express and implied, including
+ warranties or conditions of title and non-infringement, and
+ implied warranties or conditions of merchantability and fitness
+ for a particular purpose;
+
+ ii) effectively excludes on behalf of all other Contributors all
+ liability for damages, including direct, indirect, special,
+ incidental and consequential damages, such as lost profits;
+
+ iii) does not attempt to limit or alter the recipients' rights
+ in the Source Code under section 3.2; and
+
+ iv) requires any subsequent distribution of the Program by any
+ party to be under a license that satisfies the requirements
+ of this section 3.
+
+3.2 When the Program is Distributed as Source Code:
+
+ a) it must be made available under this Agreement, or if the
+ Program (i) is combined with other material in a separate file or
+ files made available under a Secondary License, and (ii) the initial
+ Contributor attached to the Source Code the notice described in
+ Exhibit A of this Agreement, then the Program may be made available
+ under the terms of such Secondary Licenses, and
+
+ b) a copy of this Agreement must be included with each copy of
+ the Program.
+
+3.3 Contributors may not remove or alter any copyright, patent,
+trademark, attribution notices, disclaimers of warranty, or limitations
+of liability ("notices") contained within the Program from any copy of
+the Program which they Distribute, provided that Contributors may add
+their own appropriate notices.
+
+4. COMMERCIAL DISTRIBUTION
+
+Commercial distributors of software may accept certain responsibilities
+with respect to end users, business partners and the like. While this
+license is intended to facilitate the commercial use of the Program,
+the Contributor who includes the Program in a commercial product
+offering should do so in a manner which does not create potential
+liability for other Contributors. Therefore, if a Contributor includes
+the Program in a commercial product offering, such Contributor
+("Commercial Contributor") hereby agrees to defend and indemnify every
+other Contributor ("Indemnified Contributor") against any losses,
+damages and costs (collectively "Losses") arising from claims, lawsuits
+and other legal actions brought by a third party against the Indemnified
+Contributor to the extent caused by the acts or omissions of such
+Commercial Contributor in connection with its distribution of the Program
+in a commercial product offering. The obligations in this section do not
+apply to any claims or Losses relating to any actual or alleged
+intellectual property infringement. In order to qualify, an Indemnified
+Contributor must: a) promptly notify the Commercial Contributor in
+writing of such claim, and b) allow the Commercial Contributor to control,
+and cooperate with the Commercial Contributor in, the defense and any
+related settlement negotiations. The Indemnified Contributor may
+participate in any such claim at its own expense.
+
+For example, a Contributor might include the Program in a commercial
+product offering, Product X. That Contributor is then a Commercial
+Contributor. If that Commercial Contributor then makes performance
+claims, or offers warranties related to Product X, those performance
+claims and warranties are such Commercial Contributor's responsibility
+alone. Under this section, the Commercial Contributor would have to
+defend claims against the other Contributors related to those performance
+claims and warranties, and if a court requires any other Contributor to
+pay any damages as a result, the Commercial Contributor must pay
+those damages.
+
+5. NO WARRANTY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT
+PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS"
+BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR
+IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF
+TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR
+PURPOSE. Each Recipient is solely responsible for determining the
+appropriateness of using and distributing the Program and assumes all
+risks associated with its exercise of rights under this Agreement,
+including but not limited to the risks and costs of program errors,
+compliance with applicable laws, damage to or loss of data, programs
+or equipment, and unavailability or interruption of operations.
+
+6. DISCLAIMER OF LIABILITY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT
+PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS
+SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST
+PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE
+EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+7. GENERAL
+
+If any provision of this Agreement is invalid or unenforceable under
+applicable law, it shall not affect the validity or enforceability of
+the remainder of the terms of this Agreement, and without further
+action by the parties hereto, such provision shall be reformed to the
+minimum extent necessary to make such provision valid and enforceable.
+
+If Recipient institutes patent litigation against any entity
+(including a cross-claim or counterclaim in a lawsuit) alleging that the
+Program itself (excluding combinations of the Program with other software
+or hardware) infringes such Recipient's patent(s), then such Recipient's
+rights granted under Section 2(b) shall terminate as of the date such
+litigation is filed.
+
+All Recipient's rights under this Agreement shall terminate if it
+fails to comply with any of the material terms or conditions of this
+Agreement and does not cure such failure in a reasonable period of
+time after becoming aware of such noncompliance. If all Recipient's
+rights under this Agreement terminate, Recipient agrees to cease use
+and distribution of the Program as soon as reasonably practicable.
+However, Recipient's obligations under this Agreement and any licenses
+granted by Recipient relating to the Program shall continue and survive.
+
+Everyone is permitted to copy and distribute copies of this Agreement,
+but in order to avoid inconsistency the Agreement is copyrighted and
+may only be modified in the following manner. The Agreement Steward
+reserves the right to publish new versions (including revisions) of
+this Agreement from time to time. No one other than the Agreement
+Steward has the right to modify this Agreement. The Eclipse Foundation
+is the initial Agreement Steward. The Eclipse Foundation may assign the
+responsibility to serve as the Agreement Steward to a suitable separate
+entity. Each new version of the Agreement will be given a distinguishing
+version number. The Program (including Contributions) may always be
+Distributed subject to the version of the Agreement under which it was
+received. In addition, after a new version of the Agreement is published,
+Contributor may elect to Distribute the Program (including its
+Contributions) under the new version.
+
+Except as expressly stated in Sections 2(a) and 2(b) above, Recipient
+receives no rights or licenses to the intellectual property of any
+Contributor under this Agreement, whether expressly, by implication,
+estoppel or otherwise. All rights in the Program not expressly granted
+under this Agreement are reserved. Nothing in this Agreement is intended
+to be enforceable by any entity that is not a Contributor or Recipient.
+No third-party beneficiary rights are created under this Agreement.
+
+Exhibit A - Form of Secondary Licenses Notice
+
+"This Source Code may also be made available under the following
+Secondary Licenses when the conditions for such availability set forth
+in the Eclipse Public License, v. 2.0 are satisfied: {name license(s),
+version(s), and exceptions or additional permissions here}."
+
+ Simply including a copy of this Agreement, including this Exhibit A
+ is not sufficient to license the Source Code under Secondary Licenses.
+
+ If it is not possible or desirable to put the notice in a particular
+ file, then You may include the notice in a location (such as a LICENSE
+ file in a relevant directory) where a recipient would be likely to
+ look for such a notice.
+
+ You may add additional accurate notices of copyright ownership.
diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml
new file mode 100644
index 000000000..5f2016806
--- /dev/null
+++ b/integration-tests/pom.xml
@@ -0,0 +1,182 @@
+
+
+ 4.0.0
+
+
+ org.eclipse.store
+ store-parent
+ 5.0.0-SNAPSHOT
+ ../pom.xml
+
+
+ integration-tests
+ EclipseStore Integration Tests
+ https://projects.eclipse.org/projects/technology.store
+
+
+ true
+ 6.0.3
+
+
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ ${junit-jupiter.version}
+
+
+ org.junit.jupiter
+ junit-jupiter-params
+ ${junit-jupiter.version}
+
+
+ org.junit
+ junit-bom
+ ${junit-jupiter.version}
+ pom
+ import
+
+
+
+
+
+
+
+ org.eclipse.store
+ storage-embedded
+ ${project.version}
+
+
+ org.eclipse.store
+ storage-embedded-configuration
+ ${project.version}
+
+
+ org.eclipse.store
+ storage-embedded-tools-storage-converter
+ ${project.version}
+
+
+ org.eclipse.store
+ gigamap
+ ${project.version}
+
+
+ org.eclipse.store
+ cache
+ ${project.version}
+
+
+
+
+ org.eclipse.serializer
+ persistence-binary-jdk8
+ ${eclipse.serializer.version}
+
+
+ org.eclipse.serializer
+ configuration-yaml
+ ${eclipse.serializer.version}
+
+
+ org.eclipse.serializer
+ configuration-hocon
+ ${eclipse.serializer.version}
+
+
+ org.eclipse.serializer
+ nativememory
+ ${eclipse.serializer.version}
+
+
+ org.eclipse.serializer
+ serializer
+ ${eclipse.serializer.version}
+ test
+
+
+ org.eclipse.serializer
+ test-fixtures
+ ${eclipse.serializer.version}
+ test
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+ org.awaitility
+ awaitility
+ 4.2.2
+ test
+
+
+ net.datafaker
+ datafaker
+ 2.5.3
+ test
+
+
+ org.apache.commons
+ commons-lang3
+ 3.20.0
+ test
+
+
+ commons-io
+ commons-io
+ 2.21.0
+ test
+
+
+ org.javassist
+ javassist
+ 3.30.2-GA
+ test
+
+
+ org.slf4j
+ slf4j-simple
+ ${slf4j.version}
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ false
+ false
+
+ --add-opens java.base/java.lang=ALL-UNNAMED
+ --add-opens java.base/java.lang.ref=ALL-UNNAMED
+ --add-opens java.base/java.lang.reflect=ALL-UNNAMED
+ --add-opens java.base/java.net=ALL-UNNAMED
+ --add-opens java.base/java.time=ALL-UNNAMED
+ --add-opens java.base/java.time.zone=ALL-UNNAMED
+ --add-opens java.base/java.util=ALL-UNNAMED
+ --add-opens java.base/java.util.concurrent=ALL-UNNAMED
+ --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED
+ --add-opens java.base/java.util.concurrent.locks=ALL-UNNAMED
+ --add-opens java.base/java.util.regex=ALL-UNNAMED
+ --add-opens java.base/sun.net.www.protocol.http=ALL-UNNAMED
+ --add-opens java.base/sun.nio.fs=ALL-UNNAMED
+ --add-opens java.base/sun.util.calendar=ALL-UNNAMED
+ --add-exports java.base/jdk.internal.misc=ALL-UNNAMED
+
+
+
+
+
+
diff --git a/integration-tests/src/test/java/test/eclipse/store/behavior/BackupTxLogDuplicateBugRepro.java b/integration-tests/src/test/java/test/eclipse/store/behavior/BackupTxLogDuplicateBugRepro.java
new file mode 100644
index 000000000..e69fdd7b3
--- /dev/null
+++ b/integration-tests/src/test/java/test/eclipse/store/behavior/BackupTxLogDuplicateBugRepro.java
@@ -0,0 +1,155 @@
+package test.eclipse.store.behavior;
+
+/*-
+ * #%L
+ * EclipseStore Integration Tests
+ * %%
+ * Copyright (C) 2023 - 2026 MicroStream Software
+ * %%
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ * #L%
+ */
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+
+import org.eclipse.serializer.afs.types.AFile;
+import org.eclipse.serializer.chars.VarString;
+import org.eclipse.serializer.configuration.types.ByteSize;
+import org.eclipse.serializer.configuration.types.ByteUnit;
+import org.eclipse.serializer.io.XIO;
+import org.eclipse.store.afs.nio.types.NioFileSystem;
+import org.eclipse.store.storage.embedded.configuration.types.EmbeddedStorageConfigurationBuilder;
+import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager;
+import org.eclipse.store.storage.types.StorageTransactionsAnalysis;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.RepeatedTest;
+import org.junit.jupiter.api.io.TempDir;
+
+import test.eclipse.store.configuration.Customer;
+import test.eclipse.store.configuration.CustomerGenerator;
+
+/**
+ * Minimal reproduction of an Eclipse Store backup-handler bug.
+ *
+ * Symptom: after housekeeping which creates a new head file, the continuous
+ * backup transactions log contains exactly one CREATION entry MORE than the
+ * live transactions log -- 26 bytes (one {@code LENGTH_COMMON_NUMBERED}
+ * entry) over the live size. The duplicate entry is byte-identical to the
+ * preceding entry (same timestamp, same file number, fileLength=0). Data
+ * files ({@code channel_X_N.dat}) remain byte-identical between live and
+ * backup; only the {@code transactions_X.sft} log diverges.
+ *
+ * Reproduction:
+ *
+ * - Default housekeeping settings (no head-file cleanup, default
+ * minimum-use-ratio).
+ * - No deletion directory needed.
+ * - Two sessions are required: bug does NOT reproduce in a single
+ * session, only after reopen -- pointing at backup synchronization
+ * at session 2 startup.
+ * - Customer count needs to be high enough that several non-head data
+ * files exist so housekeeping consolidates them into a new head file.
+ * Around 75 customers at 1-2 KiB data files reproduces ~70 % of the
+ * time on macOS over 10 iterations.
+ *
+ *
+ * Suspected cause: race between
+ * {@code StorageFileWriterBackupping.writeTransactionEntryCreate} (which
+ * enqueues a {@code CopyItem} into the backup queue) and
+ * {@code StorageBackupHandler.synchronizeTransactionFile} (which deletes
+ * the backup transactions file and copies the whole live transactions
+ * log when sizes mismatch). When synchronize fires before the backup
+ * queue drains the pending {@code CopyItem}, the copy-all path picks up
+ * the new entry, and then the backup queue thread later appends the same
+ * 26 bytes again -> duplicate.
+ *
+ * The original {@code DeletionDirectoryBehaviorTest#continuousBackupReflectsCurrentLiveStorageNotArchive}
+ * surfaces the same bug under a larger workload with a deletion directory.
+ * This test trims the scenario to the smallest configuration that still
+ * triggers it.
+ */
+@Disabled("https://github.com/microstream-one/internal/issues/52")
+class BackupTxLogDuplicateBugRepro
+{
+
+ private static final int CUSTOMER_COUNT = 75;
+
+ private static final ByteSize DATA_FILE_MIN = ByteSize.New(1, ByteUnit.KiB);
+ private static final ByteSize DATA_FILE_MAX = ByteSize.New(2, ByteUnit.KiB);
+
+ //@Test
+ @RepeatedTest(10)
+ void backupTransactionsLogMatchesLive(@TempDir final Path workDir) throws IOException
+ {
+ final Path storageDir = workDir.resolve("storage");
+ final Path backupDir = workDir.resolve("backup");
+
+ // Session 1: fresh storage with a continuous backup, write enough data so that
+ // multiple data files are produced and at least one becomes a non-head file.
+ final EmbeddedStorageConfigurationBuilder builder = EmbeddedStorageConfigurationBuilder.New()
+ .setStorageDirectory(storageDir.toString())
+ .setBackupDirectory(backupDir.toString())
+ .setChannelCount(1)
+ .setDataFileMinimumSize(DATA_FILE_MIN)
+ .setDataFileMaximumSize(DATA_FILE_MAX);
+
+ EmbeddedStorageManager mgr = builder
+ .createEmbeddedStorageFoundation()
+ .createEmbeddedStorageManager(new ArrayList<>(CustomerGenerator.generateCustomers(CUSTOMER_COUNT)))
+ .start();
+ mgr.storeRoot();
+ mgr.shutdown();
+
+ // Session 2: orphan everything, force GC + file check. Housekeeping
+ // consolidation creates a new head file and writes a CREATION entry.
+ mgr = builder
+ .createEmbeddedStorageFoundation()
+ .createEmbeddedStorageManager()
+ .start();
+ mgr.setRoot(new ArrayList());
+ mgr.storeRoot();
+ mgr.issueFullGarbageCollection();
+ mgr.issueFullFileCheck();
+ mgr.shutdown();
+
+ final Path liveTx = storageDir.resolve("channel_0/transactions_0.sft");
+ final Path backupTx = backupDir.resolve("channel_0/transactions_0.sft");
+
+ final long liveLen = Files.size(liveTx);
+ final long backupLen = Files.size(backupTx);
+
+ // Uncomment to print both transaction logs in human-readable form to see
+ // exactly which entry diverges between live and backup. Same output that
+ // org.eclipse.store.storage.util.MainUtilTransactionFileConverter produces.
+ dumpTransactionLog("LIVE", liveTx);
+ dumpTransactionLog("BACKUP", backupTx);
+
+ assertEquals(liveLen, backupLen,
+ "Backup transactions log differs from live: live=" + liveLen
+ + " backup=" + backupLen + " delta=" + (backupLen - liveLen) + " bytes");
+ }
+
+ /**
+ * Print a parsed transactions log to {@code System.out}. Mirrors the logic of
+ * {@link org.eclipse.store.storage.util.MainUtilTransactionFileConverter} so that
+ * {@code .sft} files can be inspected without rebuilding the storage tooling.
+ */
+ @SuppressWarnings("unused")
+ private static void dumpTransactionLog(final String label, final Path path)
+ {
+ final AFile file = NioFileSystem.New().ensureFile(XIO.Path(path.toString()));
+ final VarString vs = VarString.New("=== " + label + " " + path + " ===").lf();
+ StorageTransactionsAnalysis.EntryAssembler.assembleHeader(vs, "\t").lf();
+ StorageTransactionsAnalysis.Logic.parseFile(file, vs).lf();
+ System.out.println(vs.toString());
+ }
+}
diff --git a/integration-tests/src/test/java/test/eclipse/store/behavior/DeletionDirectoryBehaviorTest.java b/integration-tests/src/test/java/test/eclipse/store/behavior/DeletionDirectoryBehaviorTest.java
new file mode 100644
index 000000000..f065c4fff
--- /dev/null
+++ b/integration-tests/src/test/java/test/eclipse/store/behavior/DeletionDirectoryBehaviorTest.java
@@ -0,0 +1,690 @@
+package test.eclipse.store.behavior;
+
+/*-
+ * #%L
+ * EclipseStore Integration Tests
+ * %%
+ * Copyright (C) 2023 - 2026 MicroStream Software
+ * %%
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ * #L%
+ */
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.*;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+
+import org.eclipse.serializer.afs.types.ADirectory;
+import org.eclipse.serializer.configuration.types.ByteSize;
+import org.eclipse.serializer.configuration.types.ByteUnit;
+import org.eclipse.store.afs.nio.types.NioFileSystem;
+import org.eclipse.store.storage.embedded.configuration.types.EmbeddedStorageConfigurationBuilder;
+import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager;
+import org.eclipse.store.storage.types.StorageConfiguration;
+import org.eclipse.store.storage.util.StorageObjectRestorer;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import test.eclipse.store.configuration.Customer;
+import test.eclipse.store.configuration.CustomerGenerator;
+
+/**
+ * Behavioral tests for the deletion-directory configuration.
+ *
+ * The classic configuration tests only assert that some file appears
+ * under the deletion directory after housekeeping. These tests verify what the
+ * feature is actually expected to deliver in production:
+ *
+ * - Objects whose data files are moved aside by housekeeping can be
+ * recovered from the deletion directory using
+ * {@link StorageObjectRestorer}, with their original field values intact
+ * (single-channel and multi-channel).
+ * - Live storage stays consistent and queryable after housekeeping moves
+ * garbage-only data files into the deletion directory.
+ *
+ */
+class DeletionDirectoryBehaviorTest
+{
+
+ private static final int CUSTOMER_COUNT = 200;
+
+ private static final ByteSize DATA_FILE_MIN = ByteSize.New(1, ByteUnit.KiB);
+ private static final ByteSize DATA_FILE_MAX = ByteSize.New(2, ByteUnit.KiB);
+
+ @Test
+ @Disabled("https://github.com/microstream-one/internal/issues/56")
+ void recoverableFromDeletionDirectorySingleChannel(@TempDir final Path workDir) throws IOException
+ {
+ assertRecoverableThroughDeletionDirectory(workDir, 1);
+ }
+
+ @Test
+ @Disabled("https://github.com/microstream-one/internal/issues/56")
+ void recoverableFromDeletionDirectoryMultiChannel(@TempDir final Path workDir) throws IOException
+ {
+ assertRecoverableThroughDeletionDirectory(workDir, 4);
+ }
+
+ @Test
+ void noCustomerIsLostAfterHousekeeping(@TempDir final Path workDir) throws IOException
+ {
+ // Crash-safety invariant under the documented design (StorageFileManager.deleteFile
+ // writes a transaction-log entry BEFORE physical delete, and StorageFileWriter
+ // returns early if the rescue-into-deletion-dir succeeded so the source file is never
+ // blindly deleted): for every persisted object, after housekeeping the object must
+ // still be locatable -- either in active storage, or in the deletion directory, or both.
+ //
+ // The test runs with the *default* file-cleanup settings (no head-file cleanup, default
+ // minimum-use-ratio) so that the assertion reflects real-world configurations rather
+ // than the aggressive settings used by the recoverable-* tests.
+
+ final Path storageDir = workDir.resolve("storage");
+ final Path deletionDir = workDir.resolve("deleted");
+ final int channelCount = 1;
+
+ final List kept = CustomerGenerator.generateCustomers(5);
+ final List ephemerals = CustomerGenerator.generateCustomers(CUSTOMER_COUNT);
+ final long[] allOids = new long[kept.size() + ephemerals.size()];
+
+ runDefaultFreshSession(storageDir, deletionDir, channelCount, new ArrayList<>(kept), mgr ->
+ {
+ mgr.storeRoot();
+ int idx = 0;
+ for (final Customer c : kept) {
+ allOids[idx++] = mgr.persistenceManager().lookupObjectId(c);
+ }
+ for (final Customer c : ephemerals) {
+ mgr.store(c);
+ allOids[idx++] = mgr.persistenceManager().lookupObjectId(c);
+ }
+ });
+
+ runDefaultReopenSession(storageDir, deletionDir, channelCount, mgr ->
+ {
+ mgr.issueFullGarbageCollection();
+ mgr.issueFullFileCheck();
+ });
+
+ // Phase A: which OIDs are still in active storage? Open storage, try to load each
+ // one. lookupObject returns the cached instance; getObject falls back to a loader
+ // that scans live data files.
+ final Set foundInActive = new HashSet<>();
+ runDefaultReopenSession(storageDir, deletionDir, channelCount, mgr ->
+ {
+ for (final long oid : allOids) {
+ try {
+ final Object o = mgr.persistenceManager().getObject(oid);
+ if (o != null) {
+ foundInActive.add(oid);
+ }
+ } catch (final Exception ignored) {
+ // not in active storage -- that's expected for some
+ }
+ }
+ });
+
+ // Phase B: storage closed; for OIDs not found in active storage, check deletion-dir
+ // via the restorer. The restorer scans only deletion-dir files.
+ final StorageConfiguration restorerConfig = defaultConfigBuilder(storageDir, deletionDir, channelCount)
+ .createEmbeddedStorageFoundation()
+ .getConfiguration();
+ final StorageObjectRestorer restorer = new StorageObjectRestorer.Default(restorerConfig);
+
+ final List lost = new ArrayList<>();
+ for (final long oid : allOids) {
+ if (foundInActive.contains(oid)) {
+ continue;
+ }
+ if (!restorer.restoreObject(oid)) {
+ lost.add(oid);
+ }
+ }
+
+ assertTrue(lost.isEmpty(),
+ "Some object ids were lost -- not present in active storage and not recoverable from "
+ + "deletion directory either. Count=" + lost.size() + " oids=" + lost);
+ }
+
+ @Test
+ @Disabled("https://github.com/microstream-one/internal/issues/52")
+ void continuousBackupReflectsCurrentLiveStorageNotArchive(@TempDir final Path workDir)
+ throws IOException, NoSuchAlgorithmException, InterruptedException
+ {
+ // Continuous backup behavior: when housekeeping rescues a file from the main
+ // storage into the main deletion-directory, the corresponding backup file is
+ // processed by StorageBackupHandler.deleteFile -- which calls
+ // StorageFileWriter.deleteFile with the BACKUP file provider. The backup
+ // provider, when configured via EmbeddedStorageConfigurationBuilder.setBackupDirectory(...),
+ // has NO deletion-directory of its own, so the backup copy is physically removed.
+ //
+ // In other words: continuous backup is a live-state mirror, not an archive.
+ // To preserve rescued files in the backup as well, the backup file provider
+ // would need its own deletion-directory configured via lower-level API.
+
+ final Path storageDir = workDir.resolve("storage");
+ final Path deletionDir = workDir.resolve("deleted");
+ final Path backupDir = workDir.resolve("backup");
+
+ final List customers = CustomerGenerator.generateCustomers(CUSTOMER_COUNT);
+
+ runFreshSessionWithBackup(storageDir, deletionDir, backupDir, new ArrayList<>(customers), mgr ->
+ {
+ mgr.storeRoot();
+ });
+
+ runReopenSessionWithBackup(storageDir, deletionDir, backupDir, mgr ->
+ {
+ mgr.setRoot(new ArrayList());
+ mgr.storeRoot();
+ mgr.issueFullGarbageCollection();
+ mgr.issueFullFileCheck();
+ });
+
+ // Continuous backup processes its queue asynchronously. After shutdown the
+ // drain should be complete, but Files.walk might race with FS sync on some
+ // platforms. Wait for backup-dir state to stabilize.
+// Awaitility.await()
+// .atMost(Duration.ofSeconds(10))
+// .pollInterval(Duration.ofMillis(100))
+// .until(() -> hashesEqual(collectFileHashes(storageDir), collectFileHashes(backupDir)));
+
+ Thread.sleep(10_000);
+ System.out.println(backupDir);
+ System.out.println(storageDir);
+
+ assertTrue(Files.isDirectory(backupDir), "Backup directory was not created");
+ assertTrue(Files.isDirectory(backupDir.resolve("channel_0")),
+ "Backup directory has no channel_0 -- continuous backup did not mirror channels");
+
+ // 1) Main deletion-dir DID receive rescued files.
+ assertFalse(collectFileHashes(deletionDir).isEmpty(),
+ "Sanity check failed: main deletion directory is empty after housekeeping");
+
+ // 2) Backup directory does NOT have a deletion subdirectory by default --
+ // confirms that backup file provider has no deletion-directory configured.
+ assertFalse(Files.isDirectory(backupDir.resolve("deleted")),
+ "Backup directory unexpectedly contains a 'deleted' subdirectory; "
+ + "expected default config to give backup no deletion-directory of its own");
+
+ // 3) Backup mirrors current live storage byte-for-byte.
+ final Set liveHashes = collectFileHashes(storageDir);
+ final Set backupHashes = collectFileHashes(backupDir);
+ if (!liveHashes.equals(backupHashes)) {
+ System.err.println("=== DIAG: live vs backup mismatch ===");
+ System.err.println("--- live ---");
+ dumpFilesWithMeta(storageDir);
+ System.err.println("--- backup ---");
+ dumpFilesWithMeta(backupDir);
+ System.err.println("--- deletion-dir ---");
+ dumpFilesWithMeta(deletionDir);
+
+ final Path debugDir = Path.of("/tmp/dd-debug/" + System.currentTimeMillis());
+ Files.createDirectories(debugDir);
+ final Path liveTx = storageDir.resolve("channel_0/transactions_0.sft");
+ final Path backupTx = backupDir.resolve("channel_0/transactions_0.sft");
+ if (Files.exists(liveTx)) {
+ Files.copy(liveTx, debugDir.resolve("live_transactions_0.sft"));
+ }
+ if (Files.exists(backupTx)) {
+ Files.copy(backupTx, debugDir.resolve("backup_transactions_0.sft"));
+ }
+ System.err.println("Transaction logs copied to: " + debugDir);
+ }
+ assertEquals(liveHashes, backupHashes,
+ "Backup is not a byte-identical mirror of current live storage. "
+ + "Live=" + liveHashes.size() + " backup=" + backupHashes.size());
+
+ // 4) Files rescued into the main deletion-dir are NOT preserved in the backup.
+ // (In default config the backup just drops them.)
+ final Set deletedHashes = collectFileHashes(deletionDir);
+ final Set intersect = new HashSet<>(deletedHashes);
+ intersect.retainAll(backupHashes);
+ assertTrue(intersect.isEmpty(),
+ "Backup unexpectedly retained files that were rescued into the main "
+ + "deletion-directory. Overlap=" + intersect.size());
+ }
+
+ @Test
+ void issueFullBackupExportsLiveStorageNotDeletedFiles(@TempDir final Path workDir)
+ throws IOException, NoSuchAlgorithmException
+ {
+ // issueFullBackup is a one-shot snapshot of the current live state (it
+ // calls exportChannels under the hood). It should not export the
+ // deletion-directory contents -- those are housekeeping rescue copies, not
+ // live data.
+
+ final Path storageDir = workDir.resolve("storage");
+ final Path deletionDir = workDir.resolve("deleted");
+ final Path fullBackupDir = workDir.resolve("fullbackup");
+ Files.createDirectories(fullBackupDir);
+
+ runFreshSession(storageDir, deletionDir, 1,
+ new ArrayList<>(CustomerGenerator.generateCustomers(CUSTOMER_COUNT)),
+ EmbeddedStorageManager::storeRoot);
+
+ runReopenSession(storageDir, deletionDir, 1, mgr ->
+ {
+ mgr.setRoot(new ArrayList());
+ mgr.storeRoot();
+ mgr.issueFullGarbageCollection();
+ mgr.issueFullFileCheck();
+
+ final ADirectory backupADir = NioFileSystem.New().ensureDirectoryPath(fullBackupDir.toString());
+ mgr.persistenceManager().objectRegistry(); // touch to ensure connection initialized
+ mgr.issueFullBackup(backupADir);
+ });
+
+ assertTrue(Files.isDirectory(fullBackupDir.resolve("channel_0")),
+ "Full backup did not produce a channel_0 directory");
+ assertFalse(Files.isDirectory(fullBackupDir.resolve("deleted")),
+ "Full backup unexpectedly contains a 'deleted' subdirectory");
+
+ // Hashes in full backup should equal current live storage; deletion-dir hashes
+ // should NOT appear in the full backup.
+ final Set liveHashes = collectFileHashes(storageDir);
+ final Set fullBackupHashes = collectFileHashes(fullBackupDir);
+ final Set deletedHashes = collectFileHashes(deletionDir);
+
+ final Set intersect = new HashSet<>(fullBackupHashes);
+ intersect.retainAll(deletedHashes);
+ assertTrue(intersect.isEmpty(),
+ "issueFullBackup unexpectedly exported files that were rescued into the "
+ + "deletion-directory. Overlap=" + intersect.size());
+
+ // The full backup may carry the type dictionary in addition to data files,
+ // so it can be a strict superset of live data files but must not contain
+ // rescued files.
+ assertTrue(fullBackupHashes.containsAll(intersectionWith(liveHashes, fullBackupHashes)),
+ "Full backup is missing live storage data files");
+ }
+
+ @Test
+ void liveStorageRemainsUsableAfterHousekeepingMovesFilesAside(@TempDir final Path workDir)
+ throws IOException, NoSuchAlgorithmException
+ {
+ final Path storageDir = workDir.resolve("storage");
+ final Path deletionDir = workDir.resolve("deleted");
+
+ final List kept = CustomerGenerator.generateCustomers(5);
+
+ // Session 1: 5 customers reachable from the root + many ephemeral
+ // customers stored as orphans (unreachable from the root, garbage on GC).
+ runFreshSession(storageDir, deletionDir, 1, new ArrayList<>(kept), mgr ->
+ {
+ mgr.storeRoot();
+ for (int i = 0; i < CUSTOMER_COUNT; i++) {
+ mgr.store(CustomerGenerator.generateNewCustomer());
+ }
+ });
+
+ // Session 2: explicit GC + file check moves orphan-bearing files aside.
+ runReopenSession(storageDir, deletionDir, 1, mgr ->
+ {
+ mgr.issueFullGarbageCollection();
+ mgr.issueFullFileCheck();
+ });
+
+ assertTrue(Files.isDirectory(deletionDir), "Deletion directory was not created");
+ assertFalse(collectFileHashes(deletionDir).isEmpty(),
+ "No file moved into the deletion directory; the test did not exercise housekeeping");
+
+ // Session 3: reopen and verify the kept customers are still loadable
+ // from live storage with their original field values.
+ runReopenSession(storageDir, deletionDir, 1, mgr ->
+ {
+ @SuppressWarnings("unchecked") final List loaded = (List) mgr.root();
+ assertNotNull(loaded, "Root was not loaded after reopen");
+ assertEquals(kept.size(), loaded.size(),
+ "Live root size changed after housekeeping moved files aside");
+ for (int i = 0; i < kept.size(); i++) {
+ assertCustomerEquals(kept.get(i), loaded.get(i), "kept[" + i + "]");
+ }
+ });
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // scenario //
+ /////////////
+
+ /**
+ * Round-trip scenario: store customers, capture their object ids, orphan them
+ * via housekeeping, then verify that {@link StorageObjectRestorer} can
+ * recover each one from the deletion directory and that the recovered
+ * customers carry the original field values.
+ */
+ private static void assertRecoverableThroughDeletionDirectory(
+ final Path workDir,
+ final int channelCount
+ )
+ throws IOException
+ {
+ final Path storageDir = workDir.resolve("storage");
+ final Path deletionDir = workDir.resolve("deleted");
+
+ final List originals = CustomerGenerator.generateCustomers(CUSTOMER_COUNT);
+ final long[] oids = new long[originals.size()];
+
+ // Session 1: store every customer as part of the root list, capture OIDs.
+ runFreshSession(storageDir, deletionDir, channelCount, new ArrayList<>(originals), mgr ->
+ {
+ mgr.storeRoot();
+ for (int i = 0; i < originals.size(); i++) {
+ final long oid = mgr.persistenceManager().lookupObjectId(originals.get(i));
+ assertNotEquals(0L, oid,
+ "Customer at index " + i + " has no object id after storeRoot");
+ oids[i] = oid;
+ }
+ });
+
+ // Session 2: orphan everything and force housekeeping to move the
+ // now-garbage data files into the deletion directory. Head file cleanup
+ // is enabled and the use-ratio threshold is raised so even sparsely-used
+ // files are dissolved -- otherwise some customers would survive in the
+ // active head file and would not be recoverable from deletion-dir.
+ runReopenSession(storageDir, deletionDir, channelCount, mgr ->
+ {
+ mgr.setRoot(new ArrayList());
+ mgr.storeRoot();
+ mgr.issueFullGarbageCollection();
+ mgr.issueFullFileCheck();
+ });
+
+ // Restorer must run on a stopped storage. Build a fresh foundation just
+ // to extract a configuration that points at the same directories.
+ final StorageConfiguration restorerConfig = configBuilder(storageDir, deletionDir, channelCount)
+ .createEmbeddedStorageFoundation()
+ .getConfiguration();
+
+ final StorageObjectRestorer restorer = new StorageObjectRestorer.Default(restorerConfig);
+
+ int recovered = 0;
+ final List failedIndexes = new ArrayList<>();
+ for (int i = 0; i < oids.length; i++) {
+ if (restorer.restoreObject(oids[i])) {
+ recovered++;
+ } else {
+ failedIndexes.add(i);
+ }
+ }
+
+ assertEquals(originals.size(), recovered,
+ "Some customers could not be recovered from deletion directory: indexes " + failedIndexes);
+
+ // Session 3: reopen and look up each customer by its original OID. Field
+ // values must equal the originals -- this is the actual proof that the
+ // data preserved in the deletion directory is the data we intended.
+ runReopenSession(storageDir, deletionDir, channelCount, mgr ->
+ {
+ for (int i = 0; i < originals.size(); i++) {
+ final Object loaded = mgr.persistenceManager().getObject(oids[i]);
+ assertNotNull(loaded,
+ "Customer with oid " + oids[i] + " (index " + i + ") not loadable after restore");
+ assertTrue(loaded instanceof Customer,
+ "Loaded object for oid " + oids[i] + " is not a Customer: " + loaded.getClass());
+ assertCustomerEquals(originals.get(i), (Customer) loaded, "originals[" + i + "]");
+ }
+ });
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // helpers //
+
+ /// /////////
+
+ private static void assertCustomerEquals(final Customer expected, final Customer actual, final String label)
+ {
+ assertEquals(expected.getFirstName(), actual.getFirstName(), label + ".firstName");
+ assertEquals(expected.getSecondName(), actual.getSecondName(), label + ".secondName");
+ assertEquals(expected.getStreet(), actual.getStreet(), label + ".street");
+ assertEquals(expected.getCity(), actual.getCity(), label + ".city");
+ }
+
+ private static EmbeddedStorageConfigurationBuilder configBuilder(
+ final Path storageDir,
+ final Path deletionDir,
+ final int channelCount
+ )
+ {
+ return EmbeddedStorageConfigurationBuilder.New()
+ .setStorageDirectory(storageDir.toString())
+ .setDeletionDirectory(deletionDir.toString())
+ .setChannelCount(channelCount)
+ .setDataFileMinimumSize(DATA_FILE_MIN)
+ .setDataFileMaximumSize(DATA_FILE_MAX)
+ .setDataFileMinimumUseRatio(0.99)
+ .setDataFileCleanupHeadFile(true);
+ }
+
+ /**
+ * Builder with only deletion-dir + small data file sizes set, so housekeeping
+ * uses default thresholds (head-file cleanup off, default minimum-use-ratio).
+ * Used by {@link #noCustomerIsLostAfterHousekeeping} to test the
+ * never-lose-an-object invariant under realistic configurations.
+ */
+ private static EmbeddedStorageConfigurationBuilder defaultConfigBuilder(
+ final Path storageDir,
+ final Path deletionDir,
+ final int channelCount
+ )
+ {
+ return EmbeddedStorageConfigurationBuilder.New()
+ .setStorageDirectory(storageDir.toString())
+ .setDeletionDirectory(deletionDir.toString())
+ .setChannelCount(channelCount)
+ .setDataFileMinimumSize(DATA_FILE_MIN)
+ .setDataFileMaximumSize(DATA_FILE_MAX);
+ }
+
+ private static void runFreshSession(
+ final Path storageDir,
+ final Path deletionDir,
+ final int channelCount,
+ final Object initialRoot,
+ final Consumer action
+ )
+ {
+ final EmbeddedStorageManager mgr = configBuilder(storageDir, deletionDir, channelCount)
+ .createEmbeddedStorageFoundation()
+ .createEmbeddedStorageManager(initialRoot)
+ .start();
+ try {
+ action.accept(mgr);
+ } finally {
+ mgr.shutdown();
+ }
+ }
+
+ private static void runReopenSession(
+ final Path storageDir,
+ final Path deletionDir,
+ final int channelCount,
+ final Consumer action
+ )
+ {
+ final EmbeddedStorageManager mgr = configBuilder(storageDir, deletionDir, channelCount)
+ .createEmbeddedStorageFoundation()
+ .createEmbeddedStorageManager()
+ .start();
+ try {
+ action.accept(mgr);
+ } finally {
+ mgr.shutdown();
+ }
+ }
+
+ private static void runDefaultFreshSession(
+ final Path storageDir,
+ final Path deletionDir,
+ final int channelCount,
+ final Object initialRoot,
+ final Consumer action
+ )
+ {
+ final EmbeddedStorageManager mgr = defaultConfigBuilder(storageDir, deletionDir, channelCount)
+ .createEmbeddedStorageFoundation()
+ .createEmbeddedStorageManager(initialRoot)
+ .start();
+ try {
+ action.accept(mgr);
+ } finally {
+ mgr.shutdown();
+ }
+ }
+
+ private static EmbeddedStorageConfigurationBuilder backupConfigBuilder(
+ final Path storageDir,
+ final Path deletionDir,
+ final Path backupDir,
+ final int channelCount
+ )
+ {
+ return EmbeddedStorageConfigurationBuilder.New()
+ .setStorageDirectory(storageDir.toString())
+ .setDeletionDirectory(deletionDir.toString())
+ .setBackupDirectory(backupDir.toString())
+ .setChannelCount(channelCount)
+ .setDataFileMinimumSize(DATA_FILE_MIN)
+ .setDataFileMaximumSize(DATA_FILE_MAX)
+ .setDataFileMinimumUseRatio(0.99)
+ .setDataFileCleanupHeadFile(true);
+ }
+
+ private static void runFreshSessionWithBackup(
+ final Path storageDir,
+ final Path deletionDir,
+ final Path backupDir,
+ final Object initialRoot,
+ final Consumer action
+ )
+ {
+ final EmbeddedStorageManager mgr = backupConfigBuilder(storageDir, deletionDir, backupDir, 1)
+ .createEmbeddedStorageFoundation()
+ .createEmbeddedStorageManager(initialRoot)
+ .start();
+ try {
+ action.accept(mgr);
+ } finally {
+ mgr.shutdown();
+ }
+ }
+
+ private static void runReopenSessionWithBackup(
+ final Path storageDir,
+ final Path deletionDir,
+ final Path backupDir,
+ final Consumer action
+ )
+ {
+ final EmbeddedStorageManager mgr = backupConfigBuilder(storageDir, deletionDir, backupDir, 1)
+ .createEmbeddedStorageFoundation()
+ .createEmbeddedStorageManager()
+ .start();
+ try {
+ action.accept(mgr);
+ } finally {
+ mgr.shutdown();
+ }
+ }
+
+ private static boolean hashesEqual(final Set a, final Set b)
+ {
+ return a.equals(b);
+ }
+
+ private static Set intersectionWith(final Set a, final Set b)
+ {
+ final Set result = new HashSet<>(a);
+ result.retainAll(b);
+ return result;
+ }
+
+ private static void runDefaultReopenSession(
+ final Path storageDir,
+ final Path deletionDir,
+ final int channelCount,
+ final Consumer action
+ )
+ {
+ final EmbeddedStorageManager mgr = defaultConfigBuilder(storageDir, deletionDir, channelCount)
+ .createEmbeddedStorageFoundation()
+ .createEmbeddedStorageManager()
+ .start();
+ try {
+ action.accept(mgr);
+ } finally {
+ mgr.shutdown();
+ }
+ }
+
+ private static void dumpFilesWithMeta(final Path dir)
+ throws IOException, NoSuchAlgorithmException
+ {
+ if (!Files.exists(dir)) {
+ System.err.println(" (does not exist: " + dir + ")");
+ return;
+ }
+ final MessageDigest md = MessageDigest.getInstance("SHA-256");
+ try (Stream stream = Files.walk(dir)) {
+ final List files = stream
+ .filter(Files::isRegularFile)
+ .sorted(Comparator.naturalOrder())
+ .toList();
+ for (final Path f : files) {
+ final byte[] data = Files.readAllBytes(f);
+ md.reset();
+ final byte[] digest = md.digest(data);
+ final StringBuilder sb = new StringBuilder(digest.length * 2);
+ for (final byte b : digest) {
+ sb.append(String.format("%02x", b));
+ }
+ System.err.println(" " + dir.relativize(f) + " size=" + data.length + " sha=" + sb);
+ }
+ }
+ }
+
+ private static Set collectFileHashes(final Path dir)
+ throws IOException, NoSuchAlgorithmException
+ {
+ if (!Files.exists(dir)) {
+ return new HashSet<>();
+ }
+
+ final Set hashes = new HashSet<>();
+ final MessageDigest md = MessageDigest.getInstance("SHA-256");
+
+ try (Stream stream = Files.walk(dir)) {
+ final List files = stream
+ .filter(Files::isRegularFile)
+ .sorted(Comparator.naturalOrder())
+ .toList();
+
+ for (final Path f : files) {
+ final byte[] data = Files.readAllBytes(f);
+ if (data.length == 0) {
+ continue;
+ }
+ md.reset();
+ final byte[] digest = md.digest(data);
+ final StringBuilder sb = new StringBuilder(digest.length * 2);
+ for (final byte b : digest) {
+ sb.append(String.format("%02x", b));
+ }
+ hashes.add(sb.toString());
+ }
+ }
+
+ return hashes;
+ }
+}
diff --git a/integration-tests/src/test/java/test/eclipse/store/cache/CacheEvictTest.java b/integration-tests/src/test/java/test/eclipse/store/cache/CacheEvictTest.java
new file mode 100644
index 000000000..c8308d8ed
--- /dev/null
+++ b/integration-tests/src/test/java/test/eclipse/store/cache/CacheEvictTest.java
@@ -0,0 +1,92 @@
+package test.eclipse.store.cache;
+
+/*-
+ * #%L
+ * EclipseStore Integration Tests
+ * %%
+ * Copyright (C) 2023 - 2026 MicroStream Software
+ * %%
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ * #L%
+ */
+
+
+import javax.cache.Cache;
+import javax.cache.CacheManager;
+import javax.cache.Caching;
+import javax.cache.configuration.MutableConfiguration;
+import javax.cache.expiry.CreatedExpiryPolicy;
+import javax.cache.expiry.Duration;
+import javax.cache.spi.CachingProvider;
+import java.nio.file.Path;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.serializer.reference.LazyReferenceManager;
+import org.eclipse.store.cache.types.CacheConfiguration;
+import org.eclipse.store.storage.embedded.types.EmbeddedStorage;
+import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+
+public class CacheEvictTest
+{
+
+ private static final String CACHED_VALUE = "one";
+ private static final int KEY = 1;
+
+ @Test
+ void cacheItemEvicted() throws InterruptedException
+ {
+ CachingProvider provider = Caching.getCachingProvider();
+ CacheManager cacheManager = provider.getCacheManager();
+
+
+ MutableConfiguration configuration = new MutableConfiguration()
+ .setExpiryPolicyFactory(CreatedExpiryPolicy.factoryOf(new Duration(TimeUnit.MILLISECONDS, 500)))
+ .setStoreByValue(true);
+
+ Cache cache = cacheManager.createCache("jCache1", configuration);
+ cache.put(KEY, CACHED_VALUE);
+
+ Assertions.assertEquals(CACHED_VALUE, cache.get(KEY));
+
+ Thread.sleep(700); // More then the 1 sec expiration policy
+
+ Assertions.assertNull(cache.get(KEY));
+ }
+
+ @Test
+ void cacheItemEvictedWithStorage(@TempDir Path tempdir) throws InterruptedException
+ {
+ EmbeddedStorageManager storageManager = EmbeddedStorage.start(tempdir);
+
+ CachingProvider provider = Caching.getCachingProvider();
+ CacheManager cacheManager = provider.getCacheManager();
+
+
+ CacheConfiguration configuration = CacheConfiguration
+ .Builder(Integer.class, String.class, "jCache", storageManager)
+ .expiryPolicyFactory(CreatedExpiryPolicy.factoryOf(new Duration(TimeUnit.MILLISECONDS, 500)))
+ .build();
+
+ Cache cache = cacheManager.createCache("jCache", configuration);
+ cache.put(KEY, CACHED_VALUE);
+
+ Assertions.assertEquals(CACHED_VALUE, cache.get(KEY));
+
+ Thread.sleep(700); // More than the 1 sec expiration policy
+
+ Assertions.assertNull(cache.get(KEY));
+
+ storageManager.close();
+
+ LazyReferenceManager referenceManager = LazyReferenceManager.get();
+ referenceManager.stop();
+ }
+}
diff --git a/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/Export.java b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/Export.java
new file mode 100644
index 000000000..d8c09feb9
--- /dev/null
+++ b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/Export.java
@@ -0,0 +1,97 @@
+package test.eclipse.store.collections.lazy.arraylist;
+
+/*-
+ * #%L
+ * EclipseStore Integration Tests
+ * %%
+ * Copyright (C) 2023 - 2026 MicroStream Software
+ * %%
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ * #L%
+ */
+
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import org.eclipse.serializer.collections.types.XSequence;
+import org.eclipse.serializer.util.cql.CQL;
+import org.eclipse.store.afs.nio.types.NioFileSystem;
+import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager;
+import org.eclipse.store.storage.types.*;
+
+public class Export
+{
+
+ public static XSequence export(final EmbeddedStorageManager storage, final String target)
+ {
+
+ final NioFileSystem fileSystem = NioFileSystem.New();
+
+ final String fileSuffix = "bin";
+ final StorageConnection connection = storage.createConnection();
+ final StorageEntityTypeExportStatistics exportResult = connection.exportTypes(
+ new StorageEntityTypeExportFileProvider.Default(
+ fileSystem.ensureDirectoryPath(target),
+ fileSuffix
+ ),
+ typeHandler -> true // export all, customize if necessary
+ );
+ final XSequence exportFiles = CQL
+ .from(exportResult.typeStatistics().values())
+ .project(s -> Paths.get(s.file().toPathString()))
+ .execute();
+
+ return exportFiles;
+ }
+
+ public static void convertToCSV(final EmbeddedStorageManager storage, final XSequence files, final String target)
+ {
+
+ final NioFileSystem fileSystem = NioFileSystem.New();
+
+ final StorageDataConverterTypeBinaryToCsv converter =
+ new StorageDataConverterTypeBinaryToCsv.UTF8(
+ StorageDataConverterCsvConfiguration.defaultConfiguration(),
+ new StorageEntityTypeConversionFileProvider.Default(
+ fileSystem.ensureDirectoryPath(target),
+ "csv"
+ ),
+ storage.typeDictionary(),
+ null, // no type name mapping
+ 4096, // read buffer size
+ 4096 // write buffer size
+ );
+
+ files.forEach(file -> converter.convertDataFile(
+ fileSystem.ensureFile(file).tryUseReading()
+ ));
+ }
+
+ public static void exportAsCSV(final EmbeddedStorageManager storage, final Path targetDir)
+ {
+
+ System.out.println("deleting target dir before export " + targetDir.toString());
+
+ try {
+ Utils.deleteAll(targetDir);
+ } catch (final IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ return;
+ }
+
+ final Path binDir = targetDir.resolve("bin");
+ final XSequence exports = export(storage, binDir.toString());
+
+ final Path csvDir = targetDir.resolve("csv");
+ convertToCSV(storage, exports, csvDir.toString());
+
+ }
+
+}
diff --git a/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/LazyArrayListAutoUnloadTest.java b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/LazyArrayListAutoUnloadTest.java
new file mode 100644
index 000000000..c83f712e8
--- /dev/null
+++ b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/LazyArrayListAutoUnloadTest.java
@@ -0,0 +1,232 @@
+package test.eclipse.store.collections.lazy.arraylist;
+
+/*-
+ * #%L
+ * EclipseStore Integration Tests
+ * %%
+ * Copyright (C) 2023 - 2026 MicroStream Software
+ * %%
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ * #L%
+ */
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.lang.reflect.Field;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.eclipse.serializer.collections.lazy.LazyArrayList;
+import org.eclipse.serializer.collections.lazy.LazySegmentUnloader;
+import org.eclipse.store.storage.embedded.types.EmbeddedStorage;
+import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+public class LazyArrayListAutoUnloadTest
+{
+
+
+ @Test
+ void removedSegments(@TempDir final Path path)
+ {
+
+ LazyArrayList list = new LazyArrayList<>(3, new LazySegmentUnloader.Default(1));
+ for (int i = 0; i < 27; i++) {
+ list.add("Entry_" + i);
+ }
+
+ try (final EmbeddedStorageManager storage = EmbeddedStorage.start(list, path)) {
+
+
+ storage.setRoot(list);
+ storage.storeRoot();
+ //System.out.println("total segments: " + countSegments(list));
+ assertEquals(9, countSegments(list));
+
+ list.remove(0);
+ list.remove(0);
+ list.remove(0);
+ list.remove(0);
+ list.remove(0);
+ list.remove(0);
+ list.get(20);
+
+ storage.store(list);
+ //System.out.println("total segment after remove: " + countSegments(list));
+ assertEquals(7, countSegments(list));
+
+ //System.out.println("loaded segments after remove: \n" + getLoadedSegments(list).size() + "\n");
+ assertEquals(1, getLoadedSegments(list).size());
+
+ }
+ }
+
+
+ @Test
+ void devTest(@TempDir final Path path)
+ {
+
+ LazyArrayList list = new LazyArrayList<>(5);
+
+ for (int i = 0; i < 27; i++) {
+ list.add("Entry_" + i);
+ }
+
+ try (final EmbeddedStorageManager storage = EmbeddedStorage.start(list, path)) {
+
+ assertFalse(list.contains("not existing"));
+ //System.out.println("loaded segments after contains (not found): \n" + getLoadedSegments(list) + "\n");
+ assertLoadedSegment(list);
+
+ assertTrue(list.contains("Entry_18"));
+ //System.out.println("loaded segments after contains (found) : \n" + getLoadedSegments(list) + "\n");
+ assertLoadedSegment(list);
+
+ list.add("Entry_28");
+ storage.store(list);
+ //System.out.println("loaded segments after add : \n" + getLoadedSegments(list) + "\n");
+ assertLoadedSegment(list);
+
+ list.add(9, "Entry_add_9");
+ storage.store(list);
+ //System.out.println("\n loaded segments after add index : \n" + getLoadedSegments(list) + "\n");
+ assertLoadedSegment(list);
+
+ list.set(19, "Entry_set_18");
+ storage.store(list);
+ //System.out.println("loaded segments after set : \n" + getLoadedSegments(list) + "\n");
+ assertLoadedSegment(list);
+
+ list.get(11);
+ storage.store(list);
+ //System.out.println("loaded segments after get : \n" + getLoadedSegments(list) + "\n");
+ assertLoadedSegment(list);
+
+ list.forEach(e -> e.concat("Hallo"));
+ storage.store(list);
+ //System.out.println("loaded segments after forEach : \n" + getLoadedSegments(list) + "\n");
+ assertLoadedSegment(list);
+
+ list.remove("Entry_13");
+ storage.store(list);
+ //System.out.println("loaded segments after remove1 : \n" + getLoadedSegments(list) + "\n");
+ assertLoadedSegment(list);
+
+ list.remove("Entry_28");
+ storage.store(list);
+ //System.out.println("loaded segments after remove2 : \n" + getLoadedSegments(list) + "\n");
+ assertLoadedSegment(list);
+
+ list.remove(list.size() - 1);
+ list.remove(list.size() - 1);
+ storage.store(list);
+ //System.out.println("loaded segments after remove3 : \n" + getLoadedSegments(list) + "\n");
+ assertLoadedSegment(list);
+
+ list.remove(14);
+ list.remove(15);
+ list.remove(16);
+ list.remove(17);
+ //System.out.println("loaded segments after remove4 : \n" + getLoadedSegments(list) + "\n");
+ storage.store(list);
+ //System.out.println("loaded segments after remove4 : \n" + getLoadedSegments(list) + "\n");
+ list.get(0);
+ //System.out.println("loaded segments after remove4 : \n" + getLoadedSegments(list) + "\n");
+ assertLoadedSegment(list);
+
+ }
+
+
+ //System.out.println("--------------------------------- reloading storage ---------------------------------");
+
+ try (final EmbeddedStorageManager storageReloaded = EmbeddedStorage.start(path)) {
+
+ @SuppressWarnings("unchecked")
+ LazyArrayList listReloaded = (LazyArrayList) storageReloaded.root();
+ //listReloaded.forEach(e -> System.out.println(e));
+ //System.out.println("loaded segments after forEach : \n" + getLoadedSegments(listReloaded) + "\n");
+ assertLoadedSegment(list);
+
+ }
+ }
+
+
+ static void unloadAll(LazyArrayList> list)
+ {
+
+ final Iterable extends LazyArrayList>.Segment> segments = list.segments();
+ segments.forEach(LazyArrayList.Segment::unloadSegment);
+
+ }
+
+
+ void assertLoadedSegment(LazyArrayList> list)
+ {
+
+ int expected = 0;
+
+ try {
+ final Field unloader = list.getClass()
+ .getDeclaredField("unloader");
+ unloader.setAccessible(true);
+ final Field load = unloader.get(list)
+ .getClass()
+ .getDeclaredField("desiredLoadCount");
+ load.setAccessible(true);
+ final Object ul = unloader.get(list);
+ expected = load.getInt(ul);
+
+ } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+
+
+ assertTrue(getLoadedSegments(list).size() <= expected);
+ }
+
+ void assertNoLoadedSegment(LazyArrayList> list)
+ {
+ assertTrue(getLoadedSegments(list).size() == 0);
+ }
+
+ static int countSegments(LazyArrayList> list)
+ {
+
+ final AtomicInteger count = new AtomicInteger();
+
+ list.segments()
+ .forEach(s -> count.incrementAndGet());
+
+ return count.get();
+ }
+
+ static List.Segment> getLoadedSegments(LazyArrayList> list)
+ {
+
+ final Iterable extends LazyArrayList>.Segment> segments = list.segments();
+
+ final List.Segment> loadedSegments = new ArrayList<>();
+ segments.forEach(s -> {
+ if (s.isLoaded()) {
+ loadedSegments.add(s);
+ }
+ });
+
+ return loadedSegments;
+ }
+
+ static EmbeddedStorageManager startStorage(final Path path)
+ {
+ final EmbeddedStorageManager storage = EmbeddedStorage
+ .Foundation(path)
+ .start();
+ return storage;
+ }
+}
diff --git a/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/LazyArrayListIterateLoadedFirstTest.java b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/LazyArrayListIterateLoadedFirstTest.java
new file mode 100644
index 000000000..38cd00a43
--- /dev/null
+++ b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/LazyArrayListIterateLoadedFirstTest.java
@@ -0,0 +1,258 @@
+package test.eclipse.store.collections.lazy.arraylist;
+
+/*-
+ * #%L
+ * EclipseStore Integration Tests
+ * %%
+ * Copyright (C) 2023 - 2026 MicroStream Software
+ * %%
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ * #L%
+ */
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.ConcurrentModificationException;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.eclipse.serializer.collections.lazy.LazyArrayList;
+import org.eclipse.store.storage.embedded.types.EmbeddedStorage;
+import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+public class LazyArrayListIterateLoadedFirstTest
+{
+
+ @Test
+ void concurrentModificationNext(@TempDir final Path path)
+ {
+ try (final EmbeddedStorageManager storage = createListStorage(path, 5, 8)) {
+
+ @SuppressWarnings("unchecked")
+ LazyArrayList list = (LazyArrayList) storage.root();
+
+ final Iterator iter = list.loadedFirstIterator();
+ list.add(new ListEntry("unexpected"));
+
+ assertThrows(ConcurrentModificationException.class, () -> iter.next());
+ }
+ }
+
+ @Disabled
+ @Test
+ void concurrentModificationRemove(@TempDir final Path path)
+ {
+ try (final EmbeddedStorageManager storage = createListStorage(path, 5, 8)) {
+
+ @SuppressWarnings("unchecked")
+ LazyArrayList list = (LazyArrayList) storage.root();
+
+ final Iterator iter = list.loadedFirstIterator();
+ iter.next();
+ list.add(new ListEntry("unexpected"));
+
+ assertThrows(ConcurrentModificationException.class, () -> iter.remove());
+
+ }
+ }
+
+
+ @Test
+ void emptyList(@TempDir final Path path)
+ {
+ try (final EmbeddedStorageManager storage = createListStorage(path, 5, 0)) {
+
+ @SuppressWarnings("unchecked")
+ LazyArrayList list = (LazyArrayList) storage.root();
+
+ final Iterator iter = list.loadedFirstIterator();
+ assertFalse(iter.hasNext());
+
+ }
+ }
+
+ @Test
+ void RemoveSome(@TempDir final Path path)
+ {
+ try (final EmbeddedStorageManager storage = createListStorage(path, 3, 67)) {
+
+
+ final List referenceList = new ArrayList<>();
+ for (int i = 0; i < 67; i++) {
+ referenceList.add(new ListEntry("Entry-" + i));
+ }
+ referenceList.removeIf(e -> e.id.contains("5"));
+
+
+ @SuppressWarnings("unchecked")
+ LazyArrayList list = (LazyArrayList) storage.root();
+
+ final Iterator iter = list.loadedFirstIterator();
+
+ while (iter.hasNext()) {
+ final ListEntry item = iter.next();
+ if (item.id.contains("5")) iter.remove();
+ }
+
+ assertIterableEquals(referenceList, list);
+
+ }
+ }
+
+ @Test
+ void removeAll(@TempDir final Path path)
+ {
+ try (final EmbeddedStorageManager storage = createListStorage(path, 3, 7)) {
+
+ @SuppressWarnings("unchecked")
+ LazyArrayList list = (LazyArrayList) storage.root();
+
+ final Iterator iter = list.loadedFirstIterator();
+
+ while (iter.hasNext()) {
+ ListEntry next = iter.next();
+ iter.remove();
+ }
+
+ assertEquals(0, list.size());
+ }
+ }
+
+ @Test
+ void iterateAllUnloaded(@TempDir final Path path)
+ {
+ try (final EmbeddedStorageManager storage = createListStorage(path, 5, 19)) {
+
+ final List referenceList = new ArrayList<>();
+ for (int i = 0; i < 19; i++) {
+ referenceList.add(new ListEntry("Entry-" + i));
+ }
+
+ @SuppressWarnings("unchecked")
+ LazyArrayList list = (LazyArrayList) storage.root();
+
+ final Iterator iter = list.loadedFirstIterator();
+ this.checkIter(iter, referenceList);
+
+ }
+ }
+
+ @Test
+ void iterateAllLoaded(@TempDir final Path path)
+ {
+ try (final EmbeddedStorageManager storage = createListStorage(path, 5, 19)) {
+
+ final List referenceList = new ArrayList<>();
+ for (int i = 0; i < 19; i++) {
+ referenceList.add(new ListEntry("Entry-" + i));
+ }
+
+ @SuppressWarnings("unchecked")
+ LazyArrayList list = (LazyArrayList) storage.root();
+
+
+ list.iterateLazyReferences(l -> {
+ l.get();
+ assertTrue(l.isLoaded());
+ });
+
+
+ final Iterator iter = list.loadedFirstIterator();
+ this.checkIter(iter, referenceList);
+
+ }
+ }
+
+ @Test
+ void iterateSomeLoaded(@TempDir final Path path)
+ {
+ try (final EmbeddedStorageManager storage = createListStorage(path, 5, 19)) {
+
+
+ final List referenceList = new ArrayList<>();
+ for (int i = 0; i < 19; i++) {
+ referenceList.add(new ListEntry("Entry-" + i));
+ }
+
+ @SuppressWarnings("unchecked")
+ LazyArrayList list = (LazyArrayList) storage.root();
+ final AtomicInteger counter = new AtomicInteger();
+ list.iterateLazyReferences(l -> {
+ if (counter.getAndIncrement() % 2 == 0) {
+ l.get();
+ }
+ });
+
+ final Iterator iter = list.loadedFirstIterator();
+ this.checkIter(iter, referenceList);
+ }
+ }
+
+ @Test
+ void unloadedAfterIterate(@TempDir final Path path)
+ {
+ try (final EmbeddedStorageManager storage = createListStorage(path, 5, 19)) {
+
+ final List referenceList = new ArrayList<>();
+ for (int i = 0; i < 19; i++) {
+ referenceList.add(new ListEntry("Entry-" + i));
+ }
+
+ @SuppressWarnings("unchecked")
+ LazyArrayList list = (LazyArrayList) storage.root();
+ list.iterateLazyReferences(l -> {
+ l.get();
+ assertTrue(l.isLoaded());
+ });
+
+ final Iterator iter = list.loadedFirstIterator();
+ this.checkIter(iter, referenceList);
+
+ AtomicInteger loadedSegments = new AtomicInteger();
+ list.iterateLazyReferences(l -> {
+ if (l.isLoaded()) loadedSegments.incrementAndGet();
+ });
+ assertEquals(2, loadedSegments.get());
+
+ }
+ }
+
+ void checkIter(final Iterator> iter, final List> reference)
+ {
+ while (iter.hasNext()) {
+ assertTrue(reference.remove(iter.next()), "Iterator returned element that is not in the reference list!");
+ }
+ assertTrue(reference.isEmpty(), "reference list should be empty after removing all iterated elements!");
+ }
+
+ static EmbeddedStorageManager createListStorage(final Path path, final int segmentSize, final int entries)
+ {
+ final LazyArrayList lazyList = createLazyList(segmentSize, entries);
+ try (final EmbeddedStorageManager storage = EmbeddedStorage.start(lazyList, path)) {
+
+ }
+
+ return EmbeddedStorage.start(path);
+ }
+
+ static LazyArrayList createLazyList(final int segmentSize, final int entries)
+ {
+ final LazyArrayList lazyList = new LazyArrayList<>(segmentSize);
+
+ for (int i = 0; i < entries; i++) {
+ lazyList.add(new ListEntry("Entry-" + i));
+ }
+ return lazyList;
+ }
+
+}
diff --git a/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/LazyArrayListIteratorTest.java b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/LazyArrayListIteratorTest.java
new file mode 100644
index 000000000..9eac0487e
--- /dev/null
+++ b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/LazyArrayListIteratorTest.java
@@ -0,0 +1,74 @@
+package test.eclipse.store.collections.lazy.arraylist;
+
+/*-
+ * #%L
+ * EclipseStore Integration Tests
+ * %%
+ * Copyright (C) 2023 - 2026 MicroStream Software
+ * %%
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ * #L%
+ */
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import org.eclipse.serializer.collections.lazy.LazyArrayList;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+public class LazyArrayListIteratorTest
+{
+
+ private static Class> clazz;
+
+ @BeforeAll
+ static void init()
+ {
+ final Class>[] declaredClass = LazyArrayList.class.getDeclaredClasses();
+ final Optional> opt = Stream.of(declaredClass).filter(e -> e.getName().equals("one.microstream.collections.lazy.LazyArrayList$Itr")).findFirst();
+ if (opt.isPresent()) {
+ clazz = opt.get();
+ }
+ }
+
+
+ static void main(final String[] args)
+ {
+
+ final int numElements = 255000;
+ final List list = LazyArrayListPersistenceTest.createLazyList(10, numElements);
+ }
+
+
+ @Test
+ @Disabled("to long test")
+ void iterateNext()
+ {
+
+ final int numElements = 255000;
+ final LazyArrayList lazyList = LazyArrayListPersistenceTest.createLazyList(10, numElements);
+ final Iterator iter = lazyList.iterator();
+
+ //assertInstanceOf(clazz, iter);
+
+
+ int counter = 0;
+ while (iter.hasNext()) {
+ final ListEntry e = iter.next();
+ assertEquals("Entry-" + counter, e.id);
+ counter++;
+ }
+ assertEquals(numElements, counter);
+ }
+
+}
diff --git a/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/LazyArrayListLoadedTest.java b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/LazyArrayListLoadedTest.java
new file mode 100644
index 000000000..05dd2ba2f
--- /dev/null
+++ b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/LazyArrayListLoadedTest.java
@@ -0,0 +1,86 @@
+package test.eclipse.store.collections.lazy.arraylist;
+
+/*-
+ * #%L
+ * EclipseStore Integration Tests
+ * %%
+ * Copyright (C) 2023 - 2026 MicroStream Software
+ * %%
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ * #L%
+ */
+
+import java.nio.file.Path;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.eclipse.serializer.collections.lazy.LazyArrayList;
+import org.eclipse.store.storage.embedded.types.EmbeddedStorage;
+import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import test.eclipse.serializer.fixtures.types.PrimitiveTypes;
+
+public class LazyArrayListLoadedTest
+{
+
+ @TempDir
+ Path storageLocation;
+
+ @Test
+ public void lazyArraylistLoadTest() throws InterruptedException
+ {
+
+ LazyArrayList list = Stream.generate(PrimitiveTypes::new)
+ .limit(100_000)
+ .collect(Collectors.toCollection(LazyArrayList::new));
+
+ PrimitiveTypes types = new PrimitiveTypes();
+ types.fillSampleData();
+ list.add(50, types);
+
+ AtomicInteger isLoaded = new AtomicInteger(0);
+
+ try (EmbeddedStorageManager storage = EmbeddedStorage.start(list, storageLocation)) {
+
+
+ list.iterateLazyReferences(i -> i.clear());
+
+ list.iterateLazyReferences(i -> {
+ if (i.isLoaded()) {
+ isLoaded.incrementAndGet();
+ }
+ }
+ );
+
+
+ list.remove(60);
+
+ list.iterateLazyReferences(i -> {
+ if (i.isLoaded()) {
+ isLoaded.incrementAndGet();
+ }
+ }
+ );
+
+
+ Assertions.assertEquals(1, isLoaded.get());
+ }
+ }
+
+ private static void printMemory()
+ {
+ Long memoryAmount = Runtime.getRuntime()
+ .totalMemory() - Runtime.getRuntime()
+ .freeMemory();
+ //System.out.println(memoryAmount);
+ }
+
+}
diff --git a/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/LazyArrayListModCountTest.java b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/LazyArrayListModCountTest.java
new file mode 100644
index 000000000..bb76b3d2d
--- /dev/null
+++ b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/LazyArrayListModCountTest.java
@@ -0,0 +1,214 @@
+package test.eclipse.store.collections.lazy.arraylist;
+
+/*-
+ * #%L
+ * EclipseStore Integration Tests
+ * %%
+ * Copyright (C) 2023 - 2026 MicroStream Software
+ * %%
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ * #L%
+ */
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.lang.reflect.Field;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+
+import org.eclipse.serializer.collections.lazy.LazyArrayList;
+import org.eclipse.serializer.exceptions.IllegalAccessRuntimeException;
+import org.eclipse.serializer.reflect.XReflect;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+
+@Disabled
+public class LazyArrayListModCountTest
+{
+
+ private static Field modCount;
+
+ @BeforeAll
+ static void initTests()
+ {
+ modCount = XReflect.setAccessible(LazyArrayList.class, XReflect.getAnyField(LazyArrayList.class, "modCount"));
+ }
+
+ @Test
+ void add()
+ {
+ final LazyArrayList lal = LazyArrayListPersistenceTest.createLazyList(4, 0);
+ final int initialModCount = getField_int(modCount, lal);
+
+ lal.add(new ListEntry("new entry"));
+
+ assertEquals(initialModCount + 1, getField_int(modCount, lal));
+ }
+
+ @Test
+ void addIndex()
+ {
+ final LazyArrayList lal = LazyArrayListPersistenceTest.createLazyList(4, 2);
+ final int initialModCount = getField_int(modCount, lal);
+
+ lal.add(1, new ListEntry("new entry"));
+
+ assertEquals(initialModCount + 1, getField_int(modCount, lal));
+ }
+
+ @Test
+ void addAll()
+ {
+ final LazyArrayList lal = LazyArrayListPersistenceTest.createLazyList(4, 2);
+ final int initialModCount = getField_int(modCount, lal);
+
+ lal.addAll(List.of(new ListEntry("new entry 1"), new ListEntry("new entry 2"), new ListEntry("new entry 3"), new ListEntry("new entry 4")));
+
+ assertEquals(initialModCount + 1, getField_int(modCount, lal));
+ }
+
+ @Test
+ void addAllIndex()
+ {
+ final LazyArrayList lal = LazyArrayListPersistenceTest.createLazyList(4, 2);
+ final int initialModCount = getField_int(modCount, lal);
+
+ lal.addAll(1, List.of(new ListEntry("new entry 1"), new ListEntry("new entry 2"), new ListEntry("new entry 3"), new ListEntry("new entry 4")));
+
+ assertEquals(initialModCount + 1, getField_int(modCount, lal));
+ }
+
+ @Test
+ void clear()
+ {
+ final LazyArrayList lal = LazyArrayListPersistenceTest.createLazyList(4, 4);
+ final int initialModCount = getField_int(modCount, lal);
+
+ lal.clear();
+
+ assertEquals(initialModCount + 1, getField_int(modCount, lal));
+ }
+
+ @Test
+ void consolidate_nothingTodo()
+ {
+ final LazyArrayList lal = LazyArrayListPersistenceTest.createLazyList(4, 4);
+ final int initialModCount = getField_int(modCount, lal);
+
+ lal.consolidate();
+
+ assertEquals(initialModCount, getField_int(modCount, lal));
+ }
+
+ @Test
+ void consolidate()
+ {
+ final LazyArrayList lal = LazyArrayListPersistenceTest.createLazyList(2, 6);
+ lal.removeAll(List.of(new ListEntry("Entry-1"), new ListEntry("Entry-2"), new ListEntry("Entry-3"), new ListEntry("Entry-4")));
+ final int initialModCount = getField_int(modCount, lal);
+
+ lal.consolidate();
+
+ assertEquals(initialModCount + 2, getField_int(modCount, lal));
+ }
+
+ @Test
+ void remove()
+ {
+ final LazyArrayList lal = LazyArrayListPersistenceTest.createLazyList(4, 4);
+ final int initialModCount = getField_int(modCount, lal);
+
+ lal.remove(new ListEntry("Entry-1"));
+
+ assertEquals(initialModCount + 1, getField_int(modCount, lal));
+ }
+
+ @Test
+ void removeIndex()
+ {
+ final LazyArrayList lal = LazyArrayListPersistenceTest.createLazyList(4, 4);
+ final int initialModCount = getField_int(modCount, lal);
+
+ lal.remove(2);
+
+ assertEquals(initialModCount + 1, getField_int(modCount, lal));
+ }
+
+ @Test
+ void removeAll()
+ {
+ final LazyArrayList lal = LazyArrayListPersistenceTest.createLazyList(4, 7);
+ final int initialModCount = getField_int(modCount, lal);
+
+ lal.removeAll(List.of(new ListEntry("Entry-1"), new ListEntry("Entry-2"), new ListEntry("Entry-3"), new ListEntry("Entry-4")));
+
+ assertTrue(initialModCount < getField_int(modCount, lal));
+ }
+
+ @Test
+ void retainAll()
+ {
+ final LazyArrayList lal = LazyArrayListPersistenceTest.createLazyList(4, 7);
+ final int initialModCount = getField_int(modCount, lal);
+
+ lal.retainAll(List.of(new ListEntry("Entry-1"), new ListEntry("Entry-2"), new ListEntry("Entry-3"), new ListEntry("Entry-3")));
+
+ assertTrue(initialModCount < getField_int(modCount, lal));
+ }
+
+ @Test
+ void removeIf()
+ {
+ final LazyArrayList lal = LazyArrayListPersistenceTest.createLazyList(4, 7);
+ final int initialModCount = getField_int(modCount, lal);
+
+ lal.removeIf(e -> (e.id.contains("2") || e.id.contains("4")));
+
+ assertTrue(initialModCount < getField_int(modCount, lal));
+ }
+
+ @Test
+ void iteratorRemove()
+ {
+ final LazyArrayList lal = LazyArrayListPersistenceTest.createLazyList(4, 7);
+ final int initialModCount = getField_int(modCount, lal);
+
+ final Iterator iter = lal.iterator();
+ iter.next();
+ while (iter.hasNext()) {
+ iter.next();
+ iter.remove();
+ }
+
+ assertTrue(initialModCount < getField_int(modCount, lal));
+ }
+
+ @Test
+ void iteratorAdd()
+ {
+ final LazyArrayList lal = LazyArrayListPersistenceTest.createLazyList(4, 7);
+ final int initialModCount = getField_int(modCount, lal);
+
+ final ListIterator iter = lal.listIterator(6);
+ iter.add(new ListEntry("ListEntry"));
+
+ assertTrue(initialModCount < getField_int(modCount, lal));
+ }
+
+ public static int getField_int(final Field f, final Object obj) throws IllegalAccessRuntimeException
+ {
+ try {
+ return f.getInt(obj);
+ } catch (final IllegalAccessException e) {
+ throw new IllegalAccessRuntimeException(e);
+ }
+ }
+}
diff --git a/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/LazyArrayListPersistenceTest.java b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/LazyArrayListPersistenceTest.java
new file mode 100644
index 000000000..cdc00fcc8
--- /dev/null
+++ b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/LazyArrayListPersistenceTest.java
@@ -0,0 +1,496 @@
+package test.eclipse.store.collections.lazy.arraylist;
+
+/*-
+ * #%L
+ * EclipseStore Integration Tests
+ * %%
+ * Copyright (C) 2023 - 2026 MicroStream Software
+ * %%
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ * #L%
+ */
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Consumer;
+
+import org.eclipse.serializer.collections.lazy.LazyArrayList;
+import org.eclipse.store.storage.embedded.types.EmbeddedStorage;
+import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+public class LazyArrayListPersistenceTest
+{
+
+
+ @Test
+ void nullElements(@TempDir final Path path)
+ {
+ final LazyArrayList lazyList = createLazyList(3, 7);
+ lazyList.add(null);
+ lazyList.add(null);
+ lazyList.add(null);
+ lazyList.add(null);
+
+ assertThrows(NullPointerException.class, () -> lazyList.addAll(null));
+
+ final ListEntry element7 = lazyList.get(7);
+ assertNull(element7);
+ lazyList.remove(7);
+
+ try (final EmbeddedStorageManager storage = EmbeddedStorage.start(path)) {
+
+ storage.setRoot(lazyList);
+ storage.storeRoot();
+ }
+
+ loadAndCompare(path, lazyList, l -> {
+ assertNull(l.get(7));
+ assertEquals(10, l.size());
+ });
+ }
+
+ @Test
+ void lastIndexOf(@TempDir final Path path)
+ {
+
+ final LazyArrayList lazyList = createLazyList(3, 7);
+ lazyList.add(0, new ListEntry("added"));
+ lazyList.add(4, new ListEntry("added"));
+ lazyList.add(6, new ListEntry("added"));
+
+ try (final EmbeddedStorageManager storage = EmbeddedStorage.start(lazyList, path)) {
+
+ }
+
+ loadAndCompare(path, lazyList, l -> {
+ assertEquals(6, l.lastIndexOf(new ListEntry("added")));
+ });
+ }
+
+ @Test
+ void lastIndexOfNotFound(@TempDir final Path path)
+ {
+ final LazyArrayList lazyList = createLazyList(3, 7);
+
+ try (final EmbeddedStorageManager storage = EmbeddedStorage.start(lazyList, path)) {
+
+ }
+
+ loadAndCompare(path, lazyList, l -> {
+ assertEquals(-1, l.lastIndexOf(new ListEntry("added")));
+ });
+ }
+
+
+ @Test
+ void indexOf(@TempDir final Path path)
+ {
+ final LazyArrayList lazyList = createLazyList(3, 7);
+
+ try (final EmbeddedStorageManager storage = EmbeddedStorage.start(lazyList, path)) {
+ }
+
+ loadAndCompare(path, lazyList, l -> {
+ assertEquals(4, l.indexOf(new ListEntry("Entry-4")));
+ });
+ }
+
+ @Test
+ void indexOfNotFound(@TempDir final Path path)
+ {
+ final LazyArrayList lazyList = createLazyList(3, 7);
+ try (final EmbeddedStorageManager storage = EmbeddedStorage.start(lazyList, path)) {
+ }
+
+ loadAndCompare(path, lazyList, l -> {
+ assertEquals(-1, l.indexOf(new ListEntry("not in list")));
+ });
+ }
+
+
+ @Test
+ void get(@TempDir final Path path)
+ {
+ final LazyArrayList lazyList = createLazyList(3, 7);
+
+ try (final EmbeddedStorageManager storage = EmbeddedStorage.start(lazyList, path)) {
+ }
+
+ loadAndCompare(path, lazyList, l -> {
+ assertEquals(new ListEntry("Entry-4"), l.get(4));
+ });
+ }
+
+ @Test
+ void contains(@TempDir final Path path)
+ {
+ final LazyArrayList lazyList = createLazyList(3, 7);
+
+ try (final EmbeddedStorageManager storage = EmbeddedStorage.start(lazyList, path)) {
+ }
+
+ loadAndCompare(path, lazyList, l -> {
+ assertTrue(l.contains(new ListEntry("Entry-4")), "LazyArrayList does not contain expected object new String(\"Entry-4\")");
+ });
+ }
+
+ @Test
+ void consolidate(@TempDir final Path path)
+ {
+ final LazyArrayList lazyList = createLazyList(3, 7);
+ lazyList.add(1, new ListEntry("added"));
+ lazyList.add(4, new ListEntry("added"));
+
+ try (final EmbeddedStorageManager storage = EmbeddedStorage.start(lazyList, path)) {
+
+ lazyList.iterateLazyReferences(l -> l.clear());
+
+ lazyList.consolidate();
+ storage.store(lazyList);
+ }
+
+ loadAndCompare(path, lazyList, l -> {
+ //after consolidate the list should have 3 segments with 3 elements each;
+ assertEquals(3, l.getSegmentCount());
+ l.segments()
+ .forEach(s -> {
+ assertEquals(3, s.getSize());
+ });
+ });
+ }
+
+ @Test
+ void clear(@TempDir final Path path)
+ {
+ final LazyArrayList lazyList = createLazyList(3, 7);
+
+ try (final EmbeddedStorageManager storage = EmbeddedStorage.start(lazyList, path)) {
+ lazyList.iterateLazyReferences(l -> l.clear());
+ lazyList.clear();
+ storage.store(lazyList);
+ }
+
+ loadAndCompare(path, lazyList, l -> {
+ assertTrue(l.isEmpty());
+ });
+ }
+
+
+ @Test
+ void addAllAtIndex(@TempDir final Path path)
+ {
+ final LazyArrayList lazyList = createLazyList(3, 7);
+
+ try (final EmbeddedStorageManager storage = EmbeddedStorage.start(lazyList, path)) {
+ lazyList.iterateLazyReferences(l -> l.clear());
+
+ final List newEntries = Arrays.asList(
+ new ListEntry("New entry 1"), new ListEntry("New entry 2"),
+ new ListEntry("New entry 3"), new ListEntry("New entry 4"),
+ new ListEntry("New entry 5"), new ListEntry("New entry 6"));
+
+ lazyList.addAll(4, newEntries);
+ storage.store(lazyList);
+ }
+
+ loadAndCompare(path, lazyList, l -> {
+ assertEquals(13, l.size());
+ });
+ }
+
+ @Test
+ void addAllAtEnd(@TempDir final Path path)
+ {
+ final LazyArrayList lazyList = createLazyList(3, 7);
+ try (final EmbeddedStorageManager storage = EmbeddedStorage.start(lazyList, path)) {
+
+ lazyList.iterateLazyReferences(l -> l.clear());
+
+ final List newEntries = Arrays.asList(
+ new ListEntry("New entry 1"), new ListEntry("New entry 2"),
+ new ListEntry("New entry 3"), new ListEntry("New entry 4"),
+ new ListEntry("New entry 5"), new ListEntry("New entry 6"));
+
+ lazyList.addAll(newEntries);
+ storage.store(lazyList);
+ }
+
+ loadAndCompare(path, lazyList, l -> {
+ assertEquals(13, l.size());
+ });
+
+ }
+
+ @Test
+ void unloadPartialTest(@TempDir final Path path)
+ {
+ final LazyArrayList lazyList = createLazyList(3, 7);
+ try (final EmbeddedStorageManager storage = EmbeddedStorage.start(lazyList, path)) {
+
+
+ lazyList.add(new ListEntry("not stored entry"));
+
+ lazyList.iterateLazyReferences(l -> l.clear());
+
+ lazyList.segments()
+ .forEach(s -> {
+ if (s.getOffset() >= 6) {
+ assertTrue(s.isLoaded(), "Expected dirty references NOT to be unloaded!");
+ } else {
+ assertFalse(s.isLoaded(), "Expected non dirty references to be unloaded!");
+ }
+ });
+
+ }
+
+ //The loaded list must not be the same as the modified but not stored one.
+ assertThrows(org.opentest4j.AssertionFailedError.class, () -> loadAndCompare(path, lazyList, null));
+
+ }
+
+ @Test
+ void unloadAllTest(@TempDir final Path path)
+ {
+ final LazyArrayList lazyList = createLazyList(3, 14);
+ try (final EmbeddedStorageManager storage = EmbeddedStorage.start(lazyList, path)) {
+
+ lazyList.iterateLazyReferences(l -> l.clear());
+
+ lazyList.iterateLazyReferences(l ->
+ assertFalse(l.isLoaded(), "Expected all lazy references to be unloaded!")
+ );
+
+ }
+
+ loadAndCompare(path, lazyList, null);
+ }
+
+ @Test
+ void setElementInList(@TempDir final Path path)
+ {
+
+ final LazyArrayList lazyList = createLazyList(5, 12);
+ try (final EmbeddedStorageManager storage = EmbeddedStorage.start(lazyList, path)) {
+
+ lazyList.iterateLazyReferences(l -> l.clear());
+
+ lazyList.set(3, new ListEntry("lazyList.set"));
+ storage.store(lazyList);
+
+ }
+
+ loadAndCompare(path, lazyList, l -> {
+ assertEquals(new ListEntry("lazyList.set"), l.get(3));
+ });
+ }
+
+ /**
+ * Remove a single element from a persisted list using its index
+ *
+ * @param path
+ */
+ @Test
+ void removeIndex(@TempDir final Path path)
+ {
+ final LazyArrayList lazyList = createLazyList(5, 12);
+
+ try (final EmbeddedStorageManager storage = EmbeddedStorage.start(lazyList, path)) {
+
+ lazyList.iterateLazyReferences(l -> l.clear());
+
+ lazyList.remove(3);
+ storage.store(lazyList);
+
+ }
+
+ loadAndCompare(path, lazyList, null);
+ }
+
+ @Test
+ void removeObject(@TempDir final Path path)
+ {
+ final LazyArrayList lazyList = createLazyList(5, 12);
+
+ try (final EmbeddedStorageManager storage = EmbeddedStorage.start(lazyList, path)) {
+ lazyList.iterateLazyReferences(l -> l.clear());
+
+ lazyList.remove(new ListEntry("Entry-6"));
+ storage.store(lazyList);
+
+ }
+ loadAndCompare(path, lazyList, null);
+ }
+
+ @Test
+ void RemoveAll(@TempDir final Path path)
+ {
+ final LazyArrayList lazyList = createLazyList(3, 7);
+ try (final EmbeddedStorageManager storage = EmbeddedStorage.start(lazyList, path)) {
+
+ lazyList.iterateLazyReferences(l -> l.clear());
+
+ final List newEntries = Arrays.asList(
+ new ListEntry("Entry-1"),
+ new ListEntry("Entry-3"),
+ new ListEntry("Entry-5"));
+
+ lazyList.removeAll(newEntries);
+ storage.store(lazyList);
+ }
+
+ loadAndCompare(path, lazyList, l -> {
+ assertEquals(4, l.size());
+ });
+ }
+
+ @Test
+ void RemoveIf(@TempDir final Path path)
+ {
+ final LazyArrayList lazyList = createLazyList(3, 7);
+
+ try (final EmbeddedStorageManager storage = EmbeddedStorage.start(lazyList, path)) {
+
+ lazyList.iterateLazyReferences(l -> l.clear());
+
+ final List newEntries = Arrays.asList(
+ new ListEntry("Entry-1"),
+ new ListEntry("Entry-3"),
+ new ListEntry("Entry-5"));
+
+ lazyList.removeIf(newEntries::contains);
+ storage.store(lazyList);
+ }
+
+ loadAndCompare(path, lazyList, l -> {
+ assertEquals(4, l.size());
+ });
+ }
+
+ @Test
+ void RetainAll(@TempDir final Path path)
+ {
+ final LazyArrayList lazyList = createLazyList(3, 7);
+ try (final EmbeddedStorageManager storage = EmbeddedStorage.start(lazyList, path)) {
+
+ lazyList.iterateLazyReferences(l -> l.clear());
+
+ final List newEntries = Arrays.asList(
+ new ListEntry("Entry-1"),
+ new ListEntry("Entry-3"),
+ new ListEntry("Entry-5"));
+
+ lazyList.retainAll(newEntries);
+ storage.store(lazyList);
+ }
+
+ loadAndCompare(path, lazyList, l -> {
+ assertEquals(3, l.size());
+ });
+ }
+
+ @Test
+ void addToEmptyList(@TempDir final Path path)
+ {
+ final LazyArrayList lazyList = createLazyList(5, 12);
+ try (final EmbeddedStorageManager storage = EmbeddedStorage.start(lazyList, path)) {
+
+ }
+ loadAndCompare(path, lazyList, null);
+ }
+
+ @Test
+ void addAtIndex(@TempDir final Path path)
+ {
+ final LazyArrayList lazyList = createLazyList(5, 12);
+ try (final EmbeddedStorageManager storage = EmbeddedStorage.start(lazyList, path)) {
+
+ lazyList.iterateLazyReferences(l -> l.clear());
+
+ lazyList.add(4, new ListEntry("Entry add at 4"));
+ storage.store(lazyList);
+ }
+
+ loadAndCompare(path, lazyList, l -> {
+ assertEquals(new ListEntry("Entry add at 4"), l.get(4));
+ });
+ }
+
+ @Test
+ void toObjectArray(@TempDir final Path path)
+ {
+ final LazyArrayList lazyList = createLazyList(5, 12);
+ try (final EmbeddedStorageManager storage = EmbeddedStorage.start(lazyList, path)) {
+
+ lazyList.iterateLazyReferences(l -> l.clear());
+ storage.store(lazyList);
+ }
+
+ loadAndCompare(path, lazyList, l -> {
+ final Object[] array = l.toArray();
+ for (int i = 0; i < array.length; i++) {
+ assertEquals(lazyList.get(i), array[i]);
+ }
+ });
+ }
+
+ @Test
+ void toTypedArray(@TempDir final Path path)
+ {
+
+ final LazyArrayList lazyList = createLazyList(5, 12);
+
+ try (final EmbeddedStorageManager storage = EmbeddedStorage.start(lazyList, path)) {
+ lazyList.iterateLazyReferences(l -> l.clear());
+ storage.store(lazyList);
+ }
+
+ loadAndCompare(path, lazyList, l -> {
+ final Object[] array = l.toArray(new ListEntry[0]);
+ for (int i = 0; i < array.length; i++) {
+ assertEquals(lazyList.get(i), array[i]);
+ }
+ });
+ }
+
+ static LazyArrayList loadAndCompare(final Path path,
+ final LazyArrayList lazyList,
+ final Consumer> f
+ )
+ {
+
+ final LazyArrayList lazyListReloaded = new LazyArrayList<>();
+ try (final EmbeddedStorageManager storageReloaded = EmbeddedStorage.start(lazyListReloaded, path)) {
+
+ assertNotSame(lazyList, lazyListReloaded, "Lists are reference equal");
+ assertEquals(lazyList.size(), lazyListReloaded.size(), "Size mismatch!");
+ assertEquals(lazyList.getMaxSegmentSize(), lazyListReloaded.getMaxSegmentSize(), "getMaxSegmentSize mismatch!");
+ assertEquals(lazyList.getSegmentCount(), lazyListReloaded.getSegmentCount(), "getSegmentCount mismatch!");
+ assertIterableEquals(lazyList, lazyListReloaded);
+
+ if (f != null)
+ f.accept(lazyListReloaded);
+ }
+
+ return lazyListReloaded;
+ }
+
+ static LazyArrayList createLazyList(final int segmentSize, final int entries)
+ {
+ final LazyArrayList lazyList = new LazyArrayList<>(segmentSize);
+
+ for (int i = 0; i < entries; i++) {
+ lazyList.add(new ListEntry("Entry-" + i));
+ }
+ return lazyList;
+ }
+
+}
diff --git a/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/LazyArrayListSmokePersistenceTest.java b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/LazyArrayListSmokePersistenceTest.java
new file mode 100644
index 000000000..23d5ade63
--- /dev/null
+++ b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/LazyArrayListSmokePersistenceTest.java
@@ -0,0 +1,420 @@
+package test.eclipse.store.collections.lazy.arraylist;
+
+/*-
+ * #%L
+ * EclipseStore Integration Tests
+ * %%
+ * Copyright (C) 2023 - 2026 MicroStream Software
+ * %%
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ * #L%
+ */
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.function.UnaryOperator;
+
+import org.apache.commons.lang3.tuple.ImmutablePair;
+import org.eclipse.serializer.collections.lazy.LazyArrayList;
+import org.eclipse.store.storage.embedded.types.EmbeddedStorage;
+import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+public class LazyArrayListSmokePersistenceTest
+{
+
+
+ @TempDir
+ Path location;
+
+ @Test
+ public void addItem()
+ {
+ LazyArrayList list = new LazyArrayList<>();
+
+ try (EmbeddedStorageManager storage = EmbeddedStorage.start(list, location)) {
+
+ assertTrue(list.isEmpty());
+ assertEquals(0, list.size());
+ assertTrue(list.add(1));
+ storage.store(list);
+ LazyArrayList reloaded = (LazyArrayList) storage.root();
+
+ assertEquals(1, reloaded.size());
+ assertEquals(1, reloaded.get(0));
+
+ assertTrue(list.addAll(list));
+ assertEquals(2, list.size());
+
+ assertTrue(list.add(null));
+
+ storage.store(list);
+ list = (LazyArrayList) storage.root();
+ assertEquals(3, list.size());
+ }
+
+ LazyArrayList copy = new LazyArrayList<>();
+
+ try (EmbeddedStorageManager storageManager = EmbeddedStorage.start(copy, location)) {
+ assertEquals(3, list.size());
+ }
+ }
+
+ @Test
+ public void isEmpty()
+ {
+ LazyArrayList list = new LazyArrayList<>();
+ try (EmbeddedStorageManager storageManager = EmbeddedStorage.start(list, location)) {
+ assertTrue(list.isEmpty());
+ }
+ }
+
+ @Test
+ public void contains()
+ {
+ String s = "Hello, i would to be a great object";
+
+ LazyArrayList list = new LazyArrayList<>();
+ list.add(s);
+
+ try (EmbeddedStorageManager storageManager = EmbeddedStorage.start(list, location)) {
+ }
+
+ LazyArrayList copy = new LazyArrayList<>();
+ try (EmbeddedStorageManager storageManager = EmbeddedStorage.start(copy, location)) {
+ assertTrue(copy.contains(s));
+ }
+
+ }
+
+
+ @Test
+ public void iterator()
+ {
+ String s = "Hello, i would to be a great object";
+
+ LazyArrayList list = new LazyArrayList<>();
+ list.add(s);
+ list.add(s);
+ list.add(s);
+
+ LazyArrayList copy = new LazyArrayList<>();
+
+ ImmutablePair, EmbeddedStorageManager> pair = null;
+ try {
+ pair = Util.storeAndLoadList(list, location);
+ ListIterator listIterator = pair.getKey()
+ .listIterator();
+
+ int i = 0;
+
+ while (listIterator.hasNext()) {
+ i++;
+ listIterator.next();
+ }
+
+ assertEquals(3, i);
+ } finally {
+ if ((pair != null) && (pair.getValue() != null)) {
+ pair.getValue()
+ .shutdown();
+ }
+
+ }
+ }
+
+ @Test
+ public void toArray()
+ {
+ List list = new LazyArrayList<>();
+ list.add(1);
+ list.add(2);
+ list.add(3);
+
+ try (EmbeddedStorageManager storage = EmbeddedStorage.start(list, location)) {
+
+ }
+
+ List copy = new LazyArrayList<>();
+ try (EmbeddedStorageManager storage = EmbeddedStorage.start(copy, location)) {
+ Object[] intArray = copy.toArray();
+
+
+ Object[] expectIntArray = {1, 2, 3};
+ assertArrayEquals(expectIntArray, intArray);
+
+ }
+
+ }
+
+ @Test
+ public void toArrayWithType()
+ {
+ LazyArrayList list = new LazyArrayList<>();
+ list.add(1);
+ list.add(2);
+ list.add(3);
+
+
+ ImmutablePair, EmbeddedStorageManager> pair = null;
+ try {
+ pair = Util.storeAndLoadList(list, location);
+ LazyArrayList copy = pair.getKey();
+
+ Integer[] intArray = copy.toArray(new Integer[0]);
+
+ Integer[] expectIntArray = {1, 2, 3};
+ assertArrayEquals(expectIntArray, intArray);
+ } finally {
+ if ((pair != null) && (pair.getValue() != null)) {
+ pair.getValue()
+ .shutdown();
+ }
+
+ }
+
+ }
+
+ @Test
+ public void remove()
+ {
+ LazyArrayList list = new LazyArrayList<>();
+ list.add(1);
+ list.add(2);
+ list.add(3);
+
+
+ ImmutablePair, EmbeddedStorageManager> pair = null;
+ try {
+ pair = Util.storeAndLoadList(list, location);
+ LazyArrayList copy = pair.getKey();
+
+ copy.remove(2);
+
+ assertEquals(2, copy.size());
+
+ } finally {
+ if ((pair != null) && (pair.getValue() != null)) {
+ pair.getValue()
+ .shutdown();
+ }
+ }
+
+ }
+
+ @Test
+ public void containsAll()
+ {
+ LazyArrayList list = new LazyArrayList<>();
+ list.add(1);
+ list.add(2);
+ list.add(3);
+
+ List list2 = new ArrayList<>();
+ list2.add(1);
+ list2.add(2);
+ list2.add(3);
+
+ assertTrue(list.containsAll(list2));
+
+ ImmutablePair, EmbeddedStorageManager> pair = null;
+ try {
+ pair = Util.storeAndLoadList(list, location);
+ LazyArrayList copy = pair.getKey();
+
+ assertTrue(copy.containsAll(list2));
+
+ } finally {
+ if ((pair != null) && (pair.getValue() != null)) {
+ pair.getValue()
+ .shutdown();
+ }
+ }
+ }
+
+ @Test
+ public void addAllIndex()
+ {
+ LazyArrayList list = new LazyArrayList<>();
+ list.add(1);
+ list.add(2);
+ list.add(3);
+
+ List list2 = new ArrayList<>();
+ list2.add(1);
+ list2.add(2);
+ list2.add(3);
+
+ LazyArrayList copy;
+ ImmutablePair, EmbeddedStorageManager> pair = null;
+ try {
+ pair = Util.storeAndLoadList(list, location);
+ copy = pair.getKey();
+
+ assertTrue(copy.containsAll(list2));
+
+ } finally {
+ if ((pair != null) && (pair.getValue() != null)) {
+ pair.getValue()
+ .shutdown();
+ }
+ }
+
+ assertTrue(copy.addAll(2, list2));
+
+
+ List list3 = new LazyArrayList<>();
+ list3.add(1);
+ list3.add(2);
+ list3.add(1);
+ list3.add(2);
+ list3.add(3);
+ list3.add(3);
+
+ assertIterableEquals(list3, copy);
+ }
+
+ @Test
+ public void removeAllCollection()
+ {
+ LazyArrayList list = new LazyArrayList<>();
+ list.add(1);
+ list.add(2);
+ list.add(3);
+
+ List list2 = new ArrayList<>();
+ list2.add(1);
+ list2.add(2);
+ list2.add(3);
+
+
+ LazyArrayList copy;
+ ImmutablePair, EmbeddedStorageManager> pair = null;
+ try {
+ pair = Util.storeAndLoadList(list, location);
+ copy = pair.getKey();
+
+ assertTrue(copy.containsAll(list2));
+
+ } finally {
+ if ((pair != null) && (pair.getValue() != null)) {
+ pair.getValue()
+ .shutdown();
+ }
+ }
+
+ assertTrue(copy.removeAll(list2));
+
+ assertTrue(copy.isEmpty());
+ }
+
+ @Test
+ public void retainAll()
+ {
+ LazyArrayList list = new LazyArrayList<>();
+ list.add(1);
+ list.add(2);
+ list.add(3);
+
+ List list2 = new ArrayList<>();
+ list2.add(1);
+ list2.add(3);
+
+
+ LazyArrayList copy;
+ ImmutablePair, EmbeddedStorageManager> pair = null;
+ try {
+ pair = Util.storeAndLoadList(list, location);
+ copy = pair.getKey();
+
+ copy.iterateLazyReferences(e -> e.get());
+
+
+ } finally {
+ if ((pair != null) && (pair.getValue() != null)) {
+ pair.getValue()
+ .shutdown();
+ }
+ }
+
+ assertTrue(copy.retainAll(list2));
+
+ List resultList = new LazyArrayList<>();
+ resultList.add(1);
+ resultList.add(3);
+
+ assertIterableEquals(resultList, copy);
+ }
+
+
+ @Test
+ public void replaceAllUnaryOperator()
+ {
+ class Op implements UnaryOperator
+ {
+ public String apply(String str)
+ {
+ return str.toUpperCase();
+ }
+ }
+
+ List list = new LazyArrayList<>();
+ list.add("Java");
+ list.add("JavaScript");
+ //System.out.println("Contents of the list: " + list);
+ list.replaceAll(new Op());
+
+ List expectedList = new LazyArrayList<>();
+ expectedList.add("JAVA");
+ expectedList.add("JAVASCRIPT");
+
+ assertIterableEquals(expectedList, list);
+ }
+
+ @Test
+ public void sort()
+ {
+
+ Comparator valueComparator = (Integer o1, Integer o2) -> o1 - o2;
+
+ LazyArrayList list = new LazyArrayList<>();
+ list.add(3);
+ list.add(2);
+ list.add(1);
+
+ LazyArrayList copy;
+ ImmutablePair, EmbeddedStorageManager> pair = null;
+ try {
+ pair = Util.storeAndLoadList(list, location);
+ copy = pair.getKey();
+ copy.sort(valueComparator);
+
+ pair.getValue().store(copy);
+
+ } finally {
+ if ((pair != null) && (pair.getValue() != null)) {
+ pair.getValue()
+ .shutdown();
+ }
+ }
+
+
+ List resultList = new LazyArrayList<>();
+ resultList.add(1);
+ resultList.add(2);
+ resultList.add(3);
+
+ assertIterableEquals(resultList, copy);
+ }
+
+}
diff --git a/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/LazyArrayListUnloadingTests.java b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/LazyArrayListUnloadingTests.java
new file mode 100644
index 000000000..032be64c9
--- /dev/null
+++ b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/LazyArrayListUnloadingTests.java
@@ -0,0 +1,110 @@
+package test.eclipse.store.collections.lazy.arraylist;
+
+/*-
+ * #%L
+ * EclipseStore Integration Tests
+ * %%
+ * Copyright (C) 2023 - 2026 MicroStream Software
+ * %%
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ * #L%
+ */
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.file.Path;
+
+import org.eclipse.serializer.collections.lazy.LazyArrayList;
+import org.eclipse.store.storage.embedded.types.EmbeddedStorage;
+import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+public class LazyArrayListUnloadingTests
+{
+
+ @Test
+ void UnloadStored(@TempDir final Path path)
+ {
+
+ try (final EmbeddedStorageManager storage = EmbeddedStorage.start(path)) {
+ LazyArrayList list = createLazyList(22);
+
+ storage.setRoot(list);
+ storage.storeRoot();
+
+ list.iterateLazyReferences(l -> l.clear());
+ list.iterateLazyReferences(l -> assertFalse(l.isLoaded()));
+
+ }
+ }
+
+ @Test
+ void UnloadNotStored(@TempDir final Path path)
+ {
+
+ try (final EmbeddedStorageManager storage = EmbeddedStorage.start(path)) {
+ LazyArrayList list = createLazyList(44);
+
+ storage.setRoot(list);
+
+ list.iterateLazyReferences(l -> l.clear());
+ list.iterateLazyReferences(l -> assertTrue(l.isLoaded()));
+ }
+ }
+
+ @Test
+ void UnloadNotStored_reloadedStorage(@TempDir final Path path)
+ {
+
+ try (final EmbeddedStorageManager storageReloaded = createReloadedStorageWithList(path)) {
+ @SuppressWarnings("unchecked")
+ LazyArrayList listReloaded = (LazyArrayList) storageReloaded.root();
+ listReloaded.iterateLazyReferences(l -> assertFalse(l.isLoaded()));
+
+ listReloaded.add("New Entry");
+
+ listReloaded.iterateLazyReferences(l -> l.clear());
+
+ listReloaded.segments()
+ .forEach(s -> {
+ if (s.isModified()) {
+ assertTrue(s.isLoaded(), "expected modified segment to be loaded!");
+ } else {
+ assertFalse(s.isLoaded(), "expected unmodified segment to be unloaded!");
+ }
+ });
+
+ storageReloaded.store(listReloaded);
+ listReloaded.iterateLazyReferences(l -> l.clear());
+ listReloaded.iterateLazyReferences(l -> assertFalse(l.isLoaded()));
+ }
+ }
+
+
+ static EmbeddedStorageManager createReloadedStorageWithList(final Path path)
+ {
+ try (final EmbeddedStorageManager storage = EmbeddedStorage.start(path)) {
+ LazyArrayList list = createLazyList(44);
+ storage.setRoot(list);
+ storage.storeRoot();
+ }
+ return EmbeddedStorage.start(path);
+ }
+
+
+ private static LazyArrayList createLazyList(final int size)
+ {
+ LazyArrayList list = new LazyArrayList<>();
+
+ for (int i = 0; i < size; i++) {
+ list.add("Entry " + i);
+ }
+ return list;
+ }
+}
diff --git a/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/ListEntry.java b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/ListEntry.java
new file mode 100644
index 000000000..d1de510f1
--- /dev/null
+++ b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/ListEntry.java
@@ -0,0 +1,56 @@
+package test.eclipse.store.collections.lazy.arraylist;
+
+/*-
+ * #%L
+ * EclipseStore Integration Tests
+ * %%
+ * Copyright (C) 2023 - 2026 MicroStream Software
+ * %%
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ * #L%
+ */
+
+import java.util.Objects;
+
+public class ListEntry
+{
+
+ String id;
+
+ public ListEntry(final String id)
+ {
+ super();
+ this.id = id;
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(this.id);
+ }
+
+ @Override
+ public boolean equals(final Object obj)
+ {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (this.getClass() != obj.getClass())
+ return false;
+ final ListEntry other = (ListEntry) obj;
+ return Objects.equals(this.id, other.id);
+ }
+
+ @Override
+ public String toString()
+ {
+ return "ListEntry{" +
+ "id='" + id + '\'' +
+ '}';
+ }
+}
diff --git a/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/PersistenceTest.java b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/PersistenceTest.java
new file mode 100644
index 000000000..ca0023e40
--- /dev/null
+++ b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/PersistenceTest.java
@@ -0,0 +1,76 @@
+package test.eclipse.store.collections.lazy.arraylist;
+
+/*-
+ * #%L
+ * EclipseStore Integration Tests
+ * %%
+ * Copyright (C) 2023 - 2026 MicroStream Software
+ * %%
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ * #L%
+ */
+
+import static org.junit.jupiter.api.Assertions.assertIterableEquals;
+
+import java.nio.file.Path;
+
+import org.eclipse.serializer.collections.lazy.LazyArrayList;
+import org.eclipse.store.storage.embedded.types.EmbeddedStorage;
+import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+public class PersistenceTest
+{
+
+
+ @Test
+ public void storeAndReload(@TempDir final Path path)
+ {
+
+ LazyArrayList myLazyList = new LazyArrayList<>(3);
+ for (int i = 0; i < 10; i++) {
+ myLazyList.add("Entry " + i);
+ }
+
+ try (final EmbeddedStorageManager storage = EmbeddedStorage.start(myLazyList, path)) {
+ }
+
+ LazyArrayList reloadedList;
+ try (final EmbeddedStorageManager storageReloaded = EmbeddedStorage.start(path)) {
+
+ reloadedList = (LazyArrayList) storageReloaded.root();
+
+ assertIterableEquals(myLazyList, reloadedList);
+
+ reloadedList.add(7, "AddedEntry");
+ storageReloaded.store(reloadedList);
+ }
+
+
+ LazyArrayList reloadedList2;
+ try (final EmbeddedStorageManager storageReloaded2 = EmbeddedStorage.start(path)) {
+
+ reloadedList2 = (LazyArrayList) storageReloaded2.root();
+
+ assertIterableEquals(reloadedList, reloadedList2);
+
+ reloadedList2.add("nochnadd");
+ reloadedList2.consolidate();
+ storageReloaded2.store(reloadedList2);
+ }
+
+ try (final EmbeddedStorageManager storageReloaded3 = EmbeddedStorage.start(path)) {
+ @SuppressWarnings("unchecked")
+ LazyArrayList reloadedList3 = (LazyArrayList) storageReloaded3.root();
+ assertIterableEquals(reloadedList2, reloadedList3);
+
+ }
+
+ }
+
+}
diff --git a/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/SpliteratorTest.java b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/SpliteratorTest.java
new file mode 100644
index 000000000..bb28ba7c6
--- /dev/null
+++ b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/SpliteratorTest.java
@@ -0,0 +1,179 @@
+package test.eclipse.store.collections.lazy.arraylist;
+
+/*-
+ * #%L
+ * EclipseStore Integration Tests
+ * %%
+ * Copyright (C) 2023 - 2026 MicroStream Software
+ * %%
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ * #L%
+ */
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Spliterator;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Stream;
+
+import org.eclipse.serializer.collections.lazy.LazyArrayList;
+import org.eclipse.store.storage.embedded.types.EmbeddedStorage;
+import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+public class SpliteratorTest
+{
+
+ @Test
+ void createSpliterator()
+ {
+
+ final ArrayList arrayList = new ArrayList<>(10);
+ for (int i = 0; i < 10; i++) {
+ arrayList.add("entry " + i);
+ }
+
+ final Spliterator arrayListSpliterator = arrayList.spliterator();
+ final Spliterator splitArrayList = arrayListSpliterator.trySplit();
+
+
+ final LazyArrayList lazyList = new LazyArrayList<>(4);
+ for (int i = 0; i < 20; i++) {
+ lazyList.add("entry " + i);
+ }
+
+ final Spliterator lazySpliterator = lazyList.segmentSpliterator();
+ final Spliterator splitLazy = lazySpliterator.trySplit();
+ final Spliterator splitLazy2 = lazySpliterator.trySplit();
+
+ final AtomicInteger counter = new AtomicInteger();
+ lazySpliterator.forEachRemaining(s -> counter.getAndAdd(1));
+ splitLazy.forEachRemaining(s -> counter.getAndAdd(1));
+ splitLazy2.forEachRemaining(s -> counter.getAndAdd(1));
+
+
+ assertEquals(20, counter.get(), "total iterated elements");
+ }
+
+ @Test
+ void createSpliteratorRecursive()
+ {
+
+ final int maxSegmentSize = 4;
+ final int numElements = 20;
+
+ final LazyArrayList lazyList = new LazyArrayList<>(maxSegmentSize);
+
+ for (int i = 0; i < numElements; i++) {
+ lazyList.add(new ListEntry("Entry " + i));
+ }
+
+ final List> spliterators = this.splitAll(lazyList.spliterator());
+
+ assertEquals(5, spliterators.size(), "expected to get one spliterator for each segment (" + numElements / maxSegmentSize + ")");
+ }
+
+
+ @Test
+ void lateBinding()
+ {
+
+ final int maxSegmentSize = 4;
+ final int numElements = 20;
+
+ final LazyArrayList lazyList = new LazyArrayList<>(maxSegmentSize);
+
+ for (int i = 0; i < numElements - 10; i++) {
+ lazyList.add(new ListEntry("Entry " + i));
+ }
+
+ final Spliterator spliterator = lazyList.spliterator();
+
+ for (int i = 10; i < numElements; i++) {
+ lazyList.add(new ListEntry("Entry " + i));
+ }
+
+ final List> spliterators = this.splitAll(spliterator);
+
+ assertEquals(5, spliterators.size(), "expected to get one spliterator for each segment (" + numElements / maxSegmentSize + ")");
+ }
+
+ @Test
+ void readStreamAndCloseTest(@TempDir final Path path)
+ {
+ try (final EmbeddedStorageManager storage = EmbeddedStorage.start(path)) {
+ final LazyArrayList lazyList = Util.createLazyList(3, 25);
+
+ storage.setRoot(lazyList);
+ storage.storeRoot();
+ lazyList.iterateLazyReferences(l -> l.clear());
+ lazyList.iterateLazyReferences(l -> assertFalse(l.isLoaded(), "list segment loaded!"));
+
+ long countFours;
+ try (final Stream stream = lazyList.parallelStream()) {
+ countFours = stream.filter(e -> e.id.contains("4"))
+ .count();
+ }
+
+ assertEquals(3, countFours);
+
+ AtomicInteger loadedSegments = new AtomicInteger();
+ lazyList.iterateLazyReferences(l -> {
+ if (l.isLoaded()) loadedSegments.incrementAndGet();
+ });
+ assertEquals(0, loadedSegments.get());
+ }
+ }
+
+ @Test
+ @Disabled
+ void deleteStreamTest(@TempDir final Path path)
+ {
+ try (final EmbeddedStorageManager storage = EmbeddedStorage.start(path)) {
+ final LazyArrayList lazyList = Util.createLazyList(3, 25);
+
+ storage.setRoot(lazyList);
+ storage.storeRoot();
+ lazyList.iterateLazyReferences(l -> l.clear());
+ lazyList.iterateLazyReferences(l -> assertFalse(l.isLoaded(), "list segment loaded!"));
+
+ {
+ final long countTwos = lazyList.parallelStream()
+ .filter(e -> e.id.contains("4"))
+ .count();
+ assertEquals(3, countTwos);
+ }
+
+ System.gc();
+
+ lazyList.iterateLazyReferences(l -> assertFalse(l.isLoaded(), "list segment loaded!"));
+ }
+ }
+
+
+ private List> splitAll(final Spliterator spliterator)
+ {
+ final List> spliterators = new ArrayList<>();
+ final Spliterator split = spliterator.trySplit();
+
+ if (split != null) {
+ spliterators.addAll(this.splitAll(spliterator));
+ spliterators.addAll(this.splitAll(split));
+ } else {
+ spliterators.add(spliterator);
+ }
+
+ return spliterators;
+ }
+
+}
diff --git a/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/TwoStoragesTest.java b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/TwoStoragesTest.java
new file mode 100644
index 000000000..2947c1a49
--- /dev/null
+++ b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/TwoStoragesTest.java
@@ -0,0 +1,125 @@
+package test.eclipse.store.collections.lazy.arraylist;
+
+/*-
+ * #%L
+ * EclipseStore Integration Tests
+ * %%
+ * Copyright (C) 2023 - 2026 MicroStream Software
+ * %%
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ * #L%
+ */
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.nio.file.Path;
+
+import org.eclipse.serializer.collections.lazy.LazyArrayList;
+import org.eclipse.serializer.persistence.exceptions.PersistenceException;
+import org.eclipse.serializer.reference.Lazy;
+import org.eclipse.store.storage.embedded.types.EmbeddedStorage;
+import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+//@Disabled("Will not work beacuse auf Lazy.Default also does not support this")
+public class TwoStoragesTest
+{
+
+ @TempDir
+ Path location;
+
+ private static PrintStream originalErr;
+
+ @BeforeAll
+ static void setupErr()
+ {
+ originalErr = System.err;
+ System.setErr(new PrintStream(new OutputStream()
+ {
+ @Override
+ public void write(int b)
+ {
+ // Discard output
+ }
+ }));
+ }
+
+ @AfterAll
+ static void restoreErr()
+ {
+ System.setErr(originalErr);
+ }
+
+
+ @Test
+ public void removeTest(@TempDir final Path path)
+ {
+ LazyArrayList list = new LazyArrayList<>();
+
+ list.add("some text");
+
+ try (EmbeddedStorageManager storageManager = EmbeddedStorage.start(list, path)) {
+ //no op
+ }
+
+ try (EmbeddedStorageManager storageManager = EmbeddedStorage.start(list, path)) {
+
+ list.remove(0);
+ }
+ }
+
+
+ public static class MyRoot
+ {
+ Lazy lazy;
+
+ public MyRoot(final String content)
+ {
+ super();
+ this.lazy = Lazy.Reference(content);
+ }
+
+ }
+
+ @Test
+ public void saveDefaultLazySecondTimeTest(@TempDir final Path secondLocation)
+ {
+ final MyRoot myRoot = new MyRoot("Hello World");
+
+ try (EmbeddedStorageManager storageManager = EmbeddedStorage.start(myRoot, this.location)) {
+ //no op
+ }
+
+ myRoot.lazy.clear();
+
+ assertThrows(PersistenceException.class, () -> EmbeddedStorage.start(myRoot, secondLocation));
+
+ }
+
+ public static LazyArrayList generateList(final Integer count)
+ {
+ return generateList(count, 0);
+ }
+
+ public static LazyArrayList generateList(final Integer count, final int keyStart)
+ {
+
+ LazyArrayList list = new LazyArrayList<>();
+
+ for (int i = 0; i < count; i++) {
+ list.add("Hello World " + i);
+ }
+
+ return list;
+ }
+
+}
diff --git a/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/Util.java b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/Util.java
new file mode 100644
index 000000000..87f648f96
--- /dev/null
+++ b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/Util.java
@@ -0,0 +1,93 @@
+package test.eclipse.store.collections.lazy.arraylist;
+
+/*-
+ * #%L
+ * EclipseStore Integration Tests
+ * %%
+ * Copyright (C) 2023 - 2026 MicroStream Software
+ * %%
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ * #L%
+ */
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.nio.file.Path;
+
+import org.apache.commons.lang3.tuple.ImmutablePair;
+import org.eclipse.serializer.collections.lazy.LazyArrayList;
+import org.eclipse.store.storage.embedded.types.EmbeddedStorage;
+import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager;
+
+public class Util
+{
+
+
+ static ImmutablePair, EmbeddedStorageManager> storeAndLoadList(LazyArrayList list, Path location)
+ {
+ try (EmbeddedStorageManager storage = startStorage(list, location)) {
+
+ }
+
+ LazyArrayList copy = new LazyArrayList<>();
+ EmbeddedStorageManager storage = startStorage(copy, location);
+
+ return ImmutablePair.of(copy, storage);
+
+ }
+
+ static EmbeddedStorageManager startStorage(Path path)
+ {
+ final EmbeddedStorageManager storage = EmbeddedStorage
+ .Foundation(path)
+ .start();
+ return storage;
+ }
+
+// static EmbeddedStorageManager startStorage( LazyArrayList root, final Path path) {
+// final EmbeddedStorageManager storage = EmbeddedStorage
+// .Foundation(path)
+// .registerTypeHandler(BinaryHandlerLazyArrayList.New())
+// .start(root);
+// return storage;
+// }
+
+ static EmbeddedStorageManager startStorage(LazyArrayList root, final Path path)
+ {
+ final EmbeddedStorageManager storage = EmbeddedStorage
+ .Foundation(path)
+ .start(root);
+ return storage;
+ }
+
+ static LazyArrayList createLazyList(final int segmentSize, final int entries)
+ {
+ final LazyArrayList lazyList = new LazyArrayList<>(segmentSize);
+
+ for (int i = 0; i < entries; i++) {
+ lazyList.add(new ListEntry("Entry-" + i));
+ }
+ return lazyList;
+ }
+
+ static LazyArrayList loadAndCompare(final LazyArrayList lazyList, EmbeddedStorageManager storage)
+ {
+
+ LazyArrayList lazyListReloaded;
+
+ lazyListReloaded = (LazyArrayList) storage.root();
+
+ assertAll("load nad compare list",
+ () -> assertEquals(lazyList.size(), lazyListReloaded.size(), "Size mismatch!"),
+ () -> assertEquals(lazyList.getMaxSegmentSize(), lazyListReloaded.getMaxSegmentSize(), "getMaxSegmentSize mismatch!"),
+ () -> assertEquals(lazyList.getSegmentCount(), lazyListReloaded.getSegmentCount(), "getSegmentCount mismatch!"),
+ () -> assertIterableEquals(lazyList, lazyListReloaded)
+ );
+
+ return lazyListReloaded;
+ }
+}
diff --git a/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/Utils.java b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/Utils.java
new file mode 100644
index 000000000..cb1768e73
--- /dev/null
+++ b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/Utils.java
@@ -0,0 +1,66 @@
+package test.eclipse.store.collections.lazy.arraylist;
+
+/*-
+ * #%L
+ * EclipseStore Integration Tests
+ * %%
+ * Copyright (C) 2023 - 2026 MicroStream Software
+ * %%
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ * #L%
+ */
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Comparator;
+
+public class Utils
+{
+
+ public static void deleteAll(final Path path) throws IOException
+ {
+ if (path.toFile().exists()) {
+ Files.walk(path)
+ .sorted(Comparator.reverseOrder())
+ .map(Path::toFile)
+ .forEach(File::delete);
+ }
+ }
+
+ public static void deleteAll(final String path) throws IOException
+ {
+ deleteAll(Paths.get(path));
+ }
+
+ public static void pressAnyKeyToContinue()
+ {
+ System.out.println("Press Enter key to continue...");
+ try {
+ System.in.read();
+ } catch (final IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ }
+
+ public static void pressAnyKeyToContinue(final String string)
+ {
+ System.out.println(string + " Press Enter key to continue...");
+ try {
+ System.in.read();
+ } catch (final IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+
+
+}
diff --git a/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/unload/Generator.java b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/unload/Generator.java
new file mode 100644
index 000000000..f68349eba
--- /dev/null
+++ b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/unload/Generator.java
@@ -0,0 +1,55 @@
+package test.eclipse.store.collections.lazy.arraylist.unload;
+
+/*-
+ * #%L
+ * EclipseStore Integration Tests
+ * %%
+ * Copyright (C) 2023 - 2026 MicroStream Software
+ * %%
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ * #L%
+ */
+
+import java.util.List;
+
+import org.eclipse.serializer.collections.lazy.LazyArrayList;
+import org.eclipse.serializer.collections.lazy.LazySegmentUnloader;
+
+import net.datafaker.Faker;
+
+public class Generator
+{
+
+ static final int PERSON_COUNT = 200;
+
+ static Faker faker = new Faker();
+
+ public static List generatePersons()
+ {
+ return generatePersons(PERSON_COUNT, new LazySegmentUnloader.Default());
+ }
+
+ public static List generatePersons(int count, LazySegmentUnloader unloader)
+ {
+ return generatePersons(count, unloader, 1000);
+ }
+
+ public static List generatePersons(int count, LazySegmentUnloader unloader, int MaxSegmentSize)
+ {
+
+ List persons = new LazyArrayList<>(1000, unloader);
+ for (int i = 0; i < count; i++) {
+ Person person = new Person();
+ person.setFirstname(faker.name().firstName());
+ person.setLastname(faker.name().lastName());
+ person.setMail(faker.internet().emailAddress());
+ person.setIp(faker.internet().ipV4Address());
+ persons.add(person);
+ }
+ return persons;
+ }
+}
diff --git a/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/unload/MoreThreadTest.java b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/unload/MoreThreadTest.java
new file mode 100644
index 000000000..64160fbdd
--- /dev/null
+++ b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/unload/MoreThreadTest.java
@@ -0,0 +1,79 @@
+package test.eclipse.store.collections.lazy.arraylist.unload;
+
+/*-
+ * #%L
+ * EclipseStore Integration Tests
+ * %%
+ * Copyright (C) 2023 - 2026 MicroStream Software
+ * %%
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ * #L%
+ */
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.eclipse.serializer.collections.lazy.LazyArrayList;
+import org.eclipse.serializer.collections.lazy.LazySegmentUnloader;
+import org.eclipse.store.storage.embedded.types.EmbeddedStorage;
+import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+
+public class MoreThreadTest
+{
+
+ @TempDir
+ Path location;
+
+ private EmbeddedStorageManager storageManager;
+
+ @AfterEach
+ void stopStorage()
+ {
+ if (storageManager != null) {
+ if (storageManager.isActive()) {
+ storageManager.shutdown();
+ }
+ }
+ }
+
+ @Test
+ void manual_unloader_test_tryUnload_false() throws InterruptedException
+ {
+ LazyArrayList personList;
+ personList = (LazyArrayList) Generator.generatePersons(20_000, new LazySegmentUnloader.Default());
+
+ List copyPersons = new ArrayList<>(personList);
+
+ storageManager = EmbeddedStorage.start(personList, location);
+
+ Assertions.assertIterableEquals(copyPersons, personList);
+
+ int threadCount = 5;
+ Thread[] threads = new Thread[threadCount];
+ for (int i = 0; i < threadCount; i++) {
+ Thread thread = new Thread(() -> {
+ personList.parallelStream()
+ .map(p -> p.toString())
+ .collect(Collectors.toList());
+ Assertions.assertIterableEquals(copyPersons, personList);
+ });
+ threads[i] = thread;
+ thread.start();
+ }
+
+ for (int i = 0; i < threadCount; i++) {
+ threads[i].join();
+ }
+ }
+}
diff --git a/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/unload/Person.java b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/unload/Person.java
new file mode 100644
index 000000000..23cdadb58
--- /dev/null
+++ b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/unload/Person.java
@@ -0,0 +1,97 @@
+package test.eclipse.store.collections.lazy.arraylist.unload;
+
+/*-
+ * #%L
+ * EclipseStore Integration Tests
+ * %%
+ * Copyright (C) 2023 - 2026 MicroStream Software
+ * %%
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ * #L%
+ */
+
+public class Person
+{
+ private String firstname;
+ private String lastname;
+ private String mail;
+ private String gender;
+ private String ip;
+ private String value1;
+ private String value2;
+
+ public String getFirstname()
+ {
+ return firstname;
+ }
+
+ public void setFirstname(String firstname)
+ {
+ this.firstname = firstname;
+ }
+
+ public String getLastname()
+ {
+ return lastname;
+ }
+
+ public void setLastname(String lastname)
+ {
+ this.lastname = lastname;
+ }
+
+ public String getMail()
+ {
+ return mail;
+ }
+
+ public void setMail(String mail)
+ {
+ this.mail = mail;
+ }
+
+ public String getGender()
+ {
+ return gender;
+ }
+
+ public void setGender(String gender)
+ {
+ this.gender = gender;
+ }
+
+ public String getIp()
+ {
+ return ip;
+ }
+
+ public void setIp(String ip)
+ {
+ this.ip = ip;
+ }
+
+ public String getValue1()
+ {
+ return value1;
+ }
+
+ public void setValue1(String value1)
+ {
+ this.value1 = value1;
+ }
+
+ public String getValue2()
+ {
+ return value2;
+ }
+
+ public void setValue2(String value2)
+ {
+ this.value2 = value2;
+ }
+
+}
diff --git a/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/unload/Unloader.java b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/unload/Unloader.java
new file mode 100644
index 000000000..599463987
--- /dev/null
+++ b/integration-tests/src/test/java/test/eclipse/store/collections/lazy/arraylist/unload/Unloader.java
@@ -0,0 +1,205 @@
+package test.eclipse.store.collections.lazy.arraylist.unload;
+
+/*-
+ * #%L
+ * EclipseStore Integration Tests
+ * %%
+ * Copyright (C) 2023 - 2026 MicroStream Software
+ * %%
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ * #L%
+ */
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.eclipse.serializer.collections.lazy.LazyArrayList;
+import org.eclipse.serializer.collections.lazy.LazySegmentUnloader;
+import org.eclipse.store.storage.embedded.types.EmbeddedStorage;
+import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+
+public class Unloader
+{
+
+ @TempDir
+ Path location;
+
+ @Test
+ void unloadTestTimedUnloader() throws InterruptedException
+ {
+ LazyArrayList personList;
+ personList = (LazyArrayList) Generator.generatePersons(8_000, new LazySegmentUnloader.Timed(100));
+
+ try (EmbeddedStorageManager storageManager = EmbeddedStorage.start(personList, location)) {
+
+ List collect = personList.stream()
+ .map(p -> p.getLastname())
+ .collect(Collectors.toList());
+
+ Thread.sleep(210);
+ personList.get(0);
+
+ Assertions.assertEquals(1, getLoadedSegments(personList).size());
+ }
+ }
+
+ @Test
+ void unloadTestDefaultUnloader() throws InterruptedException
+ {
+ LazyArrayList personList;
+ personList = (LazyArrayList