Compare commits

...

33 Commits

Author SHA1 Message Date
Ayaz Salikhov
c35649eb6e chore: Commits for 2.7.0-rc1 (#2846) 2025-12-10 16:26:07 +00:00
Ayaz Salikhov
f2f5a6ab19 chore: Switch to xrpl/3.0.0 (#2843) 2025-12-10 16:06:21 +00:00
Ayaz Salikhov
1469d4b198 chore: Add systemd file to the debian package (#2844) 2025-12-10 16:02:43 +00:00
yinyiqian1
06ea05891d feat: Add DynamicMPT in account_mptoken_issuances (#2820)
Support DynamicMPT for the  account_mptoken_issuances handler.

Related commit:
eed757e0c4

The original spec for `DynamicMPT` can be found here:
https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-0094-dynamic-MPT

---------

Co-authored-by: Sergey Kuznetsov <skuznetsov@ripple.com>
2025-12-10 11:36:24 +00:00
Ayaz Salikhov
c7c270cc03 style: Use shfmt for shell scripts (#2841) 2025-12-09 18:51:56 +00:00
Alex Kremer
c1f2f5b100 chore: Less delay in ETL taskman (#2802) 2025-12-09 12:25:00 +00:00
github-actions[bot]
bea0b51c8b style: clang-tidy auto fixes (#2840) 2025-12-09 10:36:53 +00:00
Alex Kremer
69b8e5bd06 feat: Add observable value util (#2831)
This implements a simple observable value. Can be used for a more
reactive approach. Will be used in ETL state and across the codebase
with time.
2025-12-08 16:44:43 +00:00
dependabot[bot]
33dc4ad95a ci: [DEPENDABOT] Bump ytanikin/pr-conventional-commits from 1.4.2 to 1.5.1 (#2835)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-08 10:52:52 +00:00
dependabot[bot]
13cbb405c7 ci: [DEPENDABOT] Bump peter-evans/create-pull-request from 7.0.9 to 7.0.11 (#2836)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-08 10:52:02 +00:00
dependabot[bot]
8a37a2e083 ci: [DEPENDABOT] Bump actions/checkout from 6.0.0 to 6.0.1 (#2837)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-08 10:51:32 +00:00
github-actions[bot]
f8b6c98219 style: Update pre-commit hooks (#2825)
Co-authored-by: mathbunnyru <12270691+mathbunnyru@users.noreply.github.com>
2025-12-07 18:42:50 +00:00
dependabot[bot]
92883bf012 ci: [DEPENDABOT] Bump docker/metadata-action from 5.9.0 to 5.10.0 in /.github/actions/build-docker-image (#2826)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-07 18:42:34 +00:00
Alex Kremer
88881e95dd chore: TSAN fix async-signal-unsafe (#2824)
Co-authored-by: Sergey Kuznetsov <skuznetsov@ripple.com>
2025-12-02 17:36:36 +00:00
Alex Kremer
94e70e4026 chore: Add mathbunnyru to maintainers (#2823) 2025-11-27 19:24:05 +00:00
github-actions[bot]
b534570cdd style: clang-tidy auto fixes (#2822)
Co-authored-by: godexsoft <385326+godexsoft@users.noreply.github.com>
2025-11-27 09:46:53 +00:00
Ayaz Salikhov
56fbfc63c2 chore: Update lockfile (#2818) 2025-11-26 17:06:38 +00:00
Ayaz Salikhov
80978657c0 ci: Update images to use latest Ninja (#2817) 2025-11-26 16:15:18 +00:00
Ayaz Salikhov
067449c3f8 chore: Install latest Ninja in images (#2813) 2025-11-26 13:54:19 +00:00
Ayaz Salikhov
946976546a chore: Use boost::asio::ssl::stream instead of boost::beast::ssl_stream (#2814) 2025-11-26 12:27:48 +00:00
github-actions[bot]
73e90b0a3f style: clang-tidy auto fixes (#2816)
Co-authored-by: godexsoft <385326+godexsoft@users.noreply.github.com>
2025-11-26 09:44:53 +00:00
Ayaz Salikhov
7681c58a3a style: Add black pre-commit hook (#2811) 2025-11-25 17:13:29 +00:00
Alex Kremer
391e7b07ab chore: WebServerAdminTestsSuit TSAN issues (#2809) 2025-11-25 12:17:24 +00:00
Alex Kremer
4eadaa85fa chore: Repeat-based tests TSAN fixes (#2810) 2025-11-25 12:15:43 +00:00
Ayaz Salikhov
1b1a46c429 feat: Handle prometheus requests in WorkQueue (#2790) 2025-11-24 16:17:45 +00:00
Ayaz Salikhov
89707d9668 ci: Run clang-tidy 3 times to make sure we don't have to fix again (#2803) 2025-11-24 12:19:34 +00:00
Ayaz Salikhov
ae260d1229 chore: Update spdlog and fmt libraries (#2804) 2025-11-24 11:27:29 +00:00
dependabot[bot]
058c05cfb6 ci: [DEPENDABOT] Bump actions/checkout from 5.0.0 to 6.0.0 (#2806)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 09:59:28 +00:00
dependabot[bot]
b2a7d185cb ci: [DEPENDABOT] Bump peter-evans/create-pull-request from 7.0.8 to 7.0.9 (#2805)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 09:58:51 +00:00
github-actions[bot]
9ea61ba6b9 style: clang-tidy auto fixes (#2801)
Co-authored-by: mathbunnyru <12270691+mathbunnyru@users.noreply.github.com>
2025-11-21 11:27:40 +00:00
github-actions[bot]
19157dec74 style: clang-tidy auto fixes (#2799)
Co-authored-by: mathbunnyru <12270691+mathbunnyru@users.noreply.github.com>
2025-11-21 11:02:07 +00:00
github-actions[bot]
42a6f516dc style: clang-tidy auto fixes (#2797) 2025-11-21 10:17:56 +00:00
emrearıyürek
2cd8226a11 refactor: Make getLedgerIndex return std::expected instead of throwing (#2788)
Co-authored-by: Ayaz Salikhov <mathbunnyru@users.noreply.github.com>
Co-authored-by: Sergey Kuznetsov <kuzzz99@gmail.com>
2025-11-20 17:46:15 +00:00
104 changed files with 3180 additions and 681 deletions

View File

@@ -52,7 +52,7 @@ runs:
cache-image: false
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
- uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
id: meta
with:
images: ${{ inputs.images }}

View File

@@ -4,7 +4,7 @@ import json
LINUX_OS = ["heavy", "heavy-arm64"]
LINUX_CONTAINERS = [
'{ "image": "ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7" }'
'{ "image": "ghcr.io/xrplf/clio-ci:067449c3f8ae6755ea84752ea2962b589fe56c8f" }'
]
LINUX_COMPILERS = ["gcc", "clang"]

View File

@@ -40,9 +40,9 @@ mkdir -p "$PROFILES_DIR"
if [[ "$(uname)" == "Darwin" ]]; then
create_profile_with_sanitizers "apple-clang" "$APPLE_CLANG_PROFILE"
echo "include(apple-clang)" > "$PROFILES_DIR/default"
echo "include(apple-clang)" >"$PROFILES_DIR/default"
else
create_profile_with_sanitizers "clang" "$CLANG_PROFILE"
create_profile_with_sanitizers "gcc" "$GCC_PROFILE"
echo "include(gcc)" > "$PROFILES_DIR/default"
echo "include(gcc)" >"$PROFILES_DIR/default"
fi

View File

@@ -22,8 +22,8 @@ fi
TEST_BINARY=$1
if [[ ! -f "$TEST_BINARY" ]]; then
echo "Test binary not found: $TEST_BINARY"
exit 1
echo "Test binary not found: $TEST_BINARY"
exit 1
fi
TESTS=$($TEST_BINARY --gtest_list_tests | awk '/^ / {print suite $1} !/^ / {suite=$1}')
@@ -35,12 +35,12 @@ export TSAN_OPTIONS="die_after_fork=0"
export MallocNanoZone='0' # for MacOSX
for TEST in $TESTS; do
OUTPUT_FILE="$OUTPUT_DIR/${TEST//\//_}.log"
$TEST_BINARY --gtest_filter="$TEST" > "$OUTPUT_FILE" 2>&1
OUTPUT_FILE="$OUTPUT_DIR/${TEST//\//_}.log"
$TEST_BINARY --gtest_filter="$TEST" >"$OUTPUT_FILE" 2>&1
if [ $? -ne 0 ]; then
echo "'$TEST' failed a sanitizer check."
else
rm "$OUTPUT_FILE"
fi
if [ $? -ne 0 ]; then
echo "'$TEST' failed a sanitizer check."
else
rm "$OUTPUT_FILE"
fi
done

View File

@@ -20,5 +20,5 @@ for artifact_name in $(ls); do
rm "${artifact_name}/${BINARY_NAME}"
rm -r "${artifact_name}"
sha256sum "./${artifact_name}.zip" > "./${artifact_name}.zip.sha256sum"
sha256sum "./${artifact_name}.zip" >"./${artifact_name}.zip.sha256sum"
done

View File

@@ -48,7 +48,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Download Clio binary from artifact
if: ${{ inputs.artifact_name != null }}

View File

@@ -49,7 +49,7 @@ jobs:
build_type: [Release, Debug]
container:
[
'{ "image": "ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7" }',
'{ "image": "ghcr.io/xrplf/clio-ci:067449c3f8ae6755ea84752ea2962b589fe56c8f" }',
]
static: [true]
@@ -79,7 +79,7 @@ jobs:
uses: ./.github/workflows/reusable-build.yml
with:
runs_on: heavy
container: '{ "image": "ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7" }'
container: '{ "image": "ghcr.io/xrplf/clio-ci:067449c3f8ae6755ea84752ea2962b589fe56c8f" }'
conan_profile: gcc
build_type: Debug
download_ccache: true
@@ -98,7 +98,7 @@ jobs:
uses: ./.github/workflows/reusable-build.yml
with:
runs_on: heavy
container: '{ "image": "ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7" }'
container: '{ "image": "ghcr.io/xrplf/clio-ci:067449c3f8ae6755ea84752ea2962b589fe56c8f" }'
conan_profile: gcc
build_type: Release
download_ccache: true
@@ -115,10 +115,10 @@ jobs:
needs: build-and-test
runs-on: heavy
container:
image: ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7
image: ghcr.io/xrplf/clio-ci:067449c3f8ae6755ea84752ea2962b589fe56c8f
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:

View File

@@ -21,10 +21,10 @@ jobs:
name: Build Clio / `libXRPL ${{ github.event.client_payload.version }}`
runs-on: heavy
container:
image: ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7
image: ghcr.io/xrplf/clio-ci:067449c3f8ae6755ea84752ea2962b589fe56c8f
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
@@ -69,7 +69,7 @@ jobs:
needs: build
runs-on: heavy
container:
image: ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7
image: ghcr.io/xrplf/clio-ci:067449c3f8ae6755ea84752ea2962b589fe56c8f
steps:
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
@@ -92,7 +92,7 @@ jobs:
issues: write
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Create an issue
uses: ./.github/actions/create-issue

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: ytanikin/pr-conventional-commits@b72758283dcbee706975950e96bc4bf323a8d8c0 # 1.4.2
- uses: ytanikin/pr-conventional-commits@fda730cb152c05a849d6d84325e50c6182d9d1e9 # 1.5.1
with:
task_types: '["build","feat","fix","docs","test","ci","style","refactor","perf","chore"]'
add_label: false

View File

@@ -31,7 +31,7 @@ jobs:
if: github.event_name != 'push' || contains(github.event.head_commit.message, 'clang-tidy auto fixes')
runs-on: heavy
container:
image: ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7
image: ghcr.io/xrplf/clio-ci:067449c3f8ae6755ea84752ea2962b589fe56c8f
permissions:
contents: write
@@ -39,7 +39,7 @@ jobs:
pull-requests: write
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
@@ -62,27 +62,30 @@ jobs:
uses: XRPLF/actions/.github/actions/get-nproc@046b1620f6bfd6cd0985dc82c3df02786801fe0a
id: nproc
- name: Run clang-tidy
- name: Run clang-tidy (several times)
continue-on-error: true
id: run_clang_tidy
id: clang_tidy
run: |
run-clang-tidy-${{ env.LLVM_TOOLS_VERSION }} -p build -j "${{ steps.nproc.outputs.nproc }}" -fix -quiet 1>output.txt
# We run clang-tidy several times, because some fixes may enable new fixes in subsequent runs.
CLANG_TIDY_COMMAND="run-clang-tidy-${{ env.LLVM_TOOLS_VERSION }} -p build -j ${{ steps.nproc.outputs.nproc }} -fix -quiet"
${CLANG_TIDY_COMMAND} ||
${CLANG_TIDY_COMMAND} ||
${CLANG_TIDY_COMMAND}
- name: Check for changes
id: files_changed
continue-on-error: true
run: |
git diff --exit-code
- name: Fix local includes and clang-format style
if: ${{ steps.run_clang_tidy.outcome != 'success' }}
if: ${{ steps.files_changed.outcome != 'success' }}
run: |
pre-commit run --all-files fix-local-includes || true
pre-commit run --all-files clang-format || true
- name: Print issues found
if: ${{ steps.run_clang_tidy.outcome != 'success' }}
run: |
sed -i '/error\||/!d' ./output.txt
cat output.txt
rm output.txt
- name: Create an issue
if: ${{ steps.run_clang_tidy.outcome != 'success' && github.event_name != 'pull_request' }}
if: ${{ (steps.clang_tidy.outcome != 'success' || steps.files_changed.outcome != 'success') && github.event_name != 'pull_request' }}
id: create_issue
uses: ./.github/actions/create-issue
env:
@@ -95,7 +98,7 @@ jobs:
List of the issues found: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/
- uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6.3.0
if: ${{ steps.run_clang_tidy.outcome != 'success' && github.event_name != 'pull_request' }}
if: ${{ steps.files_changed.outcome != 'success' && github.event_name != 'pull_request' }}
with:
gpg_private_key: ${{ secrets.ACTIONS_GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.ACTIONS_GPG_PASSPHRASE }}
@@ -103,8 +106,8 @@ jobs:
git_commit_gpgsign: true
- name: Create PR with fixes
if: ${{ steps.run_clang_tidy.outcome != 'success' && github.event_name != 'pull_request' }}
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
if: ${{ steps.files_changed.outcome != 'success' && github.event_name != 'pull_request' }}
uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}
@@ -119,5 +122,5 @@ jobs:
reviewers: "godexsoft,kuznetsss,PeterChen13579,mathbunnyru"
- name: Fail the job
if: ${{ steps.run_clang_tidy.outcome != 'success' }}
if: ${{ steps.clang_tidy.outcome != 'success' || steps.files_changed.outcome != 'success' }}
run: exit 1

View File

@@ -18,11 +18,11 @@ jobs:
build:
runs-on: ubuntu-latest
container:
image: ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7
image: ghcr.io/xrplf/clio-ci:067449c3f8ae6755ea84752ea2962b589fe56c8f
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
lfs: true

View File

@@ -43,17 +43,17 @@ jobs:
conan_profile: gcc
build_type: Release
static: true
container: '{ "image": "ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7" }'
container: '{ "image": "ghcr.io/xrplf/clio-ci:067449c3f8ae6755ea84752ea2962b589fe56c8f" }'
- os: heavy
conan_profile: gcc
build_type: Debug
static: true
container: '{ "image": "ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7" }'
container: '{ "image": "ghcr.io/xrplf/clio-ci:067449c3f8ae6755ea84752ea2962b589fe56c8f" }'
- os: heavy
conan_profile: gcc.ubsan
build_type: Release
static: false
container: '{ "image": "ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7" }'
container: '{ "image": "ghcr.io/xrplf/clio-ci:067449c3f8ae6755ea84752ea2962b589fe56c8f" }'
uses: ./.github/workflows/reusable-build-test.yml
with:
@@ -77,7 +77,7 @@ jobs:
include:
- os: heavy
conan_profile: clang
container: '{ "image": "ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7" }'
container: '{ "image": "ghcr.io/xrplf/clio-ci:067449c3f8ae6755ea84752ea2962b589fe56c8f" }'
static: true
- os: macos15
conan_profile: apple-clang
@@ -145,7 +145,7 @@ jobs:
issues: write
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Create an issue
uses: ./.github/actions/create-issue

View File

@@ -11,4 +11,4 @@ jobs:
uses: XRPLF/actions/.github/workflows/pre-commit.yml@34790936fae4c6c751f62ec8c06696f9c1a5753a
with:
runs_on: heavy
container: '{ "image": "ghcr.io/xrplf/clio-pre-commit:c117f470f2ef954520ab5d1c8a5ed2b9e68d6f8a" }'
container: '{ "image": "ghcr.io/xrplf/clio-pre-commit:067449c3f8ae6755ea84752ea2962b589fe56c8f" }'

View File

@@ -29,7 +29,7 @@ jobs:
conan_profile: gcc
build_type: Release
static: true
container: '{ "image": "ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7" }'
container: '{ "image": "ghcr.io/xrplf/clio-ci:067449c3f8ae6755ea84752ea2962b589fe56c8f" }'
uses: ./.github/workflows/reusable-build-test.yml
with:

View File

@@ -90,7 +90,7 @@ jobs:
if: ${{ runner.os == 'macOS' }}
uses: XRPLF/actions/.github/actions/cleanup-workspace@ea9970b7c211b18f4c8bcdb28c29f5711752029f
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
# We need to fetch tags to have correct version in the release

View File

@@ -46,7 +46,7 @@ jobs:
release:
runs-on: heavy
container:
image: ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7
image: ghcr.io/xrplf/clio-ci:067449c3f8ae6755ea84752ea2962b589fe56c8f
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}
@@ -55,7 +55,7 @@ jobs:
contents: write
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0

View File

@@ -54,7 +54,7 @@ jobs:
if: ${{ runner.os == 'macOS' }}
uses: XRPLF/actions/.github/actions/cleanup-workspace@ea9970b7c211b18f4c8bcdb28c29f5711752029f
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0

View File

@@ -44,7 +44,7 @@ jobs:
uses: ./.github/workflows/reusable-build-test.yml
with:
runs_on: heavy
container: '{ "image": "ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7" }'
container: '{ "image": "ghcr.io/xrplf/clio-ci:067449c3f8ae6755ea84752ea2962b589fe56c8f" }'
download_ccache: false
upload_ccache: false
conan_profile: ${{ matrix.compiler }}${{ matrix.sanitizer_ext }}

View File

@@ -56,7 +56,7 @@ jobs:
needs: repo
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Get changed files
id: changed-files
@@ -94,7 +94,7 @@ jobs:
needs: repo
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Get changed files
id: changed-files
@@ -132,7 +132,7 @@ jobs:
needs: [repo, gcc-amd64, gcc-arm64]
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Get changed files
id: changed-files
@@ -183,7 +183,7 @@ jobs:
needs: repo
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Get changed files
id: changed-files
@@ -219,7 +219,7 @@ jobs:
needs: [repo, gcc-merge]
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Get changed files
id: changed-files
@@ -250,7 +250,7 @@ jobs:
needs: [repo, gcc-merge]
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Get changed files
id: changed-files
@@ -281,7 +281,7 @@ jobs:
needs: [repo, tools-amd64, tools-arm64]
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Get changed files
id: changed-files
@@ -316,7 +316,7 @@ jobs:
needs: [repo, tools-merge]
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: ./.github/actions/build-docker-image
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -338,7 +338,7 @@ jobs:
needs: [repo, gcc-merge, clang, tools-merge]
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: ./.github/actions/build-docker-image
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -50,7 +50,7 @@ jobs:
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Calculate conan matrix
id: set-matrix
@@ -73,7 +73,7 @@ jobs:
CONAN_PROFILE: ${{ matrix.compiler }}${{ matrix.sanitizer_ext }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Prepare runner
uses: XRPLF/actions/.github/actions/prepare-runner@8abb0722cbff83a9a2dc7d06c473f7a4964b7382

View File

@@ -29,12 +29,12 @@ repos:
# Autoformat: YAML, JSON, Markdown, etc.
- repo: https://github.com/rbubley/mirrors-prettier
rev: 5ba47274f9b181bce26a5150a725577f3c336011 # frozen: v3.6.2
rev: 3c603eae8faac85303ae675fd33325cff699a797 # frozen: v3.7.3
hooks:
- id: prettier
- repo: https://github.com/igorshubovych/markdownlint-cli
rev: 192ad822316c3a22fb3d3cc8aa6eafa0b8488360 # frozen: v0.45.0
rev: c8fd5003603dd6f12447314ecd935ba87c09aff5 # frozen: v0.46.0
hooks:
- id: markdownlint-fix
exclude: LICENSE.md
@@ -58,6 +58,17 @@ repos:
--ignore-words=pre-commit-hooks/codespell_ignore.txt,
]
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 2892f1f81088477370d4fbc56545c05d33d2493f # frozen: 25.11.0
hooks:
- id: black
- repo: https://github.com/scop/pre-commit-shfmt
rev: 2a30809d16bc7a60d9b97353c797f42b510d3368 # frozen: v3.12.0-2
hooks:
- id: shfmt
args: ["-i", "4", "--write"]
# Running some C++ hooks before clang-format
# to ensure that the style is consistent.
- repo: local
@@ -83,7 +94,7 @@ repos:
language: script
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: 719856d56a62953b8d2839fb9e851f25c3cfeef8 # frozen: v21.1.2
rev: 4c26f99731e7c22a047c35224150ee9e43d7c03e # frozen: v21.1.6
hooks:
- id: clang-format
args: [--style=file]

View File

@@ -180,6 +180,7 @@ Existing maintainers can resign, or be subject to a vote for removal at the behe
- [kuznetsss](https://github.com/kuznetsss) (Ripple)
- [legleux](https://github.com/legleux) (Ripple)
- [PeterChen13579](https://github.com/PeterChen13579) (Ripple)
- [mathbunnyru](https://github.com/mathbunnyru) (Ripple)
### Honorable ex-Maintainers

View File

@@ -0,0 +1,17 @@
[Unit]
Description=Clio XRPL API server
Documentation=https://github.com/XRPLF/clio.git
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=@CLIO_INSTALL_DIR@/bin/clio_server @CLIO_INSTALL_DIR@/etc/config.json
Restart=on-failure
User=clio
Group=clio
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target

View File

@@ -11,3 +11,6 @@ file(READ docs/examples/config/example-config.json config)
string(REGEX REPLACE "./clio_log" "/var/log/clio/" config "${config}")
file(WRITE ${CMAKE_BINARY_DIR}/install-config.json "${config}")
install(FILES ${CMAKE_BINARY_DIR}/install-config.json DESTINATION etc RENAME config.json)
configure_file("${CMAKE_SOURCE_DIR}/cmake/install/clio.service.in" "${CMAKE_BINARY_DIR}/clio.service")
install(FILES "${CMAKE_BINARY_DIR}/clio.service" DESTINATION /lib/systemd/system)

View File

@@ -10,37 +10,36 @@ CLIO_BIN="$CLIO_PREFIX/bin/${CLIO_EXECUTABLE}"
CLIO_CONFIG="$CLIO_PREFIX/etc/config.json"
case "$1" in
configure)
if ! id -u "$USER_NAME" >/dev/null 2>&1; then
# Users who should not have a home directory should have their home directory set to /nonexistent
# https://www.debian.org/doc/debian-policy/ch-opersys.html#non-existent-home-directories
useradd \
--system \
--home-dir /nonexistent \
--no-create-home \
--shell /usr/sbin/nologin \
--comment "system user for ${CLIO_EXECUTABLE}" \
--user-group \
${USER_NAME}
fi
configure)
if ! id -u "$USER_NAME" >/dev/null 2>&1; then
# Users who should not have a home directory should have their home directory set to /nonexistent
# https://www.debian.org/doc/debian-policy/ch-opersys.html#non-existent-home-directories
useradd \
--system \
--home-dir /nonexistent \
--no-create-home \
--shell /usr/sbin/nologin \
--comment "system user for ${CLIO_EXECUTABLE}" \
--user-group \
${USER_NAME}
fi
install -d -o "$USER_NAME" -g "$GROUP_NAME" /var/log/clio
install -d -o "$USER_NAME" -g "$GROUP_NAME" /var/log/clio
if [ -f "$CLIO_CONFIG" ]; then
chown "$USER_NAME:$GROUP_NAME" "$CLIO_CONFIG"
fi
if [ -f "$CLIO_CONFIG" ]; then
chown "$USER_NAME:$GROUP_NAME" "$CLIO_CONFIG"
fi
chown -R "$USER_NAME:$GROUP_NAME" "$CLIO_PREFIX"
chown -R "$USER_NAME:$GROUP_NAME" "$CLIO_PREFIX"
ln -sf "$CLIO_BIN" "/usr/bin/${CLIO_EXECUTABLE}"
ln -sf "$CLIO_BIN" "/usr/bin/${CLIO_EXECUTABLE}"
;;
abort-upgrade|abort-remove|abort-deconfigure)
;;
*)
echo "postinst called with unknown argument \`$1'" >&2
exit 1
;;
;;
abort-upgrade | abort-remove | abort-deconfigure) ;;
*)
echo "postinst called with unknown argument \`$1'" >&2
exit 1
;;
esac
exit 0

View File

@@ -3,13 +3,13 @@
"requires": [
"zlib/1.3.1#b8bc2603263cf7eccbd6e17e66b0ed76%1756234269.497",
"xxhash/0.8.3#681d36a0a6111fc56e5e45ea182c19cc%1756234289.683",
"xrpl/3.0.0-rc1#f5c8ecd42bdf511ad36f57bc702dacd2%1762975621.294",
"xrpl/3.0.0#534d3f65a336109eee929b88962bae4e%1765375071.547",
"sqlite3/3.49.1#8631739a4c9b93bd3d6b753bac548a63%1756234266.869",
"spdlog/1.15.3#3ca0e9e6b83af4d0151e26541d140c86%1754401846.61",
"spdlog/1.16.0#942c2c39562ae25ba575d9c8e2bdf3b6%1763984117.108",
"soci/4.0.3#a9f8d773cd33e356b5879a4b0564f287%1756234262.318",
"re2/20230301#dfd6e2bf050eb90ddd8729cfb4c844a4%1756234257.976",
"re2/20230301#ca3b241baec15bd31ea9187150e0b333%1764175362.029",
"rapidjson/cci.20220822#1b9d8c2256876a154172dc5cfbe447c6%1754325007.656",
"protobuf/3.21.12#d927114e28de9f4691a6bbcdd9a529d1%1756234251.614",
"protobuf/3.21.12#44ee56c0a6eea0c19aeeaca680370b88%1764175361.456",
"openssl/1.1.1w#a8f0792d7c5121b954578a7149d23e03%1756223730.729",
"nudb/2.0.9#fb8dfd1a5557f5e0528114c2da17721e%1763150366.909",
"minizip/1.2.13#9e87d57804bd372d6d1e32b1871517a3%1754325004.374",
@@ -17,41 +17,45 @@
"libuv/1.46.0#dc28c1f653fa197f00db5b577a6f6011%1754325003.592",
"libiconv/1.17#1e65319e945f2d31941a9d28cc13c058%1756223727.64",
"libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1756230911.03",
"libarchive/3.8.1#5cf685686322e906cb42706ab7e099a8%1756234256.696",
"libarchive/3.8.1#ffee18995c706e02bf96e7a2f7042e0d%1764175360.142",
"http_parser/2.9.4#98d91690d6fd021e9e624218a85d9d97%1754325001.385",
"gtest/1.14.0#f8f0757a574a8dd747d16af62d6eb1b7%1754325000.842",
"grpc/1.50.1#02291451d1e17200293a409410d1c4e1%1756234248.958",
"fmt/11.2.0#579bb2cdf4a7607621beea4eb4651e0f%1754324999.086",
"fmt/12.1.0#50abab23274d56bb8f42c94b3b9a40c7%1763984116.926",
"doctest/2.4.11#a4211dfc329a16ba9f280f9574025659%1756234220.819",
"date/3.0.4#f74bbba5a08fa388256688743136cb6f%1756234217.493",
"cassandra-cpp-driver/2.17.0#e50919efac8418c26be6671fd702540a%1754324997.363",
"c-ares/1.34.5#b78b91e7cfb1f11ce777a285bbf169c6%1756234217.915",
"bzip2/1.0.8#00b4a4658791c1f06914e087f0e792f5%1756234261.716",
"boost/1.83.0#5d975011d65b51abb2d2f6eb8386b368%1754325043.336",
"date/3.0.4#862e11e80030356b53c2c38599ceb32b%1763584497.32",
"cassandra-cpp-driver/2.17.0#bd3934138689482102c265d01288a316%1764175359.611",
"c-ares/1.34.5#5581c2b62a608b40bb85d965ab3ec7c8%1764175359.429",
"bzip2/1.0.8#c470882369c2d95c5c77e970c0c7e321%1764175359.429",
"boost/1.83.0#91d8b1572534d2c334d6790e3c34d0c1%1764175359.61",
"benchmark/1.9.4#ce4403f7a24d3e1f907cd9da4b678be4%1754578869.672",
"abseil/20230802.1#f0f91485b111dc9837a68972cb19ca7b%1756234220.907"
"abseil/20230802.1#90ba607d4ee8fb5fb157c3db540671fc%1764175359.429"
],
"build_requires": [
"zlib/1.3.1#b8bc2603263cf7eccbd6e17e66b0ed76%1756234269.497",
"protobuf/3.21.12#d927114e28de9f4691a6bbcdd9a529d1%1756234251.614",
"cmake/3.31.8#dde3bde00bb843687e55aea5afa0e220%1756234232.89",
"protobuf/3.21.12#44ee56c0a6eea0c19aeeaca680370b88%1764175361.456",
"cmake/4.2.0#ae0a44f44a1ef9ab68fd4b3e9a1f8671%1764175359.44",
"cmake/3.31.10#313d16a1aa16bbdb2ca0792467214b76%1764175359.429",
"b2/5.3.3#107c15377719889654eb9a162a673975%1756234226.28"
],
"python_requires": [],
"overrides": {
"boost/1.83.0": [
null,
"boost/1.83.0#5d975011d65b51abb2d2f6eb8386b368"
"boost/1.83.0#91d8b1572534d2c334d6790e3c34d0c1"
],
"protobuf/3.21.12": [
null,
"protobuf/3.21.12#d927114e28de9f4691a6bbcdd9a529d1"
"protobuf/3.21.12#44ee56c0a6eea0c19aeeaca680370b88"
],
"lz4/1.9.4": [
"lz4/1.10.0"
],
"sqlite3/3.44.2": [
"sqlite3/3.49.1"
],
"fmt/12.0.0": [
"fmt/12.1.0"
]
},
"config_requires": []

View File

@@ -3,62 +3,60 @@ from conan.tools.cmake import CMake, CMakeToolchain, cmake_layout
class ClioConan(ConanFile):
name = 'clio'
license = 'ISC'
author = 'Alex Kremer <akremer@ripple.com>, John Freeman <jfreeman@ripple.com>, Ayaz Salikhov <asalikhov@ripple.com>'
url = 'https://github.com/xrplf/clio'
description = 'Clio RPC server'
settings = 'os', 'compiler', 'build_type', 'arch'
name = "clio"
license = "ISC"
author = "Alex Kremer <akremer@ripple.com>, John Freeman <jfreeman@ripple.com>, Ayaz Salikhov <asalikhov@ripple.com>"
url = "https://github.com/xrplf/clio"
description = "Clio RPC server"
settings = "os", "compiler", "build_type", "arch"
options = {}
requires = [
'boost/1.83.0',
'cassandra-cpp-driver/2.17.0',
'fmt/11.2.0',
'protobuf/3.21.12',
'grpc/1.50.1',
'openssl/1.1.1w',
'xrpl/3.0.0-rc1',
'zlib/1.3.1',
'libbacktrace/cci.20210118',
'spdlog/1.15.3',
"boost/1.83.0",
"cassandra-cpp-driver/2.17.0",
"protobuf/3.21.12",
"grpc/1.50.1",
"openssl/1.1.1w",
"xrpl/3.0.0",
"zlib/1.3.1",
"libbacktrace/cci.20210118",
"spdlog/1.16.0",
]
default_options = {
'xrpl/*:tests': False,
'xrpl/*:rocksdb': False,
'cassandra-cpp-driver/*:shared': False,
'date/*:header_only': True,
'grpc/*:shared': False,
'grpc/*:secure': True,
'libpq/*:shared': False,
'lz4/*:shared': False,
'openssl/*:shared': False,
'protobuf/*:shared': False,
'protobuf/*:with_zlib': True,
'snappy/*:shared': False,
'gtest/*:no_main': True,
"xrpl/*:tests": False,
"xrpl/*:rocksdb": False,
"cassandra-cpp-driver/*:shared": False,
"date/*:header_only": True,
"grpc/*:shared": False,
"grpc/*:secure": True,
"libpq/*:shared": False,
"lz4/*:shared": False,
"openssl/*:shared": False,
"protobuf/*:shared": False,
"protobuf/*:with_zlib": True,
"snappy/*:shared": False,
"gtest/*:no_main": True,
}
exports_sources = (
'CMakeLists.txt', 'cmake/*', 'src/*'
)
exports_sources = ("CMakeLists.txt", "cmake/*", "src/*")
def requirements(self):
self.requires('gtest/1.14.0')
self.requires('benchmark/1.9.4')
self.requires("gtest/1.14.0")
self.requires("benchmark/1.9.4")
self.requires("fmt/12.1.0", force=True)
def configure(self):
if self.settings.compiler == 'apple-clang':
self.options['boost'].visibility = 'global'
if self.settings.compiler == "apple-clang":
self.options["boost"].visibility = "global"
def layout(self):
cmake_layout(self)
# Fix this setting to follow the default introduced in Conan 1.48
# to align with our build instructions.
self.folders.generators = 'build/generators'
self.folders.generators = "build/generators"
generators = 'CMakeDeps'
generators = "CMakeDeps"
def generate(self):
tc = CMakeToolchain(self)

View File

@@ -36,7 +36,6 @@ RUN apt-get update \
libmpfr-dev \
libncurses-dev \
make \
ninja-build \
wget \
zip \
&& apt-get clean \
@@ -107,6 +106,7 @@ COPY --from=clio-tools \
/usr/local/bin/git-cliff \
/usr/local/bin/gh \
/usr/local/bin/gdb \
/usr/local/bin/ninja \
/usr/local/bin/
WORKDIR /root

View File

@@ -15,6 +15,7 @@ The image is based on Ubuntu 20.04 and contains:
- gh 2.82.1
- git-cliff 2.10.1
- mold 2.40.4
- Ninja 1.13.2
- Python 3.8
- and some other useful tools

View File

@@ -1,6 +1,6 @@
services:
clio_develop:
image: ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7
image: ghcr.io/xrplf/clio-ci:067449c3f8ae6755ea84752ea2962b589fe56c8f
volumes:
- clio_develop_conan_data:/root/.conan2/p
- clio_develop_ccache:/root/.ccache

View File

@@ -2,7 +2,7 @@
script_dir=$(dirname $0)
pushd $script_dir > /dev/null
pushd $script_dir >/dev/null
function start_container {
if [ -z "$(docker ps -q -f name=clio_develop)" ]; then
@@ -41,21 +41,26 @@ EOF
}
case $1 in
-h|--help)
print_help ;;
-h | --help)
print_help
;;
-t|--terminal)
open_terminal ;;
-t | --terminal)
open_terminal
;;
-s|--stop)
stop_container ;;
-s | --stop)
stop_container
;;
-*)
echo "Unknown option: $1"
print_help ;;
-*)
echo "Unknown option: $1"
print_help
;;
*)
run "$@" ;;
*)
run "$@"
;;
esac
popd > /dev/null
popd >/dev/null

View File

@@ -12,7 +12,6 @@ ARG BUILD_VERSION=0
RUN apt-get update \
&& apt-get install -y --no-install-recommends --no-install-suggests \
ninja-build \
python3 \
python3-pip \
software-properties-common \
@@ -24,6 +23,15 @@ RUN apt-get update \
WORKDIR /tmp
ARG NINJA_VERSION=1.13.2
RUN wget --progress=dot:giga "https://github.com/ninja-build/ninja/archive/refs/tags/v${NINJA_VERSION}.tar.gz" \
&& tar xf "v${NINJA_VERSION}.tar.gz" \
&& cd "ninja-${NINJA_VERSION}" \
&& ./configure.py --bootstrap \
&& mv ninja /usr/local/bin/ninja \
&& rm -rf /tmp/* /var/tmp/*
ARG MOLD_VERSION=2.40.4
RUN wget --progress=dot:giga "https://github.com/rui314/mold/archive/refs/tags/v${MOLD_VERSION}.tar.gz" \
&& tar xf "v${MOLD_VERSION}.tar.gz" \

View File

@@ -191,7 +191,7 @@ Open the `index.html` file in your browser to see the documentation pages.
It is also possible to build Clio using [Docker](https://www.docker.com/) if you don't want to install all the dependencies on your machine.
```sh
docker run -it ghcr.io/xrplf/clio-ci:77387d8f9f13aea8f23831d221ac3e7683bb69b7
docker run -it ghcr.io/xrplf/clio-ci:067449c3f8ae6755ea84752ea2962b589fe56c8f
git clone https://github.com/XRPLF/clio
cd clio
```

View File

@@ -45,7 +45,7 @@ if [[ "1.14.0" > "$version" ]]; then
ERROR
-----------------------------------------------------------------------------
A minimum of version 1.14 of `which doxygen` is required.
A minimum of version 1.14 of $(which doxygen) is required.
Your version is $version. Please upgrade it.
Your changes may fail CI checks.
@@ -55,26 +55,26 @@ EOF
exit 0
fi
mkdir -p ${DOCDIR} > /dev/null 2>&1
pushd ${DOCDIR} > /dev/null 2>&1
mkdir -p ${DOCDIR} >/dev/null 2>&1
pushd ${DOCDIR} >/dev/null 2>&1
cat ${ROOT}/docs/Doxyfile | \
sed \
-e "s/\${LINT}/YES/" \
-e "s/\${WARN_AS_ERROR}/NO/" \
-e "s!\${SOURCE}!${ROOT}!" \
-e "s/\${USE_DOT}/NO/" \
-e "s/\${EXCLUDES}/impl/" \
| ${DOXYGEN} - 2> ${TMPFILE} 1> /dev/null
cat ${ROOT}/docs/Doxyfile |
sed \
-e "s/\${LINT}/YES/" \
-e "s/\${WARN_AS_ERROR}/NO/" \
-e "s!\${SOURCE}!${ROOT}!" \
-e "s/\${USE_DOT}/NO/" \
-e "s/\${EXCLUDES}/impl/" |
${DOXYGEN} - 2>${TMPFILE} 1>/dev/null
# We don't want to check for default values and typedefs as well as for member variables
OUT=$(cat ${TMPFILE} \
| grep -v "=default" \
| grep -v "\(variable\)" \
| grep -v "\(typedef\)")
OUT=$(cat ${TMPFILE} |
grep -v "=default" |
grep -v "\(variable\)" |
grep -v "\(typedef\)")
rm -rf ${TMPFILE} > /dev/null 2>&1
popd > /dev/null 2>&1
rm -rf ${TMPFILE} >/dev/null 2>&1
popd >/dev/null 2>&1
if [[ ! -z "$OUT" ]]; then
cat <<EOF

View File

@@ -23,10 +23,10 @@ fix_includes() {
file_path_fixed="${file_path}.tmp.fixed"
# Make all includes to be <...> style
sed -E 's|#include "(.*)"|#include <\1>|g' "$file_path" > "$file_path_all_global"
sed -E 's|#include "(.*)"|#include <\1>|g' "$file_path" >"$file_path_all_global"
# Make local includes to be "..." style
sed -E "s|#include <(($main_src_dirs)/.*)>|#include \"\1\"|g" "$file_path_all_global" > "$file_path_fixed"
sed -E "s|#include <(($main_src_dirs)/.*)>|#include \"\1\"|g" "$file_path_all_global" >"$file_path_fixed"
rm "$file_path_all_global"
# Check if the temporary file is different from the original file

View File

@@ -4,7 +4,6 @@ import argparse
import re
from pathlib import Path
PATTERN = r'R"JSON\((.*?)\)JSON"'
@@ -40,6 +39,7 @@ def fix_colon_spacing(cpp_content: str) -> str:
raw_json = match.group(1)
raw_json = re.sub(r'":\n\s*(\[|\{)', r'": \1', raw_json)
return f'R"JSON({raw_json})JSON"'
return re.sub(PATTERN, replace_json, cpp_content, flags=re.DOTALL)
@@ -49,12 +49,12 @@ def fix_indentation(cpp_content: str) -> str:
lines = cpp_content.splitlines()
ends_with_newline = cpp_content.endswith('\n')
ends_with_newline = cpp_content.endswith("\n")
def find_indentation(line: str) -> int:
return len(line) - len(line.lstrip())
for (line_num, (line, next_line)) in enumerate(zip(lines[:-1], lines[1:])):
for line_num, (line, next_line) in enumerate(zip(lines[:-1], lines[1:])):
if "JSON(" in line and ")JSON" not in line:
indent = find_indentation(line)
next_indent = find_indentation(next_line)
@@ -69,7 +69,11 @@ def fix_indentation(cpp_content: str) -> str:
if ")JSON" in lines[i]:
lines[i] = " " * indent + lines[i].lstrip()
break
lines[i] = lines[i][by_how_much:] if by_how_much > 0 else " " * (-by_how_much) + lines[i]
lines[i] = (
lines[i][by_how_much:]
if by_how_much > 0
else " " * (-by_how_much) + lines[i]
)
result = "\n".join(lines)

View File

@@ -4,7 +4,7 @@
#
set -e -o pipefail
if ! command -v gofmt &> /dev/null ; then
if ! command -v gofmt &>/dev/null; then
echo "gofmt not installed or available in the PATH" >&2
exit 1
fi

View File

@@ -1,5 +1,4 @@
#!/bin/sh
#!/bin/bash
# git for-each-ref refs/tags # see which tags are annotated and which are lightweight. Annotated tags are "tag" objects.
# # Set these so your commits and tags are always signed
@@ -7,7 +6,7 @@
# git config tag.gpgsign true
verify_commit_signed() {
if git verify-commit HEAD &> /dev/null; then
if git verify-commit HEAD &>/dev/null; then
:
# echo "HEAD commit seems signed..."
else
@@ -17,7 +16,7 @@ verify_commit_signed() {
}
verify_tag() {
if git describe --exact-match --tags HEAD &> /dev/null; then
if git describe --exact-match --tags HEAD &>/dev/null; then
: # You might be ok to push
# echo "Tag is annotated."
return 0
@@ -28,7 +27,7 @@ verify_tag() {
}
verify_tag_signed() {
if git verify-tag "$version" &> /dev/null ; then
if git verify-tag "$version" &>/dev/null; then
: # ok, I guess we'll let you push
# echo "Tag appears signed"
return 0
@@ -40,11 +39,11 @@ verify_tag_signed() {
}
# Check some things if we're pushing a branch called "release/"
if echo "$PRE_COMMIT_REMOTE_BRANCH" | grep ^refs\/heads\/release\/ &> /dev/null ; then
if echo "$PRE_COMMIT_REMOTE_BRANCH" | grep ^refs\/heads\/release\/ &>/dev/null; then
version=$(git tag --points-at HEAD)
echo "Looks like you're trying to push a $version release..."
echo "Making sure you've signed and tagged it."
if verify_commit_signed && verify_tag && verify_tag_signed ; then
if verify_commit_signed && verify_tag && verify_tag_signed; then
: # Ok, I guess you can push
else
exit 1

View File

@@ -91,6 +91,7 @@ ClioApplication::ClioApplication(util::config::ClioConfigDefinition const& confi
{
LOG(util::LogService::info()) << "Clio version: " << util::build::getClioFullVersionString();
signalsHandler_.subscribeToStop([this]() { appStopper_.stop(); });
appStopper_.setOnComplete([this]() { signalsHandler_.notifyGracefulShutdownComplete(); });
}
int
@@ -182,7 +183,7 @@ ClioApplication::run(bool const useNgWebServer)
return EXIT_FAILURE;
}
httpServer->onGet("/metrics", MetricsHandler{adminVerifier});
httpServer->onGet("/metrics", MetricsHandler{adminVerifier, workQueue});
httpServer->onGet("/health", HealthCheckHandler{});
httpServer->onGet("/cache_state", CacheStateHandler{cache});
auto requestHandler = RequestHandler{adminVerifier, handler};

View File

@@ -38,7 +38,18 @@ Stopper::~Stopper()
void
Stopper::setOnStop(std::function<void(boost::asio::yield_context)> cb)
{
util::spawn(ctx_, std::move(cb));
util::spawn(ctx_, [this, cb = std::move(cb)](auto yield) {
cb(yield);
if (onCompleteCallback_)
onCompleteCallback_();
});
}
void
Stopper::setOnComplete(std::function<void()> cb)
{
onCompleteCallback_ = std::move(cb);
}
void

View File

@@ -43,6 +43,7 @@ namespace app {
class Stopper {
boost::asio::io_context ctx_;
std::thread worker_;
std::function<void()> onCompleteCallback_;
public:
/**
@@ -58,6 +59,14 @@ public:
void
setOnStop(std::function<void(boost::asio::yield_context)> cb);
/**
* @brief Set the callback to be called when graceful shutdown completes.
*
* @param cb The callback to be called when shutdown completes.
*/
void
setOnComplete(std::function<void()> cb);
/**
* @brief Stop the application and run the shutdown tasks.
*/

View File

@@ -19,7 +19,10 @@
#include "app/WebHandlers.hpp"
#include "rpc/Errors.hpp"
#include "rpc/WorkQueue.hpp"
#include "util/Assert.hpp"
#include "util/CoroutineGroup.hpp"
#include "util/prometheus/Http.hpp"
#include "web/AdminVerificationStrategy.hpp"
#include "web/SubscriptionContextInterface.hpp"
@@ -31,6 +34,7 @@
#include <boost/asio/spawn.hpp>
#include <boost/beast/http/status.hpp>
#include <functional>
#include <memory>
#include <optional>
#include <string>
@@ -76,8 +80,8 @@ DisconnectHook::operator()(web::ng::Connection const& connection)
dosguard_.get().decrement(connection.ip());
}
MetricsHandler::MetricsHandler(std::shared_ptr<web::AdminVerificationStrategy> adminVerifier)
: adminVerifier_{std::move(adminVerifier)}
MetricsHandler::MetricsHandler(std::shared_ptr<web::AdminVerificationStrategy> adminVerifier, rpc::WorkQueue& workQueue)
: adminVerifier_{std::move(adminVerifier)}, workQueue_{std::ref(workQueue)}
{
}
@@ -86,19 +90,45 @@ MetricsHandler::operator()(
web::ng::Request const& request,
web::ng::ConnectionMetadata& connectionMetadata,
web::SubscriptionContextPtr,
boost::asio::yield_context
boost::asio::yield_context yield
)
{
auto const maybeHttpRequest = request.asHttpRequest();
ASSERT(maybeHttpRequest.has_value(), "Got not a http request in Get");
auto const& httpRequest = maybeHttpRequest->get();
std::optional<web::ng::Response> response;
util::CoroutineGroup coroutineGroup{yield, 1};
auto const onTaskComplete = coroutineGroup.registerForeign(yield);
ASSERT(onTaskComplete.has_value(), "Coroutine group can't be full");
// FIXME(#1702): Using veb server thread to handle prometheus request. Better to post on work queue.
auto maybeResponse = util::prometheus::handlePrometheusRequest(
httpRequest, adminVerifier_->isAdmin(httpRequest, connectionMetadata.ip())
bool const postSuccessful = workQueue_.get().postCoro(
[this, &request, &response, &onTaskComplete = onTaskComplete.value(), &connectionMetadata](
boost::asio::yield_context
) mutable {
auto const maybeHttpRequest = request.asHttpRequest();
ASSERT(maybeHttpRequest.has_value(), "Got not a http request in Get");
auto const& httpRequest = maybeHttpRequest->get();
auto maybeResponse = util::prometheus::handlePrometheusRequest(
httpRequest, adminVerifier_->isAdmin(httpRequest, connectionMetadata.ip())
);
ASSERT(maybeResponse.has_value(), "Got unexpected request for Prometheus");
response = web::ng::Response{std::move(maybeResponse).value(), request};
// notify the coroutine group that the foreign task is done
onTaskComplete();
},
/* isWhiteListed= */ true,
rpc::WorkQueue::Priority::High
);
ASSERT(maybeResponse.has_value(), "Got unexpected request for Prometheus");
return web::ng::Response{std::move(maybeResponse).value(), request};
if (!postSuccessful) {
return web::ng::Response{
boost::beast::http::status::too_many_requests, rpc::makeError(rpc::RippledError::rpcTOO_BUSY), request
};
}
// Put the coroutine to sleep until the foreign task is done
coroutineGroup.asyncWait(yield);
ASSERT(response.has_value(), "Woke up coroutine without setting response");
return std::move(response).value();
}
web::ng::Response

View File

@@ -21,6 +21,7 @@
#include "data/LedgerCacheInterface.hpp"
#include "rpc/Errors.hpp"
#include "rpc/WorkQueue.hpp"
#include "util/log/Logger.hpp"
#include "web/AdminVerificationStrategy.hpp"
#include "web/SubscriptionContextInterface.hpp"
@@ -119,20 +120,23 @@ public:
*/
class MetricsHandler {
std::shared_ptr<web::AdminVerificationStrategy> adminVerifier_;
std::reference_wrapper<rpc::WorkQueue> workQueue_;
public:
/**
* @brief Construct a new MetricsHandler object
*
* @param adminVerifier The AdminVerificationStrategy to use for verifying the connection for admin access.
* @param workQueue The WorkQueue to use for handling the request.
*/
MetricsHandler(std::shared_ptr<web::AdminVerificationStrategy> adminVerifier);
MetricsHandler(std::shared_ptr<web::AdminVerificationStrategy> adminVerifier, rpc::WorkQueue& workQueue);
/**
* @brief The call of the function object.
*
* @param request The request to handle.
* @param connectionMetadata The connection metadata.
* @param yield The yield context.
* @return The response to the request.
*/
web::ng::Response
@@ -140,7 +144,7 @@ public:
web::ng::Request const& request,
web::ng::ConnectionMetadata& connectionMetadata,
web::SubscriptionContextPtr,
boost::asio::yield_context
boost::asio::yield_context yield
);
};

View File

@@ -152,6 +152,7 @@ struct Amendments {
REGISTER(fixDirectoryLimit);
REGISTER(fixIncludeKeyletFields);
REGISTER(fixTokenEscrowV1);
REGISTER(LendingProtocol);
// Obsolete but supported by libxrpl
REGISTER(CryptoConditionsSuite);

View File

@@ -87,8 +87,8 @@ TaskManager::run(std::size_t numExtractors)
util::async::AnyOperation<void>
TaskManager::spawnExtractor(TaskQueue& queue)
{
// TODO: these values may be extracted to config later and/or need to be fine-tuned on a realistic system
static constexpr auto kDELAY_BETWEEN_ATTEMPTS = std::chrono::milliseconds{100u};
// TODO https://github.com/XRPLF/clio/issues/2838: the approach should be changed to a reactive one instead
static constexpr auto kDELAY_BETWEEN_ATTEMPTS = std::chrono::milliseconds{10u};
static constexpr auto kDELAY_BETWEEN_ENQUEUE_ATTEMPTS = std::chrono::milliseconds{1u};
return ctx_.execute([this, &queue](auto stopRequested) {

View File

@@ -316,8 +316,11 @@ tag_invoke(boost::json::value_to_tag<AMMInfoHandler::Input>, boost::json::value
if (jsonObject.contains(JS(ledger_hash)))
input.ledgerHash = boost::json::value_to<std::string>(jv.at(JS(ledger_hash)));
if (jsonObject.contains(JS(ledger_index)))
input.ledgerIndex = util::getLedgerIndex(jsonObject.at(JS(ledger_index)));
if (jsonObject.contains(JS(ledger_index))) {
auto const expectedLedgerIndex = util::getLedgerIndex(jsonObject.at(JS(ledger_index)));
if (expectedLedgerIndex.has_value())
input.ledgerIndex = *expectedLedgerIndex;
}
if (jsonObject.contains(JS(asset)))
input.issue1 = parseIssue(jsonObject.at(JS(asset)).as_object());

View File

@@ -154,8 +154,11 @@ tag_invoke(boost::json::value_to_tag<AccountChannelsHandler::Input>, boost::json
if (jsonObject.contains(JS(destination_account)))
input.destinationAccount = boost::json::value_to<std::string>(jv.at(JS(destination_account)));
if (jsonObject.contains(JS(ledger_index)))
input.ledgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index)));
if (jsonObject.contains(JS(ledger_index))) {
auto const expectedLedgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index)));
if (expectedLedgerIndex.has_value())
input.ledgerIndex = *expectedLedgerIndex;
}
return input;
}

View File

@@ -128,8 +128,11 @@ tag_invoke(boost::json::value_to_tag<AccountCurrenciesHandler::Input>, boost::js
if (jsonObject.contains(JS(ledger_hash)))
input.ledgerHash = boost::json::value_to<std::string>(jv.at(JS(ledger_hash)));
if (jsonObject.contains(JS(ledger_index)))
input.ledgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index)));
if (jsonObject.contains(JS(ledger_index))) {
auto const expectedLedgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index)));
if (expectedLedgerIndex.has_value())
input.ledgerIndex = *expectedLedgerIndex;
}
return input;
}

View File

@@ -204,8 +204,11 @@ tag_invoke(boost::json::value_to_tag<AccountInfoHandler::Input>, boost::json::va
if (jsonObject.contains(JS(ledger_hash)))
input.ledgerHash = boost::json::value_to<std::string>(jsonObject.at(JS(ledger_hash)));
if (jsonObject.contains(JS(ledger_index)))
input.ledgerIndex = util::getLedgerIndex(jsonObject.at(JS(ledger_index)));
if (jsonObject.contains(JS(ledger_index))) {
auto const expectedLedgerIndex = util::getLedgerIndex(jsonObject.at(JS(ledger_index)));
if (expectedLedgerIndex.has_value())
input.ledgerIndex = *expectedLedgerIndex;
}
if (jsonObject.contains(JS(signer_lists)))
input.signerLists = boost::json::value_to<JsonBool>(jsonObject.at(JS(signer_lists)));

View File

@@ -215,8 +215,11 @@ tag_invoke(boost::json::value_to_tag<AccountLinesHandler::Input>, boost::json::v
if (jsonObject.contains(JS(ignore_default)))
input.ignoreDefault = jv.at(JS(ignore_default)).as_bool();
if (jsonObject.contains(JS(ledger_index)))
input.ledgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index)));
if (jsonObject.contains(JS(ledger_index))) {
auto const expectedLedgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index)));
if (expectedLedgerIndex.has_value())
input.ledgerIndex = *expectedLedgerIndex;
}
return input;
}

View File

@@ -56,6 +56,7 @@ AccountMPTokenIssuancesHandler::addMPTokenIssuance(
{
MPTokenIssuanceResponse issuance;
issuance.MPTokenIssuanceID = ripple::strHex(sle.key());
issuance.issuer = ripple::to_string(account);
issuance.sequence = sle.getFieldU32(ripple::sfSequence);
auto const flags = sle.getFieldU32(ripple::sfFlags);
@@ -73,6 +74,24 @@ AccountMPTokenIssuancesHandler::addMPTokenIssuance(
setFlag(issuance.mptCanTransfer, ripple::lsfMPTCanTransfer);
setFlag(issuance.mptCanClawback, ripple::lsfMPTCanClawback);
if (sle.isFieldPresent(ripple::sfMutableFlags)) {
auto const mutableFlags = sle.getFieldU32(ripple::sfMutableFlags);
auto const setMutableFlag = [&](std::optional<bool>& field, std::uint32_t mask) {
if ((mutableFlags & mask) != 0u)
field = true;
};
setMutableFlag(issuance.mptCanMutateCanLock, ripple::lsmfMPTCanMutateCanLock);
setMutableFlag(issuance.mptCanMutateRequireAuth, ripple::lsmfMPTCanMutateRequireAuth);
setMutableFlag(issuance.mptCanMutateCanEscrow, ripple::lsmfMPTCanMutateCanEscrow);
setMutableFlag(issuance.mptCanMutateCanTrade, ripple::lsmfMPTCanMutateCanTrade);
setMutableFlag(issuance.mptCanMutateCanTransfer, ripple::lsmfMPTCanMutateCanTransfer);
setMutableFlag(issuance.mptCanMutateCanClawback, ripple::lsmfMPTCanMutateCanClawback);
setMutableFlag(issuance.mptCanMutateMetadata, ripple::lsmfMPTCanMutateMetadata);
setMutableFlag(issuance.mptCanMutateTransferFee, ripple::lsmfMPTCanMutateTransferFee);
}
if (sle.isFieldPresent(ripple::sfTransferFee))
issuance.transferFee = sle.getFieldU16(ripple::sfTransferFee);
@@ -164,8 +183,11 @@ tag_invoke(boost::json::value_to_tag<AccountMPTokenIssuancesHandler::Input>, boo
if (jsonObject.contains(JS(ledger_hash)))
input.ledgerHash = boost::json::value_to<std::string>(jv.at(JS(ledger_hash)));
if (jsonObject.contains(JS(ledger_index)))
input.ledgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index)));
if (jsonObject.contains(JS(ledger_index))) {
auto const expectedLedgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index)));
if (expectedLedgerIndex.has_value())
input.ledgerIndex = *expectedLedgerIndex;
}
return input;
}
@@ -198,6 +220,7 @@ tag_invoke(
)
{
auto obj = boost::json::object{
{JS(mpt_issuance_id), issuance.MPTokenIssuanceID},
{JS(issuer), issuance.issuer},
{JS(sequence), issuance.sequence},
};
@@ -224,6 +247,15 @@ tag_invoke(
setIfPresent("mpt_can_transfer", issuance.mptCanTransfer);
setIfPresent("mpt_can_clawback", issuance.mptCanClawback);
setIfPresent("mpt_can_mutate_can_lock", issuance.mptCanMutateCanLock);
setIfPresent("mpt_can_mutate_require_auth", issuance.mptCanMutateRequireAuth);
setIfPresent("mpt_can_mutate_can_escrow", issuance.mptCanMutateCanEscrow);
setIfPresent("mpt_can_mutate_can_trade", issuance.mptCanMutateCanTrade);
setIfPresent("mpt_can_mutate_can_transfer", issuance.mptCanMutateCanTransfer);
setIfPresent("mpt_can_mutate_can_clawback", issuance.mptCanMutateCanClawback);
setIfPresent("mpt_can_mutate_metadata", issuance.mptCanMutateMetadata);
setIfPresent("mpt_can_mutate_transfer_fee", issuance.mptCanMutateTransferFee);
jv = std::move(obj);
}

View File

@@ -61,6 +61,7 @@ public:
* @brief A struct to hold data for one MPTokenIssuance response.
*/
struct MPTokenIssuanceResponse {
std::string MPTokenIssuanceID;
std::string issuer;
uint32_t sequence{};
@@ -80,6 +81,15 @@ public:
std::optional<bool> mptCanTrade;
std::optional<bool> mptCanTransfer;
std::optional<bool> mptCanClawback;
std::optional<bool> mptCanMutateCanLock;
std::optional<bool> mptCanMutateRequireAuth;
std::optional<bool> mptCanMutateCanEscrow;
std::optional<bool> mptCanMutateCanTrade;
std::optional<bool> mptCanMutateCanTransfer;
std::optional<bool> mptCanMutateCanClawback;
std::optional<bool> mptCanMutateMetadata;
std::optional<bool> mptCanMutateTransferFee;
};
/**

View File

@@ -54,6 +54,7 @@ AccountMPTokensHandler::addMPToken(std::vector<MPTokenResponse>& mpts, ripple::S
MPTokenResponse token{};
auto const flags = sle.getFieldU32(ripple::sfFlags);
token.MPTokenID = ripple::strHex(sle.key());
token.account = ripple::to_string(sle.getAccountID(ripple::sfAccount));
token.MPTokenIssuanceID = ripple::strHex(sle.getFieldH192(ripple::sfMPTokenIssuanceID));
token.MPTAmount = sle.getFieldU64(ripple::sfMPTAmount);
@@ -139,8 +140,11 @@ tag_invoke(boost::json::value_to_tag<AccountMPTokensHandler::Input>, boost::json
if (jsonObject.contains(JS(ledger_hash)))
input.ledgerHash = boost::json::value_to<std::string>(jv.at(JS(ledger_hash)));
if (jsonObject.contains(JS(ledger_index)))
input.ledgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index)));
if (jsonObject.contains(JS(ledger_index))) {
auto const expectedLedgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index)));
if (expectedLedgerIndex.has_value())
input.ledgerIndex = *expectedLedgerIndex;
}
return input;
}
@@ -167,6 +171,7 @@ void
tag_invoke(boost::json::value_from_tag, boost::json::value& jv, AccountMPTokensHandler::MPTokenResponse const& mptoken)
{
auto obj = boost::json::object{
{"mpt_id", mptoken.MPTokenID},
{JS(account), mptoken.account},
{JS(mpt_issuance_id), mptoken.MPTokenIssuanceID},
{JS(mpt_amount), mptoken.MPTAmount},

View File

@@ -59,6 +59,7 @@ public:
* @brief A struct to hold data for one MPToken response.
*/
struct MPTokenResponse {
std::string MPTokenID;
std::string account;
std::string MPTokenIssuanceID;
uint64_t MPTAmount{};

View File

@@ -157,8 +157,11 @@ tag_invoke(boost::json::value_to_tag<AccountNFTsHandler::Input>, boost::json::va
if (jsonObject.contains(JS(ledger_hash)))
input.ledgerHash = boost::json::value_to<std::string>(jsonObject.at(JS(ledger_hash)));
if (jsonObject.contains(JS(ledger_index)))
input.ledgerIndex = util::getLedgerIndex(jsonObject.at(JS(ledger_index)));
if (jsonObject.contains(JS(ledger_index))) {
auto const expectedLedgerIndex = util::getLedgerIndex(jsonObject.at(JS(ledger_index)));
if (expectedLedgerIndex.has_value())
input.ledgerIndex = *expectedLedgerIndex;
}
if (jsonObject.contains(JS(limit)))
input.limit = util::integralValueAs<uint32_t>(jsonObject.at(JS(limit)));

View File

@@ -153,8 +153,11 @@ tag_invoke(boost::json::value_to_tag<AccountObjectsHandler::Input>, boost::json:
if (jsonObject.contains(JS(ledger_hash)))
input.ledgerHash = boost::json::value_to<std::string>(jv.at(JS(ledger_hash)));
if (jsonObject.contains(JS(ledger_index)))
input.ledgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index)));
if (jsonObject.contains(JS(ledger_index))) {
auto const expectedLedgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index)));
if (expectedLedgerIndex.has_value())
input.ledgerIndex = *expectedLedgerIndex;
}
if (jsonObject.contains(JS(type))) {
input.type =

View File

@@ -169,8 +169,11 @@ tag_invoke(boost::json::value_to_tag<AccountOffersHandler::Input>, boost::json::
if (jsonObject.contains(JS(ledger_hash)))
input.ledgerHash = boost::json::value_to<std::string>(jsonObject.at(JS(ledger_hash)));
if (jsonObject.contains(JS(ledger_index)))
input.ledgerIndex = util::getLedgerIndex(jsonObject.at(JS(ledger_index)));
if (jsonObject.contains(JS(ledger_index))) {
auto const expectedLedgerIndex = util::getLedgerIndex(jsonObject.at(JS(ledger_index)));
if (expectedLedgerIndex.has_value())
input.ledgerIndex = *expectedLedgerIndex;
}
if (jsonObject.contains(JS(limit)))
input.limit = util::integralValueAs<uint32_t>(jsonObject.at(JS(limit)));

View File

@@ -258,8 +258,10 @@ tag_invoke(boost::json::value_to_tag<AccountTxHandler::Input>, boost::json::valu
input.ledgerHash = boost::json::value_to<std::string>(jsonObject.at(JS(ledger_hash)));
if (jsonObject.contains(JS(ledger_index))) {
input.ledgerIndex = util::getLedgerIndex(jsonObject.at(JS(ledger_index)));
if (not input.ledgerIndex.has_value()) {
auto const expectedLedgerIndex = util::getLedgerIndex(jsonObject.at(JS(ledger_index)));
if (expectedLedgerIndex.has_value()) {
input.ledgerIndex = *expectedLedgerIndex;
} else {
// could not get the latest validated ledger seq here, using this flag to indicate that
input.usingValidatedLedger = true;
}

View File

@@ -90,8 +90,11 @@ tag_invoke(boost::json::value_to_tag<BookChangesHandler::Input>, boost::json::va
if (jsonObject.contains(JS(ledger_hash)))
input.ledgerHash = boost::json::value_to<std::string>(jv.at(JS(ledger_hash)));
if (jsonObject.contains(JS(ledger_index)))
input.ledgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index)));
if (jsonObject.contains(JS(ledger_index))) {
auto const expectedLedgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index)));
if (expectedLedgerIndex.has_value())
input.ledgerIndex = *expectedLedgerIndex;
}
return input;
}

View File

@@ -122,8 +122,11 @@ tag_invoke(boost::json::value_to_tag<BookOffersHandler::Input>, boost::json::val
if (jsonObject.contains(JS(ledger_hash)))
input.ledgerHash = boost::json::value_to<std::string>(jv.at(JS(ledger_hash)));
if (jsonObject.contains(JS(ledger_index)))
input.ledgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index)));
if (jsonObject.contains(JS(ledger_index))) {
auto const expectedLedgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index)));
if (expectedLedgerIndex.has_value())
input.ledgerIndex = *expectedLedgerIndex;
}
if (jsonObject.contains(JS(taker)))
input.taker = accountFromStringStrict(boost::json::value_to<std::string>(jv.at(JS(taker))));

View File

@@ -145,8 +145,11 @@ tag_invoke(boost::json::value_to_tag<DepositAuthorizedHandler::Input>, boost::js
if (jsonObject.contains(JS(ledger_hash)))
input.ledgerHash = boost::json::value_to<std::string>(jv.at(JS(ledger_hash)));
if (jsonObject.contains(JS(ledger_index)))
input.ledgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index)));
if (jsonObject.contains(JS(ledger_index))) {
auto const expectedLedgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index)));
if (expectedLedgerIndex.has_value())
input.ledgerIndex = *expectedLedgerIndex;
}
if (jsonObject.contains(JS(credentials)))
input.credentials = boost::json::value_to<boost::json::array>(jv.at(JS(credentials)));

View File

@@ -168,8 +168,11 @@ tag_invoke(boost::json::value_to_tag<FeatureHandler::Input>, boost::json::value
if (jsonObject.contains(JS(ledger_hash)))
input.ledgerHash = boost::json::value_to<std::string>(jv.at(JS(ledger_hash)));
if (jsonObject.contains(JS(ledger_index)))
input.ledgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index)));
if (jsonObject.contains(JS(ledger_index))) {
auto const expectedLedgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index)));
if (expectedLedgerIndex.has_value())
input.ledgerIndex = *expectedLedgerIndex;
}
return input;
}

View File

@@ -249,8 +249,11 @@ tag_invoke(boost::json::value_to_tag<GatewayBalancesHandler::Input>, boost::json
if (jsonObject.contains(JS(ledger_hash)))
input.ledgerHash = boost::json::value_to<std::string>(jv.at(JS(ledger_hash)));
if (jsonObject.contains(JS(ledger_index)))
input.ledgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index)));
if (jsonObject.contains(JS(ledger_index))) {
auto const expectedLedgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index)));
if (expectedLedgerIndex.has_value())
input.ledgerIndex = *expectedLedgerIndex;
}
if (jsonObject.contains(JS(hotwallet))) {
if (jsonObject.at(JS(hotwallet)).is_string()) {

View File

@@ -263,8 +263,11 @@ tag_invoke(boost::json::value_to_tag<GetAggregatePriceHandler::Input>, boost::js
if (jsonObject.contains(JS(ledger_hash)))
input.ledgerHash = boost::json::value_to<std::string>(jv.at(JS(ledger_hash)));
if (jsonObject.contains(JS(ledger_index)))
input.ledgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index)));
if (jsonObject.contains(JS(ledger_index))) {
auto const expectedLedgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index)));
if (expectedLedgerIndex.has_value())
input.ledgerIndex = *expectedLedgerIndex;
}
for (auto const& oracle : jsonObject.at(JS(oracles)).as_array()) {
input.oracles.push_back(

View File

@@ -208,8 +208,11 @@ tag_invoke(boost::json::value_to_tag<LedgerHandler::Input>, boost::json::value c
if (jsonObject.contains(JS(ledger_hash)))
input.ledgerHash = boost::json::value_to<std::string>(jv.at(JS(ledger_hash)));
if (jsonObject.contains(JS(ledger_index)))
input.ledgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index)));
if (jsonObject.contains(JS(ledger_index))) {
auto const expectedLedgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index)));
if (expectedLedgerIndex.has_value())
input.ledgerIndex = *expectedLedgerIndex;
}
if (jsonObject.contains(JS(transactions)))
input.transactions = jv.at(JS(transactions)).as_bool();

View File

@@ -210,8 +210,11 @@ tag_invoke(boost::json::value_to_tag<LedgerDataHandler::Input>, boost::json::val
if (jsonObject.contains(JS(ledger_hash)))
input.ledgerHash = boost::json::value_to<std::string>(jsonObject.at(JS(ledger_hash)));
if (jsonObject.contains(JS(ledger_index)))
input.ledgerIndex = util::getLedgerIndex(jsonObject.at(JS(ledger_index)));
if (jsonObject.contains(JS(ledger_index))) {
auto const expectedLedgerIndex = util::getLedgerIndex(jsonObject.at(JS(ledger_index)));
if (expectedLedgerIndex.has_value())
input.ledgerIndex = *expectedLedgerIndex;
}
if (jsonObject.contains(JS(type)))
input.type = util::LedgerTypes::getLedgerEntryTypeFromStr(boost::json::value_to<std::string>(jv.at(JS(type))));

View File

@@ -305,8 +305,11 @@ tag_invoke(boost::json::value_to_tag<LedgerEntryHandler::Input>, boost::json::va
if (jsonObject.contains(JS(ledger_hash)))
input.ledgerHash = boost::json::value_to<std::string>(jv.at(JS(ledger_hash)));
if (jsonObject.contains(JS(ledger_index)))
input.ledgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index)));
if (jsonObject.contains(JS(ledger_index))) {
auto const expectedLedgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index)));
if (expectedLedgerIndex.has_value())
input.ledgerIndex = *expectedLedgerIndex;
}
if (jsonObject.contains(JS(binary)))
input.binary = jv.at(JS(binary)).as_bool();

View File

@@ -124,8 +124,11 @@ tag_invoke(boost::json::value_to_tag<MPTHoldersHandler::Input>, boost::json::val
if (jsonObject.contains(JS(ledger_hash)))
input.ledgerHash = jsonObject.at(JS(ledger_hash)).as_string().c_str();
if (jsonObject.contains(JS(ledger_index)))
input.ledgerIndex = util::getLedgerIndex(jsonObject.at(JS(ledger_index)));
if (jsonObject.contains(JS(ledger_index))) {
auto const expectedLedgerIndex = util::getLedgerIndex(jsonObject.at(JS(ledger_index)));
if (expectedLedgerIndex.has_value())
input.ledgerIndex = *expectedLedgerIndex;
}
if (jsonObject.contains(JS(limit)))
input.limit = util::integralValueAs<uint32_t>(jsonObject.at(JS(limit)));

View File

@@ -215,8 +215,11 @@ tag_invoke(boost::json::value_to_tag<NFTHistoryHandler::Input>, boost::json::val
if (jsonObject.contains(JS(ledger_hash)))
input.ledgerHash = boost::json::value_to<std::string>(jsonObject.at(JS(ledger_hash)));
if (jsonObject.contains(JS(ledger_index)))
input.ledgerIndex = util::getLedgerIndex(jsonObject.at(JS(ledger_index)));
if (jsonObject.contains(JS(ledger_index))) {
auto const expectedLedgerIndex = util::getLedgerIndex(jsonObject.at(JS(ledger_index)));
if (expectedLedgerIndex.has_value())
input.ledgerIndex = *expectedLedgerIndex;
}
if (jsonObject.contains(JS(binary)))
input.binary = jsonObject.at(JS(binary)).as_bool();

View File

@@ -115,8 +115,11 @@ tag_invoke(boost::json::value_to_tag<NFTInfoHandler::Input>, boost::json::value
if (jsonObject.contains(JS(ledger_hash)))
input.ledgerHash = boost::json::value_to<std::string>(jsonObject.at(JS(ledger_hash)));
if (jsonObject.contains(JS(ledger_index)))
input.ledgerIndex = util::getLedgerIndex(jsonObject.at(JS(ledger_index)));
if (jsonObject.contains(JS(ledger_index))) {
auto const expectedLedgerIndex = util::getLedgerIndex(jsonObject.at(JS(ledger_index)));
if (expectedLedgerIndex.has_value())
input.ledgerIndex = *expectedLedgerIndex;
}
return input;
}

View File

@@ -194,8 +194,11 @@ tag_invoke(boost::json::value_to_tag<NFTOffersHandlerBase::Input>, boost::json::
if (jsonObject.contains(JS(ledger_hash)))
input.ledgerHash = boost::json::value_to<std::string>(jsonObject.at(JS(ledger_hash)));
if (jsonObject.contains(JS(ledger_index)))
input.ledgerIndex = util::getLedgerIndex(jsonObject.at(JS(ledger_index)));
if (jsonObject.contains(JS(ledger_index))) {
auto const expectedLedgerIndex = util::getLedgerIndex(jsonObject.at(JS(ledger_index)));
if (expectedLedgerIndex.has_value())
input.ledgerIndex = *expectedLedgerIndex;
}
if (jsonObject.contains(JS(marker)))
input.marker = boost::json::value_to<std::string>(jsonObject.at(JS(marker)));

View File

@@ -136,8 +136,11 @@ tag_invoke(boost::json::value_to_tag<NFTsByIssuerHandler::Input>, boost::json::v
if (jsonObject.contains(JS(ledger_hash)))
input.ledgerHash = boost::json::value_to<std::string>(jsonObject.at(JS(ledger_hash)));
if (jsonObject.contains(JS(ledger_index)))
input.ledgerIndex = util::getLedgerIndex(jsonObject.at(JS(ledger_index)));
if (jsonObject.contains(JS(ledger_index))) {
auto const expectedLedgerIndex = util::getLedgerIndex(jsonObject.at(JS(ledger_index)));
if (expectedLedgerIndex.has_value())
input.ledgerIndex = *expectedLedgerIndex;
}
if (jsonObject.contains(JS(limit)))
input.limit = util::integralValueAs<uint32_t>(jsonObject.at(JS(limit)));

View File

@@ -196,8 +196,11 @@ tag_invoke(boost::json::value_to_tag<NoRippleCheckHandler::Input>, boost::json::
if (jsonObject.contains(JS(ledger_hash)))
input.ledgerHash = boost::json::value_to<std::string>(jsonObject.at(JS(ledger_hash)));
if (jsonObject.contains(JS(ledger_index)))
input.ledgerIndex = util::getLedgerIndex(jsonObject.at(JS(ledger_index)));
if (jsonObject.contains(JS(ledger_index))) {
auto const expectedLedgerIndex = util::getLedgerIndex(jsonObject.at(JS(ledger_index)));
if (expectedLedgerIndex.has_value())
input.ledgerIndex = *expectedLedgerIndex;
}
return input;
}

View File

@@ -109,8 +109,11 @@ tag_invoke(boost::json::value_to_tag<TransactionEntryHandler::Input>, boost::jso
if (jsonObject.contains(JS(ledger_hash)))
input.ledgerHash = boost::json::value_to<std::string>(jv.at(JS(ledger_hash)));
if (jsonObject.contains(JS(ledger_index)))
input.ledgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index)));
if (jsonObject.contains(JS(ledger_index))) {
auto const expectedLedgerIndex = util::getLedgerIndex(jv.at(JS(ledger_index)));
if (expectedLedgerIndex.has_value())
input.ledgerIndex = *expectedLedgerIndex;
}
return input;
}

View File

@@ -177,8 +177,11 @@ tag_invoke(boost::json::value_to_tag<VaultInfoHandler::Input>, boost::json::valu
if (jsonObject.contains(JS(vault_id)))
input.vaultID = jsonObject.at(JS(vault_id)).as_string();
if (jsonObject.contains(JS(ledger_index)))
input.ledgerIndex = util::getLedgerIndex(jsonObject.at(JS(ledger_index)));
if (jsonObject.contains(JS(ledger_index))) {
auto const expectedLedgerIndex = util::getLedgerIndex(jsonObject.at(JS(ledger_index)));
if (expectedLedgerIndex.has_value())
input.ledgerIndex = *expectedLedgerIndex;
}
return input;
}

View File

@@ -23,10 +23,13 @@
#include <boost/json.hpp>
#include <boost/json/object.hpp>
#include <xrpl/beast/core/LexicalCast.h>
#include <algorithm>
#include <cctype>
#include <charconv>
#include <concepts>
#include <expected>
#include <stdexcept>
#include <string>
@@ -96,12 +99,11 @@ removeSecret(boost::json::object const& object)
*
* @tparam Type The type to cast to
* @param value The JSON value to cast
* @return Value casted to the requested type
* @throws logic_error if the underlying number is neither int64 nor uint64
* @return Value casted to the requested type or an error message
*/
template <std::integral Type>
Type
integralValueAs(boost::json::value const& value)
std::expected<Type, std::string>
tryIntegralValueAs(boost::json::value const& value)
{
if (value.is_uint64())
return static_cast<Type>(value.as_uint64());
@@ -109,29 +111,49 @@ integralValueAs(boost::json::value const& value)
if (value.is_int64())
return static_cast<Type>(value.as_int64());
throw std::logic_error("Value neither uint64 nor int64");
return std::unexpected("Value neither uint64 nor int64");
}
/**
* @brief Detects the type of number stored in value and casts it back to the requested Type.
* @note This conversion can possibly cause wrapping around or UB. Use with caution.
*
* @tparam Type The type to cast to
* @param value The JSON value to cast
* @return Value casted to the requested type
* @throws logic_error if the underlying number is neither int64 nor uint64
*/
template <std::integral Type>
Type
integralValueAs(boost::json::value const& value)
{
auto expectedResult = tryIntegralValueAs<Type>(value);
if (expectedResult.has_value())
return *expectedResult;
throw std::logic_error(std::move(expectedResult).error());
}
/**
* @brief Extracts ledger index from a JSON value which can be either a number or a string.
*
* @param value The JSON value to extract ledger index from
* @return An optional containing the ledger index if it is a number; std::nullopt otherwise
* @throws logic_error comes from integralValueAs if the underlying number is neither int64 nor uint64
* @throws std::invalid_argument or std::out_of_range if the string cannot be converted to a number
* @return The extracted ledger index or an error message
*/
[[nodiscard]] inline std::optional<uint32_t>
[[nodiscard]] inline std::expected<uint32_t, std::string>
getLedgerIndex(boost::json::value const& value)
{
std::optional<uint32_t> ledgerIndex;
if (not value.is_string()) {
ledgerIndex = util::integralValueAs<uint32_t>(value);
} else if (value.as_string() != "validated") {
ledgerIndex = std::stoi(value.as_string().c_str());
return tryIntegralValueAs<uint32_t>(value);
}
return ledgerIndex;
if (value.as_string() != "validated") {
uint32_t ledgerIndex{};
if (beast::lexicalCastChecked(ledgerIndex, value.as_string().c_str())) {
return ledgerIndex;
}
return std::unexpected("Invalid ledger index string");
}
return std::unexpected("'validated' ledger index is requested");
}
} // namespace util

View File

@@ -0,0 +1,426 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2025, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <boost/signals2/connection.hpp>
#include <boost/signals2/signal.hpp>
#include <boost/signals2/variadic_signal.hpp>
#include <atomic>
#include <concepts>
#include <type_traits>
namespace util {
template <typename T>
concept SomeAtomic = std::same_as<std::remove_cvref_t<T>, std::atomic<std::remove_cvref_t<typename T::value_type>>>;
/**
* @brief Concept defining types that can be observed for changes.
*
* A type is Observable if it satisfies all requirements for being stored
* and monitored in an ObservableValue container:
*
* - Must be equality comparable to detect changes
* - Must be copy constructible for capturing old values in guards
* - Must be move constructible for efficient value updates
*
* @note Copy assignment is intentionally not required since we use move semantics
* for value updates and only need copy construction for change detection.
*/
template <typename T>
concept Observable = std::equality_comparable<T> && std::copy_constructible<T> && std::move_constructible<T>;
namespace impl {
/**
* @brief Base class containing common ObservableValue functionality.
*
* This class contains all the observer management and notification logic
* that is shared between regular and atomic ObservableValue specializations.
*
* @tparam T The value type (for atomic specializations, this is the underlying type, not std::atomic<T>)
*/
template <Observable T>
class ObservableValueBase {
protected:
boost::signals2::signal<void(T const&)> onUpdate_;
public:
virtual ~ObservableValueBase() = default;
/**
* @brief Registers an observer callback for value changes.
* @param fn Callback function/lambda that accepts T const&
* @return Connection object for managing the subscription
*/
boost::signals2::connection
observe(std::invocable<T const&> auto&& fn)
{
return onUpdate_.connect(std::forward<decltype(fn)>(fn));
}
/**
* @brief Checks if there are any active observers.
* @return true if there are observers, false otherwise
*/
[[nodiscard]] bool
hasObservers() const
{
return not onUpdate_.empty();
}
/**
* @brief Forces notification of all observers with the current value.
*
* This method will notify all observers with the current value regardless
* of whether the value has changed since the last notification.
*/
virtual void
forceNotify() = 0;
protected:
/**
* @brief Notifies all observers with the given value.
* @param value The value to send to observers
*/
void
notifyObservers(T const& value)
{
onUpdate_(value);
}
};
} // namespace impl
// Forward declaration
template <typename T>
class ObservableValue;
/**
* @brief An observable value container that notifies observers when the value changes.
*
* ObservableValue wraps a value of type T and provides a mechanism to observe changes to that value.
* When the value is modified (and actually changes), all registered observers are notified.
*
* @tparam T The type of value to observe. Must satisfy the Observable concept.
*
* @par Thread Safety
* - Observer subscription/unsubscription (observe() and connection.disconnect()) are thread-safe
* - Value modification operations (set(), operator=) are NOT thread-safe and require external synchronization
* - Observer callbacks are invoked synchronously on the same thread that triggered the value change
* - If observers need to perform work on different threads, they must handle dispatch themselves
* (e.g., using an async execution context or message queue)
*
* @par Exception Handling
* - If an observer callback throws an exception, the exception will propagate to the caller
* - The value will still be updated even if observers throw exceptions
* - No guarantee is made about whether other observers will be called if one throws
* - It is the caller's responsibility to handle exceptions from observer callbacks
*/
template <Observable T>
requires(not SomeAtomic<T>)
class ObservableValue<T> : public impl::ObservableValueBase<T> {
T value_;
/**
* @brief RAII guard for deferred notification of value changes.
*
* ObservableGuard captures the current value when created and compares it
* with the final value when destroyed. If the values differ, observers
* are notified. This allows for multiple modifications to the value with
* only a single notification at the end.
*
* @note This class is returned by operator->() and should not be used directly.
*/
struct ObservableGuard {
T const oldValue; ///< Value captured at construction time
ObservableValue<T>& ref; ///< Reference to the observable value
/**
* @brief Constructs guard and captures current value.
* @param observable The ObservableValue to guard
*/
ObservableGuard(ObservableValue<T>& observable) : oldValue(observable), ref(observable)
{
}
/**
* @brief Destructor that triggers notification if value changed.
*
* Compares the captured value with the current value. If they differ,
* notifies all observers with the current value.
*/
~ObservableGuard()
{
if (oldValue != ref.value_)
ref.notifyObservers(ref.value_);
}
/**
* @brief Provides mutable access to the underlying value.
* @return Mutable reference to the wrapped value
*/
[[nodiscard]]
operator T&()
{
return ref.value_;
}
};
public:
/**
* @brief Constructs ObservableValue with initial value.
* @param value Initial value (must be convertible to T)
*/
ObservableValue(std::convertible_to<T> auto&& value) : value_{std::forward<decltype(value)>(value)}
{
}
/**
* @brief Constructs ObservableValue with default initial value.
*/
ObservableValue()
requires std::default_initializable<T>
: value_{}
{
}
ObservableValue(ObservableValue const&) = delete;
ObservableValue(ObservableValue&&) = default;
ObservableValue&
operator=(ObservableValue const&) = delete;
ObservableValue&
operator=(ObservableValue&&) = default;
/**
* @brief Assignment operator that updates value and notifies observers.
*
* Updates the stored value and notifies observers if the new value
* differs from the current value (using operator!=).
*
* @param val New value (must be convertible to T)
* @return Reference to this object for chaining
*
* @throws Any exception thrown by observer callbacks will propagate
*/
ObservableValue&
operator=(std::convertible_to<T> auto&& val)
{
set(val);
return *this;
}
/**
* @brief Provides deferred notification access to the value.
*
* Returns an ObservableGuard that allows modification of the value
* with notification deferred until the guard is destroyed.
*
* @return ObservableGuard for deferred notification
*/
[[nodiscard]] ObservableGuard
operator->()
{
return {*this};
}
/**
* @brief Implicit conversion to const reference of the value.
* @return Const reference to the stored value
*/
[[nodiscard]]
operator T const&() const
{
return value_;
}
/**
* @brief Explicitly gets the current value.
* @return Const reference to the stored value
*/
[[nodiscard]] T const&
get() const
{
return value_;
}
/**
* @brief Sets a new value and notifies observers if changed.
*
* Updates the stored value and notifies all observers if the new value
* differs from the current value (using operator!=). If the values are
* equal, no notification occurs.
*
* @param val New value (must be convertible to T)
*
* @throws Any exception thrown by observer callbacks will propagate
*
* @par Thread Safety
* - This method is NOT thread-safe and requires external synchronization for concurrent access
* - Observer callbacks are invoked synchronously on the calling thread
*/
void
set(std::convertible_to<T> auto&& val)
{
if (value_ != val) {
value_ = std::forward<decltype(val)>(val);
this->notifyObservers(value_);
}
}
/**
* @brief Forces notification of all observers with the current value.
*
* This method will notify all observers with the current value regardless
* of whether the value has changed since the last notification.
*/
void
forceNotify() override
{
this->notifyObservers(value_);
}
};
/**
* @brief Partial specialization of ObservableValue for atomic types.
*
* This specialization provides thread-safe observation of atomic values while
* maintaining atomic semantics. It avoids the issues of copying atomic values
* and handles race conditions properly.
*
* @tparam T The underlying type stored in the atomic
*
* @par Thread Safety
* - All operations are thread-safe
* - Observer notifications are atomic with respect to value changes
* - Multiple threads can safely modify and observe the atomic value
*
* @par Performance Considerations
* - Uses atomic compare-and-swap operations for updates
* - Minimizes atomic reads during guard operations
* - Observer notifications happen outside of atomic operations when possible
*/
template <Observable T>
class ObservableValue<std::atomic<T>> : public impl::ObservableValueBase<T> {
std::atomic<T> value_;
public:
/**
* @brief Constructs ObservableValue with initial atomic value.
* @param value Initial value (will be stored in the atomic)
*/
ObservableValue(std::convertible_to<T> auto&& value) : value_{std::forward<decltype(value)>(value)}
{
}
/**
* @brief Constructs ObservableValue with default initial value.
*/
ObservableValue()
requires std::default_initializable<T>
: value_{}
{
}
ObservableValue(ObservableValue const&) = delete;
ObservableValue(ObservableValue&&) = default;
ObservableValue&
operator=(ObservableValue const&) = delete;
ObservableValue&
operator=(ObservableValue&&) = default;
/**
* @brief Assignment operator that updates atomic value and notifies observers.
*
* Uses atomic compare-and-swap to update the value and notifies observers
* only if the value actually changed.
*
* @param val New value
* @return Reference to this object for chaining
*/
ObservableValue&
operator=(std::convertible_to<T> auto&& val)
{
set(std::forward<decltype(val)>(val));
return *this;
}
/**
* @brief Gets the current atomic value.
* @return Current value stored in the atomic
*/
[[nodiscard]] T
get() const
{
return value_.load();
}
/**
* @brief Implicit conversion to the current atomic value.
* @return Current value stored in the atomic
*/
[[nodiscard]]
operator T() const
{
return get();
}
/**
* @brief Sets a new atomic value and notifies observers if changed.
*
* Uses atomic compare-and-swap to update the value. Notifies all observers
* if the value actually changed.
*
* @param val New value
*/
void
set(std::convertible_to<T> auto&& val)
{
T newValue = std::forward<decltype(val)>(val);
T oldValue = value_.load();
// Use compare-and-swap to atomically update
while (!value_.compare_exchange_weak(oldValue, newValue)) {
// compare_exchange_weak updates oldValue with current value on failure
// Continue until we succeed
}
// Notify observers if we actually changed the value
// Note: oldValue now contains the actual previous value that was replaced
if (oldValue != newValue) {
this->notifyObservers(newValue);
}
}
/**
* @brief Forces notification of all observers with the current value.
*
* This method will notify all observers with the current atomic value
* regardless of whether the value has changed since the last notification.
*/
void
forceNotify() override
{
this->notifyObservers(value_.load());
}
};
} // namespace util

View File

@@ -19,6 +19,8 @@
#include "util/Repeat.hpp"
#include <boost/asio/post.hpp>
namespace util {
void
@@ -27,8 +29,11 @@ Repeat::stop()
if (control_->stopping)
return;
control_->stopping = true;
control_->timer.cancel();
boost::asio::post(control_->strand, [control = control_] {
control->stopping = true;
control->timer.cancel();
});
control_->semaphore.acquire();
}

View File

@@ -21,9 +21,11 @@
#include "util/Assert.hpp"
#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/post.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/strand.hpp>
#include <atomic>
#include <chrono>
@@ -41,10 +43,11 @@ namespace util {
class Repeat {
struct Control {
boost::asio::steady_timer timer;
boost::asio::strand<boost::asio::any_io_executor> strand;
std::atomic_bool stopping{true};
std::binary_semaphore semaphore{0};
Control(auto& ctx) : timer(ctx)
Control(auto& ctx) : timer(ctx), strand(boost::asio::make_strand(ctx))
{
}
};
@@ -98,15 +101,24 @@ private:
static void
startImpl(std::shared_ptr<Control> control, std::chrono::steady_clock::duration interval, Action&& action)
{
control->timer.expires_after(interval);
control->timer.async_wait([control, interval, action = std::forward<Action>(action)](auto const& ec) mutable {
if (ec or control->stopping) {
boost::asio::post(control->strand, [control, interval, action = std::forward<Action>(action)]() mutable {
if (control->stopping) {
control->semaphore.release();
return;
}
action();
startImpl(std::move(control), interval, std::forward<Action>(action));
control->timer.expires_after(interval);
control->timer.async_wait(
[control, interval, action = std::forward<Action>(action)](auto const& ec) mutable {
if (ec or control->stopping) {
control->semaphore.release();
return;
}
action();
startImpl(std::move(control), interval, std::forward<Action>(action));
}
);
});
}
};

View File

@@ -23,10 +23,13 @@
#include "util/config/ConfigDefinition.hpp"
#include "util/log/Logger.hpp"
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <csignal>
#include <functional>
#include <optional>
#include <mutex>
#include <thread>
#include <utility>
namespace util {
@@ -50,17 +53,11 @@ public:
}
static void
handleSignal(int signal)
handleSignal(int /* signal */)
{
ASSERT(installedHandler != nullptr, "SignalsHandler is not initialized");
installedHandler->stopHandler_(signal);
}
static void
handleSecondSignal(int signal)
{
ASSERT(installedHandler != nullptr, "SignalsHandler is not initialized");
installedHandler->secondSignalHandler_(signal);
installedHandler->signalReceived_ = true;
installedHandler->cv_.notify_one();
}
};
@@ -69,56 +66,109 @@ SignalsHandler* SignalsHandlerStatic::installedHandler = nullptr;
} // namespace impl
SignalsHandler::SignalsHandler(config::ClioConfigDefinition const& config, std::function<void()> forceExitHandler)
: gracefulPeriod_(0)
, context_(1)
, stopHandler_([this, forceExitHandler](int) mutable {
LOG(LogService::info()) << "Got stop signal. Stopping Clio. Graceful period is "
<< std::chrono::duration_cast<std::chrono::milliseconds>(gracefulPeriod_).count()
<< " milliseconds.";
setHandler(impl::SignalsHandlerStatic::handleSecondSignal);
timer_.emplace(context_.scheduleAfter(
gracefulPeriod_, [forceExitHandler = std::move(forceExitHandler)](auto&& stopToken, bool canceled) {
// TODO: Update this after https://github.com/XRPLF/clio/issues/1380
if (not stopToken.isStopRequested() and not canceled) {
LOG(LogService::warn()) << "Force exit at the end of graceful period.";
forceExitHandler();
}
}
));
stopSignal_();
})
, secondSignalHandler_([this, forceExitHandler = std::move(forceExitHandler)](int) {
LOG(LogService::warn()) << "Force exit on second signal.";
forceExitHandler();
cancelTimer();
setHandler();
})
: gracefulPeriod_(util::config::ClioConfigDefinition::toMilliseconds(config.get<float>("graceful_period")))
, forceExitHandler_(std::move(forceExitHandler))
{
impl::SignalsHandlerStatic::registerHandler(*this);
gracefulPeriod_ = util::config::ClioConfigDefinition::toMilliseconds(config.get<float>("graceful_period"));
workerThread_ = std::thread([this]() { runStateMachine(); });
setHandler(impl::SignalsHandlerStatic::handleSignal);
}
SignalsHandler::~SignalsHandler()
{
cancelTimer();
setHandler();
state_ = State::NormalExit;
cv_.notify_one();
if (workerThread_.joinable())
workerThread_.join();
impl::SignalsHandlerStatic::resetHandler(); // This is needed mostly for tests to reset static state
}
void
SignalsHandler::cancelTimer()
SignalsHandler::notifyGracefulShutdownComplete()
{
if (timer_.has_value())
timer_->abort();
if (state_ == State::GracefulShutdown) {
LOG(LogService::info()) << "Graceful shutdown completed successfully.";
state_ = State::NormalExit;
cv_.notify_one();
}
}
void
SignalsHandler::setHandler(void (*handler)(int))
{
for (int const signal : kHANDLED_SIGNALS) {
for (int const signal : kHANDLED_SIGNALS)
std::signal(signal, handler == nullptr ? SIG_DFL : handler);
}
void
SignalsHandler::runStateMachine()
{
while (state_ != State::NormalExit) {
auto currentState = state_.load();
switch (currentState) {
case State::WaitingForSignal: {
{
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait(lock, [this]() { return signalReceived_ or state_ == State::NormalExit; });
}
if (state_ == State::NormalExit)
return;
LOG(
LogService::info()
) << "Got stop signal. Stopping Clio. Graceful period is "
<< std::chrono::duration_cast<std::chrono::milliseconds>(gracefulPeriod_).count() << " milliseconds.";
state_ = State::GracefulShutdown;
signalReceived_ = false;
stopSignal_();
break;
}
case State::GracefulShutdown: {
bool waitResult = false;
{
std::unique_lock<std::mutex> lock(mutex_);
// Wait for either:
// 1. Graceful period to elapse (timeout)
// 2. Another signal (signalReceived_)
// 3. Graceful shutdown completion (state changes to NormalExit)
waitResult = cv_.wait_for(lock, gracefulPeriod_, [this]() {
return signalReceived_ or state_ == State::NormalExit;
});
}
if (state_ == State::NormalExit)
break;
if (signalReceived_) {
LOG(LogService::warn()) << "Force exit on second signal.";
state_ = State::ForceExit;
signalReceived_ = false;
} else if (not waitResult) {
LOG(LogService::warn()) << "Force exit at the end of graceful period.";
state_ = State::ForceExit;
}
break;
}
case State::ForceExit: {
forceExitHandler_();
state_ = State::NormalExit;
break;
}
case State::NormalExit:
return;
}
}
}

View File

@@ -19,22 +19,20 @@
#pragma once
#include "util/async/context/BasicExecutionContext.hpp"
#include "util/config/ConfigDefinition.hpp"
#include "util/log/Logger.hpp"
#include <boost/asio/executor_work_guard.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/signals2/signal.hpp>
#include <boost/signals2/variadic_signal.hpp>
#include <atomic>
#include <chrono>
#include <concepts>
#include <condition_variable>
#include <csignal>
#include <cstdlib>
#include <functional>
#include <optional>
#include <mutex>
#include <thread>
namespace util {
namespace impl {
@@ -48,13 +46,22 @@ class SignalsHandlerStatic;
* @note There could be only one instance of this class.
*/
class SignalsHandler {
/**
* @brief States of the signal handler state machine.
*/
enum class State { WaitingForSignal, GracefulShutdown, ForceExit, NormalExit };
std::chrono::steady_clock::duration gracefulPeriod_;
async::PoolExecutionContext context_;
std::optional<async::PoolExecutionContext::ScheduledOperation<void>> timer_;
std::function<void()> forceExitHandler_;
boost::signals2::signal<void()> stopSignal_;
std::function<void(int)> stopHandler_;
std::function<void(int)> secondSignalHandler_;
std::atomic<bool> signalReceived_{false};
std::atomic<State> state_{State::WaitingForSignal};
std::mutex mutex_;
std::condition_variable cv_;
std::thread workerThread_;
friend class impl::SignalsHandlerStatic;
@@ -101,15 +108,16 @@ public:
stopSignal_.connect(static_cast<int>(priority), std::forward<SomeCallback>(callback));
}
/**
* @brief Notify the signal handler that graceful shutdown has completed.
* This allows the handler to transition to NormalExit state.
*/
void
notifyGracefulShutdownComplete();
static constexpr auto kHANDLED_SIGNALS = {SIGINT, SIGTERM};
private:
/**
* @brief Cancel scheduled force exit if any.
*/
void
cancelTimer();
/**
* @brief Set signal handler for handled signals.
*
@@ -118,6 +126,12 @@ private:
static void
setHandler(void (*handler)(int) = nullptr);
/**
* @brief Run the state machine loop in a worker thread.
*/
void
runStateMachine();
static constexpr auto kDEFAULT_FORCE_EXIT_HANDLER = []() { std::exit(EXIT_FAILURE); };
};

View File

@@ -35,7 +35,6 @@
#include <boost/beast/http/status.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/verb.hpp>
#include <boost/beast/ssl/ssl_stream.hpp>
#include <openssl/err.h>
#include <openssl/tls1.h>

View File

@@ -78,8 +78,8 @@ private:
}
};
using SslTcpStreamData = SslStreamData<boost::beast::ssl_stream<boost::beast::tcp_stream>>;
using SslTcpStreamData = SslStreamData<boost::asio::ssl::stream<boost::beast::tcp_stream>>;
using SslWsStreamData =
SslStreamData<boost::beast::websocket::stream<boost::beast::ssl_stream<boost::beast::tcp_stream>>>;
SslStreamData<boost::beast::websocket::stream<boost::asio::ssl::stream<boost::beast::tcp_stream>>>;
} // namespace util::requests::impl

View File

@@ -124,6 +124,6 @@ public:
using PlainWsConnection = WsConnectionImpl<boost::beast::websocket::stream<boost::beast::tcp_stream>>;
using SslWsConnection =
WsConnectionImpl<boost::beast::websocket::stream<boost::beast::ssl_stream<boost::beast::tcp_stream>>>;
WsConnectionImpl<boost::beast::websocket::stream<boost::asio::ssl::stream<boost::beast::tcp_stream>>>;
} // namespace util::requests::impl

View File

@@ -61,7 +61,7 @@ using tcp = boost::asio::ip::tcp;
template <SomeServerHandler HandlerType>
class SslHttpSession : public impl::HttpBase<SslHttpSession, HandlerType>,
public std::enable_shared_from_this<SslHttpSession<HandlerType>> {
boost::beast::ssl_stream<boost::beast::tcp_stream> stream_;
boost::asio::ssl::stream<boost::beast::tcp_stream> stream_;
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory_;
std::uint32_t maxWsSendingQueueSize_;
@@ -113,7 +113,7 @@ public:
~SslHttpSession() override = default;
/** @return The SSL stream. */
boost::beast::ssl_stream<boost::beast::tcp_stream>&
boost::asio::ssl::stream<boost::beast::tcp_stream>&
stream()
{
return stream_;

View File

@@ -51,7 +51,7 @@ namespace web {
*/
template <SomeServerHandler HandlerType>
class SslWsSession : public impl::WsBase<SslWsSession, HandlerType> {
using StreamType = boost::beast::websocket::stream<boost::beast::ssl_stream<boost::beast::tcp_stream>>;
using StreamType = boost::beast::websocket::stream<boost::asio::ssl::stream<boost::beast::tcp_stream>>;
StreamType ws_;
public:
@@ -68,7 +68,7 @@ public:
* @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
*/
explicit SslWsSession(
boost::beast::ssl_stream<boost::beast::tcp_stream>&& stream,
boost::asio::ssl::stream<boost::beast::tcp_stream>&& stream,
std::string ip,
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,
@@ -107,7 +107,7 @@ template <SomeServerHandler HandlerType>
class SslWsUpgrader : public std::enable_shared_from_this<SslWsUpgrader<HandlerType>> {
using std::enable_shared_from_this<SslWsUpgrader<HandlerType>>::shared_from_this;
boost::beast::ssl_stream<boost::beast::tcp_stream> https_;
boost::asio::ssl::stream<boost::beast::tcp_stream> https_;
boost::optional<http::request_parser<http::string_body>> parser_;
boost::beast::flat_buffer buffer_;
std::string ip_;
@@ -133,7 +133,7 @@ public:
* @param maxWsSendingQueueSize The maximum size of the sending queue for websocket
*/
SslWsUpgrader(
boost::beast::ssl_stream<boost::beast::tcp_stream> stream,
boost::asio::ssl::stream<boost::beast::tcp_stream> stream,
std::string ip,
std::reference_wrapper<util::TagDecoratorFactory const> tagFactory,
std::reference_wrapper<dosguard::DOSGuardInterface> dosGuard,

View File

@@ -26,6 +26,7 @@
#include <boost/asio/spawn.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/asio/ssl/error.hpp>
#include <boost/asio/ssl/stream.hpp>
#include <boost/asio/ssl/stream_base.hpp>
#include <boost/asio/ssl/verify_context.hpp>
#include <boost/asio/ssl/verify_mode.hpp>
@@ -40,7 +41,6 @@
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/verb.hpp>
#include <boost/beast/http/write.hpp> // IWYU pragma: keep
#include <boost/beast/ssl/ssl_stream.hpp>
#include <boost/beast/version.hpp>
#include <openssl/err.h>
#include <openssl/tls1.h>
@@ -148,7 +148,7 @@ HttpsSyncClient::syncPost(std::string const& host, std::string const& port, std:
ctx.set_verify_mode(ssl::verify_none);
tcp::resolver resolver(ioc);
boost::beast::ssl_stream<boost::beast::tcp_stream> stream(ioc, ctx);
boost::asio::ssl::stream<boost::beast::tcp_stream> stream(ioc, ctx);
// We can't fix this so have to ignore
#pragma GCC diagnostic push

View File

@@ -1452,7 +1452,8 @@ createMptIssuanceObject(
std::optional<std::uint8_t> assetScale,
std::optional<std::uint64_t> maxAmount,
std::optional<std::uint64_t> lockedAmount,
std::optional<std::string_view> domainId
std::optional<std::string_view> domainId,
std::optional<std::uint32_t> mutableFlags
)
{
ripple::STObject mptIssuance(ripple::sfLedgerEntry);
@@ -1479,6 +1480,8 @@ createMptIssuanceObject(
}
if (domainId.has_value())
mptIssuance.setFieldH256(ripple::sfDomainID, ripple::uint256{*domainId});
if (mutableFlags.has_value())
mptIssuance.setFieldU32(ripple::sfMutableFlags, *mutableFlags);
return mptIssuance;
}
@@ -1789,7 +1792,7 @@ createVault(
vault[ripple::sfShareMPTID] = shareMPTID;
vault.setFieldNumber(ripple::sfAssetsTotal, ripple::STNumber{ripple::sfAssetsTotal, 300});
vault.setFieldNumber(ripple::sfAssetsAvailable, ripple::STNumber{ripple::sfAssetsAvailable, 300});
vault.setFieldNumber(ripple::sfLossUnrealized, ripple::STNumber{ripple::sfLossUnrealized, 0});
vault.setFieldNumber(ripple::sfLossUnrealized, ripple::STNumber{ripple::sfLossUnrealized, 1});
vault.setFieldU8(ripple::sfWithdrawalPolicy, 200);
vault.setFieldU32(ripple::sfFlags, 0);

View File

@@ -461,7 +461,8 @@ createMptIssuanceObject(
std::optional<std::uint8_t> assetScale = std::nullopt,
std::optional<std::uint64_t> maxAmount = std::nullopt,
std::optional<std::uint64_t> lockedAmount = std::nullopt,
std::optional<std::string_view> domainId = std::nullopt
std::optional<std::string_view> domainId = std::nullopt,
std::optional<std::uint32_t> mutableFlags = std::nullopt
);
[[nodiscard]] ripple::STObject

View File

@@ -80,7 +80,7 @@ public:
class WebServerSslSyncClient {
boost::asio::io_context ioc_;
std::optional<boost::beast::websocket::stream<boost::beast::ssl_stream<boost::asio::ip::tcp::socket>>> ws_;
std::optional<boost::beast::websocket::stream<boost::asio::ssl::stream<boost::asio::ip::tcp::socket>>> ws_;
public:
void

View File

@@ -169,6 +169,8 @@ target_sources(
util/BytesConverterTests.cpp
util/CoroutineTest.cpp
util/MoveTrackerTests.cpp
util/ObservableValueTest.cpp
util/ObservableValueAtomicTest.cpp
util/RandomTests.cpp
util/RepeatTests.cpp
util/ResponseExpirationCacheTests.cpp

View File

@@ -18,6 +18,7 @@
//==============================================================================
#include "util/JsonUtils.hpp"
#include "util/NameGenerator.hpp"
#include <boost/json/parse.hpp>
#include <boost/json/value.hpp>
@@ -26,7 +27,8 @@
#include <cstdint>
#include <limits>
#include <stdexcept>
#include <tuple>
#include <string>
#include <type_traits>
TEST(JsonUtils, RemoveSecrets)
{
@@ -90,28 +92,123 @@ TEST(JsonUtils, integralValueAs)
EXPECT_THROW(util::integralValueAs<int>(stringJson), std::logic_error);
}
TEST(JsonUtils, getLedgerIndex)
TEST(JsonUtils, tryIntegralValueAs)
{
auto const emptyJson = boost::json::value();
EXPECT_THROW(std::ignore = util::getLedgerIndex(emptyJson), std::logic_error);
auto const expectedResultUint64 = static_cast<uint64_t>(std::numeric_limits<int32_t>::max()) + 1u;
auto const uint64Json = boost::json::value(expectedResultUint64);
auto const boolJson = boost::json::value(true);
EXPECT_THROW(std::ignore = util::getLedgerIndex(emptyJson), std::logic_error);
auto const expectedResultInt64 = static_cast<int64_t>(std::numeric_limits<int32_t>::max()) + 1u;
auto const int64Json = boost::json::value(expectedResultInt64);
auto const numberJson = boost::json::value(12345);
auto ledgerIndex = util::getLedgerIndex(numberJson);
EXPECT_TRUE(ledgerIndex.has_value());
EXPECT_EQ(ledgerIndex.value(), 12345u);
auto checkHasValue = [&](boost::json::value const& jv, auto const& expectedValue) {
using T = std::remove_cvref_t<decltype(expectedValue)>;
auto const res = util::tryIntegralValueAs<T>(jv);
ASSERT_TRUE(res.has_value());
EXPECT_EQ(res.value(), expectedValue);
};
auto const validStringJson = boost::json::value("12345");
ledgerIndex = util::getLedgerIndex(validStringJson);
EXPECT_TRUE(ledgerIndex.has_value());
EXPECT_EQ(ledgerIndex.value(), 12345u);
auto checkError = [&](boost::json::value const& jv) {
auto res = util::tryIntegralValueAs<int>(jv);
EXPECT_FALSE(res.has_value());
EXPECT_EQ(res.error(), "Value neither uint64 nor int64");
};
auto const invalidStringJson = boost::json::value("invalid123");
EXPECT_THROW(std::ignore = util::getLedgerIndex(invalidStringJson), std::invalid_argument);
// checks for uint64Json
checkHasValue(uint64Json, std::numeric_limits<int32_t>::min());
checkHasValue(uint64Json, static_cast<uint32_t>(expectedResultUint64));
checkHasValue(uint64Json, static_cast<int64_t>(expectedResultUint64));
checkHasValue(uint64Json, expectedResultUint64);
auto const validatedJson = boost::json::value("validated");
ledgerIndex = util::getLedgerIndex(validatedJson);
EXPECT_FALSE(ledgerIndex.has_value());
// checks for int64Json
checkHasValue(int64Json, std::numeric_limits<int32_t>::min());
checkHasValue(int64Json, static_cast<uint32_t>(expectedResultInt64));
checkHasValue(int64Json, expectedResultInt64);
checkHasValue(int64Json, static_cast<uint64_t>(expectedResultInt64));
// non-integral inputs
checkError(boost::json::value());
checkError(boost::json::value(false));
checkError(boost::json::value(3.14));
checkError(boost::json::value("not a number"));
}
struct GetLedgerIndexParameterTestBundle {
std::string testName;
boost::json::value jv;
std::expected<uint32_t, std::string> expectedResult;
};
// parameterized test cases for parameters check
struct GetLedgerIndexParameterTest : ::testing::TestWithParam<GetLedgerIndexParameterTestBundle> {};
INSTANTIATE_TEST_CASE_P(
JsonUtils,
GetLedgerIndexParameterTest,
testing::Values(
GetLedgerIndexParameterTestBundle{
.testName = "EmptyValue",
.jv = boost::json::value(),
.expectedResult = std::unexpected{"Value neither uint64 nor int64"}
},
GetLedgerIndexParameterTestBundle{
.testName = "BoolValue",
.jv = boost::json::value(false),
.expectedResult = std::unexpected{"Value neither uint64 nor int64"}
},
GetLedgerIndexParameterTestBundle{
.testName = "NumberValue",
.jv = boost::json::value(123),
.expectedResult = 123u
},
GetLedgerIndexParameterTestBundle{
.testName = "StringNumberValue",
.jv = boost::json::value("123"),
.expectedResult = 123u
},
GetLedgerIndexParameterTestBundle{
.testName = "StringNumberWithPlusSignValue",
.jv = boost::json::value("+123"),
.expectedResult = 123u
},
GetLedgerIndexParameterTestBundle{
.testName = "StringEmptyValue",
.jv = boost::json::value(""),
.expectedResult = std::unexpected{"Invalid ledger index string"}
},
GetLedgerIndexParameterTestBundle{
.testName = "StringWithLeadingCharsValue",
.jv = boost::json::value("123invalid"),
.expectedResult = std::unexpected{"Invalid ledger index string"}
},
GetLedgerIndexParameterTestBundle{
.testName = "StringWithTrailingCharsValue",
.jv = boost::json::value("invalid123"),
.expectedResult = std::unexpected{"Invalid ledger index string"}
},
GetLedgerIndexParameterTestBundle{
.testName = "StringWithLeadingAndTrailingCharsValue",
.jv = boost::json::value("123invalid123"),
.expectedResult = std::unexpected{"Invalid ledger index string"}
},
GetLedgerIndexParameterTestBundle{
.testName = "ValidatedStringValue",
.jv = boost::json::value("validated"),
.expectedResult = std::unexpected{"'validated' ledger index is requested"}
}
),
tests::util::kNAME_GENERATOR
);
TEST_P(GetLedgerIndexParameterTest, getLedgerIndexParams)
{
auto const& testBundle = GetParam();
auto const ledgerIndex = util::getLedgerIndex(testBundle.jv);
if (testBundle.expectedResult.has_value()) {
EXPECT_TRUE(ledgerIndex.has_value());
EXPECT_EQ(ledgerIndex.value(), testBundle.expectedResult.value());
} else {
EXPECT_FALSE(ledgerIndex.has_value());
EXPECT_EQ(ledgerIndex.error(), testBundle.expectedResult.error());
}
}

View File

@@ -40,6 +40,7 @@ struct StopperTest : virtual public ::testing::Test {
protected:
// Order here is important, stopper_ should die before mockCallback_, otherwise UB
testing::StrictMock<testing::MockFunction<void(boost::asio::yield_context)>> mockCallback_;
testing::StrictMock<testing::MockFunction<void()>> mockCompleteCallback_;
Stopper stopper_;
};
@@ -60,6 +61,22 @@ TEST_F(StopperTest, stopCalledMultipleTimes)
stopper_.stop();
}
TEST_F(StopperTest, stopCallsCompletionCallback)
{
stopper_.setOnStop(mockCallback_.AsStdFunction());
stopper_.setOnComplete(mockCompleteCallback_.AsStdFunction());
EXPECT_CALL(mockCallback_, Call);
EXPECT_CALL(mockCompleteCallback_, Call);
stopper_.stop();
}
TEST_F(StopperTest, stopWithoutCompletionCallback)
{
stopper_.setOnStop(mockCallback_.AsStdFunction());
EXPECT_CALL(mockCallback_, Call);
stopper_.stop();
}
struct StopperMakeCallbackTest : util::prometheus::WithPrometheus, SyncAsioContextTest {
struct ServerMock : web::ServerTag {
MOCK_METHOD(void, stop, (boost::asio::yield_context), ());

View File

@@ -19,6 +19,7 @@
#include "app/WebHandlers.hpp"
#include "rpc/Errors.hpp"
#include "rpc/WorkQueue.hpp"
#include "util/AsioContextTestFixture.hpp"
#include "util/MockLedgerCache.hpp"
#include "util/MockPrometheus.hpp"
@@ -122,7 +123,9 @@ struct MetricsHandlerTests : util::prometheus::WithPrometheus, SyncAsioContextTe
std::make_shared<testing::StrictMock<AdminVerificationStrategyMock>>()
};
MetricsHandler metricsHandler{adminVerifier};
rpc::WorkQueue workQueue{1};
MetricsHandler metricsHandler{adminVerifier, workQueue};
web::ng::Request request{http::request<http::string_body>{http::verb::get, "/metrics", 11}};
};

View File

@@ -62,6 +62,7 @@ constexpr auto kISSUANCE_INDEX2 = "B6DBAFC99223B42257915A63DFC6B0C032D4070F9A574
constexpr uint64_t kISSUANCE1_MAX_AMOUNT = 10000;
constexpr uint64_t kISSUANCE1_OUTSTANDING_AMOUNT = 5000;
constexpr uint8_t kISSUANCE1_ASSET_SCALE = 2;
constexpr uint16_t kISSUANCE1_TRANSFER_FEE = 10;
// unique values for issuance2
constexpr uint64_t kISSUANCE2_MAX_AMOUNT = 20000;
@@ -75,6 +76,7 @@ constexpr auto kISSUANCE2_DOMAIN_ID_HEX = "E6DBAFC99223B42257915A63DFC6B0C032D40
// define expected JSON for mpt issuances
auto const kISSUANCE_OUT1 = fmt::format(
R"JSON({{
"mpt_issuance_id": "{}",
"issuer": "{}",
"sequence": 1,
"maximum_amount": {},
@@ -85,6 +87,7 @@ auto const kISSUANCE_OUT1 = fmt::format(
"mpt_require_auth": true,
"mpt_can_transfer": true
}})JSON",
kISSUANCE_INDEX1,
kACCOUNT,
kISSUANCE1_MAX_AMOUNT,
kISSUANCE1_OUTSTANDING_AMOUNT,
@@ -93,6 +96,7 @@ auto const kISSUANCE_OUT1 = fmt::format(
auto const kISSUANCE_OUT2 = fmt::format(
R"JSON({{
"mpt_issuance_id": "{}",
"issuer": "{}",
"sequence": 2,
"maximum_amount": {},
@@ -105,6 +109,7 @@ auto const kISSUANCE_OUT2 = fmt::format(
"mpt_locked": true,
"mpt_can_clawback": true
}})JSON",
kISSUANCE_INDEX2,
kACCOUNT,
kISSUANCE2_MAX_AMOUNT,
kISSUANCE2_OUTSTANDING_AMOUNT,
@@ -293,7 +298,7 @@ TEST_F(RPCAccountMPTokenIssuancesHandlerTest, NonExistLedgerViaLedgerIntIndex)
// ledger not found via hash (seq > max)
TEST_F(RPCAccountMPTokenIssuancesHandlerTest, LedgerSeqOutOfRangeByHash)
{
auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 31);
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 31);
EXPECT_CALL(*backend_, fetchLedgerByHash(ripple::uint256{kLEDGER_HASH}, _)).WillOnce(Return(ledgerHeader));
auto const input = json::parse(
fmt::format(
@@ -341,7 +346,7 @@ TEST_F(RPCAccountMPTokenIssuancesHandlerTest, LedgerSeqOutOfRangeByIndex)
// account not exist
TEST_F(RPCAccountMPTokenIssuancesHandlerTest, NonExistAccount)
{
auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
EXPECT_CALL(*backend_, fetchLedgerByHash(ripple::uint256{kLEDGER_HASH}, _)).WillOnce(Return(ledgerHeader));
// fetch account object return empty
EXPECT_CALL(*backend_, doFetchLedgerObject).WillOnce(Return(std::optional<Blob>{}));
@@ -369,13 +374,13 @@ TEST_F(RPCAccountMPTokenIssuancesHandlerTest, NonExistAccount)
// fetch mptoken issuances via account successfully
TEST_F(RPCAccountMPTokenIssuancesHandlerTest, DefaultParameters)
{
auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
// return non-empty account
auto account = getAccountIdWithString(kACCOUNT);
auto accountKk = ripple::keylet::account(account).key;
auto owneDirKk = ripple::keylet::ownerDir(account).key;
auto const account = getAccountIdWithString(kACCOUNT);
auto const accountKk = ripple::keylet::account(account).key;
auto const owneDirKk = ripple::keylet::ownerDir(account).key;
ON_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
// return two mptoken issuance objects
@@ -385,33 +390,36 @@ TEST_F(RPCAccountMPTokenIssuancesHandlerTest, DefaultParameters)
ON_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _)).WillByDefault(Return(ownerDir.getSerializer().peekData()));
// mocking mptoken issuance ledger objects
std::vector<Blob> bbs;
auto const issuance1 = createMptIssuanceObject(
kACCOUNT,
1,
std::nullopt,
ripple::lsfMPTCanTrade | ripple::lsfMPTRequireAuth | ripple::lsfMPTCanTransfer | ripple::lsfMPTCanEscrow,
kISSUANCE1_OUTSTANDING_AMOUNT,
std::nullopt,
kISSUANCE1_ASSET_SCALE,
kISSUANCE1_MAX_AMOUNT
);
auto const bbs = std::vector<Blob>{
createMptIssuanceObject(
kACCOUNT,
1,
std::nullopt,
ripple::lsfMPTCanTrade | ripple::lsfMPTRequireAuth | ripple::lsfMPTCanTransfer | ripple::lsfMPTCanEscrow,
kISSUANCE1_OUTSTANDING_AMOUNT,
std::nullopt,
kISSUANCE1_ASSET_SCALE,
kISSUANCE1_MAX_AMOUNT
)
.getSerializer()
.peekData(),
auto const issuance2 = createMptIssuanceObject(
kACCOUNT,
2,
kISSUANCE2_METADATA,
ripple::lsfMPTLocked | ripple::lsfMPTCanLock | ripple::lsfMPTCanClawback,
kISSUANCE2_OUTSTANDING_AMOUNT,
kISSUANCE2_TRANSFER_FEE,
std::nullopt,
kISSUANCE2_MAX_AMOUNT,
kISSUANCE2_LOCKED_AMOUNT,
kISSUANCE2_DOMAIN_ID_HEX
);
createMptIssuanceObject(
kACCOUNT,
2,
kISSUANCE2_METADATA,
ripple::lsfMPTLocked | ripple::lsfMPTCanLock | ripple::lsfMPTCanClawback,
kISSUANCE2_OUTSTANDING_AMOUNT,
kISSUANCE2_TRANSFER_FEE,
std::nullopt,
kISSUANCE2_MAX_AMOUNT,
kISSUANCE2_LOCKED_AMOUNT,
kISSUANCE2_DOMAIN_ID_HEX
)
.getSerializer()
.peekData()
};
bbs.push_back(issuance1.getSerializer().peekData());
bbs.push_back(issuance2.getSerializer().peekData());
EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs));
runSpawn([this](auto yield) {
@@ -434,7 +442,7 @@ TEST_F(RPCAccountMPTokenIssuancesHandlerTest, DefaultParameters)
kISSUANCE_OUT2
);
auto const input = json::parse(fmt::format(R"JSON({{"account": "{}"}})JSON", kACCOUNT));
auto handler = AnyHandler{AccountMPTokenIssuancesHandler{this->backend_}};
auto const handler = AnyHandler{AccountMPTokenIssuancesHandler{this->backend_}};
auto const output = handler.process(input, Context{yield});
ASSERT_TRUE(output);
@@ -445,22 +453,23 @@ TEST_F(RPCAccountMPTokenIssuancesHandlerTest, DefaultParameters)
TEST_F(RPCAccountMPTokenIssuancesHandlerTest, UseLimit)
{
constexpr int kLIMIT = 20;
auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
ON_CALL(*backend_, fetchLedgerBySequence).WillByDefault(Return(ledgerHeader));
auto account = getAccountIdWithString(kACCOUNT);
auto accountKk = ripple::keylet::account(account).key;
auto owneDirKk = ripple::keylet::ownerDir(account).key;
auto const account = getAccountIdWithString(kACCOUNT);
auto const accountKk = ripple::keylet::account(account).key;
auto const owneDirKk = ripple::keylet::ownerDir(account).key;
ON_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
std::vector<ripple::uint256> indexes;
std::vector<Blob> bbs;
for (int i = 0; i < 50; ++i) {
indexes.emplace_back(kISSUANCE_INDEX1);
auto const issuance = createMptIssuanceObject(kACCOUNT, i);
bbs.push_back(issuance.getSerializer().peekData());
}
auto const indexes = std::vector<ripple::uint256>(50, ripple::uint256{kISSUANCE_INDEX1});
auto const bbs = [&]() {
std::vector<Blob> v;
v.reserve(50);
for (int i = 0; i < 50; ++i) {
v.push_back(createMptIssuanceObject(kACCOUNT, i).getSerializer().peekData());
}
return v;
}();
ripple::STObject ownerDir = createOwnerDirLedgerObject(indexes, kISSUANCE_INDEX1);
ownerDir.setFieldU64(ripple::sfIndexNext, 99);
@@ -482,7 +491,7 @@ TEST_F(RPCAccountMPTokenIssuancesHandlerTest, UseLimit)
)
);
auto handler = AnyHandler{AccountMPTokenIssuancesHandler{this->backend_}};
auto const handler = AnyHandler{AccountMPTokenIssuancesHandler{this->backend_}};
auto const output = handler.process(input, Context{yield});
ASSERT_TRUE(output);
@@ -531,27 +540,25 @@ TEST_F(RPCAccountMPTokenIssuancesHandlerTest, MarkerOutput)
{
constexpr auto kNEXT_PAGE = 99;
constexpr auto kLIMIT = 15;
auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
auto account = getAccountIdWithString(kACCOUNT);
auto accountKk = ripple::keylet::account(account).key;
auto ownerDirKk = ripple::keylet::ownerDir(account).key;
auto ownerDir2Kk = ripple::keylet::page(ripple::keylet::ownerDir(account), kNEXT_PAGE).key;
auto const account = getAccountIdWithString(kACCOUNT);
auto const accountKk = ripple::keylet::account(account).key;
auto const ownerDirKk = ripple::keylet::ownerDir(account).key;
auto const ownerDir2Kk = ripple::keylet::page(ripple::keylet::ownerDir(account), kNEXT_PAGE).key;
ON_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
EXPECT_CALL(*backend_, doFetchLedgerObject).Times(3);
std::vector<ripple::uint256> indexes;
indexes.reserve(10);
for (int i = 0; i < 10; ++i) {
indexes.emplace_back(kISSUANCE_INDEX1);
}
std::vector<Blob> bbs;
bbs.reserve(kLIMIT);
for (int i = 0; i < kLIMIT; ++i) {
bbs.push_back(createMptIssuanceObject(kACCOUNT, i).getSerializer().peekData());
}
auto const indexes = std::vector<ripple::uint256>(10, ripple::uint256{kISSUANCE_INDEX1});
auto const bbs = [&]() {
std::vector<Blob> v;
v.reserve(kLIMIT);
for (int i = 0; i < kLIMIT; ++i) {
v.push_back(createMptIssuanceObject(kACCOUNT, i).getSerializer().peekData());
}
return v;
}();
EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs));
// mock the first directory page
@@ -577,7 +584,7 @@ TEST_F(RPCAccountMPTokenIssuancesHandlerTest, MarkerOutput)
kLIMIT
)
);
auto handler = AnyHandler{AccountMPTokenIssuancesHandler{this->backend_}};
auto const handler = AnyHandler{AccountMPTokenIssuancesHandler{this->backend_}};
auto const output = handler.process(input, Context{yield});
ASSERT_TRUE(output);
auto const& resultJson = (*output.result).as_object();
@@ -594,21 +601,24 @@ TEST_F(RPCAccountMPTokenIssuancesHandlerTest, MarkerInput)
constexpr auto kNEXT_PAGE = 99;
constexpr auto kLIMIT = 15;
auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
auto account = getAccountIdWithString(kACCOUNT);
auto accountKk = ripple::keylet::account(account).key;
auto ownerDirKk = ripple::keylet::page(ripple::keylet::ownerDir(account), kNEXT_PAGE).key;
auto const account = getAccountIdWithString(kACCOUNT);
auto const accountKk = ripple::keylet::account(account).key;
auto const ownerDirKk = ripple::keylet::page(ripple::keylet::ownerDir(account), kNEXT_PAGE).key;
ON_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
EXPECT_CALL(*backend_, doFetchLedgerObject).Times(3);
std::vector<Blob> bbs;
std::vector<ripple::uint256> indexes;
for (int i = 0; i < kLIMIT; ++i) {
indexes.emplace_back(kISSUANCE_INDEX1);
bbs.push_back(createMptIssuanceObject(kACCOUNT, i).getSerializer().peekData());
}
auto const indexes = std::vector<ripple::uint256>(kLIMIT, ripple::uint256{kISSUANCE_INDEX1});
auto const bbs = [&]() {
std::vector<Blob> v;
v.reserve(kLIMIT);
for (int i = 0; i < kLIMIT; ++i) {
v.push_back(createMptIssuanceObject(kACCOUNT, i).getSerializer().peekData());
}
return v;
}();
ripple::STObject ownerDir = createOwnerDirLedgerObject(indexes, kISSUANCE_INDEX1);
ownerDir.setFieldU64(ripple::sfIndexNext, 0);
@@ -631,7 +641,7 @@ TEST_F(RPCAccountMPTokenIssuancesHandlerTest, MarkerInput)
kNEXT_PAGE
)
);
auto handler = AnyHandler{AccountMPTokenIssuancesHandler{this->backend_}};
auto const handler = AnyHandler{AccountMPTokenIssuancesHandler{this->backend_}};
auto const output = handler.process(input, Context{yield});
ASSERT_TRUE(output);
@@ -643,47 +653,48 @@ TEST_F(RPCAccountMPTokenIssuancesHandlerTest, MarkerInput)
TEST_F(RPCAccountMPTokenIssuancesHandlerTest, LimitLessThanMin)
{
auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
auto account = getAccountIdWithString(kACCOUNT);
auto accountKk = ripple::keylet::account(account).key;
auto owneDirKk = ripple::keylet::ownerDir(account).key;
ON_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
auto const account = getAccountIdWithString(kACCOUNT);
auto const accountKk = ripple::keylet::account(account).key;
auto const owneDirKk = ripple::keylet::ownerDir(account).key;
EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillOnce(Return(Blob{'f', 'a', 'k', 'e'}));
ripple::STObject const ownerDir = createOwnerDirLedgerObject(
{ripple::uint256{kISSUANCE_INDEX1}, ripple::uint256{kISSUANCE_INDEX2}}, kISSUANCE_INDEX1
);
ON_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _)).WillByDefault(Return(ownerDir.getSerializer().peekData()));
EXPECT_CALL(*backend_, doFetchLedgerObject).Times(2);
EXPECT_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _)).WillOnce(Return(ownerDir.getSerializer().peekData()));
std::vector<Blob> bbs;
auto const issuance1 = createMptIssuanceObject(
kACCOUNT,
1,
std::nullopt,
ripple::lsfMPTCanTrade | ripple::lsfMPTRequireAuth | ripple::lsfMPTCanTransfer | ripple::lsfMPTCanEscrow,
kISSUANCE1_OUTSTANDING_AMOUNT,
std::nullopt,
kISSUANCE1_ASSET_SCALE,
kISSUANCE1_MAX_AMOUNT
);
auto const bbs = std::vector<Blob>{
createMptIssuanceObject(
kACCOUNT,
1,
std::nullopt,
ripple::lsfMPTCanTrade | ripple::lsfMPTRequireAuth | ripple::lsfMPTCanTransfer | ripple::lsfMPTCanEscrow,
kISSUANCE1_OUTSTANDING_AMOUNT,
std::nullopt,
kISSUANCE1_ASSET_SCALE,
kISSUANCE1_MAX_AMOUNT
)
.getSerializer()
.peekData(),
auto const issuance2 = createMptIssuanceObject(
kACCOUNT,
2,
kISSUANCE2_METADATA,
ripple::lsfMPTLocked | ripple::lsfMPTCanLock | ripple::lsfMPTCanClawback,
kISSUANCE2_OUTSTANDING_AMOUNT,
kISSUANCE2_TRANSFER_FEE,
std::nullopt,
kISSUANCE2_MAX_AMOUNT,
kISSUANCE2_LOCKED_AMOUNT,
kISSUANCE2_DOMAIN_ID_HEX
);
bbs.push_back(issuance1.getSerializer().peekData());
bbs.push_back(issuance2.getSerializer().peekData());
createMptIssuanceObject(
kACCOUNT,
2,
kISSUANCE2_METADATA,
ripple::lsfMPTLocked | ripple::lsfMPTCanLock | ripple::lsfMPTCanClawback,
kISSUANCE2_OUTSTANDING_AMOUNT,
kISSUANCE2_TRANSFER_FEE,
std::nullopt,
kISSUANCE2_MAX_AMOUNT,
kISSUANCE2_LOCKED_AMOUNT,
kISSUANCE2_DOMAIN_ID_HEX
)
.getSerializer()
.peekData()
};
EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs));
@@ -718,7 +729,7 @@ TEST_F(RPCAccountMPTokenIssuancesHandlerTest, LimitLessThanMin)
kISSUANCE_OUT2
);
auto handler = AnyHandler{AccountMPTokenIssuancesHandler{this->backend_}};
auto const handler = AnyHandler{AccountMPTokenIssuancesHandler{this->backend_}};
auto const output = handler.process(input, Context{yield});
ASSERT_TRUE(output);
EXPECT_EQ(json::parse(correctOutput), *output.result);
@@ -727,47 +738,48 @@ TEST_F(RPCAccountMPTokenIssuancesHandlerTest, LimitLessThanMin)
TEST_F(RPCAccountMPTokenIssuancesHandlerTest, LimitMoreThanMax)
{
auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
auto account = getAccountIdWithString(kACCOUNT);
auto accountKk = ripple::keylet::account(account).key;
auto owneDirKk = ripple::keylet::ownerDir(account).key;
ON_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
auto const account = getAccountIdWithString(kACCOUNT);
auto const accountKk = ripple::keylet::account(account).key;
auto const owneDirKk = ripple::keylet::ownerDir(account).key;
EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillOnce(Return(Blob{'f', 'a', 'k', 'e'}));
ripple::STObject const ownerDir = createOwnerDirLedgerObject(
{ripple::uint256{kISSUANCE_INDEX1}, ripple::uint256{kISSUANCE_INDEX2}}, kISSUANCE_INDEX1
);
ON_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _)).WillByDefault(Return(ownerDir.getSerializer().peekData()));
EXPECT_CALL(*backend_, doFetchLedgerObject).Times(2);
EXPECT_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _)).WillOnce(Return(ownerDir.getSerializer().peekData()));
std::vector<Blob> bbs;
auto const issuance1 = createMptIssuanceObject(
kACCOUNT,
1,
std::nullopt,
ripple::lsfMPTCanTrade | ripple::lsfMPTRequireAuth | ripple::lsfMPTCanTransfer | ripple::lsfMPTCanEscrow,
kISSUANCE1_OUTSTANDING_AMOUNT,
std::nullopt,
kISSUANCE1_ASSET_SCALE,
kISSUANCE1_MAX_AMOUNT
);
auto const bbs = std::vector<Blob>{
createMptIssuanceObject(
kACCOUNT,
1,
std::nullopt,
ripple::lsfMPTCanTrade | ripple::lsfMPTRequireAuth | ripple::lsfMPTCanTransfer | ripple::lsfMPTCanEscrow,
kISSUANCE1_OUTSTANDING_AMOUNT,
std::nullopt,
kISSUANCE1_ASSET_SCALE,
kISSUANCE1_MAX_AMOUNT
)
.getSerializer()
.peekData(),
auto const issuance2 = createMptIssuanceObject(
kACCOUNT,
2,
kISSUANCE2_METADATA,
ripple::lsfMPTLocked | ripple::lsfMPTCanLock | ripple::lsfMPTCanClawback,
kISSUANCE2_OUTSTANDING_AMOUNT,
kISSUANCE2_TRANSFER_FEE,
std::nullopt,
kISSUANCE2_MAX_AMOUNT,
kISSUANCE2_LOCKED_AMOUNT,
kISSUANCE2_DOMAIN_ID_HEX
);
bbs.push_back(issuance1.getSerializer().peekData());
bbs.push_back(issuance2.getSerializer().peekData());
createMptIssuanceObject(
kACCOUNT,
2,
kISSUANCE2_METADATA,
ripple::lsfMPTLocked | ripple::lsfMPTCanLock | ripple::lsfMPTCanClawback,
kISSUANCE2_OUTSTANDING_AMOUNT,
kISSUANCE2_TRANSFER_FEE,
std::nullopt,
kISSUANCE2_MAX_AMOUNT,
kISSUANCE2_LOCKED_AMOUNT,
kISSUANCE2_DOMAIN_ID_HEX
)
.getSerializer()
.peekData()
};
EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs));
@@ -802,7 +814,7 @@ TEST_F(RPCAccountMPTokenIssuancesHandlerTest, LimitMoreThanMax)
kISSUANCE_OUT2
);
auto handler = AnyHandler{AccountMPTokenIssuancesHandler{this->backend_}};
auto const handler = AnyHandler{AccountMPTokenIssuancesHandler{this->backend_}};
auto const output = handler.process(input, Context{yield});
ASSERT_TRUE(output);
EXPECT_EQ(json::parse(correctOutput), *output.result);
@@ -811,17 +823,16 @@ TEST_F(RPCAccountMPTokenIssuancesHandlerTest, LimitMoreThanMax)
TEST_F(RPCAccountMPTokenIssuancesHandlerTest, EmptyResult)
{
auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
auto account = getAccountIdWithString(kACCOUNT);
auto accountKk = ripple::keylet::account(account).key;
auto owneDirKk = ripple::keylet::ownerDir(account).key;
ON_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
auto const account = getAccountIdWithString(kACCOUNT);
auto const accountKk = ripple::keylet::account(account).key;
auto const owneDirKk = ripple::keylet::ownerDir(account).key;
EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillOnce(Return(Blob{'f', 'a', 'k', 'e'}));
ripple::STObject const ownerDir = createOwnerDirLedgerObject({}, kISSUANCE_INDEX1);
ON_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _)).WillByDefault(Return(ownerDir.getSerializer().peekData()));
EXPECT_CALL(*backend_, doFetchLedgerObject).Times(2);
EXPECT_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _)).WillOnce(Return(ownerDir.getSerializer().peekData()));
runSpawn([this](auto yield) {
auto const input = json::parse(
@@ -832,9 +843,292 @@ TEST_F(RPCAccountMPTokenIssuancesHandlerTest, EmptyResult)
kACCOUNT
)
);
auto handler = AnyHandler{AccountMPTokenIssuancesHandler{this->backend_}};
auto const handler = AnyHandler{AccountMPTokenIssuancesHandler{this->backend_}};
auto const output = handler.process(input, Context{yield});
ASSERT_TRUE(output);
EXPECT_EQ((*output.result).as_object().at("mpt_issuances").as_array().size(), 0);
});
}
TEST_F(RPCAccountMPTokenIssuancesHandlerTest, MutableFlags)
{
uint32_t const mutableFlags1 = ripple::lsmfMPTCanMutateCanLock | ripple::lsmfMPTCanMutateRequireAuth |
ripple::lsmfMPTCanMutateCanEscrow | ripple::lsmfMPTCanMutateCanTrade;
uint32_t const mutableFlags2 = ripple::lsmfMPTCanMutateCanTransfer | ripple::lsmfMPTCanMutateCanClawback |
ripple::lsmfMPTCanMutateMetadata | ripple::lsmfMPTCanMutateTransferFee;
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
auto const account = getAccountIdWithString(kACCOUNT);
auto const accountKk = ripple::keylet::account(account).key;
auto const owneDirKk = ripple::keylet::ownerDir(account).key;
EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillOnce(Return(Blob{'f', 'a', 'k', 'e'}));
ripple::STObject const ownerDir = createOwnerDirLedgerObject(
{ripple::uint256{kISSUANCE_INDEX1}, ripple::uint256{kISSUANCE_INDEX2}}, kISSUANCE_INDEX1
);
EXPECT_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _)).WillOnce(Return(ownerDir.getSerializer().peekData()));
auto const bbs = std::vector<Blob>{
createMptIssuanceObject(
kACCOUNT,
3,
std::nullopt,
ripple::lsfMPTCanTransfer,
kISSUANCE1_OUTSTANDING_AMOUNT,
kISSUANCE1_TRANSFER_FEE,
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
mutableFlags1
)
.getSerializer()
.peekData(),
createMptIssuanceObject(
kACCOUNT,
5,
kISSUANCE2_METADATA,
ripple::lsfMPTCanTransfer,
kISSUANCE2_OUTSTANDING_AMOUNT,
kISSUANCE2_TRANSFER_FEE,
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
mutableFlags2
)
.getSerializer()
.peekData()
};
EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs));
runSpawn([this](auto yield) {
auto const input = json::parse(
fmt::format(
R"JSON({{
"account": "{}"
}})JSON",
kACCOUNT
)
);
auto const correctOutput = fmt::format(
R"JSON({{
"account": "{}",
"ledger_hash": "{}",
"ledger_index": 30,
"validated": true,
"limit": 200,
"mpt_issuances": [
{{
"mpt_issuance_id": "{}",
"issuer": "{}",
"sequence": 3,
"outstanding_amount": {},
"transfer_fee": {},
"mpt_can_transfer": true,
"mpt_can_mutate_can_lock": true,
"mpt_can_mutate_require_auth": true,
"mpt_can_mutate_can_escrow": true,
"mpt_can_mutate_can_trade": true
}},
{{
"mpt_issuance_id": "{}",
"issuer": "{}",
"sequence": 5,
"outstanding_amount": {},
"transfer_fee": {},
"mptoken_metadata": "{}",
"mpt_can_transfer": true,
"mpt_can_mutate_can_transfer": true,
"mpt_can_mutate_can_clawback": true,
"mpt_can_mutate_metadata": true,
"mpt_can_mutate_transfer_fee": true
}}
]
}})JSON",
kACCOUNT,
kLEDGER_HASH,
kISSUANCE_INDEX1,
kACCOUNT,
kISSUANCE1_OUTSTANDING_AMOUNT,
kISSUANCE1_TRANSFER_FEE,
kISSUANCE_INDEX2,
kACCOUNT,
kISSUANCE2_OUTSTANDING_AMOUNT,
kISSUANCE2_TRANSFER_FEE,
kISSUANCE2_METADATA_HEX
);
auto const handler = AnyHandler{AccountMPTokenIssuancesHandler{this->backend_}};
auto const output = handler.process(input, Context{yield});
ASSERT_TRUE(output);
EXPECT_EQ(json::parse(correctOutput), *output.result);
});
}
struct SingleFlagTest {
std::string testName;
uint32_t flag;
std::string expectedJsonKey;
};
struct AccountMPTokenIssuancesImmutableFlagsTest : RPCAccountMPTokenIssuancesHandlerTest,
WithParamInterface<SingleFlagTest> {};
static auto
generateSingleFlagTests()
{
return std::vector<SingleFlagTest>{
{"Locked", ripple::lsfMPTLocked, "mpt_locked"},
{"CanLock", ripple::lsfMPTCanLock, "mpt_can_lock"},
{"RequireAuth", ripple::lsfMPTRequireAuth, "mpt_require_auth"},
{"CanEscrow", ripple::lsfMPTCanEscrow, "mpt_can_escrow"},
{"CanTrade", ripple::lsfMPTCanTrade, "mpt_can_trade"},
{"CanTransfer", ripple::lsfMPTCanTransfer, "mpt_can_transfer"},
{"CanClawback", ripple::lsfMPTCanClawback, "mpt_can_clawback"},
};
}
INSTANTIATE_TEST_SUITE_P(
RPCAccountMPTokenIssuancesImmutableFlagsGroup,
AccountMPTokenIssuancesImmutableFlagsTest,
ValuesIn(generateSingleFlagTests()),
tests::util::kNAME_GENERATOR
);
TEST_P(AccountMPTokenIssuancesImmutableFlagsTest, SingleFlag)
{
auto const testParams = GetParam();
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
auto const account = getAccountIdWithString(kACCOUNT);
auto const accountKk = ripple::keylet::account(account).key;
auto const owneDirKk = ripple::keylet::ownerDir(account).key;
EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillOnce(Return(Blob{'f', 'a', 'k', 'e'}));
ripple::STObject const ownerDir = createOwnerDirLedgerObject({ripple::uint256{kISSUANCE_INDEX1}}, kISSUANCE_INDEX1);
EXPECT_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _)).WillOnce(Return(ownerDir.getSerializer().peekData()));
auto const bbs = std::vector<Blob>{
createMptIssuanceObject(kACCOUNT, 1, std::nullopt, testParams.flag, 0).getSerializer().peekData()
};
EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs));
runSpawn([this, &testParams](auto yield) {
auto const input = json::parse(
fmt::format(
R"JSON({{
"account": "{}"
}})JSON",
kACCOUNT
)
);
auto const handler = AnyHandler{AccountMPTokenIssuancesHandler{this->backend_}};
auto const output = handler.process(input, Context{yield});
ASSERT_TRUE(output);
auto const& resultJson = (*output.result).as_object();
auto const& issuances = resultJson.at("mpt_issuances").as_array();
ASSERT_EQ(issuances.size(), 1);
auto const& issuanceJson = issuances[0].as_object();
EXPECT_TRUE(issuanceJson.contains(testParams.expectedJsonKey));
EXPECT_EQ(issuanceJson.at(testParams.expectedJsonKey), true);
});
}
struct SingleMutableFlagTest {
std::string testName;
uint32_t mutableFlag;
std::string expectedJsonKey;
};
struct AccountMPTokenIssuancesMutableFlagsTest : RPCAccountMPTokenIssuancesHandlerTest,
WithParamInterface<SingleMutableFlagTest> {};
static auto
generateSingleMutableFlagTests()
{
return std::vector<SingleMutableFlagTest>{
{"CanMutateCanLock", ripple::lsmfMPTCanMutateCanLock, "mpt_can_mutate_can_lock"},
{"CanMutateRequireAuth", ripple::lsmfMPTCanMutateRequireAuth, "mpt_can_mutate_require_auth"},
{"CanMutateCanEscrow", ripple::lsmfMPTCanMutateCanEscrow, "mpt_can_mutate_can_escrow"},
{"CanMutateCanTrade", ripple::lsmfMPTCanMutateCanTrade, "mpt_can_mutate_can_trade"},
{"CanMutateCanTransfer", ripple::lsmfMPTCanMutateCanTransfer, "mpt_can_mutate_can_transfer"},
{"CanMutateCanClawback", ripple::lsmfMPTCanMutateCanClawback, "mpt_can_mutate_can_clawback"},
{"CanMutateMetadata", ripple::lsmfMPTCanMutateMetadata, "mpt_can_mutate_metadata"},
{"CanMutateTransferFee", ripple::lsmfMPTCanMutateTransferFee, "mpt_can_mutate_transfer_fee"},
};
}
INSTANTIATE_TEST_SUITE_P(
RPCAccountMPTokenIssuancesMutableFlagsGroup,
AccountMPTokenIssuancesMutableFlagsTest,
ValuesIn(generateSingleMutableFlagTests()),
tests::util::kNAME_GENERATOR
);
TEST_P(AccountMPTokenIssuancesMutableFlagsTest, SingleMutableFlag)
{
auto const testParams = GetParam();
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
auto const account = getAccountIdWithString(kACCOUNT);
auto const accountKk = ripple::keylet::account(account).key;
auto const owneDirKk = ripple::keylet::ownerDir(account).key;
EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillOnce(Return(Blob{'f', 'a', 'k', 'e'}));
ripple::STObject const ownerDir = createOwnerDirLedgerObject({ripple::uint256{kISSUANCE_INDEX1}}, kISSUANCE_INDEX1);
EXPECT_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _)).WillOnce(Return(ownerDir.getSerializer().peekData()));
auto const bbs = std::vector<Blob>{createMptIssuanceObject(
kACCOUNT,
1,
std::nullopt,
0,
0,
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
testParams.mutableFlag
)
.getSerializer()
.peekData()};
EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs));
runSpawn([this, &testParams](auto yield) {
auto const input = json::parse(
fmt::format(
R"JSON({{
"account": "{}"
}})JSON",
kACCOUNT
)
);
auto const handler = AnyHandler{AccountMPTokenIssuancesHandler{this->backend_}};
auto const output = handler.process(input, Context{yield});
ASSERT_TRUE(output);
auto const& resultJson = (*output.result).as_object();
auto const& issuances = resultJson.at("mpt_issuances").as_array();
ASSERT_EQ(issuances.size(), 1);
auto const& issuanceJson = issuances[0].as_object();
EXPECT_TRUE(issuanceJson.contains(testParams.expectedJsonKey));
EXPECT_EQ(issuanceJson.at(testParams.expectedJsonKey), true);
});
}

View File

@@ -64,12 +64,14 @@ constexpr uint64_t kTOKEN2_AMOUNT = 250;
// define expected JSON for mptokens
auto const kTOKEN_OUT1 = fmt::format(
R"JSON({{
"mpt_id": "{}",
"account": "{}",
"mpt_issuance_id": "{}",
"mpt_amount": {},
"locked_amount": {},
"mpt_locked": true
}})JSON",
kTOKEN_INDEX1,
kACCOUNT,
kISSUANCE_ID_HEX,
kTOKEN1_AMOUNT,
@@ -78,11 +80,13 @@ auto const kTOKEN_OUT1 = fmt::format(
auto const kTOKEN_OUT2 = fmt::format(
R"JSON({{
"mpt_id": "{}",
"account": "{}",
"mpt_issuance_id": "{}",
"mpt_amount": {},
"mpt_authorized": true
}})JSON",
kTOKEN_INDEX2,
kACCOUNT,
kISSUANCE_ID_HEX,
kTOKEN2_AMOUNT
@@ -262,7 +266,7 @@ TEST_F(RPCAccountMPTokensHandlerTest, NonExistLedgerViaLedgerIntIndex)
TEST_F(RPCAccountMPTokensHandlerTest, LedgerSeqOutOfRangeByHash)
{
auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 31);
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 31);
EXPECT_CALL(*backend_, fetchLedgerByHash(ripple::uint256{kLEDGER_HASH}, _)).WillOnce(Return(ledgerHeader));
auto const input = json::parse(
@@ -311,7 +315,7 @@ TEST_F(RPCAccountMPTokensHandlerTest, LedgerSeqOutOfRangeByIndex)
TEST_F(RPCAccountMPTokensHandlerTest, NonExistAccount)
{
auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
EXPECT_CALL(*backend_, fetchLedgerByHash(ripple::uint256{kLEDGER_HASH}, _)).WillOnce(Return(ledgerHeader));
// fetch account object return empty
EXPECT_CALL(*backend_, doFetchLedgerObject).WillOnce(Return(std::optional<Blob>{}));
@@ -339,29 +343,31 @@ TEST_F(RPCAccountMPTokensHandlerTest, NonExistAccount)
TEST_F(RPCAccountMPTokensHandlerTest, DefaultParameters)
{
auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
ON_CALL(*backend_, fetchLedgerBySequence).WillByDefault(Return(ledgerHeader));
auto account = getAccountIdWithString(kACCOUNT);
auto accountKk = ripple::keylet::account(account).key;
auto owneDirKk = ripple::keylet::ownerDir(account).key;
auto const account = getAccountIdWithString(kACCOUNT);
auto const accountKk = ripple::keylet::account(account).key;
auto const owneDirKk = ripple::keylet::ownerDir(account).key;
ON_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
ripple::STObject const ownerDir =
createOwnerDirLedgerObject({ripple::uint256{kTOKEN_INDEX1}, ripple::uint256{kTOKEN_INDEX2}}, kTOKEN_INDEX1);
ON_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _)).WillByDefault(Return(ownerDir.getSerializer().peekData()));
std::vector<Blob> bbs;
auto const token1 = createMpTokenObject(
kACCOUNT, ripple::uint192(kISSUANCE_ID_HEX), kTOKEN1_AMOUNT, ripple::lsfMPTLocked, kTOKEN1_LOCKED_AMOUNT
);
auto const bbs = std::vector<Blob>{
createMpTokenObject(
kACCOUNT, ripple::uint192(kISSUANCE_ID_HEX), kTOKEN1_AMOUNT, ripple::lsfMPTLocked, kTOKEN1_LOCKED_AMOUNT
)
.getSerializer()
.peekData(),
auto const token2 = createMpTokenObject(
kACCOUNT, ripple::uint192(kISSUANCE_ID_HEX), kTOKEN2_AMOUNT, ripple::lsfMPTAuthorized, std::nullopt
);
bbs.push_back(token1.getSerializer().peekData());
bbs.push_back(token2.getSerializer().peekData());
createMpTokenObject(
kACCOUNT, ripple::uint192(kISSUANCE_ID_HEX), kTOKEN2_AMOUNT, ripple::lsfMPTAuthorized, std::nullopt
)
.getSerializer()
.peekData()
};
EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs));
@@ -385,7 +391,7 @@ TEST_F(RPCAccountMPTokensHandlerTest, DefaultParameters)
kTOKEN_OUT2
);
auto const input = json::parse(fmt::format(R"JSON({{"account": "{}"}})JSON", kACCOUNT));
auto handler = AnyHandler{AccountMPTokensHandler{this->backend_}};
auto const handler = AnyHandler{AccountMPTokensHandler{this->backend_}};
auto const output = handler.process(input, Context{yield});
ASSERT_TRUE(output);
EXPECT_EQ(json::parse(expected), *output.result);
@@ -395,22 +401,25 @@ TEST_F(RPCAccountMPTokensHandlerTest, DefaultParameters)
TEST_F(RPCAccountMPTokensHandlerTest, UseLimit)
{
constexpr int kLIMIT = 20;
auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
ON_CALL(*backend_, fetchLedgerBySequence).WillByDefault(Return(ledgerHeader));
auto account = getAccountIdWithString(kACCOUNT);
auto accountKk = ripple::keylet::account(account).key;
auto owneDirKk = ripple::keylet::ownerDir(account).key;
auto const account = getAccountIdWithString(kACCOUNT);
auto const accountKk = ripple::keylet::account(account).key;
auto const owneDirKk = ripple::keylet::ownerDir(account).key;
ON_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
std::vector<ripple::uint256> indexes;
std::vector<Blob> bbs;
for (int i = 0; i < 50; ++i) {
indexes.emplace_back(kTOKEN_INDEX1);
auto const token = createMpTokenObject(kACCOUNT, ripple::uint192(kISSUANCE_ID_HEX), i, 0, std::nullopt);
bbs.push_back(token.getSerializer().peekData());
}
auto const indexes = std::vector<ripple::uint256>(50, ripple::uint256{kTOKEN_INDEX1});
auto const bbs = [&]() {
std::vector<Blob> v;
v.reserve(50);
for (int i = 0; i < 50; ++i) {
v.push_back(createMpTokenObject(kACCOUNT, ripple::uint192(kISSUANCE_ID_HEX), i, 0, std::nullopt)
.getSerializer()
.peekData());
}
return v;
}();
ripple::STObject ownerDir = createOwnerDirLedgerObject(indexes, kTOKEN_INDEX1);
ownerDir.setFieldU64(ripple::sfIndexNext, 99);
@@ -432,7 +441,7 @@ TEST_F(RPCAccountMPTokensHandlerTest, UseLimit)
)
);
auto handler = AnyHandler{AccountMPTokensHandler{this->backend_}};
auto const handler = AnyHandler{AccountMPTokensHandler{this->backend_}};
auto const output = handler.process(input, Context{yield});
ASSERT_TRUE(output);
@@ -454,7 +463,7 @@ TEST_F(RPCAccountMPTokensHandlerTest, UseLimit)
)
);
auto handler = AnyHandler{AccountMPTokensHandler{this->backend_}};
auto const handler = AnyHandler{AccountMPTokensHandler{this->backend_}};
auto const output = handler.process(input, Context{yield});
ASSERT_TRUE(output);
EXPECT_EQ((*output.result).as_object().at("limit").as_uint64(), AccountMPTokensHandler::kLIMIT_MIN);
@@ -472,7 +481,7 @@ TEST_F(RPCAccountMPTokensHandlerTest, UseLimit)
)
);
auto handler = AnyHandler{AccountMPTokensHandler{this->backend_}};
auto const handler = AnyHandler{AccountMPTokensHandler{this->backend_}};
auto const output = handler.process(input, Context{yield});
ASSERT_TRUE(output);
EXPECT_EQ((*output.result).as_object().at("limit").as_uint64(), AccountMPTokensHandler::kLIMIT_MAX);
@@ -483,22 +492,25 @@ TEST_F(RPCAccountMPTokensHandlerTest, MarkerOutput)
{
constexpr auto kNEXT_PAGE = 99;
constexpr auto kLIMIT = 15;
auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
auto account = getAccountIdWithString(kACCOUNT);
auto accountKk = ripple::keylet::account(account).key;
auto ownerDirKk = ripple::keylet::ownerDir(account).key;
auto ownerDir2Kk = ripple::keylet::page(ripple::keylet::ownerDir(account), kNEXT_PAGE).key;
auto const account = getAccountIdWithString(kACCOUNT);
auto const accountKk = ripple::keylet::account(account).key;
auto const ownerDirKk = ripple::keylet::ownerDir(account).key;
auto const ownerDir2Kk = ripple::keylet::page(ripple::keylet::ownerDir(account), kNEXT_PAGE).key;
ON_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
std::vector<Blob> bbs;
bbs.reserve(kLIMIT);
for (int i = 0; i < kLIMIT; ++i) {
bbs.push_back(createMpTokenObject(kACCOUNT, ripple::uint192(kISSUANCE_ID_HEX), i, 0, std::nullopt)
.getSerializer()
.peekData());
}
auto const bbs = [&]() {
std::vector<Blob> v;
v.reserve(kLIMIT);
for (int i = 0; i < kLIMIT; ++i) {
v.push_back(createMpTokenObject(kACCOUNT, ripple::uint192(kISSUANCE_ID_HEX), i, 0, std::nullopt)
.getSerializer()
.peekData());
}
return v;
}();
EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs));
std::vector<ripple::uint256> indexes1;
@@ -528,7 +540,7 @@ TEST_F(RPCAccountMPTokensHandlerTest, MarkerOutput)
kLIMIT
)
);
auto handler = AnyHandler{AccountMPTokensHandler{this->backend_}};
auto const handler = AnyHandler{AccountMPTokensHandler{this->backend_}};
auto const output = handler.process(input, Context{yield});
ASSERT_TRUE(output);
auto const& resultJson = (*output.result).as_object();
@@ -544,22 +556,25 @@ TEST_F(RPCAccountMPTokensHandlerTest, MarkerInput)
constexpr auto kNEXT_PAGE = 99;
constexpr auto kLIMIT = 15;
auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
auto account = getAccountIdWithString(kACCOUNT);
auto accountKk = ripple::keylet::account(account).key;
auto const account = getAccountIdWithString(kACCOUNT);
auto const accountKk = ripple::keylet::account(account).key;
ON_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
auto ownerDirKk = ripple::keylet::page(ripple::keylet::ownerDir(account), kNEXT_PAGE).key;
auto const ownerDirKk = ripple::keylet::page(ripple::keylet::ownerDir(account), kNEXT_PAGE).key;
std::vector<Blob> bbs;
std::vector<ripple::uint256> indexes;
for (int i = 0; i < kLIMIT; ++i) {
indexes.emplace_back(kTOKEN_INDEX1);
bbs.push_back(createMpTokenObject(kACCOUNT, ripple::uint192(kISSUANCE_ID_HEX), i, 0, std::nullopt)
.getSerializer()
.peekData());
}
auto const indexes = std::vector<ripple::uint256>(kLIMIT, ripple::uint256{kTOKEN_INDEX1});
auto const bbs = [&]() {
std::vector<Blob> v;
v.reserve(kLIMIT);
for (int i = 0; i < kLIMIT; ++i) {
v.push_back(createMpTokenObject(kACCOUNT, ripple::uint192(kISSUANCE_ID_HEX), i, 0, std::nullopt)
.getSerializer()
.peekData());
}
return v;
}();
ripple::STObject ownerDir = createOwnerDirLedgerObject(indexes, kTOKEN_INDEX1);
ownerDir.setFieldU64(ripple::sfIndexNext, 0);
@@ -583,7 +598,7 @@ TEST_F(RPCAccountMPTokensHandlerTest, MarkerInput)
kNEXT_PAGE
)
);
auto handler = AnyHandler{AccountMPTokensHandler{this->backend_}};
auto const handler = AnyHandler{AccountMPTokensHandler{this->backend_}};
auto const output = handler.process(input, Context{yield});
ASSERT_TRUE(output);
auto const& resultJson = (*output.result).as_object();
@@ -594,29 +609,31 @@ TEST_F(RPCAccountMPTokensHandlerTest, MarkerInput)
TEST_F(RPCAccountMPTokensHandlerTest, LimitLessThanMin)
{
auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
auto account = getAccountIdWithString(kACCOUNT);
auto accountKk = ripple::keylet::account(account).key;
auto owneDirKk = ripple::keylet::ownerDir(account).key;
ON_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
auto const account = getAccountIdWithString(kACCOUNT);
auto const accountKk = ripple::keylet::account(account).key;
auto const owneDirKk = ripple::keylet::ownerDir(account).key;
EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillOnce(Return(Blob{'f', 'a', 'k', 'e'}));
ripple::STObject const ownerDir =
createOwnerDirLedgerObject({ripple::uint256{kTOKEN_INDEX1}, ripple::uint256{kTOKEN_INDEX2}}, kTOKEN_INDEX1);
ON_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _)).WillByDefault(Return(ownerDir.getSerializer().peekData()));
EXPECT_CALL(*backend_, doFetchLedgerObject).Times(2);
EXPECT_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _)).WillOnce(Return(ownerDir.getSerializer().peekData()));
std::vector<Blob> bbs;
auto const token1 = createMpTokenObject(
kACCOUNT, ripple::uint192(kISSUANCE_ID_HEX), kTOKEN1_AMOUNT, ripple::lsfMPTLocked, kTOKEN1_LOCKED_AMOUNT
);
bbs.push_back(token1.getSerializer().peekData());
auto const bbs = std::vector<Blob>{
createMpTokenObject(
kACCOUNT, ripple::uint192(kISSUANCE_ID_HEX), kTOKEN1_AMOUNT, ripple::lsfMPTLocked, kTOKEN1_LOCKED_AMOUNT
)
.getSerializer()
.peekData(),
auto const token2 = createMpTokenObject(
kACCOUNT, ripple::uint192(kISSUANCE_ID_HEX), kTOKEN2_AMOUNT, ripple::lsfMPTAuthorized, std::nullopt
);
bbs.push_back(token2.getSerializer().peekData());
createMpTokenObject(
kACCOUNT, ripple::uint192(kISSUANCE_ID_HEX), kTOKEN2_AMOUNT, ripple::lsfMPTAuthorized, std::nullopt
)
.getSerializer()
.peekData()
};
EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs));
@@ -651,7 +668,7 @@ TEST_F(RPCAccountMPTokensHandlerTest, LimitLessThanMin)
kTOKEN_OUT2
);
auto handler = AnyHandler{AccountMPTokensHandler{this->backend_}};
auto const handler = AnyHandler{AccountMPTokensHandler{this->backend_}};
auto const output = handler.process(input, Context{yield});
ASSERT_TRUE(output);
EXPECT_EQ(json::parse(correctOutput), *output.result);
@@ -660,29 +677,31 @@ TEST_F(RPCAccountMPTokensHandlerTest, LimitLessThanMin)
TEST_F(RPCAccountMPTokensHandlerTest, LimitMoreThanMax)
{
auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
auto account = getAccountIdWithString(kACCOUNT);
auto accountKk = ripple::keylet::account(account).key;
auto owneDirKk = ripple::keylet::ownerDir(account).key;
ON_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
auto const account = getAccountIdWithString(kACCOUNT);
auto const accountKk = ripple::keylet::account(account).key;
auto const owneDirKk = ripple::keylet::ownerDir(account).key;
EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillOnce(Return(Blob{'f', 'a', 'k', 'e'}));
ripple::STObject const ownerDir =
createOwnerDirLedgerObject({ripple::uint256{kTOKEN_INDEX1}, ripple::uint256{kTOKEN_INDEX2}}, kTOKEN_INDEX1);
ON_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _)).WillByDefault(Return(ownerDir.getSerializer().peekData()));
EXPECT_CALL(*backend_, doFetchLedgerObject).Times(2);
EXPECT_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _)).WillOnce(Return(ownerDir.getSerializer().peekData()));
std::vector<Blob> bbs;
auto const token1 = createMpTokenObject(
kACCOUNT, ripple::uint192(kISSUANCE_ID_HEX), kTOKEN1_AMOUNT, ripple::lsfMPTLocked, kTOKEN1_LOCKED_AMOUNT
);
bbs.push_back(token1.getSerializer().peekData());
auto const bbs = std::vector<Blob>{
createMpTokenObject(
kACCOUNT, ripple::uint192(kISSUANCE_ID_HEX), kTOKEN1_AMOUNT, ripple::lsfMPTLocked, kTOKEN1_LOCKED_AMOUNT
)
.getSerializer()
.peekData(),
auto const token2 = createMpTokenObject(
kACCOUNT, ripple::uint192(kISSUANCE_ID_HEX), kTOKEN2_AMOUNT, ripple::lsfMPTAuthorized, std::nullopt
);
bbs.push_back(token2.getSerializer().peekData());
createMpTokenObject(
kACCOUNT, ripple::uint192(kISSUANCE_ID_HEX), kTOKEN2_AMOUNT, ripple::lsfMPTAuthorized, std::nullopt
)
.getSerializer()
.peekData()
};
EXPECT_CALL(*backend_, doFetchLedgerObjects).WillOnce(Return(bbs));
@@ -717,7 +736,7 @@ TEST_F(RPCAccountMPTokensHandlerTest, LimitMoreThanMax)
kTOKEN_OUT2
);
auto handler = AnyHandler{AccountMPTokensHandler{this->backend_}};
auto const handler = AnyHandler{AccountMPTokensHandler{this->backend_}};
auto const output = handler.process(input, Context{yield});
ASSERT_TRUE(output);
EXPECT_EQ(json::parse(correctOutput), *output.result);
@@ -726,17 +745,16 @@ TEST_F(RPCAccountMPTokensHandlerTest, LimitMoreThanMax)
TEST_F(RPCAccountMPTokensHandlerTest, EmptyResult)
{
auto ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
auto const ledgerHeader = createLedgerHeader(kLEDGER_HASH, 30);
EXPECT_CALL(*backend_, fetchLedgerBySequence).WillOnce(Return(ledgerHeader));
auto account = getAccountIdWithString(kACCOUNT);
auto accountKk = ripple::keylet::account(account).key;
auto owneDirKk = ripple::keylet::ownerDir(account).key;
ON_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillByDefault(Return(Blob{'f', 'a', 'k', 'e'}));
auto const account = getAccountIdWithString(kACCOUNT);
auto const accountKk = ripple::keylet::account(account).key;
auto const owneDirKk = ripple::keylet::ownerDir(account).key;
EXPECT_CALL(*backend_, doFetchLedgerObject(accountKk, _, _)).WillOnce(Return(Blob{'f', 'a', 'k', 'e'}));
ripple::STObject const ownerDir = createOwnerDirLedgerObject({}, kTOKEN_INDEX1);
ON_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _)).WillByDefault(Return(ownerDir.getSerializer().peekData()));
EXPECT_CALL(*backend_, doFetchLedgerObject).Times(2);
EXPECT_CALL(*backend_, doFetchLedgerObject(owneDirKk, _, _)).WillOnce(Return(ownerDir.getSerializer().peekData()));
runSpawn([this](auto yield) {
auto const input = json::parse(
@@ -747,7 +765,7 @@ TEST_F(RPCAccountMPTokensHandlerTest, EmptyResult)
kACCOUNT
)
);
auto handler = AnyHandler{AccountMPTokensHandler{this->backend_}};
auto const handler = AnyHandler{AccountMPTokensHandler{this->backend_}};
auto const output = handler.process(input, Context{yield});
ASSERT_TRUE(output);
EXPECT_EQ((*output.result).as_object().at("mptokens").as_array().size(), 0);

View File

@@ -295,7 +295,7 @@ TEST_F(RPCVaultInfoHandlerTest, ValidVaultObjectQueryByVaultID)
"AssetsTotal": "300",
"Flags": 0,
"LedgerEntryType": "Vault",
"LossUnrealized": "0",
"LossUnrealized": "1",
"Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"OwnerNode": "4",
"PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000002",
@@ -378,7 +378,7 @@ TEST_F(RPCVaultInfoHandlerTest, ValidVaultObjectQueryByOwnerAndSeq)
"AssetsTotal": "300",
"Flags": 0,
"LedgerEntryType": "Vault",
"LossUnrealized": "0",
"LossUnrealized": "1",
"Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"OwnerNode": "4",
"PreviousTxnID": "0000000000000000000000000000000000000000000000000000000000000002",

View File

@@ -0,0 +1,445 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2025, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "util/ObservableValue.hpp"
#include <boost/signals2/connection.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <atomic>
#include <chrono>
#include <mutex>
#include <thread>
#include <vector>
using namespace testing;
using namespace util;
namespace {
} // namespace
class ObservableValueAtomicTest : public ::testing::Test {};
TEST_F(ObservableValueAtomicTest, BasicConstruction)
{
ObservableValue<std::atomic<int>> const obs{42};
EXPECT_EQ(obs.get(), 42);
EXPECT_EQ(static_cast<int>(obs), 42);
EXPECT_FALSE(obs.hasObservers());
}
TEST_F(ObservableValueAtomicTest, DefaultConstruction)
{
ObservableValue<std::atomic<int>> const obsInt;
EXPECT_EQ(obsInt.get(), 0);
ObservableValue<std::atomic<bool>> const obsBool;
EXPECT_FALSE(obsBool.get());
EXPECT_FALSE(obsInt.hasObservers());
EXPECT_FALSE(obsBool.hasObservers());
}
TEST_F(ObservableValueAtomicTest, BasicObservation)
{
ObservableValue<std::atomic<int>> obs{10};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(20));
obs = 20;
EXPECT_EQ(obs.get(), 20);
}
TEST_F(ObservableValueAtomicTest, SetMethod)
{
ObservableValue<std::atomic<int>> obs{5};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(15));
obs.set(15);
EXPECT_EQ(obs.get(), 15);
obs.set(15); // Same value should not notify
EXPECT_EQ(obs.get(), 15);
}
TEST_F(ObservableValueAtomicTest, AtomicBasicUsage)
{
ObservableValue<std::atomic<int>> obs{10};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(25));
obs.set(25);
EXPECT_EQ(obs.get(), 25);
}
TEST_F(ObservableValueAtomicTest, AtomicMultipleChanges)
{
ObservableValue<std::atomic<int>> obs{50};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(100)); // First change: 50→100
EXPECT_CALL(mockObserver, Call(50)); // Second change: 100→50
obs.set(100); // Should notify: 50→100
obs.set(50); // Should notify: 100→50
EXPECT_EQ(obs.get(), 50);
}
TEST_F(ObservableValueAtomicTest, AtomicNoChangeNoNotification)
{
ObservableValue<std::atomic<int>> obs{42};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
// No EXPECT_CALL since no notification should occur
obs.set(42); // Same value, should not notify
obs.set(42); // Same value again, should not notify
EXPECT_EQ(obs.get(), 42);
}
TEST_F(ObservableValueAtomicTest, AtomicSequentialChanges)
{
ObservableValue<std::atomic<int>> obs{1};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(2));
obs.set(2);
EXPECT_CALL(mockObserver, Call(3));
obs.set(3);
EXPECT_EQ(obs.get(), 3);
}
TEST_F(ObservableValueAtomicTest, MultipleObservers)
{
ObservableValue<std::atomic<int>> obs{0};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver1;
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver2;
auto conn1 = obs.observe(mockObserver1.AsStdFunction());
auto conn2 = obs.observe(mockObserver2.AsStdFunction());
EXPECT_CALL(mockObserver1, Call(42));
EXPECT_CALL(mockObserver2, Call(42));
obs = 42;
conn1.disconnect();
EXPECT_CALL(mockObserver2, Call(100));
obs = 100;
}
TEST_F(ObservableValueAtomicTest, ThreadSafetyBasic)
{
ObservableValue<std::atomic<int>> obs{0};
std::atomic<int> notificationCount{0};
std::vector<int> values;
std::mutex valuesMutex;
auto connection = obs.observe([&](int const& value) {
notificationCount.fetch_add(1);
std::lock_guard<std::mutex> const lock(valuesMutex);
values.push_back(value);
});
static constexpr auto kNUM_THREADS = 4;
static constexpr auto kINCREMENTS_PER_THREAD = 100;
std::vector<std::thread> threads;
threads.reserve(kNUM_THREADS);
for (int i = 0; i < kNUM_THREADS; ++i) {
threads.emplace_back([&obs]() {
for (int j = 0; j < kINCREMENTS_PER_THREAD; ++j) {
int const expected = obs.get();
int const newValue = expected + 1;
obs.set(newValue);
std::this_thread::sleep_for(std::chrono::microseconds(1));
}
});
}
for (auto& thread : threads)
thread.join();
// Final value may be less than kNumThreads * kIncrementsPerThread due to race conditions
EXPECT_GT(obs.get(), 0);
EXPECT_GT(notificationCount.load(), 0);
std::lock_guard<std::mutex> const lock(valuesMutex);
for (auto const& value : values) {
EXPECT_GT(value, 0);
}
}
TEST_F(ObservableValueAtomicTest, ThreadSafetyWithDirectAccess)
{
ObservableValue<std::atomic<int>> obs{0};
std::atomic<int> notificationCount{0};
auto connection = obs.observe([&](int const&) { notificationCount.fetch_add(1); });
static constexpr auto kNUM_THREADS = 4;
static constexpr auto kOPERATIONS_PER_THREAD = 50;
std::vector<std::thread> threads;
threads.reserve(kNUM_THREADS);
for (int i = 0; i < kNUM_THREADS; ++i) {
threads.emplace_back([&obs]() {
for (int j = 0; j < kOPERATIONS_PER_THREAD; ++j) {
int const current = obs.get();
obs.set(current + 1);
std::this_thread::sleep_for(std::chrono::microseconds(1));
}
});
}
for (auto& thread : threads)
thread.join();
EXPECT_GT(obs.get(), 0);
EXPECT_GT(notificationCount.load(), 0);
}
TEST_F(ObservableValueAtomicTest, AtomicBoolSpecialization)
{
ObservableValue<std::atomic<bool>> obs{false};
testing::StrictMock<testing::MockFunction<void(bool const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(true));
obs = true;
EXPECT_TRUE(obs.get());
obs = true; // Same value should not notify
EXPECT_CALL(mockObserver, Call(false));
obs.set(false);
EXPECT_FALSE(obs.get());
}
TEST_F(ObservableValueAtomicTest, CompareAndSwapBehavior)
{
ObservableValue<std::atomic<int>> obs{10};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
// Test that compare-and-swap works correctly in set()
obs.set(10); // Same value, should not notify
EXPECT_CALL(mockObserver, Call(20));
obs.set(20); // Different value, should notify
}
TEST_F(ObservableValueAtomicTest, RaceConditionNotificationIntegrity)
{
ObservableValue<std::atomic<int>> obs{0};
std::atomic<int> notificationCount{0};
std::vector<int> values;
std::mutex valuesMutex;
auto connection = obs.observe([&](int const& value) {
notificationCount.fetch_add(1);
std::lock_guard<std::mutex> const lock(valuesMutex);
values.push_back(value);
});
static constexpr auto kNUM_THREADS = 10;
static constexpr auto kOPERATIONS_PER_THREAD = 20;
std::vector<std::thread> threads;
threads.reserve(kNUM_THREADS);
for (int i = 0; i < kNUM_THREADS; ++i) {
threads.emplace_back([&obs]() {
for (int j = 0; j < kOPERATIONS_PER_THREAD; ++j) {
obs.set(j % 3);
std::this_thread::sleep_for(std::chrono::microseconds(1));
}
});
}
for (auto& thread : threads)
thread.join();
EXPECT_GT(notificationCount.load(), 0);
std::lock_guard<std::mutex> const lock(valuesMutex);
for (auto const& value : values) {
EXPECT_GE(value, 0);
EXPECT_LE(value, 2);
}
int const finalValue = obs.get();
EXPECT_GE(finalValue, 0);
EXPECT_LE(finalValue, 2);
}
TEST_F(ObservableValueAtomicTest, DeterministicNotificationTest)
{
ObservableValue<std::atomic<int>> obs{0};
std::atomic<int> notificationCount{0};
std::vector<int> values;
std::mutex valuesMutex;
auto connection = obs.observe([&](int const& value) {
notificationCount.fetch_add(1);
std::lock_guard<std::mutex> const lock(valuesMutex);
values.push_back(value);
});
static constexpr auto kNUM_THREADS = 5;
std::vector<std::thread> threads;
threads.reserve(kNUM_THREADS);
for (int i = 0; i < kNUM_THREADS; ++i) {
threads.emplace_back([&obs, i]() { obs.set(i + 1); });
}
for (auto& thread : threads)
thread.join();
// Each thread sets a unique value, so expect exactly kNumThreads notifications
EXPECT_EQ(notificationCount.load(), kNUM_THREADS);
std::lock_guard<std::mutex> const lock(valuesMutex);
EXPECT_EQ(values.size(), kNUM_THREADS);
for (auto const& value : values) {
EXPECT_GE(value, 1);
EXPECT_LE(value, kNUM_THREADS);
}
int const finalValue = obs.get();
EXPECT_GE(finalValue, 1);
EXPECT_LE(finalValue, kNUM_THREADS);
}
TEST_F(ObservableValueAtomicTest, NoNotificationForSameValue)
{
ObservableValue<std::atomic<int>> obs{42};
std::atomic<int> notificationCount{0};
auto connection = obs.observe([&](int const&) { notificationCount.fetch_add(1); });
static constexpr auto kNUM_THREADS = 10;
std::vector<std::thread> threads;
threads.reserve(kNUM_THREADS);
for (int i = 0; i < kNUM_THREADS; ++i) {
threads.emplace_back([&obs]() { obs.set(42); });
}
for (auto& thread : threads)
thread.join();
EXPECT_EQ(notificationCount.load(), 0); // No notifications since value never changed
EXPECT_EQ(obs.get(), 42);
}
TEST_F(ObservableValueAtomicTest, AtomicRaceConditionCorrectness)
{
ObservableValue<std::atomic<int>> obs{0};
std::atomic<int> notificationCount{0};
std::vector<int> values;
std::mutex valuesMutex;
auto connection = obs.observe([&](int const& value) {
notificationCount.fetch_add(1);
std::lock_guard<std::mutex> const lock(valuesMutex);
values.push_back(value);
});
static constexpr auto kNUM_THREADS = 3;
std::vector<std::thread> threads;
threads.reserve(kNUM_THREADS);
// Test that direct access properly notifies for all value changes
// Each thread will make unique changes to avoid race condition conflicts
for (int i = 0; i < kNUM_THREADS; ++i) {
threads.emplace_back([&obs, i]() {
int const baseValue = (i + 1) * 10; // 10, 20, 30
obs.set(baseValue); // Store unique values
obs.set(baseValue + 1); // Then increment
});
}
for (auto& thread : threads)
thread.join();
// We should get some notifications (exact count depends on race conditions)
// but at least one per thread since they use unique base values
EXPECT_GE(notificationCount.load(), kNUM_THREADS);
std::lock_guard<std::mutex> const lock(valuesMutex);
EXPECT_GE(values.size(), kNUM_THREADS);
for (auto const& value : values)
EXPECT_GT(value, 0);
}
TEST_F(ObservableValueAtomicTest, ForceNotify)
{
ObservableValue<std::atomic<int>> obs{42};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
obs.forceNotify();
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(42));
obs.forceNotify();
EXPECT_CALL(mockObserver, Call(42));
obs.forceNotify();
EXPECT_CALL(mockObserver, Call(100));
obs.set(100);
EXPECT_CALL(mockObserver, Call(100));
obs.forceNotify();
EXPECT_CALL(mockObserver, Call(100)).Times(3);
obs.forceNotify();
obs.forceNotify();
obs.forceNotify();
}

View File

@@ -0,0 +1,840 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2025, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "util/ObservableValue.hpp"
#include <boost/signals2/connection.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <concepts>
#include <map>
#include <memory>
#include <set>
#include <stdexcept>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
using namespace testing;
using namespace util;
namespace {
struct TestStruct {
int value = 0;
std::string name;
bool
operator==(TestStruct const& other) const
{
return value == other.value && name == other.name;
}
bool
operator!=(TestStruct const& other) const
{
return !(*this == other);
}
};
} // namespace
class ObservableValueTest : public ::testing::Test {};
TEST_F(ObservableValueTest, ConceptCompliance)
{
static_assert(Observable<int>);
static_assert(Observable<std::string>);
static_assert(Observable<double>);
static_assert(Observable<TestStruct>);
static_assert(Observable<bool>);
static_assert(Observable<char>);
static_assert(Observable<float>);
struct NonCopyable {
int value = 0;
NonCopyable() = default;
NonCopyable(NonCopyable const&) = delete;
NonCopyable(NonCopyable&&) = default;
NonCopyable&
operator=(NonCopyable const&) = delete;
NonCopyable&
operator=(NonCopyable&&) = default;
bool
operator==(NonCopyable const& other) const
{
return value == other.value;
}
};
static_assert(!Observable<NonCopyable>);
struct NonMovable {
int value = 0;
NonMovable() = default;
NonMovable(NonMovable const&) = default;
NonMovable(NonMovable&&) = delete;
NonMovable&
operator=(NonMovable const&) = default;
NonMovable&
operator=(NonMovable&&) = delete;
bool
operator==(NonMovable const& other) const
{
return value == other.value;
}
};
static_assert(!Observable<NonMovable>);
struct NonComparable {
int value = 0;
NonComparable() = default;
NonComparable(NonComparable const&) = default;
NonComparable(NonComparable&&) = default;
NonComparable&
operator=(NonComparable const&) = default;
NonComparable&
operator=(NonComparable&&) = default;
};
static_assert(!Observable<NonComparable>);
struct NonDefaultInitializable {
int value;
NonDefaultInitializable() = delete;
explicit NonDefaultInitializable(int v) : value(v)
{
}
NonDefaultInitializable(NonDefaultInitializable const&) = default;
NonDefaultInitializable(NonDefaultInitializable&&) = default;
NonDefaultInitializable&
operator=(NonDefaultInitializable const&) = default;
NonDefaultInitializable&
operator=(NonDefaultInitializable&&) = default;
bool
operator==(NonDefaultInitializable const& other) const
{
return value == other.value;
}
};
static_assert(Observable<NonDefaultInitializable>);
static_assert(!std::default_initializable<NonDefaultInitializable>);
static_assert(Observable<std::vector<int>>);
static_assert(Observable<std::map<int, int>>);
static_assert(Observable<std::set<int>>);
static_assert(Observable<std::pair<int, std::string>>);
static_assert(std::default_initializable<int>);
static_assert(std::default_initializable<std::string>);
static_assert(std::default_initializable<std::vector<int>>);
static_assert(std::default_initializable<TestStruct>);
}
TEST_F(ObservableValueTest, Construction)
{
ObservableValue<int> const obs{42};
EXPECT_EQ(static_cast<int>(obs), 42);
EXPECT_EQ(obs.get(), 42);
EXPECT_FALSE(obs.hasObservers());
}
TEST_F(ObservableValueTest, ConstructionWithDifferentTypes)
{
ObservableValue<std::string> const obsStr{"hello"};
EXPECT_EQ(obsStr.get(), "hello");
ObservableValue<double> const obsDouble{3.14};
EXPECT_DOUBLE_EQ(obsDouble.get(), 3.14);
ObservableValue<bool> const obsBool{true};
EXPECT_TRUE(obsBool.get());
}
TEST_F(ObservableValueTest, DefaultConstruction)
{
ObservableValue<int> const obsInt;
EXPECT_EQ(obsInt.get(), 0);
ObservableValue<double> const obsDouble;
EXPECT_DOUBLE_EQ(obsDouble.get(), 0.0);
ObservableValue<bool> const obsBool;
EXPECT_FALSE(obsBool.get());
ObservableValue<char> const obsChar;
EXPECT_EQ(obsChar.get(), '\0');
EXPECT_FALSE(obsInt.hasObservers());
EXPECT_FALSE(obsDouble.hasObservers());
EXPECT_FALSE(obsBool.hasObservers());
EXPECT_FALSE(obsChar.hasObservers());
}
TEST_F(ObservableValueTest, DefaultConstructionWithContainers)
{
ObservableValue<std::string> const obsString;
EXPECT_EQ(obsString.get(), "");
EXPECT_TRUE(obsString.get().empty());
ObservableValue<std::vector<int>> const obsVector;
EXPECT_TRUE(obsVector.get().empty());
EXPECT_EQ(obsVector.get().size(), 0);
ObservableValue<std::set<int>> const obsSet;
EXPECT_TRUE(obsSet.get().empty());
EXPECT_EQ(obsSet.get().size(), 0);
ObservableValue<std::map<int, std::string>> const obsMap;
EXPECT_TRUE(obsMap.get().empty());
EXPECT_EQ(obsMap.get().size(), 0);
}
TEST_F(ObservableValueTest, DefaultConstructionWithCustomType)
{
ObservableValue<TestStruct> const obsStruct;
EXPECT_EQ(obsStruct.get().value, 0);
EXPECT_EQ(obsStruct.get().name, "");
}
TEST_F(ObservableValueTest, DefaultConstructionThenAssignment)
{
ObservableValue<int> obs;
EXPECT_EQ(obs.get(), 0);
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(42));
obs = 42;
EXPECT_EQ(obs.get(), 42);
obs = 42; // Same value, should not notify
EXPECT_CALL(mockObserver, Call(100));
obs.set(100);
EXPECT_EQ(obs.get(), 100);
}
TEST_F(ObservableValueTest, DefaultConstructionWithGuard)
{
ObservableValue<std::string> obs;
EXPECT_EQ(obs.get(), "");
testing::StrictMock<testing::MockFunction<void(std::string const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call("modified through guard"));
{
auto guard = obs.operator->();
std::string& ref = guard;
ref = "modified through guard";
}
EXPECT_EQ(obs.get(), "modified through guard");
}
TEST_F(ObservableValueTest, DefaultConstructionNotificationBehavior)
{
ObservableValue<int> obs;
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(1));
obs = 1;
EXPECT_CALL(mockObserver, Call(0));
obs = 0;
obs = 0; // Same value, should not notify
}
TEST_F(ObservableValueTest, NonDefaultInitializableTypeWithParameterizedConstructor)
{
struct NonDefaultInitializable {
int value;
NonDefaultInitializable() = delete;
explicit NonDefaultInitializable(int v) : value(v)
{
}
NonDefaultInitializable(NonDefaultInitializable const&) = default;
NonDefaultInitializable(NonDefaultInitializable&&) = default;
NonDefaultInitializable&
operator=(NonDefaultInitializable const&) = default;
NonDefaultInitializable&
operator=(NonDefaultInitializable&&) = default;
bool
operator==(NonDefaultInitializable const& other) const
{
return value == other.value;
}
};
ObservableValue<NonDefaultInitializable> obs{NonDefaultInitializable{42}};
EXPECT_EQ(obs.get().value, 42);
testing::StrictMock<testing::MockFunction<void(NonDefaultInitializable const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(testing::Field(&NonDefaultInitializable::value, 100)));
obs = NonDefaultInitializable{100};
EXPECT_EQ(obs.get().value, 100);
}
TEST_F(ObservableValueTest, MoveSemantics)
{
ObservableValue<int> const obs1{100};
ObservableValue<int> const obs2 = std::move(obs1);
EXPECT_EQ(obs2.get(), 100);
ObservableValue<int> obs3{200};
obs3 = std::move(obs2);
EXPECT_EQ(obs3.get(), 100);
}
TEST_F(ObservableValueTest, CopyOperationsDeleted)
{
static_assert(!std::is_copy_constructible_v<ObservableValue<int>>);
static_assert(!std::is_copy_assignable_v<ObservableValue<int>>);
}
TEST_F(ObservableValueTest, AssignmentOperator)
{
ObservableValue<int> obs{10};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(20));
obs = 20;
EXPECT_EQ(obs.get(), 20);
obs = 20; // Same value, should not notify
EXPECT_EQ(obs.get(), 20);
}
TEST_F(ObservableValueTest, SetMethod)
{
ObservableValue<int> obs{5};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(15));
obs.set(15);
EXPECT_EQ(obs.get(), 15);
obs.set(15); // Same value, should not notify
EXPECT_EQ(obs.get(), 15);
}
TEST_F(ObservableValueTest, ObserverManagement)
{
ObservableValue<int> obs{0};
EXPECT_FALSE(obs.hasObservers());
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver1;
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver2;
auto conn1 = obs.observe(mockObserver1.AsStdFunction());
EXPECT_TRUE(obs.hasObservers());
auto conn2 = obs.observe(mockObserver2.AsStdFunction());
EXPECT_TRUE(obs.hasObservers());
EXPECT_CALL(mockObserver1, Call(42));
EXPECT_CALL(mockObserver2, Call(42));
obs = 42;
conn1.disconnect();
EXPECT_CALL(mockObserver2, Call(100));
obs = 100;
conn2.disconnect();
EXPECT_FALSE(obs.hasObservers());
obs = 200; // No observers, no calls expected
}
TEST_F(ObservableValueTest, ObservableGuardBasicUsage)
{
ObservableValue<int> obs{10};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(25));
{
auto guard = obs.operator->();
int& ref = guard;
ref = 25;
}
EXPECT_EQ(obs.get(), 25);
}
TEST_F(ObservableValueTest, ObservableGuardNoChangeNoNotification)
{
ObservableValue<int> obs{50};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
// No EXPECT_CALL since no notification should occur
{
auto guard = obs.operator->();
int& ref = guard;
ref = 100;
ref = 50; // Back to original value
}
EXPECT_EQ(obs.get(), 50);
}
TEST_F(ObservableValueTest, ObservableGuardMultipleChanges)
{
ObservableValue<int> obs{1};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(2));
{
auto guard = obs.operator->();
int& ref = guard;
ref = 2;
}
EXPECT_CALL(mockObserver, Call(3));
{
auto guard = obs.operator->();
int& ref = guard;
ref = 3;
}
EXPECT_EQ(obs.get(), 3);
}
TEST_F(ObservableValueTest, ComplexTypeObservation)
{
TestStruct const initial{.value = 42, .name = "test"};
ObservableValue<TestStruct> obs{initial};
testing::StrictMock<testing::MockFunction<void(TestStruct const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
TestStruct const newValue{.value = 100, .name = "changed"};
EXPECT_CALL(
mockObserver,
Call(testing::AllOf(testing::Field(&TestStruct::value, 100), testing::Field(&TestStruct::name, "changed")))
);
obs = newValue;
}
TEST_F(ObservableValueTest, ComplexTypeGuardModification)
{
TestStruct const initial{.value = 10, .name = "initial"};
ObservableValue<TestStruct> obs{initial};
testing::StrictMock<testing::MockFunction<void(TestStruct const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(
mockObserver,
Call(testing::AllOf(testing::Field(&TestStruct::value, 20), testing::Field(&TestStruct::name, "modified")))
);
{
auto guard = obs.operator->();
TestStruct& ref = guard;
ref.value = 20;
ref.name = "modified";
}
EXPECT_EQ(obs.get().value, 20);
EXPECT_EQ(obs.get().name, "modified");
}
TEST_F(ObservableValueTest, StringObservation)
{
ObservableValue<std::string> obs{"initial"};
testing::StrictMock<testing::MockFunction<void(std::string const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call("changed"));
obs = "changed";
EXPECT_CALL(mockObserver, Call("set_method"));
obs.set("set_method");
obs = "set_method"; // Same value, should not notify
}
TEST_F(ObservableValueTest, MultipleObserversWithDifferentLifetimes)
{
ObservableValue<int> obs{0};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver1;
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver2;
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver3;
auto conn1 = obs.observe(mockObserver1.AsStdFunction());
EXPECT_CALL(mockObserver1, Call(1));
obs = 1;
auto conn2 = obs.observe(mockObserver2.AsStdFunction());
EXPECT_CALL(mockObserver1, Call(2));
EXPECT_CALL(mockObserver2, Call(2));
obs = 2;
conn1.disconnect();
auto conn3 = obs.observe(mockObserver3.AsStdFunction());
EXPECT_CALL(mockObserver2, Call(3));
EXPECT_CALL(mockObserver3, Call(3));
obs = 3;
}
TEST_F(ObservableValueTest, NoNotificationWhenNoObservers)
{
ObservableValue<int> obs{0};
obs = 1;
obs.set(2);
{
auto guard = obs.operator->();
int& ref = guard;
ref = 3;
}
EXPECT_EQ(obs.get(), 3);
EXPECT_FALSE(obs.hasObservers());
}
TEST_F(ObservableValueTest, ManyObservers)
{
ObservableValue<int> obs{0};
std::vector<std::unique_ptr<testing::StrictMock<testing::MockFunction<void(int const&)>>>> mockObservers;
std::vector<boost::signals2::connection> connections;
constexpr int kNUM_OBSERVERS = 100;
for (int i = 0; i < kNUM_OBSERVERS; ++i) {
mockObservers.push_back(std::make_unique<testing::StrictMock<testing::MockFunction<void(int const&)>>>());
connections.push_back(obs.observe(mockObservers.back()->AsStdFunction()));
}
EXPECT_TRUE(obs.hasObservers());
for (auto const& mockObserver : mockObservers) {
EXPECT_CALL(*mockObserver, Call(42));
}
obs = 42;
for (auto& conn : connections) {
conn.disconnect();
}
EXPECT_FALSE(obs.hasObservers());
}
TEST_F(ObservableValueTest, TypeConversions)
{
ObservableValue<double> obs{1.0};
testing::StrictMock<testing::MockFunction<void(double const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(testing::DoubleEq(2.0)));
obs = 2;
EXPECT_CALL(mockObserver, Call(testing::DoubleEq(3.14)));
obs = 3.14;
EXPECT_CALL(mockObserver, Call(testing::DoubleEq(4.0)));
obs = static_cast<double>(4.0f);
}
TEST_F(ObservableValueTest, EnhancedConceptRequirements)
{
struct ComplexObservable {
std::string name;
int value{};
std::vector<int> data;
ComplexObservable() = default;
ComplexObservable(std::string n, int v, std::vector<int> d) : name(std::move(n)), value(v), data(std::move(d))
{
}
ComplexObservable(ComplexObservable const& other) = default;
ComplexObservable(ComplexObservable&& other) noexcept = default;
ComplexObservable&
operator=(ComplexObservable&& other) noexcept
{
if (this != &other) {
name = std::move(other.name);
value = other.value;
data = std::move(other.data);
}
return *this;
}
bool
operator==(ComplexObservable const& other) const
{
return name == other.name && value == other.value && data == other.data;
}
ComplexObservable&
operator=(ComplexObservable const& other)
{
if (this != &other) {
name = other.name;
value = other.value;
data = other.data;
}
return *this;
}
};
static_assert(Observable<ComplexObservable>);
ComplexObservable initial{"test", 42, {1, 2, 3}};
ObservableValue<ComplexObservable> obs{std::move(initial)};
testing::StrictMock<testing::MockFunction<void(ComplexObservable const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
ComplexObservable const newValue{"changed", 100, {4, 5, 6}};
EXPECT_CALL(
mockObserver,
Call(
testing::AllOf(
testing::Field(&ComplexObservable::name, "changed"),
testing::Field(&ComplexObservable::value, 100),
testing::Field(&ComplexObservable::data, std::vector<int>({4, 5, 6}))
)
)
);
obs = newValue;
ComplexObservable const sameValue{"changed", 100, {4, 5, 6}};
obs = sameValue; // Same value, should not notify
}
TEST_F(ObservableValueTest, ExceptionInObserver)
{
ObservableValue<int> obs{0};
testing::StrictMock<testing::MockFunction<void(int const&)>> goodMockObserver;
auto goodConnection = obs.observe(goodMockObserver.AsStdFunction());
auto throwingConnection = obs.observe([](int const&) { throw std::runtime_error("Observer exception"); });
EXPECT_CALL(goodMockObserver, Call(42));
EXPECT_THROW(obs = 42, std::runtime_error);
// Value is still updated even when observers throw
EXPECT_EQ(obs.get(), 42);
}
TEST_F(ObservableValueTest, GuardExceptionSafety)
{
ObservableValue<int> obs{10};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(20));
try {
auto guard = obs.operator->();
int& ref = guard;
ref = 20;
throw std::runtime_error("Test exception");
} catch (...) {
[[maybe_unused]] auto nothing = true;
}
EXPECT_EQ(obs.get(), 20);
}
TEST_F(ObservableValueTest, ComprehensiveIntegrationTest)
{
ObservableValue<std::string> obs{"start"};
testing::StrictMock<testing::MockFunction<void(std::string const&)>> mockObserver1;
testing::StrictMock<testing::MockFunction<void(std::string const&)>> mockObserver2;
auto conn1 = obs.observe(mockObserver1.AsStdFunction());
auto conn2 = obs.observe(mockObserver2.AsStdFunction());
EXPECT_CALL(mockObserver1, Call("first"));
EXPECT_CALL(mockObserver2, Call("first"));
obs = "first";
EXPECT_CALL(mockObserver1, Call("second"));
EXPECT_CALL(mockObserver2, Call("second"));
obs.set("second");
obs = "second"; // Same value, should not notify
EXPECT_CALL(mockObserver1, Call("third"));
EXPECT_CALL(mockObserver2, Call("third"));
{
auto guard = obs.operator->();
std::string& ref = guard;
ref = "third";
}
conn1.disconnect();
EXPECT_CALL(mockObserver2, Call("fourth"));
obs = "fourth";
EXPECT_EQ(obs.get(), "fourth");
EXPECT_TRUE(obs.hasObservers());
conn2.disconnect();
EXPECT_FALSE(obs.hasObservers());
}
TEST_F(ObservableValueTest, RegularConnectionPersistsAfterDestruction)
{
ObservableValue<int> obs{0};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
{
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(1));
obs = 1;
}
EXPECT_CALL(mockObserver, Call(2));
obs = 2;
EXPECT_TRUE(obs.hasObservers());
}
TEST_F(ObservableValueTest, ScopedConnectionDisconnectsOnDestruction)
{
ObservableValue<int> obs{0};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
{
boost::signals2::scoped_connection const scoped = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(1));
obs = 1;
EXPECT_TRUE(obs.hasObservers());
}
obs = 2; // No call expected since connection was destroyed
EXPECT_FALSE(obs.hasObservers());
}
TEST_F(ObservableValueTest, ManualDisconnectWithRegularConnection)
{
ObservableValue<int> obs{0};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(1));
obs = 1;
EXPECT_TRUE(obs.hasObservers());
connection.disconnect();
obs = 2; // No call expected since connection was disconnected
EXPECT_FALSE(obs.hasObservers());
}
TEST_F(ObservableValueTest, ScopedConnectionCanBeDisconnectedManually)
{
ObservableValue<int> obs{0};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
boost::signals2::scoped_connection const scoped = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(1));
obs = 1;
EXPECT_TRUE(obs.hasObservers());
scoped.disconnect();
obs = 2; // No call expected since connection was disconnected
EXPECT_FALSE(obs.hasObservers());
}
TEST_F(ObservableValueTest, MixedConnectionTypes)
{
ObservableValue<int> obs{0};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver1;
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver2;
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver3;
auto regularConn = obs.observe(mockObserver1.AsStdFunction());
{
boost::signals2::scoped_connection const scoped1 = obs.observe(mockObserver2.AsStdFunction());
boost::signals2::scoped_connection const scoped2 = obs.observe(mockObserver3.AsStdFunction());
EXPECT_CALL(mockObserver1, Call(1));
EXPECT_CALL(mockObserver2, Call(1));
EXPECT_CALL(mockObserver3, Call(1));
obs = 1;
EXPECT_TRUE(obs.hasObservers());
}
EXPECT_CALL(mockObserver1, Call(2));
obs = 2; // Only mockObserver1 should be called since scoped connections were destroyed
EXPECT_TRUE(obs.hasObservers());
regularConn.disconnect();
EXPECT_FALSE(obs.hasObservers());
}
TEST_F(ObservableValueTest, ForceNotify)
{
ObservableValue<int> obs{42};
testing::StrictMock<testing::MockFunction<void(int const&)>> mockObserver;
obs.forceNotify();
auto connection = obs.observe(mockObserver.AsStdFunction());
EXPECT_CALL(mockObserver, Call(42));
obs.forceNotify();
EXPECT_CALL(mockObserver, Call(42));
obs.forceNotify();
EXPECT_CALL(mockObserver, Call(100));
obs.set(100);
EXPECT_CALL(mockObserver, Call(100));
obs.forceNotify();
EXPECT_CALL(mockObserver, Call(100)).Times(3);
obs.forceNotify();
obs.forceNotify();
obs.forceNotify();
}

Some files were not shown because too many files have changed in this diff Show More