diff --git a/.github/actions/coverage/action.yml b/.github/actions/coverage/action.yml new file mode 100644 index 000000000..7984e365c --- /dev/null +++ b/.github/actions/coverage/action.yml @@ -0,0 +1,111 @@ +# +# Copyright (c) 2024 Alibaba Group Holding Limited. All Rights Reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Alibaba designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# + +name: 'Coverage test' + +inputs: + reset-commit-id: + required: false + type: string +outputs: + data: + value: ${{ steps.get-cov-report.outputs.data }} + +runs: + using: composite + steps: + - name: 'Checkout source code' + uses: actions/checkout@v4 + with: + fetch-depth: 100 + + - name: 'Reset to specific commit' + run: | + echo "${{ inputs.reset-commit-id }}" + git reset --hard ${{ inputs.reset-commit-id }} + shell: bash + if: ${{ inputs.reset-commit-id }} != "" + + - name: 'Install dependencies' + run: | + apt --help &>/dev/null + if [ $? -eq 0 ];then + sudo apt-get install -y openssl libssl-dev llvm jq + else + yum --help &>/dev/null + if [ $? -eq 0 ];then + sudo yum install -y openssl openssl-devel llvm jq + else + exit 1 + fi + fi + shell: bash + + - name: 'Install newer clang' + run: | + apt --help &>/dev/null + if [ $? -eq 0 ];then + sudo rm /etc/apt/sources.list.d/microsoft-prod.list + sudo apt-get update -y + else + yum --help &>/dev/null + [ $? -eq 0 ] && sudo yum update -y + fi + wget https://apt.llvm.org/llvm.sh -O llvm.sh + chmod +x ./llvm.sh + sudo ./llvm.sh 17 + shell: bash + + - name: 'Build and test' + id: get-cov-report + run: | + cp -r src/coro_rpc/tests/openssl_files . + rm -rf build + mkdir -p build + cd build + CC=clang-17 CXX=clang++-17 cmake .. -DCOVERAGE_TEST=ON -DYLT_ENABLE_SSL=ON -DBUILD_EXAMPLES=OFF -DBUILD_BENCHMARK=OFF + make -j + export LLVM_PROFILE_FILE="test_ylt-%m.profraw" + cd output/tests + find . -maxdepth 1 -type f -executable | xargs -I {} sh -c '{}' + llvm-profdata merge -sparse test_ylt-*.profraw -o test_ylt.profdata + if [ -n "${{ inputs.reset-commit-id }}" ];then + report=base-ylt-cov-report + else + report=ylt-cov-report + fi + llvm-cov show $(find . -maxdepth 1 -type f -executable | awk '{print "-object " $0}' | xargs) -instr-profile=test_ylt.profdata -format=html -output-dir=$report -ignore-filename-regex='thirdparty|src|template_switch|concurrentqueue|dragonbox_to_chars|dragonbox|expected' -show-instantiations=false + echo "path=build/output/tests/$report" >> $GITHUB_OUTPUT + cov_data=$(grep -w '
Totals
' $report/index.html | awk -F 'Totals' '{print $NF}' | cut -d ')' -f 2 | awk -F '>' '{print $NF}' | awk -F '%' '{print $1}') + echo "coverage data: $cov_data" + echo "report=$report" >> $GITHUB_OUTPUT + echo "data=$cov_data" >> $GITHUB_OUTPUT + shell: bash + + - name: 'Upload coverage results' + uses: actions/upload-artifact@v4.3.6 + with: + name: ${{ steps.get-cov-report.outputs.report }} + path: ${{ steps.get-cov-report.outputs.path }} + + - name: 'Checkout source code' + uses: actions/checkout@v4 + if: ${{ inputs.reset-commit-id }} != "" diff --git a/.github/workflows/clang-format.yml b/.github/workflows/clang-format.yml index 0f9cd2973..4c3abfe43 100644 --- a/.github/workflows/clang-format.yml +++ b/.github/workflows/clang-format.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: install clang-format diff --git a/.github/workflows/clean_cache.yml b/.github/workflows/clean_cache.yml index 344c2e61e..243ad816f 100644 --- a/.github/workflows/clean_cache.yml +++ b/.github/workflows/clean_cache.yml @@ -16,7 +16,7 @@ jobs: contents: read steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Cleanup run: | diff --git a/.github/workflows/comment.yml b/.github/workflows/comment.yml new file mode 100644 index 000000000..b886d94b8 --- /dev/null +++ b/.github/workflows/comment.yml @@ -0,0 +1,62 @@ +# +# Copyright (c) 2024 Alibaba Group Holding Limited. All Rights Reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Alibaba designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# + +name: Cov Report Comment + +on: + workflow_run: + workflows: ["Ubuntu 22.04 (llvm cov)"] + types: + - completed + +jobs: + comment: + runs-on: ubuntu-22.04 + permissions: write-all + steps: + - name: 'Download artifact' + uses: actions/github-script@v6 + with: + script: | + let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.payload.workflow_run.id, + }); + let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => { + return artifact.name == "action_msg" + })[0]; + let download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + }); + let fs = require('fs'); + fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/action_msg.zip`, Buffer.from(download.data)); + - name: 'Comment' + run: | + unzip action_msg.zip + pr_id=`grep 'pr_id' action_msg | cut -d '=' -f 2` + action_id=`grep 'action_id' action_msg | cut -d '=' -f 2` + content=$(echo "for detail, [goto summary](https://github.com/${{ github.repository_owner }}/${{ github.event.repository.name }}/actions/runs/${action_id}) download Artifacts \`base-ylt-cov-report\`(base commit coverage report) and \`ylt-cov-report\`(current pull request coverage report)") + curl -L -X POST "https://api.github.com/repos/${{github.repository}}/issues/${pr_id}/comments" -H "Authorization: Bearer ${{github.token}}" -H 'Content-Type: application/json' -d "{\"body\": \"$content\"}" + shell: bash diff --git a/.github/workflows/linux_llvm_cov.yml b/.github/workflows/linux_llvm_cov.yml index 08cc89cc2..82429e389 100644 --- a/.github/workflows/linux_llvm_cov.yml +++ b/.github/workflows/linux_llvm_cov.yml @@ -1,70 +1,108 @@ +# +# Copyright (c) 2024 Alibaba Group Holding Limited. All Rights Reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Alibaba designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# + name: Ubuntu 22.04 (llvm cov) on: - pull_request_target: + pull_request: branches: - main - fix_coverage_show + - 0.3.8.2 + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: - build: + prerequisites: runs-on: ubuntu-22.04 - + permissions: write-all + outputs: + id: ${{ steps.get-base-commit.outputs.id }} steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Install Dependencies + - name: 'Get Base Commit id' + id: get-base-commit run: | - sudo apt-get install openssl - sudo apt-get install libssl-dev - sudo apt-get install llvm - - - name: Install newer Clang + sudo apt install -y jq + base_commit_id=$(curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }} | jq -r .base.sha) + echo "::set-output name=id::$base_commit_id" + shell: bash + - name: 'Store Message' run: | - sudo rm /etc/apt/sources.list.d/microsoft-prod.list - sudo apt-get update - wget https://apt.llvm.org/llvm.sh - chmod +x ./llvm.sh - sudo ./llvm.sh 17 + mkdir -p ./msg + echo "action_id=${{github.run_id}}" >> ./msg/action_msg + echo "pr_id=${{github.event.pull_request.number}}" >> ./msg/action_msg + shell: bash + - name: 'Upload Action Message' + uses: actions/upload-artifact@v4 + with: + name: action_msg + path: msg/ - - name: Run Coverage - run: | - ls - cp -r src/coro_rpc/tests/openssl_files . - ls - mkdir build && cd build - CC=clang-17 CXX=clang++-17 cmake .. -DCOVERAGE_TEST=ON -DYLT_ENABLE_SSL=ON - make -j - export LLVM_PROFILE_FILE="test_ylt-%m.profraw" - cd output - cd tests - ./coro_io_test - ./coro_rpc_test - ./easylog_test - ./struct_pack_test - ./struct_pack_test_with_optimize - llvm-profdata merge -sparse test_ylt-*.profraw -o test_ylt.profdata - llvm-cov show coro_io_test -object coro_rpc_test -object easylog_test -object struct_pack_test -object struct_pack_test_with_optimize -instr-profile=test_ylt.profdata -format=html -output-dir=../../.coverage_llvm_cov -ignore-filename-regex="thirdparty|asio|src" -show-instantiations=false - echo "Done!" + base-cov-test: + needs: prerequisites + runs-on: ubuntu-22.04 + outputs: + data: ${{ steps.base-cov.outputs.data }} + steps: + - name: 'Checkout source code' + uses: actions/checkout@v4 + - name: 'Base coverage test' + id: base-cov + uses: ./.github/actions/coverage + with: + reset-commit-id: ${{ needs.prerequisites.outputs.id }} - - name: Upload Coverage Results - uses: actions/upload-artifact@v3 + cov-test: + needs: prerequisites + runs-on: ubuntu-22.04 + outputs: + data: ${{ steps.cov.outputs.data }} + steps: + - name: 'Checkout source code' + uses: actions/checkout@v4 + - name: 'Coverage test' + id: cov + uses: ./.github/actions/coverage with: - name: llvm-cov - path: ${{ github.workspace }}/build/.coverage_llvm_cov + reset-commit-id: "" - - name: Create Code Coverage Report - working-directory: ${{github.workspace}}/build/output/tests + compare-cov-data: + needs: + - base-cov-test + - cov-test + runs-on: ubuntu-22.04 + steps: + - name: 'Compare data' run: | - echo "Code Coverage Report" > tmp.log - echo "for detail, [goto summary](https://github.com/${{ github.repository_owner }}/${{ github.event.repository.name }}/actions/runs/${{github.run_id}}) download Artifacts `llvm-cov`" >> tmp.log - echo "\`\`\`" >> tmp.log - llvm-cov report coro_io_test -object coro_rpc_test -object easylog_test -object struct_pack_test -object struct_pack_test_with_optimize -instr-profile=test_ylt.profdata -ignore-filename-regex="thirdparty|asio|src" -show-region-summary=false >> tmp.log - echo "\`\`\`" >> tmp.log - - - name: Create Comment - uses: peter-evans/create-or-update-comment@v2 - with: - issue-number: ${{ github.event.pull_request.number }} - body-file: '${{github.workspace}}/build/output/tests/tmp.log' + sudo apt install -y bc + result=$(echo "${{ needs.cov-test.outputs.data }} > 70" | bc) + if [ "$result" -ne 1 ];then + echo "coverage cannot be lower than 70%!" + exit 1 + fi + result=$(echo "${{ needs.cov-test.outputs.data }} > $(echo "${{ needs.base-cov-test.outputs.data}} * 0.97" | bc)" | bc) + if [ "$result" -ne 1 ];then + echo "coverage has decreased over 3%!" + exit 1 + fi + shell: bash diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml index 790178fc0..08e3242b7 100644 --- a/.github/workflows/mac.yml +++ b/.github/workflows/mac.yml @@ -14,11 +14,11 @@ jobs: mode: [ Debug ] #mode: [Release, Debug] ssl: [ ON, OFF ] - runs-on: macos-12 + runs-on: macos-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 # - name: SetUp HomeBrew # id: set-up-homebrew diff --git a/.github/workflows/s390x.yml b/.github/workflows/s390x.yml index 613aaee9e..2fd459c54 100644 --- a/.github/workflows/s390x.yml +++ b/.github/workflows/s390x.yml @@ -12,7 +12,7 @@ jobs: name: Build Linux on s390x arch and run unit tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: uraimo/run-on-arch-action@v2 name: Test id: runcmd diff --git a/.github/workflows/ubuntu_clang.yml b/.github/workflows/ubuntu_clang.yml index 48fdd379f..13f2e79ec 100644 --- a/.github/workflows/ubuntu_clang.yml +++ b/.github/workflows/ubuntu_clang.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Dependencies run: sudo apt-get install openssl libssl-dev @@ -67,7 +67,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Dependencies run: | @@ -117,7 +117,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install liburing run: sudo apt-get install liburing-dev @@ -164,7 +164,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install ninja-build tool uses: seanmiddleditch/gha-setup-ninja@master diff --git a/.github/workflows/ubuntu_gcc.yml b/.github/workflows/ubuntu_gcc.yml index d022a32f1..e83f41339 100644 --- a/.github/workflows/ubuntu_gcc.yml +++ b/.github/workflows/ubuntu_gcc.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Dependencies run: sudo apt-get install openssl libssl-dev @@ -57,7 +57,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Dependencies run: | @@ -98,7 +98,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install liburing run: sudo apt-get install liburing-dev @@ -135,7 +135,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install ninja-build tool uses: seanmiddleditch/gha-setup-ninja@master diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 4ead005d2..8a1af92ea 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -33,7 +33,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Pages uses: actions/configure-pages@v3 - name: Install package diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 08d37b547..2b6a52037 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Enable Developer Command Prompt uses: ilammy/msvc-dev-cmd@v1.12.0 with: @@ -56,7 +56,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Enable Developer Command Prompt uses: ilammy/msvc-dev-cmd@v1.12.0 with: diff --git a/include/ylt/coro_http/coro_http_client.hpp b/include/ylt/coro_http/coro_http_client.hpp index e6fc026fd..d9ceee2ca 100644 --- a/include/ylt/coro_http/coro_http_client.hpp +++ b/include/ylt/coro_http/coro_http_client.hpp @@ -15,14 +15,26 @@ */ #pragma once #ifdef YLT_ENABLE_SSL +#ifndef CINATRA_ENABLE_SSL #define CINATRA_ENABLE_SSL #endif +#endif #include +#ifndef CINATRA_LOG_ERROR #define CINATRA_LOG_ERROR ELOG_ERROR +#endif +#ifndef CINATRA_LOG_WARNING #define CINATRA_LOG_WARNING ELOG_WARN +#endif +#ifndef CINATRA_LOG_INFO #define CINATRA_LOG_INFO ELOG_INFO +#endif +#ifndef CINATRA_LOG_DEBUG #define CINATRA_LOG_DEBUG ELOG_DEBUG +#endif +#ifndef CINATRA_LOG_TRACE #define CINATRA_LOG_TRACE ELOG_TRACE +#endif #include diff --git a/include/ylt/coro_http/coro_http_server.hpp b/include/ylt/coro_http/coro_http_server.hpp index a4bb1ecf4..16ee9059f 100644 --- a/include/ylt/coro_http/coro_http_server.hpp +++ b/include/ylt/coro_http/coro_http_server.hpp @@ -15,14 +15,26 @@ */ #pragma once #ifdef YLT_ENABLE_SSL +#ifndef CINATRA_ENABLE_SSL #define CINATRA_ENABLE_SSL #endif +#endif #include +#ifndef CINATRA_LOG_ERROR #define CINATRA_LOG_ERROR ELOG_ERROR +#endif +#ifndef CINATRA_LOG_WARNING #define CINATRA_LOG_WARNING ELOG_WARN +#endif +#ifndef CINATRA_LOG_INFO #define CINATRA_LOG_INFO ELOG_INFO +#endif +#ifndef CINATRA_LOG_DEBUG #define CINATRA_LOG_DEBUG ELOG_DEBUG +#endif +#ifndef CINATRA_LOG_TRACE #define CINATRA_LOG_TRACE ELOG_TRACE +#endif #include diff --git a/include/ylt/coro_rpc/impl/common_service.hpp b/include/ylt/coro_rpc/impl/common_service.hpp index c081232ed..af8bf6a89 100644 --- a/include/ylt/coro_rpc/impl/common_service.hpp +++ b/include/ylt/coro_rpc/impl/common_service.hpp @@ -80,36 +80,36 @@ inline bool init_ssl_context_helper(asio::ssl::context &context, auto key_file = fs::path(conf.base_path).append(conf.key_file); auto dh_file = fs::path(conf.base_path).append(conf.dh_file); - ELOGV(INFO, "current path %s", fs::current_path().string().data()); + ELOG_INFO << "current path " << fs::current_path().string(); if (file_exists(cert_file)) { - ELOGV(INFO, "load %s", cert_file.string().data()); + ELOG_INFO << "load " << cert_file.string(); context.use_certificate_chain_file(cert_file); } else { - ELOGV(ERROR, "no certificate file %s", cert_file.string().data()); + ELOG_ERROR << "no certificate file " << cert_file.string(); return false; } if (file_exists(key_file)) { - ELOGV(INFO, "load %s", key_file.string().data()); + ELOG_INFO << "load " << key_file.string(); context.use_private_key_file(key_file, asio::ssl::context::pem); } else { - ELOGV(ERROR, "no private key file %s", key_file.string().data()); + ELOG_ERROR << "no private file " << key_file.string(); return false; } if (file_exists(dh_file)) { - ELOGV(INFO, "load %s", dh_file.string().data()); + ELOG_INFO << "load " << dh_file.string(); context.use_tmp_dh_file(dh_file); } else { - ELOGV(INFO, "no temp dh file %s", dh_file.string().data()); + ELOG_INFO << "no temp dh file " << dh_file.string(); } return true; } catch (std::exception &e) { - ELOGV(INFO, "%s", e.what()); + ELOG_INFO << e.what(); return false; } } diff --git a/include/ylt/coro_rpc/impl/context.hpp b/include/ylt/coro_rpc/impl/context.hpp index cdeb8d0ba..aeef8bb78 100644 --- a/include/ylt/coro_rpc/impl/context.hpp +++ b/include/ylt/coro_rpc/impl/context.hpp @@ -47,13 +47,13 @@ class context_base { auto old_flag = self_->status_.exchange(context_status::start_response); if (old_flag != context_status::init) AS_UNLIKELY { - ELOGV(ERROR, "response message more than one time"); + ELOG_ERROR << "response message more than one time"; return false; } if (self_->has_closed()) AS_UNLIKELY { - ELOGV(DEBUG, "response_msg failed: connection has been closed"); + ELOG_DEBUG << "response_msg failed: connection has been closed"; return false; } return true; diff --git a/include/ylt/coro_rpc/impl/coro_connection.hpp b/include/ylt/coro_rpc/impl/coro_connection.hpp index b1e590392..be52bc77c 100644 --- a/include/ylt/coro_rpc/impl/coro_connection.hpp +++ b/include/ylt/coro_rpc/impl/coro_connection.hpp @@ -146,8 +146,8 @@ class coro_connection : public std::enable_shared_from_this { ~coro_connection() { if (!has_closed_) { #ifdef UNIT_TEST_INJECT - ELOGV(INFO, "~async_connection conn_id %d, client_id %d", conn_id_, - client_id_); + ELOG_INFO << "~async_connection conn_id " << conn_id_ << ", client_id " + << client_id_; #endif close(); } @@ -167,18 +167,18 @@ class coro_connection : public std::enable_shared_from_this { #ifdef YLT_ENABLE_SSL if (use_ssl_) { assert(ssl_stream_); - ELOGV(INFO, "begin to handshake conn_id %d", conn_id_); + ELOG_INFO << "begin to handshake conn_id " << conn_id_; reset_timer(); auto shake_ec = co_await coro_io::async_handshake( ssl_stream_, asio::ssl::stream_base::server); cancel_timer(); if (shake_ec) { - ELOGV(ERROR, "handshake failed: %s conn_id %d", - shake_ec.message().data(), conn_id_); + ELOG_ERROR << "handshake failed: " << shake_ec.message() << " conn_id " + << conn_id_; close(); } else { - ELOGV(INFO, "handshake ok conn_id %d", conn_id_); + ELOG_INFO << "handshake ok conn_id " << conn_id_; co_await start_impl(router, *ssl_stream_); } } @@ -204,22 +204,20 @@ class coro_connection : public std::enable_shared_from_this { // less than RPC_HEAD_LEN. Incomplete data will be discarded. // So, no special handling of eof is required. if (ec) { - ELOGV(INFO, "connection %d close: %s", conn_id_, ec.message().data()); + ELOG_INFO << "connection " << conn_id_ << " close: " << ec.message(); close(); break; } #ifdef UNIT_TEST_INJECT client_id_ = req_head_tmp.seq_num; - ELOGV(INFO, "conn_id %d, client_id %d", conn_id_, client_id_); + ELOG_INFO << "conn_id " << conn_id_ << " client_id " << client_id_; #endif #ifdef UNIT_TEST_INJECT if (g_action == inject_action::close_socket_after_read_header) { - ELOGV(WARN, - "inject action: close_socket_after_read_header, conn_id %d, " - "client_id %d", - conn_id_, client_id_); + ELOG_WARN << "inject action: close_socket_after_read_header, conn_id " + << conn_id_ << ", client_id " << client_id_; close(); break; } @@ -250,7 +248,7 @@ class coro_connection : public std::enable_shared_from_this { if (!serialize_proto.has_value()) AS_UNLIKELY { - ELOGV(ERROR, "bad serialize protocol type, conn_id %d", conn_id_); + ELOG_ERROR << "bad serialize protocol type, conn_id " << conn_id_; close(); break; } @@ -265,8 +263,8 @@ class coro_connection : public std::enable_shared_from_this { if (ec) AS_UNLIKELY { - ELOGV(ERROR, "read error: %s, conn_id %d", ec.message().data(), - conn_id_); + ELOG_ERROR << "read error: " << ec.message() << ", conn_id " + << conn_id_; close(); break; } @@ -317,8 +315,9 @@ class coro_connection : public std::enable_shared_from_this { } #ifdef UNIT_TEST_INJECT if (g_action == inject_action::close_socket_after_send_length) { - ELOGV(WARN, "inject action: close_socket_after_send_length", conn_id_, - client_id_); + ELOG_WARN + << "inject action: close_socket_after_send_length , conn_id " + << conn_id_ << ", client_id " << client_id_; std::string header_buf = rpc_protocol::prepare_response( resp_buf, req_head, 0, resp_err, ""); co_await coro_io::async_write(socket, asio::buffer(header_buf)); @@ -326,11 +325,8 @@ class coro_connection : public std::enable_shared_from_this { break; } if (g_action == inject_action::server_send_bad_rpc_result) { - ELOGV( - WARN, - "inject action: server_send_bad_rpc_result conn_id %d, client_id " - "%d", - conn_id_, client_id_); + ELOG_WARN << "inject action: server_send_bad_rpc_result , conn_id " + << conn_id_ << ", client_id " << client_id_; resp_buf[0] = resp_buf[0] + 1; } #endif @@ -362,8 +358,7 @@ class coro_connection : public std::enable_shared_from_this { if (resp_err) { resp_error_msg = std::move(resp_buf); resp_buf = {}; - ELOGV(WARNING, "rpc route/execute error, error msg: %s", - resp_error_msg.data()); + ELOG_WARN << "rpc route/execute error, error msg: " << resp_error_msg; } std::string header_buf = rpc_protocol::prepare_response( resp_buf, req_head, attachment().length(), resp_err, resp_error_msg); @@ -460,12 +455,12 @@ class coro_connection : public std::enable_shared_from_this { rpc_conn self) noexcept { if (has_closed()) AS_UNLIKELY { - ELOGV(DEBUG, "response_msg failed: connection has been closed"); + ELOG_DEBUG << "response_msg failed: connection has been closed"; co_return; } #ifdef UNIT_TEST_INJECT if (g_action == inject_action::close_socket_after_send_length) { - ELOGV(WARN, "inject action: close_socket_after_send_length"); + ELOG_WARN << "inject action: close_socket_after_send_length"; body_buf.clear(); } #endif @@ -488,11 +483,9 @@ class coro_connection : public std::enable_shared_from_this { auto &msg = write_queue_.front(); #ifdef UNIT_TEST_INJECT if (g_action == inject_action::force_inject_connection_close_socket) { - ELOGV( - WARN, - "inject action: force_inject_connection_close_socket, conn_id %d, " - "client_id %d", - conn_id_, client_id_); + ELOG_WARN + << "inject action: force_inject_connection_close_socket , conn_id " + << conn_id_ << ", client_id " << client_id_; close(); co_return; } @@ -535,8 +528,8 @@ class coro_connection : public std::enable_shared_from_this { } if (ret.first) AS_UNLIKELY { - ELOGV(ERROR, "%s, %s", ret.first.message().data(), - "async_write error"); + ELOG_ERROR << ret.first.message() << ", " + << "async_write error"; close(); co_return; } @@ -544,10 +537,8 @@ class coro_connection : public std::enable_shared_from_this { } #ifdef UNIT_TEST_INJECT if (g_action == inject_action::close_socket_after_send_length) { - ELOGV(INFO, - "inject action: close_socket_after_send_length, conn_id %d, " - "client_id %d", - conn_id_, client_id_); + ELOG_INFO << "inject action: close_socket_after_send_length , conn_id " + << conn_id_ << ", client_id " << client_id_; // Attention: close ssl stream after read error // otherwise, server will crash close(); @@ -557,7 +548,7 @@ class coro_connection : public std::enable_shared_from_this { } void close() { - ELOGV(TRACE, "connection closed"); + ELOG_TRACE << "connection closed"; if (has_closed_) { return; } @@ -580,10 +571,10 @@ class coro_connection : public std::enable_shared_from_this { [this, self = shared_from_this()](asio::error_code const &ec) { if (!ec) { #ifdef UNIT_TEST_INJECT - ELOGV(INFO, "close timeout client_id %d conn_id %d", client_id_, - conn_id_); + ELOG_INFO << "close timeout client client_id " << client_id_ + << ", conn_id " << conn_id_; #else - ELOGV(INFO, "close timeout client conn_id %d", conn_id_); + ELOG_INFO << "close timeout client conn_id " << conn_id_; #endif close(); diff --git a/include/ylt/coro_rpc/impl/coro_rpc_client.hpp b/include/ylt/coro_rpc/impl/coro_rpc_client.hpp index e84b09f6c..8ce0e8335 100644 --- a/include/ylt/coro_rpc/impl/coro_rpc_client.hpp +++ b/include/ylt/coro_rpc/impl/coro_rpc_client.hpp @@ -161,14 +161,14 @@ class coro_rpc_client { "client has been closed"}; struct config { uint64_t client_id = get_global_client_id(); - std::chrono::milliseconds timeout_duration = - std::chrono::milliseconds{30000}; - std::string host; - std::string port; + std::optional connect_timeout_duration; + std::optional request_timeout_duration; + std::string host{}; + std::string port{}; bool enable_tcp_no_delay = true; #ifdef YLT_ENABLE_SSL - std::filesystem::path ssl_cert_path; - std::string ssl_domain; + std::filesystem::path ssl_cert_path{}; + std::string ssl_domain{}; #endif }; @@ -203,6 +203,8 @@ class coro_rpc_client { const config &get_config() const { return config_; } + config &get_config() { return config_; } + [[nodiscard]] bool init_config(const config &conf) { config_ = conf; #ifdef YLT_ENABLE_SSL @@ -228,12 +230,12 @@ class coro_rpc_client { * * @param host server address * @param port server port - * @param timeout_duration RPC call timeout + * @param connect_timeout_duration RPC call timeout seconds * @return error code */ [[nodiscard]] async_simple::coro::Lazy connect( std::string host, std::string port, - std::chrono::steady_clock::duration timeout_duration = + std::chrono::steady_clock::duration connect_timeout_duration = std::chrono::seconds(30)) { auto lock_ok = connect_mutex_.tryLock(); if (!lock_ok) { @@ -241,44 +243,32 @@ class coro_rpc_client { co_return err_code{}; // do nothing, someone has reconnect the client } - config_.host = std::move(host); - config_.port = std::move(port); - config_.timeout_duration = - std::chrono::duration_cast(timeout_duration); + + if (config_.host.empty()) { + config_.host = std::move(host); + } + if (config_.port.empty()) { + config_.port = std::move(port); + } + if (!config_.connect_timeout_duration) { + config_.connect_timeout_duration = + std::chrono::duration_cast( + connect_timeout_duration); + } + auto ret = co_await connect_impl(); connect_mutex_.unlock(); co_return std::move(ret); } [[nodiscard]] async_simple::coro::Lazy connect( std::string_view endpoint, - std::chrono::steady_clock::duration timeout_duration = + std::chrono::steady_clock::duration connect_timeout_duration = std::chrono::seconds(30)) { auto pos = endpoint.find(':'); - auto lock_ok = connect_mutex_.tryLock(); - if (!lock_ok) { - co_await connect_mutex_.coScopedLock(); - co_return err_code{}; - // do nothing, someone has reconnect the client - } - config_.host = endpoint.substr(0, pos); - config_.port = endpoint.substr(pos + 1); - config_.timeout_duration = - std::chrono::duration_cast(timeout_duration); - auto ret = co_await connect_impl(); - connect_mutex_.unlock(); - co_return std::move(ret); - } + std::string host(endpoint.substr(0, pos)); + std::string port(endpoint.substr(pos + 1)); - [[nodiscard]] async_simple::coro::Lazy connect() { - auto lock_ok = connect_mutex_.tryLock(); - if (!lock_ok) { - co_await connect_mutex_.coScopedLock(); - co_return err_code{}; - // do nothing, someone has reconnect the client - } - auto ret = co_await connect_impl(); - connect_mutex_.unlock(); - co_return std::move(ret); + return connect(std::move(host), std::move(port), connect_timeout_duration); } #ifdef YLT_ENABLE_SSL @@ -323,11 +313,12 @@ class coro_rpc_client { */ template async_simple::coro::Lazy())>> - call_for(auto duration, Args &&...args) { + call_for(auto request_timeout_duration, Args &&...args) { using return_type = decltype(get_return_type()); auto async_result = co_await co_await send_request_for_with_attachment( - duration, req_attachment_, std::forward(args)...); + request_timeout_duration, req_attachment_, + std::forward(args)...); req_attachment_ = {}; if (async_result) { control_->resp_buffer_ = async_result->release_buffer(); @@ -353,13 +344,13 @@ class coro_rpc_client { uint32_t get_client_id() const { return config_.client_id; } void close() { - // ELOGV(INFO, "client_id %d close", config_.client_id); + // ELOG_INFO << "client_id " << config_.client_id << " close"; close_socket(control_); } bool set_req_attachment(std::string_view attachment) { if (attachment.size() > UINT32_MAX) { - ELOGV(ERROR, "too large rpc attachment"); + ELOG_ERROR << "too large rpc attachment"; return false; } req_attachment_ = attachment; @@ -408,11 +399,14 @@ class coro_rpc_client { #endif control_->has_closed_ = false; - ELOGV(INFO, "client_id %d begin to connect %s", config_.client_id, - config_.port.data()); - timeout(*this->timer_, config_.timeout_duration, "connect timer canceled") - .start([](auto &&) { - }); + ELOG_INFO << "client_id " << config_.client_id << " begin to connect " + << config_.port; + auto conn_timeout_dur = *config_.connect_timeout_duration; + if (conn_timeout_dur.count() >= 0) { + timeout(*this->timer_, conn_timeout_dur, "connect timer canceled") + .start([](auto &&) { + }); + } std::error_code ec = co_await coro_io::async_connect( &control_->executor_, control_->socket_, config_.host, config_.port); @@ -427,7 +421,7 @@ class coro_rpc_client { } if (control_->is_timeout_) { - ELOGV(WARN, "client_id %d connect timeout", config_.client_id); + ELOG_WARN << "client_id " << config_.client_id << " connect timeout"; co_return errc::timed_out; } if (config_.enable_tcp_no_delay == true) { @@ -440,8 +434,8 @@ class coro_rpc_client { auto shake_ec = co_await coro_io::async_handshake( control_->ssl_stream_, asio::ssl::stream_base::client); if (shake_ec) { - ELOGV(WARN, "client_id %d handshake failed: %s", config_.client_id, - shake_ec.message().data()); + ELOG_WARN << "client_id " << config_.client_id + << " handshake failed: " << shake_ec.message(); co_return errc::not_connected; } } @@ -453,16 +447,15 @@ class coro_rpc_client { [[nodiscard]] bool init_ssl_impl() { try { ssl_init_ret_ = false; - ELOGV(INFO, "init ssl: %s", config_.ssl_domain.data()); + ELOG_INFO << "init ssl: " << config_.ssl_domain; auto &cert_file = config_.ssl_cert_path; - ELOGV(INFO, "current path %s", - std::filesystem::current_path().string().data()); + ELOG_INFO << "current path: " << std::filesystem::current_path().string(); if (file_exists(cert_file)) { - ELOGV(INFO, "load %s", cert_file.string().data()); + ELOG_INFO << "load " << cert_file.string(); ssl_ctx_.load_verify_file(cert_file); } else { - ELOGV(INFO, "no certificate file %s", cert_file.string().data()); + ELOG_INFO << "no certificate file " << cert_file.string(); return ssl_init_ret_; } ssl_ctx_.set_verify_mode(asio::ssl::verify_peer); @@ -473,7 +466,7 @@ class coro_rpc_client { control_->socket_, ssl_ctx_); ssl_init_ret_ = true; } catch (std::exception &e) { - ELOGV(ERROR, "init ssl failed: %s", e.what()); + ELOG_ERROR << "init ssl failed: " << e.what(); } return ssl_init_ret_; } @@ -579,7 +572,7 @@ class coro_rpc_client { #endif auto sz = buffer.size() - coro_rpc_protocol::REQ_HEAD_LEN; if (sz > UINT32_MAX) { - ELOGV(ERROR, "too large rpc body"); + ELOG_ERROR << "too large rpc body"; return {}; } header.length = sz; @@ -626,7 +619,7 @@ class coro_rpc_client { } has_error = true; // deserialize failed. - ELOGV(WARNING, "deserilaize rpc result failed"); + ELOG_WARN << "deserilaize rpc result failed"; err = {errc::invalid_rpc_result, "failed to deserialize rpc return value"}; return rpc_result{unexpect_t{}, std::move(err)}; } @@ -747,13 +740,13 @@ class coro_rpc_client { private: template async_simple::coro::Lazy send_request_for_impl( - auto duration, uint32_t &id, coro_io::period_timer &timer, + auto request_timeout_duration, uint32_t &id, coro_io::period_timer &timer, std::string_view attachment, Args &&...args) { using R = decltype(get_return_type()); if (control_->has_closed_) AS_UNLIKELY { - ELOGV(ERROR, "client has been closed, please re-connect"); + ELOG_ERROR << "client has been closed, please re-connect"; co_return rpc_error{errc::io_error, "client has been closed, please re-connect"}; } @@ -766,9 +759,10 @@ class coro_rpc_client { static_check(); - if (duration.count() > 0) { - timeout(timer, duration, "rpc call timer canceled").start([](auto &&) { - }); + if (request_timeout_duration.count() >= 0) { + timeout(timer, request_timeout_duration, "rpc call timer canceled") + .start([](auto &&) { + }); } #ifdef YLT_ENABLE_SSL @@ -965,16 +959,20 @@ class coro_rpc_client { template async_simple::coro::Lazy())>>> - send_request_for_with_attachment(auto time_out_duration, + send_request_for_with_attachment(auto request_timeout_duration, std::string_view request_attachment, Args &&...args) { using rpc_return_t = decltype(get_return_type()); recving_guard guard(control_.get()); uint32_t id; + if (!config_.request_timeout_duration) { + config_.request_timeout_duration = request_timeout_duration; + } + auto timer = std::make_unique( control_->executor_.get_asio_executor()); auto result = co_await send_request_for_impl( - time_out_duration, id, *timer, request_attachment, + *config_.request_timeout_duration, id, *timer, request_attachment, std::forward(args)...); auto &control = *control_; if (!result) { @@ -1041,7 +1039,7 @@ class coro_rpc_client { if (g_action == inject_action::client_close_socket_after_send_header) { ret = co_await coro_io::async_write( socket, asio::buffer(buffer.data(), coro_rpc_protocol::REQ_HEAD_LEN)); - ELOGV(INFO, "client_id %d close socket", config_.client_id); + ELOG_INFO << "client_id " << config_.client_id << " close socket"; close(); co_return rpc_error{errc::io_error, ret.first.message()}; } @@ -1050,7 +1048,7 @@ class coro_rpc_client { ret = co_await coro_io::async_write( socket, asio::buffer(buffer.data(), coro_rpc_protocol::REQ_HEAD_LEN - 1)); - ELOGV(INFO, "client_id %d close socket", config_.client_id); + ELOG_INFO << "client_id " << config_.client_id << " close socket"; close(); co_return rpc_error{errc::io_error, ret.first.message()}; } @@ -1058,7 +1056,7 @@ class coro_rpc_client { inject_action::client_shutdown_socket_after_send_header) { ret = co_await coro_io::async_write( socket, asio::buffer(buffer.data(), coro_rpc_protocol::REQ_HEAD_LEN)); - ELOGV(INFO, "client_id %d shutdown", config_.client_id); + ELOG_INFO << "client_id " << config_.client_id << " shutdown"; control_->socket_.shutdown(asio::ip::tcp::socket::shutdown_send); co_return rpc_error{errc::io_error, ret.first.message()}; } @@ -1106,8 +1104,8 @@ class coro_rpc_client { #endif #ifdef UNIT_TEST_INJECT if (g_action == inject_action::client_close_socket_after_send_payload) { - ELOGV(INFO, "client_id %d client_close_socket_after_send_payload", - config_.client_id); + ELOG_INFO << "client_id " << config_.client_id + << " client_close_socket_after_send_payload"; close(); co_return rpc_error{errc::io_error, ret.first.message()}; } diff --git a/include/ylt/coro_rpc/impl/coro_rpc_server.hpp b/include/ylt/coro_rpc/impl/coro_rpc_server.hpp index 3b59f6957..d38a57c92 100644 --- a/include/ylt/coro_rpc/impl/coro_rpc_server.hpp +++ b/include/ylt/coro_rpc/impl/coro_rpc_server.hpp @@ -118,7 +118,7 @@ class coro_rpc_server_base { } ~coro_rpc_server_base() { - ELOGV(INFO, "coro_rpc_server will quit"); + ELOG_INFO << "coro_rpc_server will quit"; stop(); } @@ -153,10 +153,10 @@ class coro_rpc_server_base { std::unique_lock lock(start_mtx_); if (flag_ != stat::init) { if (flag_ == stat::started) { - ELOGV(INFO, "start again"); + ELOG_INFO << "start again"; } else if (flag_ == stat::stop) { - ELOGV(INFO, "has stoped"); + ELOG_INFO << "has stoped"; } return make_error_future( coro_rpc::err_code{coro_rpc::errc::server_has_ran}); @@ -206,7 +206,7 @@ class coro_rpc_server_base { return; } - ELOGV(INFO, "begin to stop coro_rpc_server, conn size %d", conns_.size()); + ELOG_INFO << "begin to stop coro_rpc_server, conn size " << conns_.size(); if (flag_ == stat::started) { close_acceptor(); @@ -221,15 +221,15 @@ class coro_rpc_server_base { conns_.clear(); } - ELOGV(INFO, "wait for server's thread-pool finish all work."); + ELOG_INFO << "wait for server's thread-pool finish all work."; pool_.stop(); - ELOGV(INFO, "server's thread-pool finished."); + ELOG_INFO << "server's thread-pool finished."; } if (thd_.joinable()) { thd_.join(); } - ELOGV(INFO, "stop coro_rpc_server ok"); + ELOG_INFO << "stop coro_rpc_server ok."; flag_ = stat::stop; } @@ -319,7 +319,7 @@ class coro_rpc_server_base { private: coro_rpc::err_code listen() { - ELOGV(INFO, "begin to listen"); + ELOG_INFO << "begin to listen"; using asio::ip::tcp; asio::error_code ec; asio::ip::tcp::resolver::query query(address_, std::to_string(port_)); @@ -328,15 +328,15 @@ class coro_rpc_server_base { asio::ip::tcp::resolver::iterator it_end; if (ec || it == it_end) { - ELOGV(ERROR, "resolve address %s error : %s", address_.data(), - ec.message().data()); + ELOG_ERROR << "resolve address " << address_ + << " error: " << ec.message(); return coro_rpc::errc::bad_address; } auto endpoint = it->endpoint(); acceptor_.open(endpoint.protocol(), ec); if (ec) { - ELOGV(ERROR, "open failed, error : %s", ec.message().data()); + ELOG_ERROR << "open failed, error: " << ec.message(); return coro_rpc::errc::open_error; } #ifdef __GNUC__ @@ -344,8 +344,7 @@ class coro_rpc_server_base { #endif acceptor_.bind(endpoint, ec); if (ec) { - ELOGV(ERROR, "bind port %d error : %s", port_.load(), - ec.message().data()); + ELOG_ERROR << "bind port " << port_.load() << " error: " << ec.message(); acceptor_.cancel(ec); acceptor_.close(ec); return coro_rpc::errc::address_in_used; @@ -355,8 +354,8 @@ class coro_rpc_server_base { #endif acceptor_.listen(asio::socket_base::max_listen_connections, ec); if (ec) { - ELOGV(ERROR, "port %d listen error : %s", port_.load(), - ec.message().data()); + ELOG_ERROR << "port " << port_.load() + << " listen error: " << ec.message(); acceptor_.cancel(ec); acceptor_.close(ec); return coro_rpc::errc::listen_error; @@ -364,13 +363,13 @@ class coro_rpc_server_base { auto end_point = acceptor_.local_endpoint(ec); if (ec) { - ELOGV(ERROR, "get local endpoint port %d error : %s", port_.load(), - ec.message().data()); + ELOG_ERROR << "get local endpoint port " << port_.load() + << " error: " << ec.message(); return coro_rpc::errc::address_in_used; } port_ = end_point.port(); - ELOGV(INFO, "listen port %d successfully", port_.load()); + ELOG_INFO << "listen port " << port_.load() << " successfully"; return {}; } @@ -390,7 +389,7 @@ class coro_rpc_server_base { } #endif if (error) { - ELOGV(INFO, "accept failed, error: %s", error.message().data()); + ELOG_INFO << "accept failed, error: " << error.message(); if (error == asio::error::operation_aborted || error == asio::error::bad_descriptor) { acceptor_close_waiter_.set_value(); @@ -400,7 +399,7 @@ class coro_rpc_server_base { } int64_t conn_id = ++conn_id_; - ELOGV(INFO, "new client conn_id %d coming", conn_id); + ELOG_INFO << "new client conn_id " << conn_id << " coming"; if (is_enable_tcp_no_delay_) { socket.set_option(asio::ip::tcp::no_delay(true), error); } diff --git a/include/ylt/coro_rpc/impl/protocol/coro_rpc_protocol.hpp b/include/ylt/coro_rpc/impl/protocol/coro_rpc_protocol.hpp index b74a566e3..c22fcbc80 100644 --- a/include/ylt/coro_rpc/impl/protocol/coro_rpc_protocol.hpp +++ b/include/ylt/coro_rpc/impl/protocol/coro_rpc_protocol.hpp @@ -145,7 +145,7 @@ struct coro_rpc_protocol { resp_head.attach_length = attachment_len; if (attachment_len > UINT32_MAX) AS_UNLIKELY { - ELOGV(ERROR, "attachment larger than 4G:%d", attachment_len); + ELOG_ERROR << "attachment larger than 4G: " << attachment_len; rpc_err_code = coro_rpc::errc::message_too_large; err_msg_buf = "attachment larger than 4G:" + std::to_string(attachment_len) + "B"; @@ -154,7 +154,7 @@ struct coro_rpc_protocol { else if (rpc_result.size() > UINT32_MAX) AS_UNLIKELY { auto sz = rpc_result.size(); - ELOGV(ERROR, "body larger than 4G:%d", sz); + ELOG_ERROR << "body larger than 4G: " << sz; rpc_err_code = coro_rpc::errc::message_too_large; err_msg_buf = "body larger than 4G:" + std::to_string(attachment_len) + "B"; diff --git a/include/ylt/coro_rpc/impl/router.hpp b/include/ylt/coro_rpc/impl/router.hpp index 1414b8e4e..8da6c4391 100644 --- a/include/ylt/coro_rpc/impl/router.hpp +++ b/include/ylt/coro_rpc/impl/router.hpp @@ -103,7 +103,7 @@ class router { template void regist_one_handler(Self *self) { if (self == nullptr) - AS_UNLIKELY { ELOGV(CRITICAL, "null connection!"); } + AS_UNLIKELY { ELOG_CRITICAL << "null connection!"; } route_key key{}; @@ -128,7 +128,7 @@ class router { template void regist_one_handler_impl(Self *self, const route_key &key) { if (self == nullptr) - AS_UNLIKELY { ELOGV(CRITICAL, "null connection!"); } + AS_UNLIKELY { ELOG_CRITICAL << "null connection!"; } constexpr auto name = get_func_name(); using return_type = util::function_return_type_t; @@ -143,7 +143,7 @@ class router { return std::visit(visitor, protocols); }); if (!it.second) { - ELOGV(CRITICAL, "duplication function %s register!", name.data()); + ELOG_CRITICAL << "duplication function " << name << " registered!"; } } else { @@ -161,7 +161,7 @@ class router { protocols); }); if (!it.second) { - ELOGV(CRITICAL, "duplication function %s register!", name.data()); + ELOG_CRITICAL << "duplication function " << name << " registered!"; } } @@ -197,7 +197,7 @@ class router { return std::visit(visitor, protocols); }); if (!it.second) { - ELOGV(CRITICAL, "duplication function %s register!", name.data()); + ELOG_CRITICAL << "duplication function " << name << " registered!"; } } else { @@ -214,7 +214,7 @@ class router { protocols); }); if (!it.second) { - ELOGV(CRITICAL, "duplication function %s register!", name.data()); + ELOG_CRITICAL << "duplication function " << name << " registered!"; } } id2name_.emplace(key, name); @@ -244,8 +244,7 @@ class router { AS_LIKELY { try { #ifndef NDEBUG - ELOGV(INFO, "route function name: %s", get_name(route_key).data()); - + ELOG_INFO << "route function name: " << get_name(route_key); #endif // clang-format off co_return co_await (*handler)(data, protocols); @@ -274,7 +273,7 @@ class router { AS_LIKELY { try { #ifndef NDEBUG - ELOGV(INFO, "route function name: %s", get_name(route_key).data()); + ELOG_INFO << "route function name: " << get_name(route_key); #endif return (*handler)(data, context_info, protocols); } catch (coro_rpc::rpc_error& err) { diff --git a/include/ylt/metric/counter.hpp b/include/ylt/metric/counter.hpp index d7f29e9ec..82775cd96 100644 --- a/include/ylt/metric/counter.hpp +++ b/include/ylt/metric/counter.hpp @@ -1,11 +1,13 @@ #pragma once + #include #include #include -#include +#include +#include #include -#include "metric.hpp" +#include "dynamic_metric.hpp" #include "thread_local_value.hpp" namespace ylt::metric { @@ -13,97 +15,49 @@ enum class op_type_t { INC, DEC, SET }; #ifdef CINATRA_ENABLE_METRIC_JSON struct json_counter_metric_t { - std::map labels; + std::vector labels; std::variant value; }; YLT_REFL(json_counter_metric_t, labels, value); struct json_counter_t { - std::string name; - std::string help; - std::string type; + std::string_view name; + std::string_view help; + std::string_view type; + std::vector labels_name; std::vector metrics; }; -YLT_REFL(json_counter_t, name, help, type, metrics); +YLT_REFL(json_counter_t, name, help, type, labels_name, metrics); #endif -template -inline void set_value(T &label_val, value_type value, op_type_t type) { - switch (type) { - case op_type_t::INC: { -#ifdef __APPLE__ - if constexpr (std::is_floating_point_v) { - mac_os_atomic_fetch_add(&label_val, value); - } - else { - label_val += value; - } -#else - label_val += value; -#endif - } break; - case op_type_t::DEC: -#ifdef __APPLE__ - if constexpr (std::is_floating_point_v) { - mac_os_atomic_fetch_sub(&label_val, value); - } - else { - label_val -= value; - } -#else - label_val -= value; -#endif - break; - case op_type_t::SET: - label_val = value; - break; - } -} - template class basic_static_counter : public static_metric { public: // static counter, no labels, only contains an atomic value. basic_static_counter(std::string name, std::string help, - size_t dupli_count = 2) - : static_metric(MetricType::Counter, std::move(name), std::move(help)) { - init_thread_local(dupli_count); - } + uint32_t dupli_count = (std::min)( + 128u, std::thread::hardware_concurrency())) + : static_metric(MetricType::Counter, std::move(name), std::move(help)), + dupli_count_((std::max)(1u, dupli_count)), + default_label_value_(dupli_count_) {} // static counter, contains a static labels with atomic value. basic_static_counter(std::string name, std::string help, std::map labels, - uint32_t dupli_count = 2) + uint32_t dupli_count = (std::min)( + 128u, std::thread::hardware_concurrency())) : static_metric(MetricType::Counter, std::move(name), std::move(help), - std::move(labels)) { - init_thread_local(dupli_count); - } - - void init_thread_local(uint32_t dupli_count) { - if (dupli_count > 0) { - dupli_count_ = dupli_count; - default_label_value_ = {dupli_count}; - } - - g_user_metric_count++; - } - - virtual ~basic_static_counter() { g_user_metric_count--; } + std::move(labels)), + dupli_count_((std::max)(1u, dupli_count)), + default_label_value_(dupli_count_) {} void inc(value_type val = 1) { - if (val <= 0) { + if (val < 0) { return; } - -#ifdef __APPLE__ - if constexpr (std::is_floating_point_v) { - mac_os_atomic_fetch_add(&default_label_value_.local_value(), val); - } - else { - default_label_value_.inc(val); + if (!has_change_) [[unlikely]] { + has_change_ = true; } -#else default_label_value_.inc(val); -#endif } value_type update(value_type value) { @@ -123,23 +77,33 @@ class basic_static_counter : public static_metric { return; } - serialize_head(str); + metric_t::serialize_head(str); serialize_default_label(str, value); } #ifdef CINATRA_ENABLE_METRIC_JSON void serialize_to_json(std::string &str) override { - if (default_label_value_.value() == 0) { + auto value = default_label_value_.value(); + if (value == 0 && !has_change_) { return; } - json_counter_t counter{name_, help_, std::string(metric_name())}; - auto value = default_label_value_.value(); - counter.metrics.push_back({static_labels_, value}); + json_counter_t counter{name_, help_, metric_name()}; + + counter.labels_name.reserve(static_labels_.size()); + for (auto &[k, _] : static_labels_) { + counter.labels_name.emplace_back(k); + } + counter.metrics.resize(1); + counter.metrics[0].labels.reserve(static_labels_.size()); + for (auto &[k, _] : static_labels_) { + counter.metrics[0].labels.emplace_back(k); + } + counter.metrics[0].value = value; iguana::to_json(counter, str); } #endif - + private: protected: void serialize_default_label(std::string &str, value_type value) { str.append(name_); @@ -165,159 +129,48 @@ class basic_static_counter : public static_metric { str.pop_back(); } - thread_local_value default_label_value_; - uint32_t dupli_count_ = 2; bool has_change_ = false; -}; - -template -struct array_hash { - size_t operator()(const Key &arr) const { - unsigned int seed = 131; - unsigned int hash = 0; - - for (const auto &str : arr) { - for (auto ch : str) { - hash = hash * seed + ch; - } - } - - return (hash & 0x7FFFFFFF); - } + uint32_t dupli_count_; + thread_local_value default_label_value_; }; using counter_t = basic_static_counter; using counter_d = basic_static_counter; -template -using dynamic_metric_hash_map = std::unordered_map>; - template -class basic_dynamic_counter : public dynamic_metric { +class basic_dynamic_counter + : public dynamic_metric_impl, N> { + using Base = dynamic_metric_impl, N>; + public: // dynamic labels value basic_dynamic_counter(std::string name, std::string help, - std::array labels_name, - size_t dupli_count = 2) - : dynamic_metric(MetricType::Counter, std::move(name), std::move(help), - std::move(labels_name)), - dupli_count_(dupli_count) { - g_user_metric_count++; - } - - virtual ~basic_dynamic_counter() { g_user_metric_count--; } - - void inc(const std::array &labels_value, - value_type value = 1) { - if (value == 0) { - return; - } - - std::unique_lock lock(mtx_); - if (value_map_.size() > ylt_label_capacity) { - return; - } - auto [it, r] = value_map_.try_emplace( - labels_value, thread_local_value(dupli_count_)); - lock.unlock(); - if (r) { - g_user_metric_label_count->local_value()++; - if (ylt_label_max_age.count()) { - it->second.set_created_time(std::chrono::system_clock::now()); - } - } - set_value(it->second.local_value(), value, op_type_t::INC); + std::array labels_name) + : Base(MetricType::Counter, std::move(name), std::move(help), + std::move(labels_name)) {} + using label_key_type = const std::array &; + void inc(label_key_type labels_value, value_type value = 1) { + detail::inc_impl(Base::try_emplace(labels_value).first->value, value); } - value_type update(const std::array &labels_value, - value_type value) { - std::unique_lock lock(mtx_); - if (value_map_.size() > ylt_label_capacity) { - return value_type{}; - } - if (!has_change_) [[unlikely]] - has_change_ = true; - auto [it, r] = value_map_.try_emplace( - labels_value, thread_local_value(dupli_count_)); - lock.unlock(); - if (r) { - g_user_metric_label_count->local_value()++; - if (ylt_label_max_age.count()) { - it->second.set_created_time(std::chrono::system_clock::now()); - } - } - return it->second.update(value); - } - - value_type value(const std::array &labels_value) { - std::lock_guard lock(mtx_); - if (auto it = value_map_.find(labels_value); it != value_map_.end()) { - return it->second.value(); - } - - return value_type{}; - } - - value_type reset() { - value_type val = {}; - - std::lock_guard lock(mtx_); - for (auto &[key, t] : value_map_) { - val += t.reset(); - } - - return val; - } - - dynamic_metric_hash_map, - thread_local_value> - value_map() { - [[maybe_unused]] bool has_change = false; - return value_map(has_change); + value_type update(label_key_type labels_value, value_type value) { + return Base::try_emplace(labels_value) + .first->value.exchange(value, std::memory_order::relaxed); } - dynamic_metric_hash_map, - thread_local_value> - value_map(bool &has_change) { - dynamic_metric_hash_map, - thread_local_value> - map; - { - std::lock_guard lock(mtx_); - map = value_map_; - has_change = has_change_; + value_type value(label_key_type labels_value) { + if (auto ptr = Base::find(labels_value); ptr != nullptr) { + return ptr->value.load(std::memory_order::relaxed); } - - return map; - } - - size_t label_value_count() override { - std::lock_guard lock(mtx_); - return value_map_.size(); - } - - void clean_expired_label() override { - if (ylt_label_max_age.count() == 0) { - return; + else { + return value_type{}; } - - auto now = std::chrono::system_clock::now(); - std::lock_guard lock(mtx_); - std::erase_if(value_map_, [&now](auto &pair) mutable { - bool r = std::chrono::duration_cast( - now - pair.second.get_created_time()) - .count() >= ylt_label_max_age.count(); - return r; - }); } void remove_label_value( const std::map &labels) override { - { - std::lock_guard lock(mtx_); - if (value_map_.empty()) { - return; - } + if (Base::empty()) { + return; } const auto &labels_name = this->labels_name(); @@ -325,51 +178,43 @@ class basic_dynamic_counter : public dynamic_metric { return; } - if (labels.size() == labels_name.size()) { - std::vector label_value; - for (auto &lb_name : labels_name) { - if (auto i = labels.find(lb_name); i != labels.end()) { - label_value.push_back(i->second); - } - } + // if (labels.size() == labels_name.size()) { // TODO: speed up for this + // case - std::lock_guard lock(mtx_); - std::erase_if(value_map_, [&, this](auto &pair) { - return equal(label_value, pair.first); - }); - return; - } - else { - std::vector vec; - for (auto &lb_name : labels_name) { - if (auto i = labels.find(lb_name); i != labels.end()) { - vec.push_back(i->second); - } - else { - vec.push_back(""); - } + // } + // else { + size_t count = 0; + std::vector vec; + for (auto &lb_name : labels_name) { + if (auto i = labels.find(lb_name); i != labels.end()) { + vec.push_back(i->second); } - if (vec.empty()) { - return; + else { + vec.push_back(""); + count++; } - - std::lock_guard lock(mtx_); - std::erase_if(value_map_, [&](auto &pair) { - auto &[arr, _] = pair; + } + if (count == labels_name.size()) { + return; + } + Base::erase_if([&](auto &pair) { + auto &[arr, _] = pair; + if constexpr (N > 0) { for (size_t i = 0; i < vec.size(); i++) { if (!vec[i].empty() && vec[i] != arr[i]) { return false; } } - return true; - }); - } + } + return true; + }); + //} } bool has_label_value(const std::string &value) override { - [[maybe_unused]] bool has_change = false; - auto map = value_map(has_change); - for (auto &[label_value, _] : map) { + auto map = Base::copy(); + for (auto &e : map) { + auto &label_value = e->label; if (auto it = std::find(label_value.begin(), label_value.end(), value); it != label_value.end()) { return true; @@ -380,9 +225,9 @@ class basic_dynamic_counter : public dynamic_metric { } bool has_label_value(const std::regex ®ex) override { - [[maybe_unused]] bool has_change = false; - auto map = value_map(has_change); - for (auto &[label_value, _] : map) { + auto map = Base::copy(); + for (auto &e : map) { + auto &label_value = e->label; if (auto it = std::find_if(label_value.begin(), label_value.end(), [&](auto &val) { return std::regex_match(val, regex); @@ -398,51 +243,53 @@ class basic_dynamic_counter : public dynamic_metric { bool has_label_value(const std::vector &label_value) override { std::array arr{}; size_t size = (std::min)((size_t)N, label_value.size()); + if (label_value.size() > N) { + return false; + } + for (size_t i = 0; i < size; i++) { arr[i] = label_value[i]; } - std::lock_guard lock(mtx_); - return value_map_.contains(arr); + return Base::find(arr) != nullptr; } void serialize(std::string &str) override { - bool has_change = false; - auto map = value_map(has_change); + auto map = Base::copy(); if (map.empty()) { return; } std::string value_str; - serialize_map(map, value_str, has_change); + serialize_map(map, value_str); if (!value_str.empty()) { - serialize_head(str); + Base::serialize_head(str); str.append(value_str); } } #ifdef CINATRA_ENABLE_METRIC_JSON void serialize_to_json(std::string &str) override { - std::string s; - bool has_change = false; - auto map = value_map(has_change); - json_counter_t counter{name_, help_, std::string(metric_name())}; - to_json(counter, map, str, has_change); + auto map = Base::copy(); + json_counter_t counter{Base::name_, Base::help_, Base::metric_name()}; + counter.labels_name.reserve(Base::labels_name().size()); + for (auto &e : Base::labels_name()) { + counter.labels_name.emplace_back(e); + } + to_json(counter, map, str); } template - void to_json(json_counter_t &counter, T &map, std::string &str, - bool has_change) { - for (auto &[k, v] : map) { - auto val = v.value(); - if (val == 0 && !has_change) { - continue; - } + void to_json(json_counter_t &counter, T &map, std::string &str) { + for (auto &e : map) { + auto &k = e->label; + auto &val = e->value; json_counter_metric_t metric; size_t index = 0; + metric.labels.reserve(k.size()); for (auto &label_value : k) { - metric.labels.emplace(labels_name_[index++], label_value); + metric.labels.emplace_back(label_value); } - metric.value = (int64_t)val; + metric.value = val.load(std::memory_order::relaxed); counter.metrics.push_back(std::move(metric)); } if (!counter.metrics.empty()) { @@ -453,19 +300,17 @@ class basic_dynamic_counter : public dynamic_metric { protected: template - void serialize_map(T &value_map, std::string &str, bool has_change) { - for (auto &[labels_value, value] : value_map) { - auto val = value.value(); - if (val == 0 && !has_change) { - continue; - } - str.append(name_); - if (labels_name_.empty()) { + void serialize_map(T &value_map, std::string &str) { + for (auto &e : value_map) { + auto &labels_value = e->label; + auto val = e->value.load(std::memory_order::relaxed); + str.append(Base::name_); + if (Base::labels_name_.empty()) { str.append(" "); } else { str.append("{"); - build_string(str, labels_name_, labels_value); + build_string(str, Base::labels_name_, labels_value); str.append("} "); } @@ -490,13 +335,6 @@ class basic_dynamic_counter : public dynamic_metric { } str.pop_back(); } - - std::mutex mtx_; - dynamic_metric_hash_map, - thread_local_value> - value_map_; - size_t dupli_count_ = 2; - bool has_change_ = false; }; using dynamic_counter_1t = basic_dynamic_counter; diff --git a/include/ylt/metric/dynamic_metric.hpp b/include/ylt/metric/dynamic_metric.hpp new file mode 100644 index 000000000..8858812f3 --- /dev/null +++ b/include/ylt/metric/dynamic_metric.hpp @@ -0,0 +1,132 @@ +#pragma once +#include + +#include "metric.hpp" +#include "thread_local_value.hpp" +#include "ylt/util/map_sharded.hpp" + +namespace ylt::metric { + +class dynamic_metric : public metric_t { + public: + static inline auto g_user_metric_label_count = + new thread_local_value(std::thread::hardware_concurrency()); + using metric_t::metric_t; +}; + +template +class dynamic_metric_impl : public dynamic_metric { + template + struct my_hash { + using is_transparent = void; + std::size_t operator()( + const std::span& s) const noexcept { + unsigned int hash = 0; + for (const auto& str : s) { + for (auto ch : str) { + hash = hash * seed + ch; + } + } + return hash; + } + std::size_t operator()( + const std::span& s) const noexcept { + unsigned int hash = 0; + for (const auto& str : s) { + for (auto ch : str) { + hash = hash * seed + ch; + } + } + return hash; + } + }; + struct my_equal { + bool operator()(const std::span& s1, + const std::span& s2) const noexcept { + if constexpr (N > 0) { + for (int i = 0; i < N; ++i) { + if (s1[i] != s2[i]) { + return false; + } + } + } + return true; + } + }; + using key_type = std::array; + struct metric_pair { + public: + key_type label; + core_type value; + template + metric_pair(T&& first, Args&&... args) + : label(std::forward(first)), value(std::forward(args)...) { + g_user_metric_label_count->inc(); + if (ylt_label_max_age.count()) { + tp = std::chrono::steady_clock::now(); + } + } + std::chrono::steady_clock::time_point get_created_time() const { + return tp; + } + + private: + std::chrono::steady_clock::time_point tp; + }; + + struct value_type : public std::shared_ptr { + value_type() : std::shared_ptr(nullptr) {} + template + value_type(Args&&... args) + : std::shared_ptr( + std::make_shared(std::forward(args)...)){}; + }; + + public: + using dynamic_metric::dynamic_metric; + size_t size() const { return map_.size(); } + size_t empty() const { return !size(); } + size_t label_value_count() const { return size(); } + + std::vector> copy() const { + return map_.template copy>(); + } + + void clean_expired_label() override { + erase_if([now = std::chrono::steady_clock::now()](auto& pair) mutable { + bool r = std::chrono::duration_cast( + now - pair.second->get_created_time()) + .count() >= ylt_label_max_age.count(); + return r; + }); + } + + protected: + template + std::pair, bool> try_emplace(Key&& key, + Args&&... args) { + std::span view = key; + return map_.try_emplace_with_op( + view, + [](auto result) { + if (result.second) { + *const_cast*>( + &result.first->first) = result.first->second->label; + } + }, + std::forward(key), std::forward(args)...); + } + + std::shared_ptr find(std::span key) const { + return map_.find(key); + } + size_t erase(std::span key) { return map_.erase(key); } + size_t erase_if(auto&& op) { return map_.erase_if(op); } + + private: + util::map_sharded_t, + value_type, my_hash<131>, my_equal>, + my_hash<137>> + map_{std::min(128u, std::thread::hardware_concurrency())}; +}; +} // namespace ylt::metric diff --git a/include/ylt/metric/gauge.hpp b/include/ylt/metric/gauge.hpp index 50d678c1c..41b689a4a 100644 --- a/include/ylt/metric/gauge.hpp +++ b/include/ylt/metric/gauge.hpp @@ -1,7 +1,9 @@ #pragma once +#include #include #include "counter.hpp" +#include "ylt/metric/metric.hpp" namespace ylt::metric { @@ -32,16 +34,7 @@ class basic_static_gauge : public basic_static_counter { if (!has_change_) [[unlikely]] { has_change_ = true; } -#ifdef __APPLE__ - if constexpr (std::is_floating_point_v) { - mac_os_atomic_fetch_sub(&default_label_value_.local_value(), value); - } - else { - default_label_value_.dec(value); - } -#else default_label_value_.dec(value); -#endif } }; using gauge_t = basic_static_gauge; @@ -50,44 +43,18 @@ using gauge_d = basic_static_gauge; template class basic_dynamic_gauge : public basic_dynamic_counter { using metric_t::set_metric_type; - using basic_dynamic_counter::value_map_; - using basic_dynamic_counter::mtx_; - using basic_dynamic_counter::dupli_count_; - using basic_dynamic_counter::has_change_; + using Base = basic_dynamic_counter; public: basic_dynamic_gauge(std::string name, std::string help, - std::array labels_name, - size_t dupli_count = 2) - : basic_dynamic_counter(std::move(name), std::move(help), - std::move(labels_name), - dupli_count) { + std::array labels_name) + : Base(std::move(name), std::move(help), std::move(labels_name)) { set_metric_type(MetricType::Gauge); } void dec(const std::array& labels_value, value_type value = 1) { - if (value == 0) { - return; - } - - std::unique_lock lock(mtx_); - if (value_map_.size() > ylt_label_capacity) { - return; - } - if (!has_change_) [[unlikely]] - has_change_ = true; - auto [it, r] = value_map_.try_emplace( - labels_value, thread_local_value(dupli_count_)); - lock.unlock(); - if (r) { - g_user_metric_label_count->local_value()++; - if (ylt_label_max_age.count()) { - it->second.set_created_time(std::chrono::system_clock::now()); - } - } - - set_value(it->second.local_value(), value, op_type_t::DEC); + detail::dec_impl(Base::try_emplace(labels_value).first->value, value); } }; diff --git a/include/ylt/metric/histogram.hpp b/include/ylt/metric/histogram.hpp index 66c13d018..ce897a452 100644 --- a/include/ylt/metric/histogram.hpp +++ b/include/ylt/metric/histogram.hpp @@ -6,7 +6,8 @@ #include #include "counter.hpp" -#include "metric.hpp" +#include "dynamic_metric.hpp" +#include "gauge.hpp" namespace ylt::metric { #ifdef CINATRA_ENABLE_METRIC_JSON @@ -140,8 +141,6 @@ class basic_static_histogram : public static_metric { private: void init_bucket_counter(size_t dupli_count, size_t bucket_size) { - g_user_metric_count++; - for (size_t i = 0; i < bucket_size + 1; i++) { bucket_counts_.push_back( std::make_shared("", "", dupli_count)); @@ -167,18 +166,15 @@ class basic_dynamic_histogram : public dynamic_metric { public: basic_dynamic_histogram(std::string name, std::string help, std::vector buckets, - std::array labels_name, - size_t dupli_count = 2) + std::array labels_name) : bucket_boundaries_(buckets), dynamic_metric(MetricType::Histogram, name, help, labels_name), sum_(std::make_shared>( - name, help, labels_name, dupli_count)) { - g_user_metric_count++; - + name, help, labels_name)) { for (size_t i = 0; i < buckets.size() + 1; i++) { bucket_counts_.push_back( - std::make_shared>( - name, help, labels_name, dupli_count)); + std::make_shared>(name, help, + labels_name)); } } @@ -192,6 +188,13 @@ class basic_dynamic_histogram : public dynamic_metric { bucket_counts_[bucket_index]->inc(labels_value); } + void clean_expired_label() override { + sum_->clean_expired_label(); + for (auto &m : bucket_counts_) { + m->clean_expired_label(); + } + } + auto get_bucket_counts() { return bucket_counts_; } bool has_label_value(const std::string &label_val) override { @@ -206,8 +209,10 @@ class basic_dynamic_histogram : public dynamic_metric { return sum_->has_label_value(label_value); } + size_t label_value_count() const { return sum_->label_value_count(); } + void serialize(std::string &str) override { - auto value_map = sum_->value_map(); + auto value_map = sum_->copy(); if (value_map.empty()) { return; } @@ -216,8 +221,10 @@ class basic_dynamic_histogram : public dynamic_metric { std::string value_str; auto bucket_counts = get_bucket_counts(); - for (auto &[labels_value, value] : value_map) { - if (value.value() == 0) { + for (auto &e : value_map) { + auto &labels_value = e->label; + auto &value = e->value; + if (value == 0) { continue; } @@ -244,10 +251,6 @@ class basic_dynamic_histogram : public dynamic_metric { value_str.append("\n"); } - if (value_str.empty()) { - return; - } - str.append(value_str); str.append(name_); @@ -255,7 +258,7 @@ class basic_dynamic_histogram : public dynamic_metric { build_label_string(str, sum_->labels_name(), labels_value); str.append("} "); - str.append(std::to_string(value.value())); + str.append(std::to_string(value)); str.append("\n"); str.append(name_).append("_count{"); @@ -264,11 +267,14 @@ class basic_dynamic_histogram : public dynamic_metric { str.append(std::to_string(count)); str.append("\n"); } + if (value_str.empty()) { + str.clear(); + } } #ifdef CINATRA_ENABLE_METRIC_JSON void serialize_to_json(std::string &str) override { - auto value_map = sum_->value_map(); + auto value_map = sum_->copy(); if (value_map.empty()) { return; } @@ -276,8 +282,10 @@ class basic_dynamic_histogram : public dynamic_metric { json_histogram_t hist{name_, help_, std::string(metric_name())}; auto bucket_counts = get_bucket_counts(); - for (auto &[labels_value, value] : value_map) { - if (value.value() == 0) { + for (auto &e : value_map) { + auto &labels_value = e->label; + auto &value = e->value; + if (value == 0) { continue; } diff --git a/include/ylt/metric/metric.hpp b/include/ylt/metric/metric.hpp index 8c880765e..67e90d9bc 100644 --- a/include/ylt/metric/metric.hpp +++ b/include/ylt/metric/metric.hpp @@ -1,21 +1,18 @@ #pragma once #include #include -#include -#include -#include -#include +#include +#include #include #include -#include +#include #include -#include +#include #include #include "async_simple/coro/Lazy.h" #include "async_simple/coro/SyncAwait.h" #include "cinatra/cinatra_log_wrapper.hpp" -#include "thread_local_value.hpp" #if __has_include("ylt/coro_io/coro_io.hpp") #include "ylt/coro_io/coro_io.hpp" #else @@ -34,6 +31,7 @@ inline char* to_chars_float(T value, char* buffer) { #include #endif + namespace ylt::metric { enum class MetricType { Counter, @@ -50,32 +48,18 @@ struct metric_filter_options { bool is_white = true; }; -#ifdef __APPLE__ -inline double mac_os_atomic_fetch_add(std::atomic* obj, double arg) { - double v; - do { - v = obj->load(); - } while (!std::atomic_compare_exchange_weak(obj, &v, v + arg)); - return v; -} - -inline double mac_os_atomic_fetch_sub(std::atomic* obj, double arg) { - double v; - do { - v = obj->load(); - } while (!std::atomic_compare_exchange_weak(obj, &v, v - arg)); - return v; -} -#endif - class metric_t { public: + static inline std::atomic g_user_metric_count = 0; + metric_t() = default; metric_t(MetricType type, std::string name, std::string help) : type_(type), name_(std::move(name)), help_(std::move(help)), - metric_created_time_(std::chrono::system_clock::now()) {} + metric_created_time_(std::chrono::system_clock::now()) { + g_user_metric_count.fetch_add(1, std::memory_order::relaxed); + } template metric_t(MetricType type, std::string name, std::string help, @@ -95,7 +79,9 @@ class metric_t { labels_value_.push_back(v); } } - virtual ~metric_t() {} + virtual ~metric_t() { + g_user_metric_count.fetch_sub(1, std::memory_order::relaxed); + } std::string_view name() { return name_; } @@ -105,8 +91,6 @@ class metric_t { MetricType metric_type() { return type_; } - auto get_created_time() { return metric_created_time_; } - std::string_view metric_name() { switch (type_) { case MetricType::Counter: @@ -129,8 +113,6 @@ class metric_t { return static_labels_; } - virtual size_t label_value_count() { return 0; } - virtual bool has_label_value(const std::string& label_value) { return std::find(labels_value_.begin(), labels_value_.end(), label_value) != labels_value_.end(); @@ -208,30 +190,17 @@ class static_metric : public metric_t { using metric_t::metric_t; }; -class dynamic_metric : public metric_t { - using metric_t::metric_t; -}; - -inline auto g_user_metric_label_count = new thread_local_value(2); -inline std::atomic g_user_metric_count = 0; - -inline std::atomic ylt_metric_capacity = 10000000; -inline int64_t ylt_label_capacity = 20000000; - inline std::chrono::seconds ylt_label_max_age{0}; -inline std::chrono::seconds ylt_label_check_expire_duration{0}; +inline std::chrono::seconds ylt_label_check_expire_duration{60}; +inline std::atomic ylt_metric_capacity = 10000000; inline void set_metric_capacity(int64_t max_count) { ylt_metric_capacity = max_count; } -inline void set_label_capacity(int64_t max_label_count) { - ylt_label_capacity = max_label_count; -} - inline void set_label_max_age( std::chrono::seconds max_age, - std::chrono::seconds check_duration = std::chrono::seconds(60 * 10)) { + std::chrono::seconds check_duration = std::chrono::seconds{60}) { ylt_label_max_age = max_age; ylt_label_check_expire_duration = check_duration; } diff --git a/include/ylt/metric/metric_manager.hpp b/include/ylt/metric/metric_manager.hpp index b3b90c0f0..856bf11f4 100644 --- a/include/ylt/metric/metric_manager.hpp +++ b/include/ylt/metric/metric_manager.hpp @@ -4,17 +4,18 @@ #include #include "metric.hpp" +#include "ylt/util/map_sharded.hpp" namespace ylt::metric { class manager_helper { public: static bool register_metric(auto& metric_map, auto metric) { - if (g_user_metric_count > ylt_metric_capacity) { + if (metric::metric_t::g_user_metric_count > ylt_metric_capacity) { CINATRA_LOG_ERROR << "metric count at capacity size: " - << g_user_metric_count; + << metric::metric_t::g_user_metric_count; return false; } - auto [it, r] = metric_map.try_emplace(metric->str_name(), metric); + auto&& [it, r] = metric_map.try_emplace(metric->str_name(), metric); if (!r) { CINATRA_LOG_ERROR << "duplicate registered metric name: " << metric->str_name(); @@ -36,9 +37,6 @@ class manager_helper { #ifdef CINATRA_ENABLE_METRIC_JSON static std::string serialize_to_json( const std::vector>& metrics) { - if (metrics.empty()) { - return ""; - } std::string str; str.append("["); for (auto& m : metrics) { @@ -49,7 +47,10 @@ class manager_helper { } if (str.size() == 1) { - return ""; + str.append("]"); + } + else { + str.back() = ']'; } str.back() = ']'; @@ -139,6 +140,9 @@ class manager_helper { static void filter_by_label_name( std::vector>& filtered_metrics, std::shared_ptr m, const metric_filter_options& options) { + if (!options.label_regex) { + return; + } const auto& labels_name = m->labels_name(); for (auto& label_name : labels_name) { if (std::regex_match(label_name, *options.label_regex)) { @@ -285,7 +289,6 @@ class dynamic_metric_manager { } bool register_metric(std::shared_ptr metric) { - std::unique_lock lock(mtx_); return manager_helper::register_metric(metric_map_, metric); } @@ -316,7 +319,6 @@ class dynamic_metric_manager { #endif bool remove_metric(const std::string& name) { - std::unique_lock lock(mtx_); return metric_map_.erase(name); } @@ -324,7 +326,6 @@ class dynamic_metric_manager { if (metric == nullptr) { return false; } - return remove_metric(metric->str_name()); } @@ -332,7 +333,6 @@ class dynamic_metric_manager { if (names.empty()) { return; } - for (auto& name : names) { remove_metric(name); } @@ -349,20 +349,19 @@ class dynamic_metric_manager { } void remove_label_value(const std::map& labels) { - std::unique_lock lock(mtx_); - for (auto& [_, m] : metric_map_) { - m->remove_label_value(labels); - } + metric_map_.for_each([&](auto& m) { + auto&& [_, metric] = m; + metric->remove_label_value(labels); + }); } void remove_metric_by_label( const std::map& labels) { - std::unique_lock lock(mtx_); - for (auto it = metric_map_.begin(); it != metric_map_.end();) { - auto& m = it->second; + metric_map_.erase_if([&](auto& metric) { + auto&& [_, m] = metric; const auto& labels_name = m->labels_name(); if (labels.size() > labels_name.size()) { - continue; + return false; } if (labels.size() == labels_name.size()) { @@ -372,99 +371,56 @@ class dynamic_metric_manager { label_value.push_back(i->second); } } - - std::erase_if(metric_map_, [&](auto& pair) { - return pair.second->has_label_value(label_value); - }); - if (m->has_label_value(label_value)) { - metric_map_.erase(it); - } - break; + return m->has_label_value(label_value); } else { - bool need_erase = false; - for (auto& lb_name : labels_name) { - if (auto i = labels.find(lb_name); i != labels.end()) { - if (m->has_label_value(i->second)) { - it = metric_map_.erase(it); - need_erase = true; - break; + for (auto& label : labels) { + if (auto i = std::find(labels_name.begin(), labels_name.end(), + label.first); + i != labels_name.end()) { + if (!m->has_label_value(label.second)) { + return false; } } + else { + return false; + } } - - if (!need_erase) - ++it; + return true; } - } + }); } void remove_metric_by_label_name( const std::vector& labels_name) { - std::unique_lock lock(mtx_); - for (auto& [name, m] : metric_map_) { - if (m->labels_name() == labels_name) { - metric_map_.erase(name); - break; - } - } + metric_map_.erase_one([&](auto& m) { + auto&& [name, metric] = m; + return metric->labels_name() == labels_name; + }); } void remove_metric_by_label_name(std::string_view labels_name) { - std::unique_lock lock(mtx_); - for (auto it = metric_map_.cbegin(); it != metric_map_.cend();) { - auto& names = it->second->labels_name(); - if (auto sit = std::find(names.begin(), names.end(), labels_name); - sit != names.end()) { - metric_map_.erase(it++); - } - else { - ++it; - } - } - } - - size_t metric_count() { - std::unique_lock lock(mtx_); - return metric_map_.size(); - } - - auto metric_map() { - std::unique_lock lock(mtx_); - return metric_map_; + metric_map_.erase_if([&](auto& m) { + auto&& [_, metric] = m; + auto& names = metric->labels_name(); + return std::find(names.begin(), names.end(), labels_name) != names.end(); + }); } - auto collect() { - std::vector> metrics; - { - std::unique_lock lock(mtx_); - for (auto& pair : metric_map_) { - metrics.push_back(pair.second); - } - } - return metrics; + size_t metric_count() { return metric_map_.size(); } + std::vector> collect() const { + return metric_map_.template copy>(); } template std::shared_ptr get_metric_dynamic(const std::string& name) { static_assert(std::is_base_of_v, "must be dynamic metric"); - auto map = metric_map(); - auto it = map.find(name); - if (it == map.end()) { - return nullptr; - } - return std::dynamic_pointer_cast(it->second); + return std::dynamic_pointer_cast(metric_map_.find(name)); } std::shared_ptr get_metric_by_name(std::string_view name) { - auto map = metric_map(); - auto it = map.find(name); - if (it == map.end()) { - return nullptr; - } - - return it->second; + return metric_map_.find(name); } std::vector> get_metric_by_label( @@ -479,14 +435,10 @@ class dynamic_metric_manager { std::vector> get_metric_by_label_name( const std::vector& labels_name) { - auto map = metric_map(); - std::vector> vec; - for (auto& [name, m] : map) { - if (m->labels_name() == labels_name) { - vec.push_back(m); - } - } - return vec; + return metric_map_.template copy>( + [&](auto& m) { + return m->labels_name() == labels_name; + }); } std::vector> filter_metrics_dynamic( @@ -504,10 +456,11 @@ class dynamic_metric_manager { private: void clean_label_expired() { executor_ = coro_io::create_io_context_pool(1); + auto sp = executor_; timer_ = std::make_shared(executor_->get_executor()); check_label_expired(timer_) .via(executor_->get_executor()) - .start([](auto&&) { + .start([sp](auto&&) { }); } @@ -518,21 +471,20 @@ class dynamic_metric_manager { if (timer == nullptr) { co_return; } - timer->expires_after(ylt_label_check_expire_duration); bool r = co_await timer->async_await(); if (!r) { co_return; } - - std::unique_lock lock(mtx_); - for (auto& [_, m] : metric_map_) { - m->clean_expired_label(); - } + metric_map_.for_each([](auto& metric) { + metric.second->clean_expired_label(); + }); } } - dynamic_metric_manager() { + dynamic_metric_manager() + : metric_map_( + std::min(std::thread::hardware_concurrency(), 128u)) { if (ylt_label_max_age.count() > 0) { clean_label_expired(); } @@ -540,29 +492,35 @@ class dynamic_metric_manager { std::vector> get_metric_by_label_value( const std::vector& label_value) { - auto map = metric_map(); - std::vector> vec; - for (auto& [name, m] : map) { - if (m->has_label_value(label_value)) { - vec.push_back(m); - } - } - return vec; + return metric_map_.template copy>( + [&label_value](auto& metric) { + return metric->has_label_value(label_value); + }); } void remove_metric_by_label_value( const std::vector& label_value) { - std::unique_lock lock(mtx_); - for (auto& [name, m] : metric_map_) { - if (m->has_label_value(label_value)) { - metric_map_.erase(name); - break; + metric_map_.erase_if([&](auto& metric) { + return metric.second->has_label_value(label_value); + }); + } + + template + struct my_hash { + using is_transparent = void; + std::size_t operator()(std::string_view s) const noexcept { + unsigned int hash = 0; + for (auto ch : s) { + hash = hash * seed + ch; } + return hash; } - } + }; - std::shared_mutex mtx_; - std::unordered_map> metric_map_; + util::map_sharded_t< + std::unordered_map>, + my_hash<>> + metric_map_; std::shared_ptr timer_ = nullptr; std::shared_ptr executor_ = nullptr; }; diff --git a/include/ylt/metric/summary.hpp b/include/ylt/metric/summary.hpp index 1d97e968d..5a565a8ed 100644 --- a/include/ylt/metric/summary.hpp +++ b/include/ylt/metric/summary.hpp @@ -6,7 +6,7 @@ #include #include "counter.hpp" -#include "metric.hpp" +#include "dynamic_metric.hpp" #include "summary_impl.hpp" #if __has_include("ylt/util/concurrentqueue.h") #include "ylt/util/concurrentqueue.h" @@ -17,32 +17,33 @@ namespace ylt::metric { #ifdef CINATRA_ENABLE_METRIC_JSON struct json_summary_metric_t { - std::map labels; - std::map quantiles; - int64_t count; + std::vector labels; + std::vector quantiles_value; + uint64_t count; double sum; }; -YLT_REFL(json_summary_metric_t, labels, quantiles, count, sum); +YLT_REFL(json_summary_metric_t, labels, quantiles_value, count, sum); struct json_summary_t { - std::string name; - std::string help; - std::string type; + std::string_view name; + std::string_view help; + std::string_view type; + const std::vector& labels_name; + const std::vector& quantiles_key; std::vector metrics; }; -YLT_REFL(json_summary_t, name, help, type, metrics); +YLT_REFL(json_summary_t, name, help, type, labels_name, quantiles_key, metrics); #endif class summary_t : public static_metric { public: summary_t(std::string name, std::string help, std::vector quantiles, - std::chrono::seconds max_age = std::chrono::seconds{60}) + std::chrono::seconds max_age = std::chrono::seconds{0}) : static_metric(MetricType::Summary, std::move(name), std::move(help)), quantiles_(std::move(quantiles)), impl_(quantiles_, std::chrono::duration_cast(max_age)) { if (!std::is_sorted(quantiles_.begin(), quantiles_.end())) std::sort(quantiles_.begin(), quantiles_.end()); - g_user_metric_count++; } summary_t(std::string name, std::string help, std::vector quantiles, @@ -55,7 +56,6 @@ class summary_t : public static_metric { std::chrono::duration_cast(max_age)) { if (!std::is_sorted(quantiles_.begin(), quantiles_.end())) std::sort(quantiles_.begin(), quantiles_.end()); - g_user_metric_count++; } void observe(float value) { impl_.insert(value); } @@ -116,122 +116,104 @@ class summary_t : public static_metric { return; } - double sum = 0; - uint64_t count = 0; - auto rates = get_rates(sum, count); - if (count == 0) { - return; - } - - json_summary_t summary{name_, help_, std::string(metric_name())}; - + json_summary_t summary{name_, help_, metric_name(), labels_name(), + quantiles_}; json_summary_metric_t metric; - for (size_t i = 0; i < quantiles_.size(); i++) { - for (size_t i = 0; i < labels_name_.size(); i++) { - metric.labels[labels_name_[i]] = labels_value_[i]; - } - metric.quantiles.emplace(quantiles_[i], rates[i]); + metric.quantiles_value = get_rates(metric.sum, metric.count); + if (metric.count == 0) { + return; } - - metric.sum = sum; - metric.count = count; - + metric.labels.reserve(labels_value_.size()); + for (auto& e : labels_value_) metric.labels.emplace_back(e); summary.metrics.push_back(std::move(metric)); - iguana::to_json(summary, str); } #endif private: std::vector quantiles_; - ylt::metric::detail::summary_impl<> impl_; + ylt::metric::detail::summary_impl impl_; }; template -class basic_dynamic_summary : public dynamic_metric { +class basic_dynamic_summary + : public dynamic_metric_impl, + N> { private: - auto visit(const std::array& labels_value) { - decltype(label_quantile_values_.begin()) iter; - bool has_inserted; - { - std::lock_guard guard(mutex_); - std::tie(iter, has_inserted) = - label_quantile_values_.try_emplace(labels_value, nullptr); - if (has_inserted) { - iter->second = std::make_unique>(quantiles_); - } - } - return iter; - } + using Base = + dynamic_metric_impl, N>; public: basic_dynamic_summary( std::string name, std::string help, std::vector quantiles, std::array labels_name, std::chrono::milliseconds max_age = std::chrono::seconds{60}) - : dynamic_metric(MetricType::Summary, std::move(name), std::move(help), - std::move(labels_name)), + : Base(MetricType::Summary, std::move(name), std::move(help), + std::move(labels_name)), quantiles_(std::move(quantiles)), max_age_(max_age) { if (!std::is_sorted(quantiles_.begin(), quantiles_.end())) std::sort(quantiles_.begin(), quantiles_.end()); - g_user_metric_count++; } void observe(const std::array& labels_value, float value) { - visit(labels_value)->second->insert(value); + Base::try_emplace(labels_value, quantiles_).first->value.insert(value); } std::vector get_rates(const std::array& labels_value) { double sum; uint64_t count; - return visit(labels_value)->second->get_rates(sum, count); + return Base::try_emplace(labels_value, quantiles_) + .first->value.get_rates(sum, count); } std::vector get_rates(const std::array& labels_value, uint64_t& count) { double sum; - return visit(labels_value)->second->get_rates(sum, count); + return Base::try_emplace(labels_value, quantiles_) + .first->value.get_rates(sum, count); } std::vector get_rates(const std::array& labels_value, double& sum) { uint64_t count; - return visit(labels_value)->second->get_rates(sum, count); + return Base::try_emplace(labels_value, quantiles_) + .first->value.get_rates(sum, count); } std::vector get_rates(const std::array& labels_value, double& sum, uint64_t& count) { - return visit(labels_value)->second->stat(sum, count); + return Base::try_emplace(labels_value, quantiles_) + .first->value.stat(sum, count); } virtual void serialize(std::string& str) override { double sum = 0; uint64_t count = 0; - std::lock_guard guard(mutex_); - // TODO: copy pointer to avoid big lock - for (auto& [labels_value, summary_value] : label_quantile_values_) { - auto rates = summary_value->stat(sum, count); + auto map = Base::copy(); + for (auto& e : map) { + auto& labels_value = e->label; + auto& summary_value = e->value; + auto rates = summary_value.stat(sum, count); for (size_t i = 0; i < quantiles_.size(); i++) { - str.append(name_); + str.append(Base::name_); str.append("{"); - build_label_string(str, labels_name_, labels_value); + Base::build_label_string(str, Base::labels_name_, labels_value); str.append(","); str.append("quantile=\""); str.append(std::to_string(quantiles_[i])).append("\"} "); str.append(std::to_string(rates[i])).append("\n"); } - - str.append(name_).append("_sum "); + str.append(Base::name_).append("_sum "); str.append("{"); - build_label_string(str, labels_name_, labels_value); + Base::build_label_string(str, Base::labels_name_, labels_value); str.append("} "); str.append(std::to_string(sum)).append("\n"); - str.append(name_).append("_count "); + str.append(Base::name_).append("_count "); str.append("{"); - build_label_string(str, labels_name_, labels_value); + Base::build_label_string(str, Base::labels_name_, labels_value); str.append("} "); str.append(std::to_string((uint64_t)count)).append("\n"); } @@ -239,37 +221,36 @@ class basic_dynamic_summary : public dynamic_metric { #ifdef CINATRA_ENABLE_METRIC_JSON virtual void serialize_to_json(std::string& str) override { - json_summary_t summary{name_, help_, std::string(metric_name())}; - { - std::lock_guard guard(mutex_); - for (auto& [labels_value, summary_value] : label_quantile_values_) { - json_summary_metric_t metric; - double sum = 0; - uint64_t count = 0; - auto rates = summary_value->stat(sum, count); - metric.count = count; - metric.sum = sum; - for (size_t i = 0; i < quantiles_.size(); i++) { - for (size_t i = 0; i < labels_value.size(); i++) { - metric.labels[labels_name_[i]] = labels_value[i]; - } - metric.quantiles.emplace(quantiles_[i], rates[i]); - } - summary.metrics.push_back(std::move(metric)); - } + auto map = Base::copy(); + if (map.empty()) { + return; + } + json_summary_t summary{Base::name_, Base::help_, Base::metric_name(), + Base::labels_name(), quantiles_}; + summary.metrics.reserve(map.size()); + for (size_t i = 0; i < map.size(); ++i) { + auto& labels_value = map[i]->label; + auto& summary_value = map[i]->value; + double sum = 0; + uint64_t count = 0; + auto rates = summary_value.stat(sum, count); + if (count == 0) + continue; + summary.metrics.emplace_back(); + json_summary_metric_t& metric = summary.metrics.back(); + metric.count = count; + metric.sum = sum; + metric.quantiles_value = std::move(rates); + metric.labels.reserve(labels_value.size()); + for (auto& e : labels_value) metric.labels.emplace_back(e); } iguana::to_json(summary, str); } #endif private: - using hashtable_t = dynamic_metric_hash_map< - std::array, - std::unique_ptr>>; - std::mutex mutex_; std::vector quantiles_; std::chrono::milliseconds max_age_; - hashtable_t label_quantile_values_; }; using dynamic_summary_1 = basic_dynamic_summary<1>; diff --git a/include/ylt/metric/summary_impl.hpp b/include/ylt/metric/summary_impl.hpp index 4747b6910..ce853af64 100644 --- a/include/ylt/metric/summary_impl.hpp +++ b/include/ylt/metric/summary_impl.hpp @@ -7,12 +7,15 @@ #include #include #include +#include #include namespace ylt::metric::detail { -template +template class summary_impl { + static_assert(sizeof(uint_type) >= 4); + static_assert(std::is_unsigned_v); constexpr static uint32_t decode_impl(uint16_t float16_value) { float16_value <<= (8 - frac_bit); uint32_t sign = float16_value >> 15; @@ -57,7 +60,8 @@ class summary_impl { static constexpr float float16_max = (1ull << 63) * 2.0f; // 2^64 static uint16_t encode(float flt) { - unsigned int& fltInt32 = *(unsigned int*)&flt; + static_assert(sizeof(float) == 4); + uint32_t& fltInt32 = *(uint32_t*)&flt; if (std::abs(flt) >= float16_max || std::isnan(flt)) { flt = (fltInt32 & 0x8000'0000) ? (-float16_max) : (float16_max); } @@ -73,7 +77,6 @@ class summary_impl { fltInt16 |= (fltInt32 >> 15) & 0xff; auto i = fltInt16 >> (8 - frac_bit); - auto j = decode_impl(i); return i; } @@ -89,13 +92,15 @@ class summary_impl { struct data_t { static constexpr size_t piece_size = bucket_size / piece_cnt; - using piece_t = std::array, piece_size>; + using piece_t = std::array, piece_size>; - std::atomic& operator[](std::size_t index) { + std::atomic& operator[](std::size_t index) { piece_t* piece = arr[index / piece_size]; if (piece == nullptr) { auto ptr = new piece_t{}; - arr[index / piece_size].compare_exchange_strong(piece, ptr); + if (!arr[index / piece_size].compare_exchange_strong(piece, ptr)) { + delete ptr; + } return (*arr[index / piece_size].load())[index % piece_size]; } else { @@ -104,7 +109,11 @@ class summary_impl { } void refresh() { for (auto& piece_ptr : arr) { - delete piece_ptr.exchange(nullptr); + if (piece_ptr) { + for (auto& e : *piece_ptr) { + e.store(0, std::memory_order::relaxed); + } + } } } static uint16_t get_ordered_index(int16_t raw_index) { @@ -117,7 +126,7 @@ class summary_impl { } template void stat_impl(uint64_t& count, - std::vector>& result, int i) { + std::vector>& result, int i) { auto piece = arr[i].load(std::memory_order_relaxed); if (piece) { if constexpr (inc_order) { @@ -141,7 +150,7 @@ class summary_impl { } } void stat(uint64_t& count, - std::vector>& result) { + std::vector>& result) { for (int i = piece_cnt - 1; i >= piece_cnt / 2; --i) { stat_impl(count, result, i); } @@ -177,64 +186,38 @@ class summary_impl { static inline const unsigned long ms_count = std::chrono::steady_clock::duration{std::chrono::milliseconds{1}}.count(); - template - void refresh() { - if (refresh_time_.count() <= 0) { - return; - } - uint64_t old_tp = tp_; - auto new_tp = std::chrono::steady_clock::now().time_since_epoch().count(); - auto ms = (new_tp - old_tp) / ms_count; - if (; ms > refresh_time_.count()) [[unlikely]] { - if (tp_.compare_exchange_strong(old_tp, new_tp)) { - auto pos = frontend_data_index_ ^ 1; - if (auto data = data_[pos].load(); data != nullptr) { - data_delete_checker = true; - while (*data_delete_locker > 0) { - std::this_thread::yield(); - } - /*it seems dangerours, but we block the read op, and there is no write - * op in backend after refresh time*/ - if constexpr (is_read) { - delete data_[pos].exchange(nullptr); - } - else { - (*data_[pos]).refresh(); - } - data_delete_checker = false; - } - frontend_data_index_ = pos; - } - } - } - - constexpr static unsigned int near_uint32_max = 4290000000U; + constexpr static uint32_t near_uint32_max = 4290000000U; void increase(data_t& arr, uint16_t pos) { - if (++arr[pos] > near_uint32_max) /*no overflow*/ [[likely]] { - --arr[pos]; - int upper = (pos < bucket_size / 2) ? (bucket_size / 2) : (bucket_size); - int lower = (pos < bucket_size / 2) ? (0) : (bucket_size / 2); - for (int delta = 1, lim = (std::max)(upper - pos, pos - lower + 1); - delta < lim; ++delta) { - if (pos + delta < upper) { - if (++arr[pos + delta] <= near_uint32_max) { - break; + auto res = arr[pos].fetch_add(1, std::memory_order::relaxed); + if constexpr (std::is_same_v) { + if (res > near_uint32_max) /*no overflow*/ [[likely]] { + arr[pos].fetch_sub(1, std::memory_order::relaxed); + int upper = (pos < bucket_size / 2) ? (bucket_size / 2) : (bucket_size); + int lower = (pos < bucket_size / 2) ? (0) : (bucket_size / 2); + for (int delta = 1, lim = (std::max)(upper - pos, pos - lower + 1); + delta < lim; ++delta) { + if (pos + delta < upper) { + if (arr[pos + delta].fetch_add(1, std::memory_order::relaxed) <= + near_uint32_max) { + break; + } + arr[pos + delta].fetch_sub(1, std::memory_order::relaxed); } - --arr[pos + delta]; - } - if (pos - delta >= lower) { - if (++arr[pos - delta] <= near_uint32_max) { - break; + if (pos - delta >= lower) { + if (arr[pos - delta].fetch_add(1, std::memory_order::relaxed) <= + near_uint32_max) { + break; + } + arr[pos - delta].fetch_sub(1, std::memory_order::relaxed); } - --arr[pos - delta]; } } } } struct data_copy_t { - std::vector> arr[2]; + std::vector> arr[2]; int index[2] = {}, smaller_one; void init() { if (arr[0][0] <= arr[1][0]) { @@ -254,44 +237,49 @@ class summary_impl { } } int16_t value() { return arr[smaller_one][index[smaller_one]].first; } - uint32_t count() { return arr[smaller_one][index[smaller_one]].second; } + uint_type count() { return arr[smaller_one][index[smaller_one]].second; } }; public: + void refresh() { + if (refresh_time_.count() <= 0) { + return; + } + uint64_t old_tp = tp_; + auto new_tp = std::chrono::steady_clock::now().time_since_epoch().count(); + auto ms = (new_tp - old_tp) / ms_count; + if (; ms >= refresh_time_.count()) [[unlikely]] { + if (tp_.compare_exchange_strong(old_tp, new_tp)) { + if (ms >= 2 * refresh_time_.count()) { + for (auto& data : data_) { + if (data != nullptr) { + data.load()->refresh(); + } + } + } + else { + auto pos = frontend_data_index_ ^ 1; + if (auto data = data_[pos].load(); data != nullptr) { + data->refresh(); + } + frontend_data_index_ = pos; + } + } + } + } void insert(float value) { - refresh(); + refresh(); auto& data = get_data(); increase(data, encode(value)); return; } - void refresh() { - refresh(); - return; - } - struct data_delete_guard { - summary_impl* self_; - data_delete_guard(summary_impl* self) : self_(self) { - if (self_->refresh_time_.count() >= 0) { - ++*(self_->data_delete_locker); - } - } - ~data_delete_guard() { - if (self_->refresh_time_.count() >= 0) { - --*(self_->data_delete_locker); - } - } - }; std::vector stat(double& sum, uint64_t& count) { - refresh(); - while (data_delete_checker) [[unlikely]] { - std::this_thread::yield(); - } + refresh(); count = 0; sum = 0; data_copy_t data_copy; { - data_delete_guard guard(this); data_t* ar[2] = {data_[0], data_[1]}; if (ar[0] == nullptr && ar[1] == nullptr) [[unlikely]] { return std::vector(rate_.size(), 0.0f); @@ -322,6 +310,9 @@ class summary_impl { e = 1; } auto target_count = std::min(e * count, count); + if (e == 0) { + target_count = std::min(uint64_t{1}, count); + } while (true) { if (target_count <= count_now) [[unlikely]] { result.push_back(v); @@ -360,8 +351,5 @@ class summary_impl { std::vector& rate_; std::array, 2> data_; std::atomic frontend_data_index_; - std::atomic data_delete_checker = false; - std::unique_ptr> data_delete_locker = - std::make_unique>(); }; } // namespace ylt::metric::detail diff --git a/include/ylt/metric/system_metric.hpp b/include/ylt/metric/system_metric.hpp index 74156b90c..dd786a9d0 100644 --- a/include/ylt/metric/system_metric.hpp +++ b/include/ylt/metric/system_metric.hpp @@ -357,12 +357,13 @@ inline void stat_metric() { static auto user_metric_count = system_metric_manager::instance().get_metric_static( "ylt_user_metric_count"); - user_metric_count->update(g_user_metric_count); + user_metric_count->update(metric::metric_t::g_user_metric_count); static auto user_metric_label_count = system_metric_manager::instance().get_metric_static( "ylt_user_metric_labels"); - user_metric_label_count->update(g_user_metric_label_count->value()); + user_metric_label_count->update( + dynamic_metric::g_user_metric_label_count->value()); } inline void ylt_stat() { @@ -451,4 +452,4 @@ inline bool start_system_metric() { return true; } } // namespace ylt::metric -#endif \ No newline at end of file +#endif diff --git a/include/ylt/metric/thread_local_value.hpp b/include/ylt/metric/thread_local_value.hpp index b73789dd8..e6f924ebf 100644 --- a/include/ylt/metric/thread_local_value.hpp +++ b/include/ylt/metric/thread_local_value.hpp @@ -5,14 +5,50 @@ #include #include +#include "ylt/metric/metric.hpp" + namespace ylt::metric { inline uint32_t get_round_index(uint32_t size) { static std::atomic round = 0; static thread_local uint32_t index = round++; return index % size; } + +namespace detail { +template +static value_type inc_impl(std::atomic &obj, value_type value) { + if constexpr (!requires { + std::atomic{}.fetch_add(value_type{}); + }) { + value_type v = obj.load(std::memory_order::relaxed); + while (!std::atomic_compare_exchange_weak(&obj, &v, v + value)) + ; + return v; + } + else { + return obj.fetch_add(value, std::memory_order::relaxed); + } +} +template +static value_type dec_impl(std::atomic &obj, value_type value) { + if constexpr (!requires { + std::atomic{}.fetch_add(value_type{}); + }) { + value_type v = obj.load(std::memory_order::relaxed); + while (!std::atomic_compare_exchange_weak(&obj, &v, v - value)) + ; + return v; + } + else { + return obj.fetch_sub(value, std::memory_order::relaxed); + } +} +} // namespace detail + template class thread_local_value { + friend class metric_t; + public: thread_local_value(uint32_t dupli_count = std::thread::hardware_concurrency()) : duplicates_(dupli_count) {} @@ -56,15 +92,15 @@ class thread_local_value { return *this; } - void inc(value_type value = 1) { local_value() += value; } + void inc(value_type value = 1) { detail::inc_impl(local_value(), value); } - void dec(value_type value = 1) { local_value() -= value; } + void dec(value_type value = 1) { detail::dec_impl(local_value(), value); } value_type update(value_type value = 1) { - value_type val = get_value(0).exchange(value); + value_type val = get_value(0).exchange(value, std::memory_order::relaxed); for (size_t i = 1; i < duplicates_.size(); i++) { if (duplicates_[i]) { - val += duplicates_[i].load()->exchange(0); + val += duplicates_[i].load()->exchange(0, std::memory_order::relaxed); } } return val; @@ -89,7 +125,7 @@ class thread_local_value { return *duplicates_[index]; } - value_type value() { + value_type value() const { value_type val = 0; for (auto &t : duplicates_) { if (t) { @@ -99,14 +135,8 @@ class thread_local_value { return val; } - void set_created_time(std::chrono::system_clock::time_point tm) { - created_time_ = tm; - } - - auto get_created_time() { return created_time_; } - private: std::vector *>> duplicates_; std::chrono::system_clock::time_point created_time_{}; }; -} // namespace ylt::metric +} // namespace ylt::metric \ No newline at end of file diff --git a/include/ylt/reflection/member_count.hpp b/include/ylt/reflection/member_count.hpp index 04d12a421..eac839063 100644 --- a/include/ylt/reflection/member_count.hpp +++ b/include/ylt/reflection/member_count.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include diff --git a/include/ylt/standalone/cinatra/coro_http_client.hpp b/include/ylt/standalone/cinatra/coro_http_client.hpp index 7c186a46e..176dc4201 100644 --- a/include/ylt/standalone/cinatra/coro_http_client.hpp +++ b/include/ylt/standalone/cinatra/coro_http_client.hpp @@ -1044,6 +1044,15 @@ class coro_http_client : public std::enable_shared_from_this { std::make_error_code(std::errc::no_such_file_or_directory), 404}; } } + else if constexpr (std::is_same_v || + std::is_same_v) { + std::error_code ignore; + if (!std::filesystem::exists(source, ignore)) { + co_return resp_data{ + std::make_error_code(std::errc::no_such_file_or_directory), 404}; + } + } + // get the content_length if (content_length < 0) { if constexpr (is_stream_file) { diff --git a/include/ylt/util/map_sharded.hpp b/include/ylt/util/map_sharded.hpp new file mode 100644 index 000000000..a48a2bcfd --- /dev/null +++ b/include/ylt/util/map_sharded.hpp @@ -0,0 +1,228 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ylt::util { +namespace internal { +template +class map_lock_t { + public: + using key_type = typename Map::key_type; + using value_type = typename Map::value_type; + using mapped_type = typename Map::mapped_type; + map_lock_t() : mtx_(std::make_unique()) {} + + std::shared_ptr find( + const key_type& key) const { + std::lock_guard lock(*mtx_); + if (!map_) [[unlikely]] { + return nullptr; + } + auto it = map_->find(key); + if (it == map_->end()) { + return nullptr; + } + return it->second; + } + + template + std::pair, bool> + try_emplace_with_op(const key_type& key, Op&& op, Args&&... args) { + std::lock_guard lock(*mtx_); + auto result = visit_map().try_emplace(key, std::forward(args)...); + op(result); + return {result.first->second, result.second}; + } + + size_t erase(const key_type& key) { + std::lock_guard lock(*mtx_); + if (!map_) [[unlikely]] { + return 0; + } + return map_->erase(key); + } + + template + size_t erase_if(Func&& op) { + std::lock_guard guard(*mtx_); + if (!map_) [[unlikely]] { + return 0; + } + return std::erase_if(*map_, std::forward(op)); + } + + template + bool for_each(Func&& op) { + std::lock_guard guard(*mtx_); + if (!map_) [[unlikely]] { + return true; + } + for (auto& e : *map_) { + if constexpr (requires { op(e) == true; }) { + if (!op(e)) { + break; + return false; + } + } + else { + op(e); + } + } + return true; + } + + template + bool for_each(Func&& op) const { + std::lock_guard guard(*mtx_); + if (!map_) [[unlikely]] { + return true; + } + for (const auto& e : *map_) { + if constexpr (requires { op(e) == true; }) { + if (!op(e)) { + break; + return false; + } + } + else { + op(e); + } + } + return true; + } + + private: + Map& visit_map() { + if (!map_) [[unlikely]] { + map_ = std::make_unique(); + } + return *map_; + } + + std::unique_ptr mtx_; + std::unique_ptr map_; +}; +} // namespace internal + +template +class map_sharded_t { + public: + using key_type = typename Map::key_type; + using value_type = typename Map::value_type; + using mapped_type = typename Map::mapped_type; + map_sharded_t(size_t shard_num) : shards_(shard_num) {} + + template + std::pair, bool> + try_emplace(KeyType&& key, Args&&... args) { + return try_emplace_with_op( + std::forward(key), + [](auto&&) { + }, + std::forward(args)...); + } + + template + std::pair, bool> + try_emplace_with_op(const key_type& key, Op&& func, Args&&... args) { + auto ret = get_sharded(Hash{}(key)) + .try_emplace_with_op(key, std::forward(func), + std::forward(args)...); + if (ret.second) { + size_.fetch_add(1); + } + return ret; + } + + size_t size() const { // this value is approx + int64_t val = size_.load(); + if (val < 0) [[unlikely]] { // may happen when insert & deleted frequently + val = 0; + } + return val; + } + + std::shared_ptr find( + const key_type& key) const { + return get_sharded(Hash{}(key)).find(key); + } + + size_t erase(const key_type& key) { + auto result = get_sharded(Hash{}(key)).erase(key); + if (result) { + size_.fetch_sub(result); + } + return result; + } + + template + size_t erase_if(Func&& op) { + auto total = 0; + for (auto& map : shards_) { + auto result = map.erase_if(std::forward(op)); + total += result; + size_.fetch_sub(result); + } + return total; + } + + template + size_t erase_one(Func&& op) { + auto total = 0; + for (auto& map : shards_) { + auto result = map.erase_if(std::forward(op)); + if (result) { + total += result; + size_.fetch_sub(result); + break; + } + } + return total; + } + + template + void for_each(Func&& op) { + for (auto& map : shards_) { + if (!map.for_each(op)) + break; + } + } + + template + std::vector copy(auto&& op) const { + std::vector ret; + ret.reserve(size()); + for (auto& map : shards_) { + map.for_each([&ret, &op](auto& e) { + if (op(e.second)) { + ret.push_back(e.second); + } + }); + } + return ret; + } + template + std::vector copy() const { + return copy([](auto&) { + return true; + }); + } + + private: + internal::map_lock_t& get_sharded(size_t hash) { + return shards_[hash % shards_.size()]; + } + const internal::map_lock_t& get_sharded(size_t hash) const { + return shards_[hash % shards_.size()]; + } + + std::vector> shards_; + std::atomic size_; +}; +} // namespace ylt::util \ No newline at end of file diff --git a/src/coro_http/examples/example.cpp b/src/coro_http/examples/example.cpp index 9c2431ee3..1ae867417 100644 --- a/src/coro_http/examples/example.cpp +++ b/src/coro_http/examples/example.cpp @@ -689,8 +689,9 @@ void test_outbuf() { std::string uri = "http://127.0.0.1:9000/normal"; std::vector oubuf; oubuf.resize(10); + req_context<> ctx{}; auto result = co_await client.async_request(uri, http_method::GET, - req_context<>{}, {}, oubuf); + std::move(ctx), {}, oubuf); std::cout << oubuf.data() << "\n"; std::string_view out_view(oubuf.data(), result.resp_body.size()); diff --git a/src/coro_http/tests/CMakeLists.txt b/src/coro_http/tests/CMakeLists.txt new file mode 100644 index 000000000..861a43eb1 --- /dev/null +++ b/src/coro_http/tests/CMakeLists.txt @@ -0,0 +1,58 @@ +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/output/tests) +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "Release") +endif() +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_INCLUDE_CURRENT_DIR ON) +find_package(Threads REQUIRED) +#find_package(ZLIB) +#if (ZLIB_FOUND) +# add_definitions(-DCINATRA_ENABLE_GZIP) +#endif () +link_libraries(Threads::Threads) + +include_directories(include) +include_directories(include/ylt/thirdparty) + +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fcoroutines") + #-ftree-slp-vectorize with coroutine cause link error. disable it util gcc fix. + set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -fno-tree-slp-vectorize") +endif() + +add_executable(coro_http_test + test_mixed.cpp + test_coro_http_server.cpp + test_cinatra.cpp + test_cinatra_websocket.cpp + test_http_parse.cpp + main.cpp + ) + +add_custom_command( + TARGET coro_http_test POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_CURRENT_SOURCE_DIR}/openssl_files + ${CMAKE_BINARY_DIR}/src/coro_http/openssl_files) +add_custom_command( + TARGET coro_http_test POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_CURRENT_SOURCE_DIR}/openssl_files + ${CMAKE_BINARY_DIR}/output/openssl_files) + +# add_definitions(-DINJECT_FOR_HTTP_CLIENT_TEST) +# add_definitions(-DINJECT_FOR_HTTP_SEVER_TEST) + +add_test(NAME coro_http_test COMMAND coro_http_test) +# target_compile_definitions(easylog_test PRIVATE STRUCT_PACK_ENABLE_UNPORTABLE_TYPE) +if (YLT_ENABLE_SSL) + message(STATUS "Use SSL") + find_package(OpenSSL REQUIRED) + add_definitions(-DCINATRA_ENABLE_SSL) + target_link_libraries(coro_http_test OpenSSL::SSL OpenSSL::Crypto) +endif () + +if (ZLIB_FOUND) + target_link_libraries(coro_http_test ZLIB::ZLIB) +endif () diff --git a/src/coro_http/tests/main.cpp b/src/coro_http/tests/main.cpp new file mode 100644 index 000000000..67b88c04f --- /dev/null +++ b/src/coro_http/tests/main.cpp @@ -0,0 +1,9 @@ +#define DOCTEST_CONFIG_IMPLEMENT + +#include "doctest.h" + +// doctest comments +// 'function' : must be 'attribute' - see issue #182 +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4007) +int main(int argc, char** argv) { return doctest::Context(argc, argv).run(); } +DOCTEST_MSVC_SUPPRESS_WARNING_POP \ No newline at end of file diff --git a/src/coro_http/tests/openssl_files/server.crt b/src/coro_http/tests/openssl_files/server.crt new file mode 100644 index 000000000..aca31a7e3 --- /dev/null +++ b/src/coro_http/tests/openssl_files/server.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDKDCCAhACCQDHu0UVVUEr4DANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJD +TjEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5MRwwGgYDVQQKDBNEZWZhdWx0IENvbXBh +bnkgTHRkMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMjIxMDI1MDM1NzMwWhcNMzIx +MDIyMDM1NzMwWjBWMQswCQYDVQQGEwJDTjEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5 +MRwwGgYDVQQKDBNEZWZhdWx0IENvbXBhbnkgTHRkMRIwEAYDVQQDDAlsb2NhbGhv +c3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCr6iWgRRYJ9QfKSUPT +nbw2rKZRlSBqnLeLdPam+s8RUA1p+YPoH2HJqIdxcfYmToz5t6G5OX8TFhAssShw +PalRlQm5QHp4pL7nqPV79auB3PYKv6TgOumwDUpoBxcu0l9di9fjYbC2LmpVJeVz +WQxCo+XO/g5YjXN1nPPeBgmZVkRvXLIYCTKshLlUa0nW7hj7Sl8CAV8OBNMBFkf1 +2vgcTqhs3yW9gnIwIoCFZvsdAsSbwR6zF1z96MeAYDIZWeyzUXkoZa4OCWwAhqzo ++0JWukuNuHhsQhIJDvIZWHEblT0GlentP8HPXjFnJHYGUAjx3Fj1mH8mFG0fEXXN +06qlAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAGbKTy1mfSlJF012jKuIue2valI2 +CKz8X619jmxxIzk0k7wcmAlUUrUSFIzdIddZj92wYbBC1YNOWQ4AG5zpFo3NAQaZ +kYGnlt+d2pNHLaT4IV9JM4iwTqPyi+FsOwTjUGHgaOr+tfK8fZmPbDmAE46OlC/a +VVqNPmjaJiM2c/pJOs+HV9PvEOFmV9p5Yjjz4eV3jwqHdOcxZuLJl28/oqz65uCu +LQiivkdVCuwc1IlpRFejkrbkrk28XCCJwokLt03EQj4xs0sjoTKgd92fpjls/tt+ +rw+7ILsAsuoWPIdiuCArCU1LXJDz3FDHafX/dxzdVBzpfVgP0rNpS050Mls= +-----END CERTIFICATE----- diff --git a/src/coro_http/tests/openssl_files/server.key b/src/coro_http/tests/openssl_files/server.key new file mode 100644 index 000000000..9aef5f575 --- /dev/null +++ b/src/coro_http/tests/openssl_files/server.key @@ -0,0 +1,30 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,D920B8941C56ADDC + +I2lW3QsAG/xubjtXpXh3wQ5Ru3VZiMkPNjc+G6/2JjjVr1sD+fzCWvvwdqdxGuNJ +gKdpPBHLuQfTTzGETE4NKDkYzmiPTVbZPJ77DyfL2cK1dcZtAY46RsHf+VMI5N8l +Be1jQSB5xvUa88dSIeowPTc2XSnTIoSFWCa38XuqYF7i0a3lv96eAyXpqB7Tm2r8 +SoYlm0n7/uzRpk6HWST65qnVv/j+37LuvSy6ehyh44+KDS4x9FUOZc5xwJ/37Jnl +SDC10+9zLc+jOTk6XgUuBSmG+xfZdcOrbknQ1Xj1YtseYH0plYAEWi4PsnMQkHzC +GGvK08Lgqxd7cGEKFh2MRZ/TEwriN5ud5HGm4yIHIj45rbedtRSQwl2EyHdWeW0J +rFltDy+SXnnkJaOcnBYXUD1jEwyy2lLamWRiu83VFbCv6yhOYuR6JejM6dctjgZ+ +Qf0PzH6L1bVpHKEl/GLByJ6GWYrQJqw83LAXlR+NNCC3nN7WAAaTuzA9LpgW9Vk0 +khRRs7rJGxwwwE4TfG9FbQxwuOsjKV9pRohB1x1nFMMm5IJ9SON2KjizsVdLbt7t +Gb/5M7RcSnnGvIWWXalXpFGKgciwYd8F1v0TJ+FMooZxgUp7Pmp5YKIHkBjMrnnW +rKuoxmA5oPgSNUtr4ddMJ1sTIQPhqI27+CrySTzWKH1ls45okBvsiCejpcJwfrZW +KLSkz/FsPoWm44uomBSDOikry8axrKQLB9tOVPKCx/z0VP060P9N81mu4h67bixr +xu+odIONqGhRZT/BYHL2NjDfWlFmTJQy8Drn1a7IEhp8FV7l2aY/hisrMN7MQVza +FGB0hMbVHGeFOCD9QNQwRU2wLtwpE7LT/lGNmKadQadXxeAqOWBckXrpwnrxZDEP +a8AYr2J55h/IE4Oi2DyibSEZdB+7334OJHMmr14q53eIpeit19BYVhWyu9AtORJp +As61C7s82AO+E5gOswsq05jwWV/GIIkgZ8/vswEffiihmDEf6AUZsVGW3BlpFlyU +i3g4e8HFTJ+s9Z3sTgZ1EWOP6Wd2OzyQYVA4ggBR/g/IC9s5em1wvAkVwIZaPvj7 +21BIQXyiGrw52T+vTUrAUG0l7yoHGCgVYJ+aEm+f103AiBYuReUbo39GEIY2GHLu +r3oUehtt4of0ootmPCmjrRUyY6LPeD+d+i1jJUSYFKezsVRpaiF5+J8YLGMcOPiI +8qRRNgXDMMvttwyhoxyr5+667OMv+XWr2VQj7i9MWCFwTMwNzdUoZI3PWDhXbXDO +lQJS6v3iAPw+KvLJywODe+C4shUqYdrRdUSKE0FfuB8Ajzh86+FmjJcZM+BSxM4J +hC2yjv114jDlsgjFSxQE2K1iotLUY9mfmW8QWVMO3L4LlNpr4ypNLYX0Ph2wgqzQ +kszXTFN11RFKFLUhF0Mi5m4ffMLPD5YyoqO9grpyC1Nt7vxaPPvcvPD86jK3ksqJ +MwucZGgm9HtUuAjGOSljUr0d+d+4pySJbcpH2YDIBHGVsCScYPVg8XZ1CYko3mq/ +d6jDUgydraEmQvIPiKMpTE18rW+jierv2FlB8AGcwxm2VWxuM25wQ40J2YuZLY7k +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/src/coro_http/tests/test_cinatra.cpp b/src/coro_http/tests/test_cinatra.cpp new file mode 100644 index 000000000..e464e2459 --- /dev/null +++ b/src/coro_http/tests/test_cinatra.cpp @@ -0,0 +1,2675 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "doctest.h" +#include "ylt/coro_http/coro_http_client.hpp" +#include "ylt/coro_http/coro_http_server.hpp" + +using namespace std::chrono_literals; + +using namespace cinatra; + +#ifdef CINATRA_ENABLE_GZIP +std::string_view get_header_value(auto &resp_headers, std::string_view key) { + for (const auto &[k, v] : resp_headers) { + if (k == key) + return v; + } + return {}; +} + +TEST_CASE("test for gzip") { + coro_http_server server(1, 8090); + server.set_http_handler( + "/gzip", [](coro_http_request &req, coro_http_response &res) { + CHECK(req.get_header_value("Content-Encoding") == "gzip"); + res.set_status_and_content(status_type::ok, "hello world", + content_encoding::gzip); + }); + server.async_start(); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + coro_http_client client{}; + std::string uri = "http://127.0.0.1:8090/gzip"; + client.add_header("Content-Encoding", "gzip"); + auto result = async_simple::coro::syncAwait(client.async_get(uri)); + auto content = get_header_value(result.resp_headers, "Content-Encoding"); + CHECK(get_header_value(result.resp_headers, "Content-Encoding") == "gzip"); + CHECK(result.resp_body == "hello world"); + server.stop(); +} + +TEST_CASE("test encoding type") { + coro_http_server server(1, 9001); + + server.set_http_handler("/get", [](coro_http_request &req, + coro_http_response &resp) { + auto encoding_type = req.get_encoding_type(); + + if (encoding_type == + content_encoding::gzip) { // only post request have this field + std::string decode_str; + bool r = gzip_codec::uncompress(req.get_body(), decode_str); + CHECK(decode_str == "Hello World"); + } + resp.set_status_and_content(status_type::ok, "ok", content_encoding::gzip, + req.get_accept_encoding()); + CHECK(resp.content() != "ok"); + }); + + server.set_http_handler( + "/coro", + [](coro_http_request &req, + coro_http_response &resp) -> async_simple::coro::Lazy { + resp.set_status_and_content(status_type::ok, "ok", + content_encoding::deflate, + req.get_accept_encoding()); + CHECK(resp.content() != "ok"); + co_return; + }); + + server.set_http_handler( + "/only_gzip", + [](coro_http_request &req, + coro_http_response &resp) -> async_simple::coro::Lazy { + resp.set_status_and_content(status_type::ok, "ok", + content_encoding::gzip, + req.get_accept_encoding()); + // client4 accept-encoding not allow gzip, response content no + // compression + CHECK(resp.content() == "ok"); + co_return; + }); + + server.async_start(); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + coro_http_client client1{}; + client1.add_header("Accept-Encoding", "gzip, deflate"); + auto result = async_simple::coro::syncAwait( + client1.async_get("http://127.0.0.1:9001/get")); + CHECK(result.resp_body == "ok"); + + coro_http_client client2{}; + client2.add_header("Accept-Encoding", "gzip, deflate"); + result = async_simple::coro::syncAwait( + client2.async_get("http://127.0.0.1:9001/coro")); + CHECK(result.resp_body == "ok"); + + coro_http_client client3{}; + std::unordered_map headers = { + {"Content-Encoding", "gzip"}, + }; + std::string ziped_str; + std::string_view data = "Hello World"; + gzip_codec::compress(data, ziped_str); + result = async_simple::coro::syncAwait(client3.async_post( + "http://127.0.0.1:9001/get", ziped_str, req_content_type::none, headers)); + CHECK(result.resp_body == "ok"); + + coro_http_client client4{}; + client4.add_header("Accept-Encoding", "deflate"); + result = async_simple::coro::syncAwait( + client4.async_get("http://127.0.0.1:9001/only_gzip")); + CHECK(result.resp_body == "ok"); + + server.stop(); +} +#endif + +#ifdef CINATRA_ENABLE_BROTLI +TEST_CASE("test brotli type") { + coro_http_server server(1, 9001); + + server.set_http_handler( + "/get", [](coro_http_request &req, coro_http_response &resp) { + auto encoding_type = req.get_encoding_type(); + + if (encoding_type == content_encoding::br) { + std::string decode_str; + bool r = br_codec::brotli_decompress(req.get_body(), decode_str); + CHECK(decode_str == "Hello World"); + } + resp.set_status_and_content(status_type::ok, "ok", content_encoding::br, + req.get_accept_encoding()); + }); + + server.async_start(); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + coro_http_client client{}; + std::unordered_map headers = { + {"Content-Encoding", "br"}, + }; + std::string ziped_str; + std::string_view data = "Hello World"; + bool r = br_codec::brotli_compress(data, ziped_str); + + auto result = async_simple::coro::syncAwait(client.async_post( + "http://127.0.0.1:9001/get", ziped_str, req_content_type::none, headers)); + CHECK(result.resp_body == "ok"); + server.stop(); +} +#endif + +#ifdef CINATRA_ENABLE_SSL +TEST_CASE("test ssl client") { + { + coro_http_client client4{}; + client4.set_ssl_schema(true); + auto result = client4.get("www.baidu.com"); + assert(result.status == 200); + + auto lazy = []() -> async_simple::coro::Lazy { + coro_http_client client5{}; + client5.set_ssl_schema(true); + co_await client5.connect("www.baidu.com"); + auto result = co_await client5.async_get("/"); + assert(result.status == 200); + }; + async_simple::coro::syncAwait(lazy()); + } + { + coro_http_client client{}; + auto ret = client.get("https://baidu.com"); + client.reset(); + ret = client.get("https://cn.bing.com"); + std::cout << ret.status << std::endl; + client.reset(); + ret = client.get("https://baidu.com"); + std::cout << ret.status << std::endl; + } + { + coro_http_client client{}; + auto result = client.get("https://www.bing.com"); + CHECK(result.status >= 200); + } + + { + coro_http_client client{}; + auto r = + async_simple::coro::syncAwait(client.connect("https://www.baidu.com")); + if (r.status == 200) { + auto result = client.get("/"); + CHECK(result.status >= 200); + } + } + + { + coro_http_client client{}; + auto result = client.get("http://www.bing.com"); + CHECK(result.status >= 200); + } + + { + coro_http_client client{}; + client.set_ssl_schema(true); + auto result = client.get("www.bing.com"); + CHECK(result.status >= 200); + } + + { + coro_http_client client{}; + client.set_ssl_schema(false); + auto result = client.get("https://www.bing.com"); + CHECK(result.status >= 200); + } + + { + coro_http_client client{}; + client.enable_auto_redirect(true); + bool ok = client.init_ssl(); + client.reset(); + REQUIRE_MESSAGE(ok == true, "init ssl fail, please check ssl config"); + auto result = client.get("https://www.bing.com"); + CHECK(result.status >= 200); + } + + { + coro_http_client client{}; + client.set_req_timeout(8s); + client.enable_auto_redirect(true); + std::string uri = "http://www.bing.com"; + // Make sure the host and port are matching with your proxy server + client.set_proxy("106.14.255.124", "80"); + resp_data result = async_simple::coro::syncAwait(client.async_get(uri)); + if (!result.net_err) + CHECK(result.status >= 200); + } + + { + coro_http_client client{}; + bool ok = client.init_ssl(); + REQUIRE_MESSAGE(ok == true, "init ssl fail, please check ssl config"); + auto result = client.get("https://www.bing.com"); + CHECK(result.status >= 200); + } +} + +TEST_CASE("test ssl client") { + coro_http_client client{}; + bool ok = client.init_ssl(); + REQUIRE_MESSAGE(ok == true, "init ssl fail, please check ssl config"); + // client.set_sni_hostname("https://www.bing.com"); + auto result = client.get("https://www.bing.com"); + CHECK(result.status >= 200); +} +#endif + +bool create_file(std::string_view filename, size_t file_size = 1024) { + std::ofstream out(filename.data(), std::ios::binary); + if (!out.is_open()) { + return false; + } + + std::string str; + for (int i = 0; i < file_size; ++i) { + str.push_back(rand() % 26 + 'A'); + } + out.write(str.data(), str.size()); + return true; +} + +TEST_CASE("test cinatra::string with SSO") { + std::string s = "HelloHi"; + auto oldlen = s.length(); + s.reserve(10); + memset(s.data() + oldlen + 1, 'A', 3); + cinatra::detail::resize(s, 10); + CHECK(s[10] == '\0'); + memcpy(s.data() + oldlen, "233", 3); + CHECK(strlen(s.data()) == 10); + CHECK(s == "HelloHi233"); +} + +TEST_CASE("test parse query") { + { + http_parser parser{}; + parser.parse_query("="); + parser.parse_query("&a"); + parser.parse_query("&b="); + parser.parse_query("&c=&d"); + parser.parse_query("&e=&f=1"); + parser.parse_query("&g=1&h=1"); + auto map = parser.queries(); + CHECK(map["a"].empty()); + CHECK(map["b"].empty()); + CHECK(map["c"].empty()); + CHECK(map["d"].empty()); + CHECK(map["e"].empty()); + CHECK(map["f"] == "1"); + CHECK(map["g"] == "1"); + CHECK(map["h"] == "1"); + } + { + http_parser parser{}; + parser.parse_query("test"); + parser.parse_query("test1="); + parser.parse_query("test2=&"); + parser.parse_query("test3&"); + parser.parse_query("test4&a"); + parser.parse_query("test5&b=2"); + parser.parse_query("test6=1&c=2"); + parser.parse_query("test7=1&d"); + parser.parse_query("test8=1&e="); + parser.parse_query("test9=1&f"); + parser.parse_query("test10=1&g=10&h&i=3&j"); + auto map = parser.queries(); + CHECK(map["test"].empty()); + CHECK(map.size() == 21); + } +} + +TEST_CASE("test cinatra::string without SSO") { + std::string s(1000, 'A'); + std::string s2(5000, 'B'); + std::string sum = s + s2; + auto oldlen = s.length(); + s.reserve(6000); + memset(s.data() + oldlen + 1, 'A', 5000); + cinatra::detail::resize(s, 6000); + CHECK(s[6000] == '\0'); + memcpy(s.data() + oldlen, s2.data(), s2.length()); + CHECK(strlen(s.data()) == 6000); + CHECK(s == sum); +} + +TEST_CASE("test cinatra::string SSO to no SSO") { + std::string s(10, 'A'); + std::string s2(5000, 'B'); + std::string sum = s + s2; + auto oldlen = s.length(); + s.reserve(5010); + memset(s.data() + oldlen + 1, 'A', 5000); + cinatra::detail::resize(s, 5010); + CHECK(s[5010] == '\0'); + memcpy(s.data() + oldlen, s2.data(), s2.length()); + CHECK(strlen(s.data()) == 5010); + CHECK(s == sum); +} + +TEST_CASE("test config") { + coro_http_client client{}; + coro_http_client::config conf{}; + conf.sec_key = "s//GYHa/XO7Hd2F2eOGfyA=="; + conf.proxy_host = "http://example.com"; + conf.proxy_host = "9090"; + conf.max_single_part_size = 1024 * 1024; + conf.proxy_auth_username = "cinatra"; + conf.proxy_auth_token = "cinatra"; + conf.proxy_auth_passwd = "cinatra"; + conf.enable_tcp_no_delay = true; + client.init_config(conf); + + std::unordered_map req_headers{{"test", "ok"}}; + client.set_headers(req_headers); + const auto &headers = client.get_headers(); + CHECK(req_headers == headers); + + auto &executor = client.get_executor(); + auto name = executor.name(); + CHECK(!name.empty()); + + const auto &c = client.get_config(); + CHECK(c.enable_tcp_no_delay == conf.enable_tcp_no_delay); + CHECK(c.max_single_part_size == 1024 * 1024); + + auto ret = async_simple::coro::syncAwait(client.connect("http://##test.com")); + CHECK(ret.status != 200); + CHECK(ret.net_err.value() == (int)std::errc::protocol_error); +} + +async_simple::coro::Lazy send_data(auto &ch, size_t count) { + for (int i = 0; i < count; i++) { + co_await coro_io::async_send(ch, i); + } +} + +async_simple::coro::Lazy recieve_data(auto &ch, auto &vec, size_t count) { + while (true) { + if (vec.size() == count) { + std::cout << std::this_thread::get_id() << "\n"; + break; + } + + auto [ec, i] = co_await coro_io::async_receive(ch); + vec.push_back(i); + } +} + +// TEST_CASE("test coro channel with multi thread") { +// size_t count = 10000; +// auto ch = coro_io::create_channel(count); +// send_data(ch, count).via(ch.get_executor()).start([](auto &&) { +// }); + +// std::vector vec; +// std::vector group; +// for (int i = 0; i < 10; i++) { +// group.emplace_back(std::thread([&]() { +// async_simple::coro::syncAwait( +// recieve_data(ch, vec, count).via(ch.get_executor())); +// })); +// } +// for (auto &thd : group) { +// thd.join(); +// } + +// for (int i = 0; i < count; i++) { +// CHECK(vec.at(i) == i); +// } +// } + +// TEST_CASE("test coro channel") { +// { +// auto ch = coro_io::create_shared_channel(100); +// auto ec = async_simple::coro::syncAwait( +// coro_io::async_send(*ch, std::string("test"))); +// CHECK(!ec); + +// std::string val; +// std::error_code err; +// std::tie(err, val) = +// async_simple::coro::syncAwait(coro_io::async_receive(*ch)); +// CHECK(!err); +// CHECK(val == "test"); +// } +// auto ch = coro_io::create_channel(1000); +// auto ec = async_simple::coro::syncAwait(coro_io::async_send(ch, 41)); +// CHECK(!ec); +// ec = async_simple::coro::syncAwait(coro_io::async_send(ch, 42)); +// CHECK(!ec); + +// std::error_code err; +// int val; +// std::tie(err, val) = +// async_simple::coro::syncAwait(coro_io::async_receive(ch)); +// CHECK(!err); +// CHECK(val == 41); + +// std::tie(err, val) = +// async_simple::coro::syncAwait(coro_io::async_receive(ch)); +// CHECK(!err); +// CHECK(val == 42); +// } + +// async_simple::coro::Lazy test_select_channel() { +// using namespace coro_io; +// using namespace async_simple; +// using namespace async_simple::coro; + +// auto ch1 = coro_io::create_channel(1000); +// auto ch2 = coro_io::create_channel(1000); + +// co_await async_send(ch1, 41); +// co_await async_send(ch2, 42); + +// std::array arr{41, 42}; +// int val; + +// size_t index = +// co_await select(std::pair{async_receive(ch1), +// [&val](auto value) { +// auto [ec, r] = value.value(); +// val = r; +// }}, +// std::pair{async_receive(ch2), [&val](auto value) { +// auto [ec, r] = value.value(); +// val = r; +// }}); + +// CHECK(val == arr[index]); + +// co_await async_send(ch1, 41); +// co_await async_send(ch2, 42); + +// std::vector>> vec; +// vec.push_back(async_receive(ch1)); +// vec.push_back(async_receive(ch2)); + +// index = co_await select(std::move(vec), [&](size_t i, auto result) { +// val = result.value().second; +// }); +// CHECK(val == arr[index]); + +// period_timer timer1(coro_io::get_global_executor()); +// timer1.expires_after(100ms); +// period_timer timer2(coro_io::get_global_executor()); +// timer2.expires_after(200ms); + +// int val1; +// index = co_await select(std::pair{timer1.async_await(), +// [&](auto val) { +// CHECK(val.value()); +// val1 = 0; +// }}, +// std::pair{timer2.async_await(), [&](auto val) { +// CHECK(val.value()); +// val1 = 0; +// }}); +// CHECK(index == val1); + +// int val2; +// index = co_await select(std::pair{coro_io::post([] { +// }), +// [&](auto) { +// std::cout << "post1\n"; +// val2 = 0; +// }}, +// std::pair{coro_io::post([] { +// }), +// [&](auto) { +// std::cout << "post2\n"; +// val2 = 1; +// }}); +// CHECK(index == val2); + +// co_await async_send(ch1, 43); +// auto lazy = coro_io::post([] { +// }); + +// int val3 = -1; +// index = co_await select(std::pair{async_receive(ch1), +// [&](auto result) { +// val3 = result.value().second; +// }}, +// std::pair{std::move(lazy), [&](auto) { +// val3 = 0; +// }}); + +// if (index == 0) { +// CHECK(val3 == 43); +// } +// else if (index == 1) { +// CHECK(val3 == 0); +// } +// } + +// TEST_CASE("test select coro channel") { +// using namespace coro_io; +// async_simple::coro::syncAwait(test_select_channel()); + +// auto ch = coro_io::create_channel(1000); + +// async_simple::coro::syncAwait(coro_io::async_send(ch, 41)); +// async_simple::coro::syncAwait(coro_io::async_send(ch, 42)); + +// std::error_code ec; +// int val; +// std::tie(ec, val) = +// async_simple::coro::syncAwait(coro_io::async_receive(ch)); CHECK(val == +// 41); + +// std::tie(ec, val) = +// async_simple::coro::syncAwait(coro_io::async_receive(ch)); CHECK(val == +// 42); +// } + +TEST_CASE("test bad address") { + { + coro_http_server server(1, 9001, "127.0.0.1"); + server.async_start(); + auto ec = server.get_errc(); + CHECK(!ec); + } + { + coro_http_server server(1, 9001, "localhost"); + server.async_start(); + auto ec = server.get_errc(); + CHECK(!ec); + } + { + coro_http_server server(1, 9001, "0.0.0.0"); + server.async_start(); + auto ec = server.get_errc(); + CHECK(!ec); + } + { + coro_http_server server(1, 9001); + server.async_start(); + auto ec = server.get_errc(); + CHECK(!ec); + } + { + coro_http_server server(1, "0.0.0.0:9001"); + server.async_start(); + auto ec = server.get_errc(); + CHECK(!ec); + } + { + coro_http_server server(1, "127.0.0.1:9001"); + server.async_start(); + auto ec = server.get_errc(); + CHECK(!ec); + } + { + coro_http_server server(1, "localhost:9001"); + server.async_start(); + auto ec = server.get_errc(); + CHECK(!ec); + } + + { + coro_http_server server(1, 9001, "x.x.x.x"); + server.async_start(); + auto ec = server.get_errc(); + CHECK(ec); + } + { + coro_http_server server(1, "localhost:aaa"); + server.async_start(); + auto ec = server.get_errc(); + CHECK(ec); + } +} + +async_simple::coro::Lazy test_collect_all() { + asio::io_context ioc; + std::thread thd([&] { + ioc.run(); + }); + std::vector> v; + std::vector> futures; + for (int i = 0; i < 2; ++i) { + auto client = std::make_shared(); + client->set_conn_timeout(2s); + v.push_back(client); + futures.push_back(client->async_get("http://www.baidu.com/")); + } + + auto out = co_await async_simple::coro::collectAll(std::move(futures)); + + for (auto &item : out) { + auto result = item.value(); + CHECK(result.status >= 200); + } + thd.join(); +} + +TEST_CASE("test default http handler") { + coro_http_server server(1, 9001); + server.set_default_handler( + [](coro_http_request &req, + coro_http_response &resp) -> async_simple::coro::Lazy { + resp.set_status_and_content(status_type::ok, + "It is from default handler"); + co_return; + }); + server.set_http_handler( + "/view", + [](coro_http_request &req, + coro_http_response &resp) -> async_simple::coro::Lazy { + resp.set_delay(true); + resp.set_status_and_content_view(status_type::ok, + req.get_body()); // no copy + co_await resp.get_conn()->reply(); + }); + server.async_start(); + + for (int i = 0; i < 5; i++) { + coro_http_client client{}; + async_simple::coro::syncAwait(client.connect("http://127.0.0.1:9001")); + auto data = client.get("/test"); + CHECK(data.resp_body == "It is from default handler"); + data = client.get("/test_again"); + CHECK(data.resp_body == "It is from default handler"); + data = client.get("/any"); + CHECK(data.resp_body == "It is from default handler"); + data = async_simple::coro::syncAwait( + client.async_post("/view", "post string", req_content_type::string)); + CHECK(data.status == 200); + CHECK(data.resp_body == "post string"); + } +} + +TEST_CASE("test request with out buffer") { + coro_http_server server(1, 8090); + server.set_http_handler( + "/test", [](coro_http_request &req, coro_http_response &resp) { + resp.set_status_and_content(status_type::ok, + "it is a test string, more than 10 bytes"); + }); + server.set_http_handler( + "/test1", [](coro_http_request &req, coro_http_response &resp) { + resp.set_format_type(format_type::chunked); + resp.set_status_and_content(status_type::ok, + "it is a test string, more than 10 bytes"); + }); + server.async_start(); + + std::string str; + str.resize(10); + std::string url = "http://127.0.0.1:8090/test"; + std::string url1 = "http://127.0.0.1:8090/test1"; + + { + coro_http_client client; + client.add_header("Host", "cinatra"); + auto ret = client.async_request(url, http_method::GET, req_context<>{}, {}, + std::span{str.data(), str.size()}); + auto result = async_simple::coro::syncAwait(ret); + std::cout << result.status << "\n"; + std::cout << result.net_err.message() << "\n"; + std::cout << result.resp_body << "\n"; + CHECK(result.status == 200); + CHECK(!client.is_body_in_out_buf()); + } + + { + coro_http_client client; + auto ret = client.async_request(url1, http_method::GET, req_context<>{}, {}, + std::span{str.data(), str.size()}); + auto result = async_simple::coro::syncAwait(ret); + std::cout << result.status << "\n"; + std::cout << result.net_err.message() << "\n"; + std::cout << result.resp_body << "\n"; + CHECK(result.status == 200); + CHECK(!client.is_body_in_out_buf()); + auto s = client.release_buf(); + CHECK(s == "it is a test string, more than 10 bytes"); + } + + { + detail::resize(str, 1024); + coro_http_client client; + auto ret = client.async_request(url, http_method::GET, req_context<>{}, {}, + std::span{str.data(), str.size()}); + auto result = async_simple::coro::syncAwait(ret); + bool ok = result.status == 200 || result.status == 301; + CHECK(ok); + std::string_view sv(str.data(), result.resp_body.size()); + CHECK(result.resp_body == sv); + CHECK(client.is_body_in_out_buf()); + } + + { + detail::resize(str, 1024 * 64); + coro_http_client client; + client.set_conn_timeout(3s); + client.set_req_timeout(5s); + std::string dest = "http://www.baidu.com"; + auto ret = client.async_request(dest, http_method::GET, req_context<>{}, {}, + std::span{str.data(), str.size()}); + auto result = async_simple::coro::syncAwait(ret); + bool ok = result.status == 200 || result.status == 301; + if (ok && result.resp_body.size() <= 1024 * 64) { + std::string_view sv(str.data(), result.resp_body.size()); + CHECK(client.is_body_in_out_buf()); + } + } +} + +TEST_CASE("test pass path not entire uri") { + coro_http_client client{}; + client.set_conn_timeout(2s); + auto r = + async_simple::coro::syncAwait(client.async_get("http://www.baidu.com")); + std::cout << r.resp_body.size() << "\n"; + auto buf = client.release_buf(); + std::cout << strlen(buf.data()) << "\n"; + std::cout << buf << "\n"; + CHECK(r.status >= 200); + + r = async_simple::coro::syncAwait(client.async_get("http://www.baidu.com")); + CHECK(r.status >= 200); + + r = async_simple::coro::syncAwait(client.async_get("/")); + CHECK(r.status >= 200); +} + +TEST_CASE("test coro_http_client connect/request timeout") { + { +#if !defined(_MSC_VER) + coro_http_client client{}; + cinatra::coro_http_client::config conf{.conn_timeout_duration = 1ms}; + client.init_config(conf); + auto r = + async_simple::coro::syncAwait(client.async_get("http://www.baidu.com")); + std::cout << r.net_err.value() << ", " << r.net_err.message() << "\n"; + CHECK(r.net_err != std::errc{}); +#endif + } + + { + coro_http_client client{}; + cinatra::coro_http_client::config conf{.conn_timeout_duration = 10s, + .req_timeout_duration = 1ms}; + client.init_config(conf); + auto r = + async_simple::coro::syncAwait(client.async_get("http://www.baidu.com")); + std::cout << r.net_err.message() << "\n"; + CHECK(r.net_err != std::errc{}); + } +} + +TEST_CASE("test coro_http_client async_http_connect") { + coro_http_client client{}; + cinatra::coro_http_client::config conf{.req_timeout_duration = 60s}; + client.init_config(conf); + auto r = async_simple::coro::syncAwait( + client.async_http_connect("http://www.baidu.com")); + CHECK(r.status >= 200); + for (auto [k, v] : r.resp_headers) { + std::cout << k << ", " << v << "\n"; + } + + coro_http_client client1{}; + client1.set_conn_timeout(2s); + r = async_simple::coro::syncAwait( + client1.async_http_connect("http//www.badurl.com")); + CHECK(r.status != 200); + + r = async_simple::coro::syncAwait(client1.connect("http://cn.bing.com")); + CHECK(client1.get_host() == "cn.bing.com"); + CHECK(client1.get_port() == "80"); + CHECK(r.status >= 200); + + r = async_simple::coro::syncAwait(client1.connect("http://www.baidu.com")); + + CHECK(r.status >= 200); + r = async_simple::coro::syncAwait(client1.connect("http://cn.bing.com")); + CHECK(r.status == 200); +} + +TEST_CASE("test collect all") { + async_simple::coro::syncAwait(test_collect_all()); +} + +TEST_CASE("test head put and some other request") { + coro_http_server server(1, 8090); + server.set_http_handler( + "/headers", [](coro_http_request &req, coro_http_response &resp) { + resp.add_header("Content-Type", "application/json"); + resp.add_header("Content-Length", "117"); + resp.set_status_and_content(status_type::ok, ""); + }); + server.set_http_handler( + "/", [](coro_http_request &req, coro_http_response &resp) { + resp.set_status(status_type::method_not_allowed); + }); + server.set_http_handler( + "/", [](coro_http_request &req, coro_http_response &resp) { + resp.add_header("Allow", "HEAD, OPTIONS, GET, POST, PUT"); + resp.set_status_and_content(status_type::ok, ""); + }); + server.set_http_handler( + "/put/json", [](coro_http_request &req, coro_http_response &resp) { + auto json_str = req.get_body(); + std::ofstream file("json.txt", std::ios::binary); + file.write(json_str.data(), json_str.size()); + file.close(); + resp.set_status_and_content(status_type::ok, ""); + }); + server.set_http_handler( + "/delete/:name", [](coro_http_request &req, coro_http_response &resp) { + auto &filename = req.params_["name"]; + std::error_code ec; + fs::remove(filename, ec); + std::string result = ec ? "delete failed" : "ok"; + resp.set_status_and_content(status_type::ok, result); + }); + server.set_http_handler( + "/delete/:name", [](coro_http_request &req, coro_http_response &resp) { + auto &filename = req.params_["name"]; + std::error_code ec; + fs::remove(filename, ec); + std::string result = ec ? "delete failed" : "delete ok"; + resp.set_status_and_content(status_type::ok, result); + }); + + server.async_start(); + std::this_thread::sleep_for(300ms); + + coro_http_client client{}; + + auto result = async_simple::coro::syncAwait( + client.async_head("http://127.0.0.1:8090/headers")); + CHECK(result.status == 200); + + result = async_simple::coro::syncAwait( + client.async_patch("http://127.0.0.1:8090/")); + CHECK(result.status == 405); + + result = async_simple::coro::syncAwait( + client.async_trace("http://127.0.0.1:8090/")); + CHECK(result.status == 405); + + result = async_simple::coro::syncAwait( + client.async_options("http://127.0.0.1:8090/")); + CHECK(result.status == 200); + + std::string json = R"({ + "Id": 12345, + "Customer": "John Smith", + "Quantity": 1, + "Price": 10.00 + })"; + + coro_http_client client1{}; + result = async_simple::coro::syncAwait(client1.async_put( + "http://127.0.0.1:8090/put/json", json, req_content_type::json)); + CHECK(result.status == 200); + + result = async_simple::coro::syncAwait(client1.async_post( + "http://127.0.0.1:8090/delete/json.txt", json, req_content_type::json)); + + CHECK(result.status == 404); + + result = async_simple::coro::syncAwait(client1.async_delete( + "http://127.0.0.1:8090/delete/json.txt", json, req_content_type::json)); + + CHECK(result.status == 200); +} + +TEST_CASE("test coro_http_client connect/request timeout") { + { +#if !defined(_MSC_VER) + coro_http_client client{}; + cinatra::coro_http_client::config conf{.conn_timeout_duration = 1ms}; + client.init_config(conf); + auto r = + async_simple::coro::syncAwait(client.async_get("http://www.baidu.com")); + std::cout << r.net_err.value() << ", " << r.net_err.message() << "\n"; + if (r.status != 200) + CHECK(r.net_err != std::errc{}); +#endif + } + + { + coro_http_client client{}; + cinatra::coro_http_client::config conf{.conn_timeout_duration = 10s, + .req_timeout_duration = 1ms}; + client.init_config(conf); + auto r = + async_simple::coro::syncAwait(client.async_get("http://www.baidu.com")); + std::cout << r.net_err.message() << "\n"; + CHECK(r.net_err != std::errc{}); + } +} + +TEST_CASE("test upload file") { + coro_http_server server(1, 8090); + server.set_http_handler( + "/multipart", + [](coro_http_request &req, + coro_http_response &resp) -> async_simple::coro::Lazy { + assert(req.get_content_type() == content_type::multipart); + auto boundary = req.get_boundary(); + multipart_reader_t multipart(req.get_conn()); + while (true) { + auto part_head = co_await multipart.read_part_head(boundary); + if (part_head.ec) { + co_return; + } + + std::cout << part_head.name << "\n"; + std::cout << part_head.filename << "\n"; + + std::shared_ptr file; + std::string filename; + if (!part_head.filename.empty()) { + file = std::make_shared(); + filename = std::to_string( + std::chrono::system_clock::now().time_since_epoch().count()); + + size_t pos = part_head.filename.rfind('.'); + if (pos != std::string::npos) { + auto extent = part_head.filename.substr(pos); + filename += extent; + } + + std::cout << filename << "\n"; + file->open(filename, std::ios::trunc | std::ios::out); + if (!file->is_open()) { + resp.set_status_and_content(status_type::internal_server_error, + "file open failed"); + co_return; + } + } + + auto part_body = co_await multipart.read_part_body(boundary); + if (part_body.ec) { + co_return; + } + + if (!filename.empty()) { + auto [ec, sz] = co_await file->async_write(part_body.data); + if (ec) { + co_return; + } + + file->close(); + CHECK(fs::file_size(filename) == 2 * 1024 * 1024); + } + else { + std::cout << part_body.data << "\n"; + } + + if (part_body.eof) { + break; + } + } + + resp.set_status_and_content(status_type::ok, "multipart finished"); + co_return; + }); + + server.async_start(); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + coro_http_client client{}; + std::string uri = "http://127.0.0.1:8090/multipart"; + resp_data result = + async_simple::coro::syncAwait(client.async_upload_multipart(uri)); + CHECK(result.status == 404); + + client.add_str_part("hello", "world"); + client.add_str_part("key", "value"); + CHECK(!client.add_file_part("key", "value")); + result = async_simple::coro::syncAwait(client.async_upload_multipart(uri)); + CHECK(!client.is_redirect(result)); + CHECK(result.resp_body == "multipart finished"); + + client.add_str_part("hello", "world"); + result = async_simple::coro::syncAwait( + client.async_upload_multipart("http//badurl.com")); + CHECK(result.status == 404); + + client.set_max_single_part_size(1024); + std::string test_file_name = "test1.txt"; + std::ofstream test_file; + test_file.open(test_file_name, + std::ios::binary | std::ios::out | std::ios::trunc); + std::vector test_file_data(2 * 1024 * 1024, '0'); + test_file.write(test_file_data.data(), test_file_data.size()); + test_file.close(); + result = async_simple::coro::syncAwait( + client.async_upload_multipart(uri, "test", test_file_name)); + + CHECK(result.resp_body == "multipart finished"); + + std::filesystem::remove(std::filesystem::path(test_file_name)); + + std::string not_exist_file = "notexist.txt"; + result = async_simple::coro::syncAwait(client.async_upload_multipart( + uri, "test_not_exist_file", not_exist_file)); + CHECK(result.status == 404); + + result = async_simple::coro::syncAwait(client.async_upload_multipart( + "http//badurl.com", "test_not_exist_file", not_exist_file)); + CHECK(result.status == 404); + + client.close(); + + server.stop(); +} + +TEST_CASE("test bad uri") { + coro_http_client client{}; + CHECK(client.add_header("hello", "cinatra")); + CHECK(client.add_header("hello", "cinatra")); + CHECK(!client.add_header("", "cinatra")); + client.add_str_part("hello", "world"); + auto result = async_simple::coro::syncAwait( + client.async_upload_multipart("http://www.badurlrandom.org")); + CHECK(result.status == 404); +} + +TEST_CASE("test multiple ranges download") { + coro_http_client client{}; + client.set_conn_timeout(2s); + client.set_req_timeout(5s); + std::string uri = "http://uniquegoodshiningmelody.neverssl.com/favicon.ico"; + + std::string filename = "test1.txt"; + std::error_code ec{}; + std::filesystem::remove(filename, ec); + resp_data result = async_simple::coro::syncAwait( + client.async_download(uri, filename, "1-16")); + if (result.status == 206) { + CHECK(std::filesystem::file_size(filename) == 16); + } +} + +TEST_CASE("test ranges download") { + create_file("test_range.txt", 64); + coro_http_server server(1, 8090); + server.set_static_res_dir("", ""); + server.async_start(); + + coro_http_client client{}; + client.set_req_timeout(std::chrono::seconds(8)); + std::string uri = "http://127.0.0.1:8090/test_range.txt"; + + std::string filename = "test1.txt"; + std::error_code ec{}; + std::filesystem::remove(filename, ec); + resp_data result = async_simple::coro::syncAwait( + client.async_download(uri, filename, "1-10")); + CHECK(result.status == 206); + CHECK(std::filesystem::file_size(filename) == 10); + + filename = "test2.txt"; + std::filesystem::remove(filename, ec); + result = async_simple::coro::syncAwait( + client.async_download(uri, filename, "10-15")); + CHECK(result.status == 206); + CHECK(std::filesystem::file_size(filename) == 6); +} + +TEST_CASE("test ranges download with a bad filename and multiple ranges") { + create_file("test_multiple_range.txt", 64); + coro_http_server server(1, 8090); + server.set_static_res_dir("", ""); + server.async_start(); + + coro_http_client client{}; + std::string uri = "http://127.0.0.1:8090/test_multiple_range.txt"; + + std::string filename = ""; + std::error_code ec{}; + std::filesystem::remove(filename, ec); + resp_data result = async_simple::coro::syncAwait( + client.async_download(uri, filename, "1-10,11-16")); + CHECK(result.status == 404); + CHECK(result.net_err == + std::make_error_code(std::errc::no_such_file_or_directory)); + + client.add_header("Range", "bytes=1-10,20-30"); + result = client.get(uri); + CHECK(result.status == 206); + CHECK(result.resp_body.size() == 21); + + filename = "test_ranges.txt"; + client.add_header("Range", "bytes=0-10,21-30"); + result = client.download(uri, filename); + CHECK(result.status == 206); + CHECK(fs::file_size(filename) == 21); +} + +TEST_CASE("test coro_http_client quit") { + std::promise promise; + [&] { + { coro_http_client client{}; } + promise.set_value(true); + }(); + + CHECK(promise.get_future().get()); +} + +TEST_CASE("test coro_http_client multipart upload") { + coro_http_server server(1, 8090); + server.set_http_handler( + "/multipart_upload", + [](coro_http_request &req, + coro_http_response &resp) -> async_simple::coro::Lazy { + assert(req.get_content_type() == content_type::multipart); + auto boundary = req.get_boundary(); + multipart_reader_t multipart(req.get_conn()); + while (true) { + auto part_head = co_await multipart.read_part_head(boundary); + if (part_head.ec) { + co_return; + } + + std::cout << part_head.name << "\n"; + std::cout << part_head.filename << "\n"; + + std::shared_ptr file; + std::string filename; + if (!part_head.filename.empty()) { + file = std::make_shared(); + filename = std::to_string( + std::chrono::system_clock::now().time_since_epoch().count()); + + size_t pos = part_head.filename.rfind('.'); + if (pos != std::string::npos) { + auto extent = part_head.filename.substr(pos); + filename += extent; + } + + std::cout << filename << "\n"; + file->open(filename, std::ios::trunc | std::ios::out); + if (!file->is_open()) { + resp.set_status_and_content(status_type::internal_server_error, + "file open failed"); + co_return; + } + } + + auto part_body = co_await multipart.read_part_body(boundary); + if (part_body.ec) { + co_return; + } + + if (!filename.empty()) { + auto [ec, sz] = co_await file->async_write(part_body.data); + if (ec) { + co_return; + } + + file->close(); + CHECK(fs::file_size(filename) == 1024); + } + else { + std::cout << part_body.data << "\n"; + } + + if (part_body.eof) { + break; + } + } + + resp.set_status_and_content(status_type::ok, "ok"); + co_return; + }); + + server.async_start(); + + std::string filename = "test_1024.txt"; + create_file(filename); + + coro_http_client client{}; + std::string uri = "http://127.0.0.1:8090/multipart_upload"; + client.add_str_part("test", "test value"); + client.add_file_part("test file", filename); + auto result = + async_simple::coro::syncAwait(client.async_upload_multipart(uri)); + CHECK(result.status == 200); +} + +#ifdef CINATRA_ENABLE_SSL +TEST_CASE("test ssl upload") { + coro_http_server server(1, 8091); + server.init_ssl("../openssl_files/server.crt", "../openssl_files/server.key", + "test"); + server.set_http_handler( + "/upload", + [](coro_http_request &req, + coro_http_response &resp) -> async_simple::coro::Lazy { + std::string_view filename = req.get_header_value("filename"); + uint64_t sz; + auto oldpath = fs::current_path().append(filename); + std::string newpath = fs::current_path() + .append("server_" + std::string{filename}) + .string(); + std::ofstream file(newpath, std::ios::binary); + CHECK(file.is_open()); + file.write(req.get_body().data(), req.get_body().size()); + file.flush(); + file.close(); + + size_t offset = 0; + std::string offset_s = std::string{req.get_header_value("offset")}; + if (!offset_s.empty()) { + offset = stoull(offset_s); + } + + std::string filesize = std::string{req.get_header_value("filesize")}; + + if (!filesize.empty()) { + sz = stoull(filesize); + } + else { + sz = std::filesystem::file_size(oldpath); + sz -= offset; + } + + CHECK(!filename.empty()); + CHECK(sz == std::filesystem::file_size(newpath)); + std::ifstream ifs(oldpath); + ifs.seekg(offset, std::ios::cur); + std::string str; + str.resize(sz); + ifs.read(str.data(), sz); + CHECK(str == req.get_body()); + resp.set_status_and_content(status_type::ok, std::string(filename)); + co_return; + }); + server.async_start(); + + std::string filename = "test_ssl_upload.txt"; + create_file(filename, 10); + std::string uri = "https://127.0.0.1:8091/upload"; + + { + coro_http_client client{}; + bool r = client.init_ssl(); + CHECK(r); + r = client.init_ssl(); + CHECK(r); + client.add_header("filename", filename); + auto lazy = client.async_upload(uri, http_method::PUT, filename); + auto result = async_simple::coro::syncAwait(lazy); + CHECK(result.status == 200); + } + + { + coro_http_client client{}; + client.add_header("filename", filename); + auto lazy = client.async_upload(uri, http_method::PUT, filename); + auto result = async_simple::coro::syncAwait(lazy); + CHECK(result.status == 200); + } + + cinatra::coro_http_server server1(1, 9002); + server1.init_ssl("../openssl_files/server.crt", "../openssl_files/server.key", + "test"); + server1.set_http_handler( + "/chunked", + [](coro_http_request &req, + coro_http_response &resp) -> async_simple::coro::Lazy { + assert(req.get_content_type() == content_type::chunked); + chunked_result result{}; + std::string content; + + while (true) { + result = co_await req.get_conn()->read_chunked(); + if (result.ec) { + co_return; + } + if (result.eof) { + break; + } + + content.append(result.data); + } + + std::cout << "content size: " << content.size() << "\n"; + std::cout << content << "\n"; + resp.set_format_type(format_type::chunked); + resp.set_status_and_content(status_type::ok, "chunked ok"); + }); + server1.async_start(); + + uri = "https://127.0.0.1:9002/chunked"; + { + coro_http_client client{}; + bool r = client.init_ssl(); + CHECK(r); + std::string_view file = "test_ssl_upload.txt"; + client.add_header("filename", filename); + auto lazy = client.async_upload_chunked(uri, http_method::PUT, file); + auto result = async_simple::coro::syncAwait(lazy); + CHECK(result.status == 200); + } + + { + coro_http_client client{}; + client.enable_sni_hostname(true); + bool r = client.init_ssl(); + CHECK(r); + std::unordered_map headers; + headers.emplace("filename", filename); + auto lazy = client.async_upload_chunked(uri, http_method::PUT, filename, + req_content_type::none, headers); + auto result = async_simple::coro::syncAwait(lazy); + CHECK(result.status == 200); + } +} +#endif + +TEST_CASE("test coro_http_client upload") { + auto test_upload_by_file_path = [](std::string filename, + std::size_t offset = 0, + std::size_t r_size = SIZE_MAX, + bool should_failed = false) { + coro_http_client client{}; + client.add_header("filename", filename); + client.add_header("offset", std::to_string(offset)); + if (r_size != SIZE_MAX) + client.add_header("filesize", std::to_string(r_size)); + std::string uri = "http://127.0.0.1:8090/upload"; + cinatra::resp_data result; + if (r_size != SIZE_MAX) { + auto lazy = + client.async_upload(uri, http_method::PUT, filename, offset, r_size); + result = async_simple::coro::syncAwait(lazy); + } + else { + auto lazy = client.async_upload(uri, http_method::PUT, filename, offset); + result = async_simple::coro::syncAwait(lazy); + } + CHECK(((result.status == 200) ^ should_failed)); + }; + auto test_upload_by_stream = [](std::string filename, std::size_t offset = 0, + std::size_t r_size = SIZE_MAX, + bool should_failed = false) { + coro_http_client client{}; + client.add_header("filename", filename); + client.add_header("offset", std::to_string(offset)); + if (r_size != SIZE_MAX) + client.add_header("filesize", std::to_string(r_size)); + std::string uri = "http://127.0.0.1:8090/upload"; + std::ifstream ifs(filename, std::ios::binary); + cinatra::resp_data result; + if (r_size != SIZE_MAX) { + auto lazy = + client.async_upload(uri, http_method::PUT, filename, offset, r_size); + result = async_simple::coro::syncAwait(lazy); + } + else { + auto lazy = client.async_upload(uri, http_method::PUT, filename, offset); + result = async_simple::coro::syncAwait(lazy); + } + CHECK(((result.status == 200) ^ should_failed)); + }; + auto test_upload_by_coro = [](std::string filename, + std::size_t r_size = SIZE_MAX, + bool should_failed = false) { + coro_http_client client{}; + client.add_header("filename", filename); + client.add_header("offset", "0"); + if (r_size != SIZE_MAX) + client.add_header("filesize", std::to_string(r_size)); + std::string uri = "http://127.0.0.1:8090/upload"; + coro_io::coro_file file; + file.open(filename, std::ios::in); + CHECK(file.is_open()); + std::string buf; + buf.resize(1'000'000); + auto async_read = + [&file, &buf]() -> async_simple::coro::Lazy { + auto [ec, size] = co_await file.async_read(buf.data(), buf.size()); + co_return read_result{{buf.data(), size}, file.eof(), ec}; + }; + cinatra::resp_data result; + if (r_size == SIZE_MAX) { + auto lazy = client.async_upload(uri, http_method::PUT, async_read); + result = async_simple::coro::syncAwait(lazy); + CHECK(result.status != 200); + } + else { + auto lazy = + client.async_upload(uri, http_method::PUT, async_read, 0, r_size); + result = async_simple::coro::syncAwait(lazy); + CHECK(((result.status == 200) ^ should_failed)); + } + }; + coro_http_server server(1, 8090); + server.set_http_handler( + "/upload", + [](coro_http_request &req, + coro_http_response &resp) -> async_simple::coro::Lazy { + std::string_view filename = req.get_header_value("filename"); + uint64_t sz; + auto oldpath = fs::current_path().append(filename); + std::string newpath = fs::current_path() + .append("server_" + std::string{filename}) + .string(); + std::ofstream file(newpath, std::ios::binary); + CHECK(file.is_open()); + file.write(req.get_body().data(), req.get_body().size()); + file.flush(); + file.close(); + + size_t offset = 0; + std::string offset_s = std::string{req.get_header_value("offset")}; + if (!offset_s.empty()) { + offset = stoull(offset_s); + } + + std::string filesize = std::string{req.get_header_value("filesize")}; + + if (!filesize.empty()) { + sz = stoull(filesize); + } + else { + sz = std::filesystem::file_size(oldpath); + sz -= offset; + } + + CHECK(!filename.empty()); + CHECK(sz == std::filesystem::file_size(newpath)); + std::ifstream ifs(oldpath); + ifs.seekg(offset, std::ios::cur); + std::string str; + str.resize(sz); + ifs.read(str.data(), sz); + CHECK(str == req.get_body()); + resp.set_status_and_content(status_type::ok, std::string(filename)); + co_return; + }); + server.async_start(); + std::string filename = "test_upload.txt"; + // upload without size + { + auto sizes = {1024 * 1024, 2'000'000, 1024, 100, 0}; + for (auto size : sizes) { + std::error_code ec{}; + fs::remove(filename, ec); + if (ec) { + std::cout << ec << "\n"; + } + bool r = create_file(filename, size); + CHECK(r); + test_upload_by_file_path(filename); + test_upload_by_stream(filename); + test_upload_by_coro(filename); + } + } + // upload with size + { + auto sizes = {std::pair{1024 * 1024, 1'000'000}, + std::pair{2'000'000, 1'999'999}, std::pair{200, 1}, + std::pair{100, 0}, std::pair{0, 0}}; + for (auto [size, r_size] : sizes) { + std::error_code ec{}; + fs::remove(filename, ec); + if (ec) { + std::cout << ec << "\n"; + } + bool r = create_file(filename, size); + CHECK(r); + test_upload_by_file_path(filename, 0, r_size); + test_upload_by_stream(filename, 0, r_size); + test_upload_by_coro(filename, r_size); + } + } + // upload with too large size + { + auto sizes = {std::pair{1024 * 1024, 1024 * 1024 + 2}, + std::pair{2'000'000, 2'000'001}, std::pair{200, 502}, + std::pair{0, 1}}; + for (auto [size, r_size] : sizes) { + std::error_code ec{}; + fs::remove(filename, ec); + if (ec) { + std::cout << ec << "\n"; + } + bool r = create_file(filename, size); + CHECK(r); + test_upload_by_file_path(filename, 0, r_size, true); + test_upload_by_stream(filename, 0, r_size, true); + test_upload_by_coro(filename, r_size, true); + } + } + // upload with offset + { + auto sizes = {std::pair{1024 * 1024, 1'000'000}, + std::pair{2'000'000, 1'999'999}, std::pair{200, 1}, + std::pair{100, 0}, std::pair{0, 0}}; + for (auto [size, offset] : sizes) { + std::error_code ec{}; + fs::remove(filename, ec); + if (ec) { + std::cout << ec << "\n"; + } + bool r = create_file(filename, size); + CHECK(r); + test_upload_by_file_path(filename, offset); + test_upload_by_stream(filename, offset); + } + } + // upload with size & offset + { + auto sizes = {std::tuple{1024 * 1024, 500'000, 500'000}, + std::tuple{2'000'000, 1'999'999, 1}, std::tuple{200, 1, 199}, + std::tuple{100, 100, 0}}; + for (auto [size, offset, r_size] : sizes) { + std::error_code ec{}; + fs::remove(filename, ec); + if (ec) { + std::cout << ec << "\n"; + } + bool r = create_file(filename, size); + CHECK(r); + test_upload_by_file_path(filename, offset, r_size); + test_upload_by_stream(filename, offset, r_size); + } + } + // upload with too large size & offset + { + auto sizes = {std::tuple{1024 * 1024, 1'000'000, 50'000}, + std::tuple{2'000'000, 1'999'999, 2}, std::tuple{200, 1, 200}, + std::tuple{100, 100, 1}}; + for (auto [size, offset, r_size] : sizes) { + std::error_code ec{}; + fs::remove(filename, ec); + if (ec) { + std::cout << ec << "\n"; + } + bool r = create_file(filename, size); + CHECK(r); + test_upload_by_file_path(filename, offset, r_size, true); + test_upload_by_stream(filename, offset, r_size, true); + } + } + { + filename = "some_test_file.txt"; + bool r = create_file(filename, 10); + CHECK(r); + test_upload_by_file_path(filename, 20, SIZE_MAX, true); + std::error_code ec{}; + fs::remove(filename, ec); + } +} + +TEST_CASE("test coro_http_client chunked upload and download") { + { + coro_http_server server(1, 8090); + server.set_http_handler( + "/chunked_upload", + [](coro_http_request &req, + coro_http_response &resp) -> async_simple::coro::Lazy { + assert(req.get_content_type() == content_type::chunked); + chunked_result result{}; + std::string_view filename = req.get_header_value("filename"); + + CHECK(!filename.empty()); + + auto oldpath = fs::current_path().append(filename); + std::string newpath = fs::current_path() + .append("server_" + std::string{filename}) + .string(); + std::ofstream file(newpath, std::ios::binary); + CHECK(file.is_open()); + + while (true) { + result = co_await req.get_conn()->read_chunked(); + if (result.ec) { + co_return; + } + + file.write(result.data.data(), result.data.size()); + + if (result.eof) { + break; + } + } + file.flush(); + file.close(); + auto sz = std::filesystem::file_size(oldpath); + CHECK(sz == std::filesystem::file_size(newpath)); + resp.set_status_and_content(status_type::ok, std::string(filename)); + }); + server.set_http_handler( + "/upload_stream", [](coro_http_request &req, coro_http_response &resp) { + std::cout << "body size: " << req.get_body().size() << "\n"; + resp.set_status_and_content(status_type::ok, "upload ok"); + }); + server.async_start(); + { + coro_http_client client{}; + create_file("stream_file.txt", 20); + auto stream_file = + std::make_shared("stream_file.txt", std::ios::binary); + std::string uri = "http://127.0.0.1:8090/upload_stream"; + std::unordered_map headers; + headers.emplace("filename", "stream_file.txt"); + auto result = async_simple::coro::syncAwait( + client.async_upload(uri, http_method::PUT, stream_file, 0, -1, + req_content_type::text, std::move(headers))); + CHECK(result.status == 200); + stream_file = + std::make_shared("stream_file.txt", std::ios::binary); + result = async_simple::coro::syncAwait( + client.async_upload(uri, http_method::PUT, stream_file, 0, 100)); + CHECK(result.status != 200); + + result = async_simple::coro::syncAwait(client.async_upload( + uri, http_method::PUT, "stream_file.txt"sv, 0, 0)); + CHECK(result.status == 200); + + fs::remove("stream_file.txt"); + } + { + coro_http_client client{}; + std::string uri = "http://###127.0.0.1:8090/chunked_upload"; + std::string filename = "test_chunked_upload.txt"; + auto lazy = client.async_upload(uri, http_method::PUT, filename); + auto result = async_simple::coro::syncAwait(lazy); + CHECK(result.status != 200); + + uri = "http://127.0.0.1:8090/chunked_upload"; + filename = "no_such.txt"; + auto lazy1 = client.async_upload(uri, http_method::PUT, filename); + result = async_simple::coro::syncAwait(lazy1); + CHECK(result.status != 200); + + std::shared_ptr file = nullptr; + uri = "http://127.0.0.1:8090/chunked_upload"; + auto lazy2 = client.async_upload(uri, http_method::PUT, file); + result = async_simple::coro::syncAwait(lazy2); + CHECK(result.status != 200); + + auto code = async_simple::coro::syncAwait(client.handle_shake()); + CHECK(code); + } + { + coro_http_client client{}; + std::string uri = "http://###127.0.0.1:8090/chunked_upload"; + std::string filename = "test_chunked_upload.txt"; + auto lazy = client.async_upload_chunked(uri, http_method::PUT, filename); + auto result = async_simple::coro::syncAwait(lazy); + CHECK(result.status != 200); + + uri = "http://127.0.0.1:8090/chunked_upload"; + filename = "no_such.txt"; + auto lazy1 = client.async_upload_chunked(uri, http_method::PUT, filename); + result = async_simple::coro::syncAwait(lazy1); + CHECK(result.status != 200); + + std::shared_ptr file = nullptr; + uri = "http://127.0.0.1:8090/chunked_upload"; + auto lazy2 = client.async_upload_chunked(uri, http_method::PUT, file); + result = async_simple::coro::syncAwait(lazy2); + CHECK(result.status != 200); + create_file("chunked_file.txt", 20); + std::unordered_map headers; + headers.emplace("filename", "chunked_file.txt"); + result = async_simple::coro::syncAwait(client.async_upload_chunked( + uri, http_method::PUT, "chunked_file.txt"sv, req_content_type::text, + std::move(headers))); + CHECK(result.status == 200); + + client.reset(); + client.set_conn_timeout(0ms); + result = async_simple::coro::syncAwait(client.async_upload_chunked( + uri, http_method::PUT, "chunked_file.txt"sv)); + CHECK(result.status != 200); + + client.set_conn_timeout(500ms); + client.set_req_timeout(0ms); + client.add_header("filename", "chunked_file.txt"); + result = async_simple::coro::syncAwait(client.async_upload_chunked( + uri, http_method::PUT, "chunked_file.txt"sv)); + CHECK(result.status != 200); + + fs::remove("chunked_file.txt"); + } + { + std::string uri = "http://127.0.0.1:8090/upload_stream"; + std::string filename = "test_chunked_upload.txt"; + coro_http_client client{}; + client.set_conn_timeout(0ms); + auto lazy3 = client.async_upload(uri, http_method::PUT, filename); + auto result = async_simple::coro::syncAwait(lazy3); + CHECK(result.status != 200); + client.reset(); + client.set_conn_timeout(500ms); + client.set_req_timeout(0ms); + auto lazy4 = client.async_upload(uri, http_method::PUT, filename); + result = async_simple::coro::syncAwait(lazy4); + CHECK(result.status != 200); + } + auto sizes = {1024 * 1024, 2'000'000, 1024, 100, 0}; + for ([[maybe_unused]] auto size : sizes) { + std::string filename = "test_chunked_upload.txt"; + std::error_code ec{}; + fs::remove(filename, ec); + if (ec) { + std::cout << ec << "\n"; + } + bool r = create_file(filename, 1024 * 1024 * 8); + CHECK(r); + coro_http_client client{}; + client.add_header("filename", filename); + std::string uri = "http://127.0.0.1:8090/chunked_upload"; + auto lazy = client.async_upload_chunked(uri, http_method::PUT, filename); + auto result = async_simple::coro::syncAwait(lazy); + CHECK(result.status == 200); + } + } + + { + // chunked download, not in cache + create_file("test_102.txt", 102); + create_file("test_static.txt", 1024); + coro_http_server server(1, 8090); + server.set_static_res_dir("download", ""); + server.set_max_size_of_cache_files(100); + server.set_transfer_chunked_size(100); + server.async_start(); + + coro_http_client client{}; + + std::string download_url = "http://127.0.0.1:8090/download/test_static.txt"; + std::string download_name = "test1.txt"; + auto r = client.download(download_url, download_name); + CHECK(r.status == 200); + CHECK(std::filesystem::file_size(download_name) == 1024); + + download_url = "http://127.0.0.1:8090/download/test_102.txt"; + download_name = "test2.txt"; + r = client.download(download_url, download_name); + CHECK(r.status == 200); + CHECK(std::filesystem::file_size(download_name) == 102); + } +} + +TEST_CASE("test coro_http_client get") { + coro_http_client client{}; + auto r = client.get("http://www.baidu.com"); + CHECK(!r.net_err); + CHECK(r.status < 400); +} + +TEST_CASE("test coro_http_client add header and url queries") { + coro_http_client client{}; + client.add_header("Connection", "keep-alive"); + auto r = + async_simple::coro::syncAwait(client.async_get("http://www.baidu.cn")); + CHECK(!r.net_err); + CHECK(r.status < 400); + + auto r2 = async_simple::coro::syncAwait( + client.async_get("http://www.baidu.com?name='tom'&age=20")); + CHECK(!r2.net_err); + CHECK(r2.status < 400); +} + +TEST_CASE("test coro_http_client not exist domain and bad uri") { + { + coro_http_client client{}; + auto r = async_simple::coro::syncAwait( + client.async_get("http://www.notexistwebsit.com")); + CHECK(r.net_err); + CHECK(r.status != 200); + CHECK(client.has_closed()); + } + + { + coro_http_client client{}; + auto r = async_simple::coro::syncAwait( + client.async_get("http://www.baidu.com/><")); + CHECK(r.net_err); + CHECK(r.status != 200); + CHECK(client.has_closed()); + } +} + +TEST_CASE("test coro_http_client async_get") { + coro_http_client client{}; + auto r = + async_simple::coro::syncAwait(client.async_get("http://www.baidu.com")); + CHECK(!r.net_err); + CHECK(r.status < 400); + + auto r1 = + async_simple::coro::syncAwait(client.async_get("http://www.baidu.com")); + CHECK(!r.net_err); + CHECK(r.status == 200); +} + +TEST_CASE("test basic http request") { + coro_http_server server(1, 8090); + // Setting up GET and POST handlers + server.set_http_handler( + "/", [&server](coro_http_request &, coro_http_response &res) mutable { + res.set_status_and_content(status_type::ok, "hello world"); + }); + server.set_http_handler( + "/", [&server](coro_http_request &req, coro_http_response &res) mutable { + std::string str(req.get_body()); + str.append(" reply from post"); + res.set_status_and_content(status_type::ok, std::move(str)); + }); + + // Setting up PUT handler + server.set_http_handler( + "/", [&server](coro_http_request &req, coro_http_response &res) mutable { + std::string str(req.get_body()); + str.append(" put successfully"); + res.set_status_and_content(status_type::ok, std::move(str)); + }); + + // Setting up DELETE handler + server.set_http_handler( + "/", [&server](coro_http_request &, coro_http_response &res) mutable { + res.set_status_and_content(status_type::ok, "data deleted"); + }); + + server.async_start(); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + coro_http_client client{}; + std::string uri = "http://127.0.0.1:8090"; + + // Testing PUT method + resp_data result = async_simple::coro::syncAwait(client.async_request( + uri, http_method::PUT, + req_context{.content = "data for put"})); + CHECK(result.resp_body == "data for put put successfully"); + + // Testing DELETE method + result = async_simple::coro::syncAwait(client.async_request( + uri, http_method::DEL, req_context{})); + CHECK(result.resp_body == "data deleted"); + + // Testing GET method again after DELETE + result = async_simple::coro::syncAwait(client.async_get(uri)); + CHECK(result.resp_body == "hello world"); + + size_t size = result.resp_body.size(); + auto buf = client.release_buf(); + CHECK(size == strlen(buf.data())); + CHECK(buf == "hello world"); + + // Rest of the POST tests + result = async_simple::coro::syncAwait(client.async_post( + uri, "async post hello coro_http_client", req_content_type::string)); + CHECK(result.resp_body == + "async post hello coro_http_client reply from post"); + + result = client.post(uri, "sync post hello coro_http_client", + req_content_type::string); + CHECK(result.resp_body == "sync post hello coro_http_client reply from post"); + + std::string_view uri1 = "http://127.0.0.1:8090"; + std::string_view post_str = "post hello coro_http_client"; + + result = async_simple::coro::syncAwait( + client.async_request(uri, http_method::POST, + req_context{.content = post_str})); + CHECK(result.resp_body == "post hello coro_http_client reply from post"); + + result = async_simple::coro::syncAwait( + client.async_request(uri1, http_method::POST, + req_context{.content = post_str})); + CHECK(result.resp_body == "post hello coro_http_client reply from post"); + + result = client.post(uri, "", req_content_type::string); + CHECK(result.status == 200); + + server.stop(); +} + +TEST_CASE("test coro_http_client request timeout") { + coro_http_client client{}; + cinatra::coro_http_client::config conf{.conn_timeout_duration = 10s, + .req_timeout_duration = 1ms}; + client.init_config(conf); + auto r = + async_simple::coro::syncAwait(client.connect("http://www.baidu.com")); + std::cout << r.net_err.message() << "\n"; + if (!r.net_err) { + r = async_simple::coro::syncAwait(client.async_get("/")); + if (r.net_err) { + CHECK(r.net_err == std::errc::timed_out); + } + } +} + +#ifdef INJECT_FOR_HTTP_CLIENT_TEST +TEST_CASE("test inject failed") { + // { + // coro_http_client client{}; + // inject_response_valid = ClientInjectAction::response_error; + // client.set_req_timeout(8s); + // auto result = client.get("http://purecpp.cn"); + // CHECK(result.net_err == std::errc::protocol_error); + + // inject_header_valid = ClientInjectAction::header_error; + // result = client.get("http://purecpp.cn"); + // CHECK(result.net_err == std::errc::protocol_error); + // } + + // { + // coro_http_client client{}; + // client.set_req_timeout(10s); + // std::string uri = + // "http://www.httpwatch.com/httpgallery/chunked/chunkedimage.aspx"; + // std::string filename = "test.jpg"; + // + // std::error_code ec{}; + // std::filesystem::remove(filename, ec); + // + // inject_read_failed = ClientInjectAction::read_failed; + // auto result = client.download(uri, filename); + // CHECK(result.net_err == std::make_error_code(std::errc::not_connected)); + // } + // + // { + // coro_http_client client{}; + // client.set_req_timeout(10s); + // std::string uri = + // "http://www.httpwatch.com/httpgallery/chunked/chunkedimage.aspx"; + // std::string filename = "test.jpg"; + // + // std::error_code ec{}; + // std::filesystem::remove(filename, ec); + // + // inject_chunk_valid = ClientInjectAction::chunk_error; + // auto result = client.download(uri, filename); + // CHECK(result.status == 404); + // } + + { + coro_http_client client{}; + client.add_str_part("hello", "world"); + inject_write_failed = ClientInjectAction::write_failed; + auto result = async_simple::coro::syncAwait( + client.async_upload_multipart("https://www.bing.com")); + CHECK(result.status == 404); + } +} +#endif + +TEST_CASE("test coro http proxy request") { + coro_http_client client{}; + client.set_req_timeout(8s); + std::string uri = "http://www.baidu.com"; + // Make sure the host and port are matching with your proxy server + client.set_proxy("106.14.255.124", "80"); + resp_data result = async_simple::coro::syncAwait(client.async_get(uri)); + if (!result.net_err) + CHECK(result.status >= 200); + + client.set_proxy("106.14.255.124", "80"); + result = async_simple::coro::syncAwait(client.async_get(uri)); + if (!result.net_err) + CHECK(result.status >= 200); +} + +TEST_CASE("test coro http proxy request with port") { + coro_http_client client{}; + client.set_req_timeout(8s); + std::string uri = "http://www.baidu.com:80"; + // Make sure the host and port are matching with your proxy server + client.set_proxy("106.14.255.124", "80"); + resp_data result = async_simple::coro::syncAwait(client.async_get(uri)); + if (!result.net_err) + CHECK(result.status >= 200); // maybe return 500 from that host. +} + +// TEST_CASE("test coro http basic auth request") { +// coro_http_client client{}; +// std::string uri = "http://www.purecpp.cn"; +// client.set_proxy_basic_auth("user", "pass"); +// resp_data result = async_simple::coro::syncAwait(client.async_get(uri)); +// CHECK(!result.net_err); +// CHECK(result.status == 200); +// } + +TEST_CASE("test coro http bearer token auth request") { + coro_http_client client{}; + std::string uri = "http://www.baidu.com"; + client.set_proxy_bearer_token_auth("password"); + resp_data result = async_simple::coro::syncAwait(client.async_get(uri)); + CHECK(!result.net_err); + CHECK(result.status < 400); +} + +TEST_CASE("test coro http redirect request") { + coro_http_client client{}; + client.set_req_timeout(8s); + std::string uri = "http://httpbin.org/redirect-to?url=http://httpbin.org/get"; + resp_data result = async_simple::coro::syncAwait(client.async_get(uri)); + if (result.status != 404 && !result.net_err) { + CHECK(!result.net_err); + if (result.status != 502) + CHECK(result.status == 302); + + if (client.is_redirect(result)) { + std::string redirect_uri = client.get_redirect_uri(); + result = async_simple::coro::syncAwait(client.async_get(redirect_uri)); + if (result.status != 502 && result.status != 404) + CHECK(result.status == 200); + } + + client.enable_auto_redirect(true); + result = async_simple::coro::syncAwait(client.async_get(uri)); + CHECK(result.status >= 200); + } +} + +TEST_CASE("test coro http request timeout") { + coro_http_server server(1, 8090); + server.set_http_handler( + "/", [&server](coro_http_request &, coro_http_response &res) mutable { + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + res.set_status_and_content(status_type::ok, "hello world"); + }); + + server.async_start(); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + coro_http_client client{}; + std::string uri = "http://127.0.0.1:8090"; + + resp_data result = async_simple::coro::syncAwait(client.async_get(uri)); + CHECK(result.status == 200); + + client.set_req_timeout(500ms); + result = async_simple::coro::syncAwait(client.async_get(uri)); + CHECK(result.net_err == std::errc::timed_out); + + // after timeout, the socket in client has been closed, so use a new client + // to test. + coro_http_client client1{}; + result = async_simple::coro::syncAwait(client1.async_post( + uri, "async post hello coro_http_client", req_content_type::string)); + CHECK(!result.net_err); + + server.stop(); +} + +TEST_CASE("test coro_http_client using external io_context") { + asio::io_context io_context; + std::promise promise; + auto future = promise.get_future(); + auto work = std::make_unique(io_context); + std::thread io_thd([&io_context, &promise] { + promise.set_value(); + io_context.run(); + }); + future.wait(); + + coro_http_client client(io_context.get_executor()); + auto r = + async_simple::coro::syncAwait(client.async_get("http://www.baidu.com")); + CHECK(!r.net_err); + CHECK(r.status < 400); + work.reset(); + io_context.run(); + io_thd.join(); +} + +async_simple::coro::Lazy simulate_self_join() { + coro_http_client client{}; + co_return co_await client.async_get("http://www.baidu.com"); +} + +TEST_CASE("test coro_http_client dealing with self join") { + auto r = async_simple::coro::syncAwait(simulate_self_join()); + CHECK(!r.net_err); + CHECK(r.status < 400); +} + +TEST_CASE("test coro_http_client no scheme still send request check") { + coro_http_server server(1, 8090); + server.set_http_handler( + "/", [&server](coro_http_request &, coro_http_response &res) mutable { + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + res.set_status_and_content(status_type::ok, "hello world"); + }); + + server.async_start(); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + std::string uri = "http://127.0.0.1:8090"; + + coro_http_client client{}; + auto resp = async_simple::coro::syncAwait(client.async_get("127.0.0.1:8090")); + CHECK(!resp.net_err); + CHECK(resp.status == 200); + resp = async_simple::coro::syncAwait( + client.async_get("127.0.0.1:8090/ref='http://www.baidu.com'")); + CHECK(resp.status == 404); + + server.stop(); +} + +#ifdef DSKIP_TIME_TEST +TEST_CASE("test conversion between unix time and gmt time, http format") { + std::chrono::microseconds time_cost{0}; + std::ifstream file("../../tests/files_for_test_time_parse/http_times.txt"); + if (!file) { + std::cout << "open file failed" << std::endl; + } + std::string line; + while (std::getline(file, line)) { + std::istringstream iss(line); + std::string time_to_parse; + std::string timestamp; + if (std::getline(iss, time_to_parse, '#') && std::getline(iss, timestamp)) { + std::pair result; + auto start = std::chrono::system_clock::now(); + for (int i = 0; i < 100; i++) { + result = get_timestamp(time_to_parse); + } + auto end = std::chrono::system_clock::now(); + auto duration = duration_cast(end - start); + time_cost += duration; + if (result.first == true) { + CHECK(timestamp != "invalid"); + if (timestamp != "invalid") { + CHECK(result.second == std::stoll(timestamp)); + } + } + else { + CHECK(timestamp == "invalid"); + } + } + } + file.close(); + std::cout << double(time_cost.count()) * + std::chrono::microseconds::period::num / + std::chrono::microseconds::period::den + << "s" << std::endl; +} + +TEST_CASE("test conversion between unix time and gmt time, utc format") { + std::chrono::microseconds time_cost{0}; + std::ifstream file("../../tests/files_for_test_time_parse/utc_times.txt"); + if (!file) { + std::cout << "open file failed" << std::endl; + } + std::string line; + while (std::getline(file, line)) { + std::istringstream iss(line); + std::string time_to_parse; + std::string timestamp; + if (std::getline(iss, time_to_parse, '#') && std::getline(iss, timestamp)) { + std::pair result; + auto start = std::chrono::system_clock::now(); + for (int i = 0; i < 100; i++) { + result = get_timestamp(time_to_parse); + } + auto end = std::chrono::system_clock::now(); + auto duration = duration_cast(end - start); + time_cost += duration; + if (result.first == true) { + CHECK(timestamp != "invalid"); + if (timestamp != "invalid") { + CHECK(result.second == std::stoll(timestamp)); + } + } + else { + CHECK(timestamp == "invalid"); + } + } + } + file.close(); + std::cout << double(time_cost.count()) * + std::chrono::microseconds::period::num / + std::chrono::microseconds::period::den + << "s" << std::endl; +} + +TEST_CASE( + "test conversion between unix time and gmt time, utc without punctuation " + "format") { + std::chrono::microseconds time_cost{0}; + std::ifstream file( + "../../tests/files_for_test_time_parse/" + "utc_without_punctuation_times.txt"); + if (!file) { + std::cout << "open file failed" << std::endl; + } + std::string line; + while (std::getline(file, line)) { + std::istringstream iss(line); + std::string time_to_parse; + std::string timestamp; + if (std::getline(iss, time_to_parse, '#') && std::getline(iss, timestamp)) { + std::pair result; + auto start = std::chrono::system_clock::now(); + for (int i = 0; i < 100; i++) { + result = get_timestamp( + time_to_parse); + } + auto end = std::chrono::system_clock::now(); + auto duration = duration_cast(end - start); + time_cost += duration; + if (result.first == true) { + CHECK(timestamp != "invalid"); + if (timestamp != "invalid") { + CHECK(result.second == std::stoll(timestamp)); + } + } + else { + CHECK(timestamp == "invalid"); + } + } + } + file.close(); + std::cout << double(time_cost.count()) * + std::chrono::microseconds::period::num / + std::chrono::microseconds::period::den + << "s" << std::endl; +} +#endif + +TEST_CASE("Testing get_content_type_str function") { + SUBCASE("Test HTML content type") { + CHECK(get_content_type_str(req_content_type::html) == + "text/html; charset=UTF-8"); + } + + SUBCASE("Test JSON content type") { + CHECK(get_content_type_str(req_content_type::json) == + "application/json; charset=UTF-8"); + } + + SUBCASE("Test String content type") { + CHECK(get_content_type_str(req_content_type::string) == + "text/html; charset=UTF-8"); + } + + SUBCASE("Test Multipart content type") { + std::string result = get_content_type_str(req_content_type::multipart); + std::string expectedPrefix = "multipart/form-data; boundary="; + CHECK(result.find(expectedPrefix) == + 0); // Check if the result starts with the expected prefix + + // Check if there is something after the prefix, + // this test failed. + /*CHECK(result.length() > expectedPrefix.length());*/ + } + + SUBCASE("Test Octet Stream content type") { + CHECK(get_content_type_str(req_content_type::octet_stream) == + "application/octet-stream"); + } + + SUBCASE("Test XML content type") { + CHECK(get_content_type_str(req_content_type::xml) == "application/xml"); + } +} + +TEST_CASE("test get_local_time_str with_month") { + char buf[32]; + std::string_view format = "%Y-%m-%d %H:%M:%S"; // This format includes '%m' + std::time_t t = std::time(nullptr); + + std::string_view result = cinatra::get_local_time_str(buf, t, format); + std::cout << "Local time with month: " << result << "\n"; + + // Perform a basic check + CHECK(!result.empty()); +} + +TEST_CASE("Testing base64_encode function") { + SUBCASE("Base64 encoding of an empty string") { + CHECK(base64_encode("") == ""); + } + + SUBCASE("Base64 encoding of 'Hello'") { + CHECK(base64_encode("Hello") == "SGVsbG8="); + } + + SUBCASE("Base64 encoding of a binary data") { + std::string binaryData = "\x01\x02\x03"; // Example binary data + CHECK(base64_encode(binaryData) == "AQID"); + } +} + +TEST_CASE("Testing is_valid_utf8 function") { + SUBCASE("Valid UTF-8 string") { + auto validUtf8 = std::u8string(u8"Hello, 世界"); + std::string validUtf8Converted(validUtf8.begin(), validUtf8.end()); + CHECK(is_valid_utf8((unsigned char *)validUtf8.c_str(), validUtf8.size()) == + true); + } + + SUBCASE("Invalid UTF-8 string with wrong continuation bytes") { + std::string invalidUtf8 = "Hello, \x80\x80"; // wrong continuation bytes + CHECK(is_valid_utf8((unsigned char *)invalidUtf8.c_str(), + invalidUtf8.size()) == false); + } + + SUBCASE("Empty string") { + std::string empty; + CHECK(is_valid_utf8((unsigned char *)empty.c_str(), empty.size()) == true); + } +} + +TEST_CASE("test transfer cookie to string") { + cookie cookie("name", "value"); + CHECK(cookie.get_name() == "name"); + CHECK(cookie.get_value() == "value"); + CHECK(cookie.to_string() == "name=value"); + cookie.set_path("/"); + CHECK(cookie.to_string() == "name=value; path=/"); + cookie.set_comment("comment"); + CHECK(cookie.to_string() == "name=value; path=/"); + cookie.set_domain("baidu.com"); + CHECK(cookie.to_string() == "name=value; domain=baidu.com; path=/"); + cookie.set_secure(true); + CHECK(cookie.to_string() == "name=value; domain=baidu.com; path=/; secure"); + cookie.set_http_only(true); + CHECK(cookie.to_string() == + "name=value; domain=baidu.com; path=/; secure; HttpOnly"); + cookie.set_priority("Low"); + CHECK(cookie.to_string() == + "name=value; domain=baidu.com; path=/; Priority=Low; secure; HttpOnly"); + cookie.set_priority("Medium"); + CHECK(cookie.to_string() == + "name=value; domain=baidu.com; path=/; Priority=Medium; secure; " + "HttpOnly"); + cookie.set_priority("High"); + CHECK( + cookie.to_string() == + "name=value; domain=baidu.com; path=/; Priority=High; secure; HttpOnly"); + cookie.set_priority(""); + cookie.set_http_only(false); + + cookie.set_version(1); + CHECK(cookie.to_string() == + "name=\"value\"; Comment=\"comment\"; Domain=\"baidu.com\"; " + "Path=\"/\"; secure; Version=\"1\""); + + cookie.set_secure(false); + cookie.set_max_age(100); + CHECK(cookie.to_string() == + "name=\"value\"; Comment=\"comment\"; Domain=\"baidu.com\"; " + "Path=\"/\"; Max-Age=\"100\"; Version=\"1\""); + + cookie.set_http_only(true); + CHECK(cookie.to_string() == + "name=\"value\"; Comment=\"comment\"; Domain=\"baidu.com\"; " + "Path=\"/\"; Max-Age=\"100\"; HttpOnly; Version=\"1\""); + + cookie.set_priority("Low"); + CHECK( + cookie.to_string() == + "name=\"value\"; Comment=\"comment\"; Domain=\"baidu.com\"; Path=\"/\"; " + "Priority=\"Low\"; Max-Age=\"100\"; HttpOnly; Version=\"1\""); + cookie.set_priority("Medium"); + CHECK( + cookie.to_string() == + "name=\"value\"; Comment=\"comment\"; Domain=\"baidu.com\"; Path=\"/\"; " + "Priority=\"Medium\"; Max-Age=\"100\"; HttpOnly; Version=\"1\""); + cookie.set_priority("High"); + CHECK( + cookie.to_string() == + "name=\"value\"; Comment=\"comment\"; Domain=\"baidu.com\"; Path=\"/\"; " + "Priority=\"High\"; Max-Age=\"100\"; HttpOnly; Version=\"1\""); +} + +std::vector get_header_values( + std::span &resp_headers, std::string_view key) { + std::vector values{}; + for (const auto &p : resp_headers) { + if (p.name == key) + values.push_back(p.value); + } + return values; +} + +std::string cookie_str1 = ""; +std::string cookie_str2 = ""; + +TEST_CASE("test cookie") { + coro_http_server server(5, 8090); + server.set_http_handler( + "/construct_cookies", + [](coro_http_request &req, coro_http_response &res) { + auto session = req.get_session(); + session->get_session_cookie().set_path("/"); + cookie_str1 = session->get_session_cookie().to_string(); + + cookie another_cookie("test", "cookie"); + another_cookie.set_http_only(true); + another_cookie.set_domain("baidu.com"); + res.add_cookie(another_cookie); + cookie_str2 = another_cookie.to_string(); + + res.set_status_and_content(status_type::ok, session->get_session_id()); + }); + + server.set_http_handler( + "/check_session_cookie", + [](coro_http_request &req, coro_http_response &res) { + auto session_id = req.get_header_value("Cookie"); + CHECK(session_id == + CSESSIONID + "=" + req.get_session()->get_session_id()); + res.set_status(status_type::ok); + }); + + server.async_start(); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + coro_http_client client{}; + auto r1 = async_simple::coro::syncAwait( + client.async_get("http://127.0.0.1:8090/construct_cookies")); + auto cookie_strs = get_header_values(r1.resp_headers, "Set-Cookie"); + CHECK(cookie_strs.size() == 2); + bool check1 = + (cookie_strs[0] == cookie_str1 && cookie_strs[1] == cookie_str2); + bool check2 = + (cookie_strs[1] == cookie_str1 && cookie_strs[0] == cookie_str2); + CHECK((check1 || check2)); + CHECK(r1.status == 200); + + std::string session_cookie = + CSESSIONID + "=" + std::string(r1.resp_body.data(), r1.resp_body.size()); + + client.add_header("Cookie", session_cookie); + auto r2 = async_simple::coro::syncAwait( + client.async_get("http://127.0.0.1:8090/check_session_cookie")); + CHECK(r2.status == 200); + + server.stop(); +} + +std::string session_id_login = ""; +std::string session_id_logout = ""; +std::string session_id_check_login = ""; +std::string session_id_check_logout = ""; + +TEST_CASE("test session") { + coro_http_server server(5, 8090); + server.set_http_handler( + "/login", [](coro_http_request &req, coro_http_response &res) { + auto session = req.get_session(); + session_id_login = session->get_session_id(); + session->set_data("login", true); + res.set_status(status_type::ok); + }); + server.set_http_handler( + "/logout", [](coro_http_request &req, coro_http_response &res) { + auto session = req.get_session(); + session_id_logout = session->get_session_id(); + session->remove_data("login"); + res.set_status(status_type::ok); + }); + server.set_http_handler( + "/check_login", [](coro_http_request &req, coro_http_response &res) { + auto session = req.get_session(); + session_id_check_login = session->get_session_id(); + bool login = session->get_data("login").value_or(false); + CHECK(login == true); + res.set_status(status_type::ok); + }); + server.set_http_handler( + "/check_logout", [](coro_http_request &req, coro_http_response &res) { + auto session = req.get_session(); + session_id_check_logout = session->get_session_id(); + bool login = session->get_data("login").value_or(false); + CHECK(login == false); + res.set_status(status_type::ok); + }); + server.async_start(); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + coro_http_client client{}; + auto r1 = async_simple::coro::syncAwait( + client.async_get("http://127.0.0.1:8090/check_logout")); + CHECK(r1.status == 200); + + auto r2 = async_simple::coro::syncAwait( + client.async_get("http://127.0.0.1:8090/login")); + CHECK(r2.status == 200); + CHECK(session_id_login != session_id_check_logout); + + std::string session_cookie = CSESSIONID + "=" + session_id_login; + + client.add_header("Cookie", session_cookie); + auto r3 = async_simple::coro::syncAwait( + client.async_get("http://127.0.0.1:8090/check_login")); + CHECK(r3.status == 200); + CHECK(session_id_login == session_id_check_login); + + client.add_header("Cookie", session_cookie); + auto r4 = async_simple::coro::syncAwait( + client.async_get("http://127.0.0.1:8090/logout")); + CHECK(r4.status == 200); + CHECK(session_id_login == session_id_logout); + + client.add_header("Cookie", session_cookie); + auto r5 = async_simple::coro::syncAwait( + client.async_get("http://127.0.0.1:8090/check_logout")); + CHECK(r5.status == 200); + CHECK(session_id_login == session_id_check_logout); + + server.stop(); +} + +std::string session_id = ""; +TEST_CASE("test session timeout") { + coro_http_server server(5, 8090); + + server.set_http_handler( + "/construct_session", + [](coro_http_request &req, coro_http_response &res) { + auto session = req.get_session(); + session_id = session->get_session_id(); + session->set_session_timeout(1); + res.set_status(status_type::ok); + }); + + server.set_http_handler("/no_sleep", [](coro_http_request &req, + coro_http_response &res) { + CHECK(session_manager::get().check_session_existence(session_id) == true); + res.set_status(status_type::ok); + }); + + server.set_http_handler("/after_sleep_2s", [](coro_http_request &req, + coro_http_response &res) { + CHECK(session_manager::get().check_session_existence(session_id) == false); + res.set_status(status_type::ok); + }); + + session_manager::get().set_check_session_duration(10ms); + server.async_start(); + + coro_http_client client{}; + auto r1 = async_simple::coro::syncAwait( + client.async_get("http://127.0.0.1:8090/construct_session")); + CHECK(r1.status == 200); + + auto r2 = async_simple::coro::syncAwait( + client.async_get("http://127.0.0.1:8090/no_sleep")); + CHECK(r2.status == 200); + + std::this_thread::sleep_for(2s); + auto r3 = async_simple::coro::syncAwait( + client.async_get("http://127.0.0.1:8090/after_sleep_2s")); + CHECK(r3.status == 200); + + server.stop(); +} + +TEST_CASE("test session validate") { + coro_http_server server(5, 8090); + + server.set_http_handler( + "/construct_session", + [](coro_http_request &req, coro_http_response &res) { + auto session = req.get_session(); + session_id = session->get_session_id(); + res.set_status(status_type::ok); + }); + + server.set_http_handler( + "/invalidate_session", + [](coro_http_request &req, coro_http_response &res) { + CHECK(session_manager::get().check_session_existence(session_id) == + true); + session_manager::get().get_session(session_id)->invalidate(); + res.set_status(status_type::ok); + }); + + server.set_http_handler("/after_sleep_2s", [](coro_http_request &req, + coro_http_response &res) { + CHECK(session_manager::get().check_session_existence(session_id) == false); + res.set_status(status_type::ok); + }); + + session_manager::get().set_check_session_duration(10ms); + server.async_start(); + + coro_http_client client{}; + auto r1 = async_simple::coro::syncAwait( + client.async_get("http://127.0.0.1:8090/construct_session")); + CHECK(r1.status == 200); + + auto r2 = async_simple::coro::syncAwait( + client.async_get("http://127.0.0.1:8090/invalidate_session")); + CHECK(r2.status == 200); + + std::this_thread::sleep_for(2s); + auto r3 = async_simple::coro::syncAwait( + client.async_get("http://127.0.0.1:8090/after_sleep_2s")); + CHECK(r3.status == 200); + + server.stop(); +} \ No newline at end of file diff --git a/src/coro_http/tests/test_cinatra_websocket.cpp b/src/coro_http/tests/test_cinatra_websocket.cpp new file mode 100644 index 000000000..e60e68d02 --- /dev/null +++ b/src/coro_http/tests/test_cinatra_websocket.cpp @@ -0,0 +1,343 @@ +#include +#include +#include +#include +#include + +#include "doctest.h" +#include "ylt/coro_http/coro_http_client.hpp" +#include "ylt/coro_http/coro_http_server.hpp" + +using namespace std::chrono_literals; + +using namespace cinatra; + +#ifdef CINATRA_ENABLE_SSL +TEST_CASE("test wss client") { + cinatra::coro_http_server server(1, 9001); + server.init_ssl("../openssl_files/server.crt", "../openssl_files/server.key", + "test"); + server.set_http_handler( + "/", + [](coro_http_request &req, + coro_http_response &resp) -> async_simple::coro::Lazy { + CHECK(req.get_content_type() == content_type::websocket); + websocket_result result{}; + while (true) { + result = co_await req.get_conn()->read_websocket(); + if (result.ec) { + break; + } + + auto ec = co_await req.get_conn()->write_websocket(result.data); + if (ec) { + break; + } + } + }); + server.async_start(); + std::this_thread::sleep_for(200ms); + + coro_http_client client{}; + bool ok = + client.init_ssl(asio::ssl::verify_peer, "../openssl_files/server.crt"); + REQUIRE_MESSAGE(ok == true, "init ssl fail, please check ssl config"); + + async_simple::coro::syncAwait(client.connect("wss://localhost:9001")); + + async_simple::coro::syncAwait(client.write_websocket("hello")); + auto data = async_simple::coro::syncAwait(client.read_websocket()); + CHECK(data.resp_body == "hello"); + + client.close(); + + server.stop(); +} +#endif + +async_simple::coro::Lazy test_websocket(coro_http_client &client) { + auto r = co_await client.connect("ws://localhost:8090/ws"); + if (r.net_err) { + co_return; + } + + auto result = co_await client.write_websocket("hello websocket"); + auto data = co_await client.read_websocket(); + CHECK(data.resp_body == "hello websocket"); + co_await client.write_websocket("test again"); + data = co_await client.read_websocket(); + CHECK(data.resp_body == "test again"); + co_await client.write_websocket_close("ws close"); + data = co_await client.read_websocket(); + CHECK(data.resp_body == "ws close"); + CHECK(data.net_err == asio::error::eof); +} + +TEST_CASE("test websocket") { + cinatra::coro_http_server server(1, 8090); + server.set_http_handler( + "/ws", + [](coro_http_request &req, + coro_http_response &resp) -> async_simple::coro::Lazy { + CHECK(req.get_content_type() == content_type::websocket); + websocket_result result{}; + while (true) { + result = co_await req.get_conn()->read_websocket(); + if (result.ec) { + break; + } + + auto ec = co_await req.get_conn()->write_websocket(result.data); + if (ec) { + break; + } + } + }); + server.async_start(); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + coro_http_client client{}; + client.set_ws_sec_key("s//GYHa/XO7Hd2F2eOGfyA=="); + + async_simple::coro::syncAwait(test_websocket(client)); + + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + + // client->async_close(); +} + +void test_websocket_content(size_t len) { + cinatra::coro_http_server server(1, 8090); + server.set_http_handler( + "/", + [](coro_http_request &req, + coro_http_response &resp) -> async_simple::coro::Lazy { + CHECK(req.get_content_type() == content_type::websocket); + websocket_result result{}; + while (true) { + result = co_await req.get_conn()->read_websocket(); + if (result.ec) { + break; + } + + auto ec = co_await req.get_conn()->write_websocket(result.data); + if (ec) { + break; + } + } + }); + server.async_start(); + + auto lazy = [len]() -> async_simple::coro::Lazy { + coro_http_client client{}; + co_await client.connect("ws://localhost:8090"); + std::string send_str(len, 'a'); + co_await client.write_websocket(std::string(send_str)); + auto data = co_await client.read_websocket(); + REQUIRE(data.resp_body.size() == send_str.size()); + CHECK(data.resp_body == send_str); + }; + + async_simple::coro::syncAwait(lazy()); + + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + + server.stop(); +} + +TEST_CASE("test websocket content lt 126") { + test_websocket_content(1); + test_websocket_content(125); +} + +TEST_CASE("test websocket content ge 126") { + test_websocket_content(126); + test_websocket_content(127); +} + +TEST_CASE("test websocket content ge 65535") { + test_websocket_content(65535); + test_websocket_content(65536); +} + +TEST_CASE("test send after server stop") { + cinatra::coro_http_server server(1, 8090); + server.async_start(); + + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + + coro_http_client client{}; + async_simple::coro::syncAwait(client.connect("ws://127.0.0.1:8090")); + + server.stop(); + + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + + async_simple::coro::syncAwait(client.write_websocket("")); + auto data = async_simple::coro::syncAwait(client.read_websocket()); + CHECK(data.net_err); +} + +TEST_CASE("test read write in different threads") { + cinatra::coro_http_server server(1, 8090); + server.set_http_handler( + "/", + [](coro_http_request &req, + coro_http_response &resp) -> async_simple::coro::Lazy { + CHECK(req.get_content_type() == content_type::websocket); + websocket_result result{}; + while (true) { + result = co_await req.get_conn()->read_websocket(); + if (result.ec) { + break; + } + + auto ec = co_await req.get_conn()->write_websocket(result.data); + if (ec) { + break; + } + } + }); + server.async_start(); + + auto client = std::make_shared(); + std::string send_str(100, 'a'); + std::weak_ptr weak = client; + auto another_thread_lazy = [client, + send_str]() -> async_simple::coro::Lazy { + for (int i = 0; i < 100; i++) { + auto data = co_await client->read_websocket(); + if (data.net_err) { + co_return; + } + REQUIRE(data.resp_body.size() == send_str.size()); + CHECK(data.resp_body == send_str); + } + }; + another_thread_lazy().via(coro_io::get_global_executor()).start([](auto &&) { + }); + + auto lazy = [client, weak, &send_str]() -> async_simple::coro::Lazy { + co_await client->connect("ws://localhost:8090"); + for (int i = 0; i < 100; i++) { + auto data = co_await client->write_websocket(std::string(send_str)); + if (data.net_err) { + co_return; + } + } + }; + + async_simple::coro::syncAwait(lazy()); + + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + + server.stop(); +} + +async_simple::coro::Lazy test_websocket() { + coro_http_client client{}; + auto r = co_await client.connect("ws://127.0.0.1:8089/ws_echo"); + if (r.net_err) { + co_return; + } + + co_await client.write_websocket(std::string_view("test2fdsaf"), + opcode::binary); + auto data = co_await client.read_websocket(); + CHECK(data.resp_body == "test2fdsaf"); + + co_await client.write_websocket_close("ws close"); + data = co_await client.read_websocket(); + CHECK(data.net_err == asio::error::eof); + CHECK(data.resp_body == "ws close"); +} + +TEST_CASE("test client quit after send msg") { + coro_http_server server(1, 8089); + server.set_http_handler( + "/ws_echo", + [](coro_http_request &req, + coro_http_response &resp) -> async_simple::coro::Lazy { + websocket_result result{}; + + while (true) { + result = co_await req.get_conn()->read_websocket(); + if (result.ec) { + break; + } + + if (result.type == ws_frame_type::WS_CLOSE_FRAME) { + break; + } + + co_await resp.get_conn()->write_websocket(result.data); + } + }); + server.async_start(); + + async_simple::coro::syncAwait(test_websocket()); +} + +#ifdef CINATRA_ENABLE_GZIP +TEST_CASE("test websocket permessage defalte") { + coro_http_server server(1, 8090); + server.set_http_handler( + "/ws_extesion", + [](coro_http_request &req, + coro_http_response &resp) -> async_simple::coro::Lazy { + websocket_result result{}; + while (true) { + result = co_await req.get_conn()->read_websocket(); + if (result.ec) { + break; + } + + if (result.type == ws_frame_type::WS_CLOSE_FRAME) { + std::cout << "close frame\n"; + break; + } + + if (result.type == ws_frame_type::WS_TEXT_FRAME || + result.type == ws_frame_type::WS_BINARY_FRAME) { + CHECK(result.data == "test"); + } + else if (result.type == ws_frame_type::WS_PING_FRAME || + result.type == ws_frame_type::WS_PONG_FRAME) { + // ping pong frame just need to continue, no need echo anything, + // because framework has reply ping/pong msg to client + // automatically. + continue; + } + else { + // error frame + break; + } + + auto ec = co_await req.get_conn()->write_websocket(result.data); + if (ec) { + break; + } + } + }); + + server.async_start(); + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + + coro_http_client client{}; + client.set_ws_deflate(true); + async_simple::coro::syncAwait( + client.connect("ws://localhost:8090/ws_extesion")); + + std::string send_str("test"); + + async_simple::coro::syncAwait(client.write_websocket(send_str)); + auto data = async_simple::coro::syncAwait(client.read_websocket()); + CHECK(data.resp_body == "test"); + + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + + server.stop(); + client.close(); +} +#endif diff --git a/src/coro_http/tests/test_coro_http_server.cpp b/src/coro_http/tests/test_coro_http_server.cpp new file mode 100644 index 000000000..1eb41ec47 --- /dev/null +++ b/src/coro_http/tests/test_coro_http_server.cpp @@ -0,0 +1,1533 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "doctest.h" +#include "ylt/coro_http/coro_http_client.hpp" +#include "ylt/coro_http/coro_http_server.hpp" + +using namespace cinatra; + +using namespace std::chrono_literals; + +TEST_CASE("test parse ranges") { + bool is_valid = true; + auto vec = parse_ranges("200-999", 10000, is_valid); + CHECK(is_valid); + CHECK(vec == std::vector>{{200, 999}}); + + vec = parse_ranges("-", 10000, is_valid); + CHECK(is_valid); + CHECK(vec == std::vector>{{0, 9999}}); + + vec = parse_ranges("-a", 10000, is_valid); + CHECK(!is_valid); + CHECK(vec.empty()); + + vec = parse_ranges("--100", 10000, is_valid); + CHECK(!is_valid); + CHECK(vec.empty()); + + vec = parse_ranges("abc", 10000, is_valid); + CHECK(!is_valid); + CHECK(vec.empty()); + + is_valid = true; + vec = parse_ranges("-900", 10000, is_valid); + CHECK(is_valid); + CHECK(vec == std::vector>{{9100, 9999}}); + + vec = parse_ranges("900", 10000, is_valid); + CHECK(is_valid); + CHECK(vec == std::vector>{{900, 9999}}); + + vec = parse_ranges("200-999, 2000-2499", 10000, is_valid); + CHECK(is_valid); + CHECK(vec == std::vector>{{200, 999}, {2000, 2499}}); + + vec = parse_ranges("200-999, 2000-2499, 9500-", 10000, is_valid); + CHECK(is_valid); + CHECK(vec == std::vector>{ + {200, 999}, {2000, 2499}, {9500, 9999}}); + + vec = parse_ranges("", 10000, is_valid); + CHECK(is_valid); + CHECK(vec == std::vector>{{0, 9999}}); +} + +TEST_CASE("coro_io post") { + auto t1 = async_simple::coro::syncAwait(coro_io::post([] { + })); + CHECK(!t1.hasError()); + auto t2 = async_simple::coro::syncAwait(coro_io::post([] { + throw std::invalid_argument("e"); + })); + CHECK(t2.hasError()); + + auto t3 = async_simple::coro::syncAwait(coro_io::post([] { + return 1; + })); + int r3 = t3.value(); + CHECK(r3 == 1); + + auto t4 = async_simple::coro::syncAwait(coro_io::post([] { + throw std::invalid_argument("e"); + return 1; + })); + CHECK(t4.hasError()); + + try { + std::rethrow_exception(t4.getException()); + } catch (const std::exception &e) { + CHECK(e.what() == std::string("e")); + std::cout << e.what() << "\n"; + } +} + +TEST_CASE("coro_server example, will block") { + return; // remove this line when you run the coro server. + cinatra::coro_http_server server(std::thread::hardware_concurrency(), 9001); + server.set_http_handler( + "/", [](coro_http_request &req, coro_http_response &resp) { + // response in io thread. + std::this_thread::sleep_for(10ms); + resp.set_status_and_content(cinatra::status_type::ok, "hello world"); + }); + + server.set_http_handler( + "/coro", + [](coro_http_request &req, + coro_http_response &resp) -> async_simple::coro::Lazy { + co_await coro_io::post([&]() { + // coroutine in other thread. + std::this_thread::sleep_for(10ms); + resp.set_status_and_content(cinatra::status_type::ok, "hello world"); + }); + co_return; + }); + + server.set_http_handler( + "/echo", [](coro_http_request &req, coro_http_response &resp) { + // response in io thread. + resp.set_status_and_content(cinatra::status_type::ok, "hello world"); + }); + server.sync_start(); + CHECK(server.port() > 0); +} + +template +bool create_file(View filename, size_t file_size = 1024) { + std::cout << "begin to open file: " << filename << "\n"; + std::ofstream out(filename, std::ios::binary); + if (!out.is_open()) { + std::cout << "open file: " << filename << " failed\n"; + return false; + } + std::cout << "open file: " << filename << " ok\n"; + std::string str(file_size, 'A'); + out.write(str.data(), str.size()); + return true; +} + +TEST_CASE("test redirect") { + coro_http_server server(1, 9001); + server.set_http_handler( + "/", [](coro_http_request &req, coro_http_response &resp) { + resp.redirect("/test"); + }); + + server.set_http_handler( + "/test", [](coro_http_request &req, coro_http_response &resp) { + resp.set_status_and_content(status_type::ok, "redirect ok"); + }); + + server.async_start(); + + coro_http_client client{}; + auto result = client.get("http://127.0.0.1:9001/"); + CHECK(result.status == 302); + for (auto [k, v] : result.resp_headers) { + if (k == "Location") { + auto r = client.get(std::string(v)); + CHECK(r.resp_body == "redirect ok"); + break; + } + } +} + +TEST_CASE("test post") { + cinatra::coro_http_server server(1, 9001); + server.set_http_handler( + "/echo", + [](coro_http_request &req, + coro_http_response &resp) -> async_simple::coro::Lazy { + resp.set_status_and_content(status_type::ok, + std::string(req.get_body())); + co_return; + }); + + server.async_start(); + std::this_thread::sleep_for(200ms); + + coro_http_client client{}; + std::string str = "test"; + auto r = + client.post("http://127.0.0.1:9001/echo", str, req_content_type::text); + CHECK(r.status == 200); + CHECK(r.resp_body == "test"); + + r = client.post("/echo", "", req_content_type::text); + CHECK(r.status == 200); + CHECK(r.resp_body == ""); +} + +TEST_CASE("test multiple download") { + coro_http_server server(1, 9001); + server.set_http_handler( + "/", + [](coro_http_request &req, + coro_http_response &resp) -> async_simple::coro::Lazy { + // multipart_reader_t multipart(resp.get_conn()); + bool ok; + if (ok = co_await resp.get_conn()->begin_multipart(); !ok) { + co_return; + } + + std::vector vec{"hello", " world", " ok"}; + + for (auto &str : vec) { + if (ok = co_await resp.get_conn()->write_multipart(str, "text/plain"); + !ok) { + co_return; + } + } + + ok = co_await resp.get_conn()->end_multipart(); + }); + + server.async_start(); + + coro_http_client client{}; + auto result = client.get("http://127.0.0.1:9001/"); + CHECK(result.status == 200); + CHECK(result.resp_body == "hello world ok"); +} + +TEST_CASE("test range download") { + create_file("range_test.txt", 64); +#ifdef ASIO_WINDOWS +#else + create_file("中文测试.txt", 64); + create_file(fs::path(u8"utf8中文.txt").string(), 64); +#endif + std::cout << fs::current_path() << "\n"; + coro_http_server server(1, 9001); + server.set_static_res_dir("", ""); + server.set_file_resp_format_type(file_resp_format_type::range); + server.async_start(); + std::this_thread::sleep_for(300ms); + +#ifdef ASIO_WINDOWS +#else + { + // test Chinese file name + coro_http_client client{}; + std::string local_filename = "temp.txt"; + + std::string base_uri = "http://127.0.0.1:9001/"; + std::string path = code_utils::url_encode("中文测试.txt"); + auto result = client.download(base_uri + path, local_filename); + CHECK(result.status == 200); + CHECK(fs::file_size(local_filename) == 64); + } + + { + coro_http_client client{}; + std::string local_filename = "temp1.txt"; + std::string base_uri = "http://127.0.0.1:9001/"; + std::string path = + code_utils::url_encode(fs::path(u8"utf8中文.txt").string()); + auto result = client.download(base_uri + path, local_filename); + CHECK(result.status == 200); + CHECK(fs::file_size(local_filename) == 64); + } +#endif + + coro_http_client client{}; + std::string filename = "test1.txt"; + std::error_code ec{}; + std::filesystem::remove(filename, ec); + + std::string uri = "http://127.0.0.1:9001/range_test.txt"; + resp_data result = async_simple::coro::syncAwait( + client.async_download(uri, filename, "1-16")); + CHECK(result.status == 206); + CHECK(fs::file_size(filename) == 16); + + filename = "test2.txt"; + result = async_simple::coro::syncAwait( + client.async_download(uri, filename, "0-63")); + CHECK(result.status == 200); + CHECK(fs::file_size(filename) == 64); + + filename = "test2.txt"; + result = async_simple::coro::syncAwait( + client.async_download(uri, filename, "-10")); + CHECK(result.status == 206); + CHECK(fs::file_size(filename) == 10); + + filename = "test2.txt"; + result = async_simple::coro::syncAwait( + client.async_download(uri, filename, "0-200")); + CHECK(result.status == 200); + CHECK(fs::file_size(filename) == 64); + + filename = "test3.txt"; + result = async_simple::coro::syncAwait( + client.async_download(uri, filename, "100-200")); + CHECK(result.status == 416); + + result = async_simple::coro::syncAwait( + client.async_download(uri, filename, "aaa-200")); + CHECK(result.status == 416); +} + +class my_object { + public: + void normal(coro_http_request &req, coro_http_response &response) { + response.set_status_and_content(status_type::ok, "ok"); + } + + async_simple::coro::Lazy lazy(coro_http_request &req, + coro_http_response &response) { + response.set_status_and_content(status_type::ok, "ok lazy"); + co_return; + } +}; + +TEST_CASE("set http handler") { + cinatra::coro_http_server server(1, 9001); + auto &router = server.get_router(); + auto &handlers = router.get_handlers(); + + server.set_http_handler( + "/", [](coro_http_request &req, coro_http_response &response) { + response.set_status_and_content(status_type::ok, "ok"); + }); + CHECK(handlers.size() == 1); + server.set_http_handler( + "/", [](coro_http_request &req, coro_http_response &response) { + response.set_status_and_content(status_type::ok, "ok"); + }); + CHECK(handlers.size() == 1); + server.set_http_handler( + "/aa", [](coro_http_request &req, coro_http_response &response) { + response.set_status_and_content(status_type::ok, "ok"); + }); + CHECK(handlers.size() == 2); + + server.set_http_handler( + "/bb", [](coro_http_request &req, coro_http_response &response) { + response.set_status_and_content(status_type::ok, "ok"); + }); + CHECK(handlers.size() == 4); + + cinatra::coro_http_server server2(1, 9001); + server2.set_http_handler( + "/", [](coro_http_request &req, coro_http_response &response) { + response.set_status_and_content(status_type::ok, "ok"); + }); + + auto &handlers2 = server2.get_router().get_handlers(); + CHECK(handlers2.size() == 1); + + my_object o{}; + // member function + server2.set_http_handler("/test", &my_object::normal, o); + server2.set_http_handler("/test_lazy", &my_object::lazy, o); + CHECK(handlers2.size() == 2); + + auto coro_func = + [](coro_http_request &req, + coro_http_response &response) -> async_simple::coro::Lazy { + response.set_status_and_content(status_type::ok, "ok"); + co_return; + }; + + auto &coro_handlers = router.get_coro_handlers(); + server.set_http_handler("/", coro_func); + CHECK(coro_handlers.size() == 1); + server.set_http_handler("/", coro_func); + CHECK(coro_handlers.size() == 1); + server.set_http_handler("/aa", coro_func); + CHECK(coro_handlers.size() == 2); + + server.set_http_handler("/bb", coro_func); + CHECK(coro_handlers.size() == 4); +} + +TEST_CASE("test server start and stop") { + cinatra::coro_http_server server(1, 9000); + auto future = server.async_start(); + + cinatra::coro_http_server server2(1, 9000); + auto future2 = server2.async_start(); + future2.wait(); + auto ec = future2.value(); + CHECK(ec == asio::error::address_in_use); +} + +TEST_CASE("test server sync_start and stop") { + cinatra::coro_http_server server(1, 0); + + std::promise promise; + std::error_code ec; + std::thread thd([&] { + promise.set_value(); + ec = server.sync_start(); + }); + promise.get_future().wait(); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + server.stop(); + thd.join(); + CHECK(server.port() > 0); + CHECK(ec == asio::error::operation_aborted); +} + +TEST_CASE("get post") { + cinatra::coro_http_server server(1, 9001); + server.set_shrink_to_fit(true); + server.set_http_handler( + "/test", [](coro_http_request &req, coro_http_response &resp) { + auto value = req.get_header_value("connection"); + CHECK(!value.empty()); + + auto value1 = req.get_header_value("connection1"); + CHECK(value1.empty()); + + auto value2 = req.get_query_value("aa"); + CHECK(value2 == "1"); + + auto value3 = req.get_query_value("bb"); + CHECK(value3 == "test"); + + auto value4 = req.get_query_value("cc"); + CHECK(value4.empty()); + + auto headers = req.get_headers(); + CHECK(!headers.empty()); + + auto queries = req.get_queries(); + CHECK(!queries.empty()); + + resp.set_keepalive(true); + resp.set_status_and_content(cinatra::status_type::ok, "hello world"); + }); + + server.set_http_handler( + "/test1", [](coro_http_request &req, coro_http_response &resp) { + CHECK(req.get_method() == "POST"); + CHECK(req.get_url() == "/test1"); + CHECK(req.get_conn()->local_address() == "127.0.0.1:9001"); + CHECK(req.get_conn()->remote_address().find("127.0.0.1:") != + std::string::npos); + resp.add_header("Host", "Cinatra"); + resp.set_status_and_content(cinatra::status_type::ok, "hello world"); + }); + + server.set_http_handler( + "/test_coro", + [](coro_http_request &req, + coro_http_response &resp) -> async_simple::coro::Lazy { + co_await coro_io::post([&] { + resp.set_status(cinatra::status_type::ok); + resp.set_content("hello world in coro"); + }); + }); + + server.set_http_handler( + "/empty", [](coro_http_request &req, coro_http_response &resp) { + resp.add_header("Host", "Cinatra"); + resp.set_status_and_content(cinatra::status_type::ok, ""); + }); + + server.set_http_handler( + "/close", [](coro_http_request &req, coro_http_response &resp) { + resp.set_keepalive(false); + resp.set_status_and_content(cinatra::status_type::ok, "hello"); + }); + + server.async_start(); + std::this_thread::sleep_for(200ms); + + coro_http_client client{}; + resp_data result; + result = client.get("http://127.0.0.1:9001/test?aa=1&bb=test"); + CHECK(result.status == 200); + CHECK(result.resp_body == "hello world"); + + result = + client.post("http://127.0.0.1:9001/test1", "", req_content_type::text); + CHECK(result.status == 200); + CHECK(result.resp_body == "hello world"); + + result = client.get("http://127.0.0.1:9001/test_coro"); + CHECK(result.status == 200); + CHECK(result.resp_body == "hello world in coro"); + + result = client.get("http://127.0.0.1:9001/not_exist"); + CHECK(result.status == 404); + + result = client.get("http://127.0.0.1:9001/empty"); + CHECK(result.status == 200); + auto &headers = result.resp_headers; + auto it = + std::find_if(headers.begin(), headers.end(), [](http_header &header) { + return header.name == "Host" && header.value == "Cinatra"; + }); + CHECK(it != headers.end()); + CHECK(result.resp_body.empty()); + + client.add_header("Connection", "close"); + result = client.get("http://127.0.0.1:9001/close"); + CHECK(result.status == 200); + + server.stop(); +} + +TEST_CASE("test alias") { + http_server server(1, 9001); + server.set_http_handler("/", [](request &req, response &resp) { + resp.set_status_and_content(status_type::ok, "ok"); + }); + server.async_start(); + std::this_thread::sleep_for(300ms); + + coro_http_client client{}; + auto result = client.get("http://127.0.0.1:9001/"); + CHECK(result.resp_body == "ok"); +} + +struct log_t { + bool before(coro_http_request &, coro_http_response &) { + std::cout << "before log" << std::endl; + return true; + } + + bool after(coro_http_request &, coro_http_response &res) { + std::cout << "after log" << std::endl; + res.add_header("aaaa", "bbcc"); + return true; + } +}; + +struct check_t { + bool before(coro_http_request &, coro_http_response &) { + std::cout << "check before" << std::endl; + return true; + } +}; + +struct get_data { + bool before(coro_http_request &req, coro_http_response &res) { + req.set_aspect_data("hello", "world"); + return true; + } +}; + +TEST_CASE("test aspects") { + coro_http_server server(1, 9001); + server.set_static_res_dir("", ""); + server.set_max_size_of_cache_files(100); + create_file("test_aspect.txt", 64); // in cache + create_file("test_file.txt", 200); // not in cache + + server.set_static_res_dir("", "", log_t{}, check_t{}); + server.set_http_handler( + "/", + [](coro_http_request &req, coro_http_response &resp) { + resp.add_header("aaaa", "bbcc"); + resp.set_status_and_content(status_type::ok, "ok"); + }, + log_t{}, check_t{}); + + server.set_http_handler( + "/aspect", + [](coro_http_request &req, + coro_http_response &resp) -> async_simple::coro::Lazy { + auto &val = req.get_aspect_data(); + CHECK(val[0] == "hello"); + CHECK(val[1] == "world"); + resp.set_status_and_content(status_type::ok, "ok"); + co_return; + }, + get_data{}); + server.async_start(); + std::this_thread::sleep_for(300ms); + + coro_http_client client{}; + auto result = client.get("http://127.0.0.1:9001/"); + + auto check = [](auto &result) { + bool has_str = false; + for (auto [k, v] : result.resp_headers) { + if (k == "aaaa") { + if (v == "bbcc") { + has_str = true; + } + break; + } + } + CHECK(has_str); + }; + + check(result); + + result = client.get("http://127.0.0.1:9001/test_aspect.txt"); + CHECK(result.status == 200); + + result = client.get("http://127.0.0.1:9001/test_file.txt"); + CHECK(result.status == 200); + + result = client.get("http://127.0.0.1:9001/aspect"); + CHECK(result.status == 200); +} + +TEST_CASE("use out context") { + asio::io_context out_ctx; + auto work = std::make_unique(out_ctx); + std::thread thd([&] { + out_ctx.run(); + }); + + cinatra::coro_http_server server(out_ctx, 9001); + server.set_http_handler( + "/out_ctx", [](coro_http_request &req, coro_http_response &resp) { + resp.set_status_and_content(status_type::ok, "use out ctx"); + }); + + server.async_start(); + std::this_thread::sleep_for(200ms); + + { + coro_http_client client1{}; + auto result = client1.get("http://127.0.0.1:9001/out_ctx"); + CHECK(result.status == 200); + CHECK(result.resp_body == "use out ctx"); + } + + server.stop(); + + work.reset(); + thd.join(); +} + +TEST_CASE("delay reply, server stop, form-urlencode, qureies, throw") { + cinatra::coro_http_server server(1, 9001); + + server.set_http_handler( + "/delay2", + [](coro_http_request &req, + coro_http_response &resp) -> async_simple::coro::Lazy { + resp.set_delay(true); + std::this_thread::sleep_for(200ms); + resp.set_status_and_content(status_type::ok, "delay reply in coro"); + co_await resp.get_conn()->reply(); + }); + + server.set_http_handler( + "/form-urlencode", [](coro_http_request &req, coro_http_response &resp) { + CHECK(req.get_body() == "theCityName=58367&aa=%22bbb%22"); + CHECK(req.get_query_value("theCityName") == "58367"); + CHECK(req.get_decode_query_value("aa") == "\"bbb\""); + CHECK(req.get_decode_query_value("no_such-key").empty()); + CHECK(!req.is_upgrade()); + resp.set_status_and_content(status_type::ok, "form-urlencode"); + }); + + server.set_http_handler( + "/throw", [](coro_http_request &req, coro_http_response &resp) { + CHECK(req.get_boundary().empty()); + throw std::invalid_argument("invalid arguments"); + resp.set_status_and_content(status_type::ok, "ok"); + }); + + server.async_start(); + std::this_thread::sleep_for(200ms); + + resp_data result; + coro_http_client client1{}; + result = client1.get("http://127.0.0.1:9001/delay2"); + CHECK(result.status == 200); + CHECK(result.resp_body == "delay reply in coro"); + + result = client1.post("http://127.0.0.1:9001/form-urlencode", + "theCityName=58367&aa=%22bbb%22", + req_content_type::form_url_encode); + CHECK(result.status == 200); + CHECK(result.resp_body == "form-urlencode"); + + result = client1.get("http://127.0.0.1:9001/throw"); + CHECK(result.status == 503); + + server.stop(); + std::cout << "ok\n"; +} + +async_simple::coro::Lazy chunked_upload1(coro_http_client &client) { + std::string filename = "test.txt"; + create_file(filename, 1010); + + coro_io::coro_file file{}; + file.open(filename, std::ios::in); + + std::string buf; + detail::resize(buf, 100); + + auto fn = [&file, &buf]() -> async_simple::coro::Lazy { + auto [ec, size] = co_await file.async_read(buf.data(), buf.size()); + co_return read_result{{buf.data(), buf.size()}, file.eof(), ec}; + }; + + auto result = co_await client.async_upload_chunked( + "http://127.0.0.1:9001/chunked"sv, http_method::POST, std::move(fn)); + co_return result; +} + +TEST_CASE("chunked request") { + cinatra::coro_http_server server(1, 9001); + server.set_http_handler( + "/chunked", + [](coro_http_request &req, + coro_http_response &resp) -> async_simple::coro::Lazy { + assert(req.get_content_type() == content_type::chunked); + chunked_result result{}; + std::string content; + + while (true) { + result = co_await req.get_conn()->read_chunked(); + if (result.ec) { + co_return; + } + if (result.eof) { + break; + } + + content.append(result.data); + } + + std::cout << "content size: " << content.size() << "\n"; + std::cout << content << "\n"; + resp.set_format_type(format_type::chunked); + resp.set_status_and_content(status_type::ok, "chunked ok"); + }); + + server.set_http_handler( + "/write_chunked", + [](coro_http_request &req, + coro_http_response &resp) -> async_simple::coro::Lazy { + resp.set_format_type(format_type::chunked); + bool ok; + if (ok = co_await resp.get_conn()->begin_chunked(); !ok) { + co_return; + } + + std::vector vec{"hello", " world", " ok"}; + + for (auto &str : vec) { + if (ok = co_await resp.get_conn()->write_chunked(str); !ok) { + co_return; + } + } + + ok = co_await resp.get_conn()->end_chunked(); + }); + + server.async_start(); + std::this_thread::sleep_for(200ms); + + coro_http_client client{}; + auto r = async_simple::coro::syncAwait(chunked_upload1(client)); + CHECK(r.status == 200); + CHECK(r.resp_body == "chunked ok"); + + auto ss = std::make_shared(); + *ss << "hello world"; + auto result = async_simple::coro::syncAwait(client.async_upload_chunked( + "http://127.0.0.1:9001/chunked"sv, http_method::POST, ss)); + CHECK(result.status == 200); + CHECK(result.resp_body == "chunked ok"); + + result = client.get("http://127.0.0.1:9001/write_chunked"); + CHECK(result.status == 200); + CHECK(result.resp_body == "hello world ok"); +} + +TEST_CASE("test websocket with chunked") { + int ws_chunk_size = 100; + cinatra::coro_http_server server(1, 9001); + server.set_http_handler( + "/ws_source", + [ws_chunk_size](coro_http_request &req, coro_http_response &resp) + -> async_simple::coro::Lazy { + CHECK(req.get_content_type() == content_type::websocket); + std::string out_str; + websocket_result result{}; + while (!result.eof) { + result = co_await req.get_conn()->read_websocket(); + if (result.ec) { + break; + } + + if (result.type == ws_frame_type::WS_CLOSE_FRAME) { + std::cout << "close frame\n"; + CHECK(result.data.empty()); + break; + } + + std::cout << result.data.size() << "\n"; + + if (result.data.size() < ws_chunk_size) { + CHECK(result.data.size() == 24); + CHECK(result.eof); + } + else { + CHECK(result.data.size() == ws_chunk_size); + CHECK(!result.eof); + } + out_str.append(result.data); + + auto ec = co_await req.get_conn()->write_websocket(result.data); + if (ec) { + continue; + } + } + + CHECK(out_str.size() == 1024); + std::cout << out_str << "\n"; + }); + server.async_start(); + + coro_http_client client{}; + async_simple::coro::syncAwait( + client.connect("ws://127.0.0.1:9001/ws_source")); + + std::string filename = "test.tmp"; + create_file(filename); + std::ifstream in(filename, std::ios::binary); + + std::string str; + str.resize(ws_chunk_size); + + auto source_fn = [&]() -> async_simple::coro::Lazy { + size_t size = in.read(str.data(), str.size()).gcount(); + bool eof = in.eof(); + co_return read_result{{str.data(), size}, eof}; + }; + + async_simple::coro::syncAwait( + client.write_websocket(std::move(source_fn), opcode::binary)); + + auto data = async_simple::coro::syncAwait(client.read_websocket()); + if (data.net_err) { + std::cout << "ws_msg net error " << data.net_err.message() << "\n"; + return; + } + + size_t msg_len = data.resp_body.size(); + + std::cout << "ws msg len: " << msg_len << std::endl; + CHECK(!data.resp_body.empty()); + std::this_thread::sleep_for(300ms); + server.stop(); +} + +TEST_CASE("test websocket") { + cinatra::coro_http_server server(1, 9001); + server.set_http_handler( + "/ws_echo", + [](coro_http_request &req, + coro_http_response &resp) -> async_simple::coro::Lazy { + CHECK(req.get_content_type() == content_type::websocket); + std::ofstream out_file("test.temp", std::ios::binary); + websocket_result result{}; + while (true) { + result = co_await req.get_conn()->read_websocket(); + if (result.ec) { + out_file.close(); + break; + } + + if (result.type == ws_frame_type::WS_CLOSE_FRAME) { + std::cout << "close frame\n"; + out_file.close(); + break; + } + + if (result.type == ws_frame_type::WS_TEXT_FRAME || + result.type == ws_frame_type::WS_BINARY_FRAME) { + CHECK(!result.data.empty()); + std::cout << result.data << "\n"; + out_file << result.data; + } + else { + std::cout << result.data << "\n"; + if (result.type == ws_frame_type::WS_PING_FRAME || + result.type == ws_frame_type::WS_PONG_FRAME) { + std::cout << "ping or pong msg\n"; + // ping pong frame just need to continue, no need echo anything, + // because framework has reply ping/pong to client automatically. + continue; + } + else { + // error frame + break; + } + } + + auto ec = co_await req.get_conn()->write_websocket(result.data); + if (ec) { + break; + } + } + }); + server.async_start(); + std::this_thread::sleep_for(200ms); // wait for server handle all messages + + auto lazy = []() -> async_simple::coro::Lazy { + coro_http_client client{}; + co_await client.connect("ws://127.0.0.1:9001/ws_echo"); + co_await client.write_websocket(std::string_view("test2fdsaf"), + opcode::binary); + auto data = co_await client.read_websocket(); + CHECK(data.resp_body == "test2fdsaf"); + co_await client.write_websocket("test_ws"); + data = co_await client.read_websocket(); + CHECK(data.resp_body == "test_ws"); + co_await client.write_websocket("PING", opcode::ping); + data = co_await client.read_websocket(); + CHECK(data.resp_body == "pong"); + co_await client.write_websocket("PONG", opcode::pong); + data = co_await client.read_websocket(); + CHECK(data.resp_body == "ping"); + co_await client.write_websocket_close("normal close"); + data = co_await client.read_websocket(); + CHECK(data.resp_body == "normal close"); + CHECK(data.net_err == asio::error::eof); + }; + + async_simple::coro::syncAwait(lazy()); +} + +TEST_CASE("check small ws file") { + std::string filename = "test.temp"; + std::error_code ec; + size_t file_size = std::filesystem::file_size(filename, ec); + if (ec) { + return; + } + std::ifstream file(filename, std::ios::binary); + if (!file) { + return; + } + std::string str; + str.resize(file_size); + + file.read(str.data(), str.size()); + CHECK(str == "test2fdsaftest_ws"); + std::filesystem::remove(filename, ec); +} + +TEST_CASE("test websocket binary data") { + cinatra::coro_http_server server(1, 9001); + server.set_http_handler( + "/short_binary", + [](coro_http_request &req, + coro_http_response &resp) -> async_simple::coro::Lazy { + CHECK(req.get_content_type() == content_type::websocket); + websocket_result result{}; + while (true) { + result = co_await req.get_conn()->read_websocket(); + if (result.ec) { + break; + } + + if (result.type == ws_frame_type::WS_CLOSE_FRAME) { + std::cout << "close frame\n"; + CHECK(result.data.empty()); + break; + } + + if (result.type == ws_frame_type::WS_BINARY_FRAME) { + CHECK(result.data.size() == 127); + } + } + }); + server.set_http_handler( + "/medium_binary", + [](coro_http_request &req, + coro_http_response &resp) -> async_simple::coro::Lazy { + CHECK(req.get_content_type() == content_type::websocket); + websocket_result result{}; + while (true) { + result = co_await req.get_conn()->read_websocket(); + if (result.ec) { + break; + } + + if (result.type == ws_frame_type::WS_CLOSE_FRAME) { + std::cout << "close frame\n"; + CHECK(result.data.empty()); + break; + } + + if (result.type == ws_frame_type::WS_BINARY_FRAME) { + CHECK(result.data.size() == 65535); + } + } + }); + server.set_http_handler( + "/long_binary", + [](coro_http_request &req, + coro_http_response &resp) -> async_simple::coro::Lazy { + CHECK(req.get_content_type() == content_type::websocket); + websocket_result result{}; + while (true) { + result = co_await req.get_conn()->read_websocket(); + if (result.ec) { + break; + } + + if (result.type == ws_frame_type::WS_CLOSE_FRAME) { + std::cout << "close frame\n"; + CHECK(result.data.empty()); + break; + } + + if (result.type == ws_frame_type::WS_BINARY_FRAME) { + CHECK(result.data.size() == 65536); + } + } + }); + server.async_start(); + + auto client1 = std::make_shared(); + async_simple::coro::syncAwait( + client1->connect("ws://127.0.0.1:9001/short_binary")); + + std::string short_str(127, 'A'); + async_simple::coro::syncAwait( + client1->write_websocket(std::move(short_str), opcode::binary)); + + auto client2 = std::make_shared(); + async_simple::coro::syncAwait( + client2->connect("ws://127.0.0.1:9001/medium_binary")); + + std::string medium_str(65535, 'A'); + async_simple::coro::syncAwait( + client2->write_websocket(std::move(medium_str), opcode::binary)); + + auto client3 = std::make_shared(); + async_simple::coro::syncAwait( + client3->connect("ws://127.0.0.1:9001/long_binary")); + + std::string long_str(65536, 'A'); + async_simple::coro::syncAwait( + client3->write_websocket(std::move(long_str), opcode::binary)); + + async_simple::coro::syncAwait(client1->write_websocket_close()); + async_simple::coro::syncAwait(client2->write_websocket_close()); + async_simple::coro::syncAwait(client3->write_websocket_close()); +} + +TEST_CASE("check connecton timeout") { + cinatra::coro_http_server server(1, 9001); + server.set_check_duration(std::chrono::microseconds(600)); + server.set_timeout_duration(std::chrono::microseconds(500)); + server.set_http_handler( + "/", [](coro_http_request &req, coro_http_response &response) { + response.set_status_and_content(status_type::ok, "ok"); + }); + + server.async_start(); + std::this_thread::sleep_for(200ms); + + coro_http_client client; + client.get("http://127.0.0.1:9001/"); + + // wait for timeout, the timeout connections will be removed by server. + std::this_thread::sleep_for(std::chrono::seconds(1)); + CHECK(server.connection_count() == 0); +} + +TEST_CASE("test websocket with different message size") { + cinatra::coro_http_server server(1, 9001); + server.set_http_handler( + "/ws_echo1", + [](cinatra::coro_http_request &req, + cinatra::coro_http_response &resp) -> async_simple::coro::Lazy { + REQUIRE(req.get_content_type() == cinatra::content_type::websocket); + cinatra::websocket_result result{}; + + while (true) { + req.get_conn()->set_ws_max_size( + 70000); // default max size 8M, you can control the size. + result = co_await req.get_conn()->read_websocket(); + if (result.ec) { + break; + } + + if (result.type == cinatra::ws_frame_type::WS_CLOSE_FRAME) { + REQUIRE(result.data.empty()); + break; + } + + auto ec = co_await req.get_conn()->write_websocket(result.data); + if (ec) { + break; + } + } + }); + server.async_start(); + + auto lazy = [](std::string &str) -> async_simple::coro::Lazy { + coro_http_client client{}; + co_await client.connect("ws://127.0.0.1:9001/ws_echo1"); + co_await client.write_websocket(str); + auto data = co_await client.read_websocket(); + CHECK(data.resp_body.size() == str.size()); + co_await client.write_websocket_close(); + data = co_await client.read_websocket(); + CHECK(data.resp_body.empty()); + CHECK(data.net_err == asio::error::eof); + }; + + SUBCASE("medium message - 16 bit length") { + std::string medium_message( + 65535, 'x'); // 65,535 'x' characters for the medium message test. + + async_simple::coro::syncAwait(lazy(medium_message)); + } + + SUBCASE("large message - 64 bit length") { + std::string large_message( + 70000, 'x'); // 70,000 'x' characters for the large message test. + + async_simple::coro::syncAwait(lazy(large_message)); + } + + server.stop(); +} + +#ifdef CINATRA_ENABLE_SSL +TEST_CASE("test ssl server") { + cinatra::coro_http_server server(1, 9001); + + server.init_ssl("../openssl_files/server.crt", "../openssl_files/server.key", + "test"); + server.set_http_handler( + "/ssl", [](coro_http_request &req, coro_http_response &resp) { + resp.set_status_and_content(status_type::ok, "ssl"); + }); + + server.async_start(); + std::this_thread::sleep_for(200ms); + + coro_http_client client{}; + [[maybe_unused]] auto r = client.init_ssl(asio::ssl::verify_peer, + "../../include/cinatra/server.crt"); + + auto result = client.get("https://127.0.0.1:9001/ssl"); + CHECK(result.status == 200); + CHECK(result.resp_body == "ssl"); + std::cout << "ssl ok\n"; +} +#endif + +TEST_CASE("test http download server") { + cinatra::coro_http_server server(1, 9001); + std::string filename = "test_download.txt"; + create_file(filename, 1010); + + // curl http://127.0.0.1:9001/download/test_download.txt will download + // test_download.txt file + server.set_transfer_chunked_size(100); + server.set_static_res_dir("download", ""); + server.async_start(); + std::this_thread::sleep_for(200ms); + + { + coro_http_client client{}; + auto result = async_simple::coro::syncAwait(client.async_download( + "http://127.0.0.1:9001/download/test_download.txt", "download.txt")); + + CHECK(result.status == 200); + std::string download_file = fs::absolute("download.txt").string(); + std::ifstream ifs(download_file, std::ios::binary); + std::string content((std::istreambuf_iterator(ifs)), + (std::istreambuf_iterator())); + CHECK(content.size() == 1010); + CHECK(content[0] == 'A'); + } + + { + coro_http_client client{}; + auto result = async_simple::coro::syncAwait(client.async_download( + "http://127.0.0.1:9001/download/test_download.txt", "download1.txt", + "0-")); + + CHECK(result.status == 200); + std::string download_file = fs::absolute("download1.txt").string(); + std::ifstream ifs(download_file, std::ios::binary); + std::string content((std::istreambuf_iterator(ifs)), + (std::istreambuf_iterator())); + CHECK(content.size() == 1010); + CHECK(content[0] == 'A'); + } +} + +TEST_CASE("test restful api") { + cinatra::coro_http_server server(1, 9001); + + server.set_http_handler( + "/test2/{}/test3/{}", + [](coro_http_request &req, + coro_http_response &resp) -> async_simple::coro::Lazy { + co_await coro_io::post([&]() { + // coroutine in other thread. + CHECK(req.matches_.str(1) == "name"); + CHECK(req.matches_.str(2) == "test"); + resp.set_status_and_content(cinatra::status_type::ok, "hello world"); + }); + co_return; + }); + + server.set_http_handler( + R"(/numbers/(\d+)/test/(\d+))", + [](coro_http_request &req, coro_http_response &response) { + CHECK(req.matches_.str(1) == "100"); + CHECK(req.matches_.str(2) == "200"); + response.set_status_and_content(status_type::ok, "number regex ok"); + }); + + server.async_start(); + std::this_thread::sleep_for(200ms); + + coro_http_client client; + client.get("http://127.0.0.1:9001/test2/name/test3/test"); + client.get("http://127.0.0.1:9001/numbers/100/test/200"); +} + +TEST_CASE("test radix tree restful api") { + cinatra::coro_http_server server(1, 9001); + + server.set_http_handler( + "/", [](coro_http_request &req, coro_http_response &response) { + response.set_status_and_content(status_type::ok, "ok"); + }); + + server.set_http_handler( + "/user/:id", [](coro_http_request &req, coro_http_response &response) { + CHECK(req.params_["id"] == "cinatra"); + response.set_status_and_content(status_type::ok, "ok"); + }); + + server.set_http_handler( + "/user/:id/subscriptions", + [](coro_http_request &req, coro_http_response &response) { + CHECK(req.params_["id"] == "subid"); + response.set_status_and_content(status_type::ok, "ok"); + }); + + server.set_http_handler( + "/users/:userid/subscriptions/:subid", + [](coro_http_request &req, coro_http_response &response) { + CHECK(req.params_["userid"] == "ultramarines"); + CHECK(req.params_["subid"] == "guilliman"); + response.set_status_and_content(status_type::ok, "ok"); + }); + + server.set_http_handler( + "/values/:x/:y/:z", + [](coro_http_request &req, coro_http_response &response) { + CHECK(req.params_["x"] == "guilliman"); + CHECK(req.params_["y"] == "cawl"); + CHECK(req.params_["z"] == "yvraine"); + response.set_status_and_content(status_type::ok, "ok"); + }); + + server.async_start(); + std::this_thread::sleep_for(200ms); + + coro_http_client client; + client.get("http://127.0.0.1:9001/user/cinatra"); + client.get("http://127.0.0.1:9001/user/subid/subscriptions"); + client.get("http://127.0.0.1:9001/user/ultramarines/subscriptions/guilliman"); + client.get("http://127.0.0.1:9001/value/guilliman/cawl/yvraine"); + + client.post("http://127.0.0.1:9001/user/cinatra", "hello", + req_content_type::string); + client.post("http://127.0.0.1:9001/user/subid/subscriptions", "hello", + req_content_type::string); + client.post("http://127.0.0.1:9001/user/ultramarines/subscriptions/guilliman", + "hello", req_content_type::string); + client.post("http://127.0.0.1:9001/value/guilliman/cawl/yvraine", "hello", + req_content_type::string); +} + +TEST_CASE("test coro radix tree restful api") { + cinatra::coro_http_server server(1, 9001); + + server.set_http_handler( + "/", + [](coro_http_request &req, + coro_http_response &response) -> async_simple::coro::Lazy { + co_await coro_io::post([&]() { + response.set_status_and_content(status_type::ok, "ok"); + }); + }); + + server.set_http_handler( + "/user/:id", + [](coro_http_request &req, + coro_http_response &response) -> async_simple::coro::Lazy { + co_await coro_io::post([&]() { + CHECK(req.params_["id"] == "cinatra"); + response.set_status_and_content(status_type::ok, "ok"); + }); + }); + + server.set_http_handler( + "/user/:id/subscriptions", + [](coro_http_request &req, + coro_http_response &response) -> async_simple::coro::Lazy { + co_await coro_io::post([&] { + CHECK(req.params_["id"] == "subid"); + response.set_status_and_content(status_type::ok, "ok"); + }); + }); + + server.set_http_handler( + "/users/:userid/subscriptions/:subid", + [](coro_http_request &req, + coro_http_response &response) -> async_simple::coro::Lazy { + co_await coro_io::post([&] { + CHECK(req.params_["userid"] == "ultramarines"); + CHECK(req.params_["subid"] == "guilliman"); + response.set_status_and_content(status_type::ok, "ok"); + }); + }); + + server.set_http_handler( + "/values/:x/:y/:z", + [](coro_http_request &req, + coro_http_response &response) -> async_simple::coro::Lazy { + co_await coro_io::post([&] { + CHECK(req.params_["x"] == "guilliman"); + CHECK(req.params_["y"] == "cawl"); + CHECK(req.params_["z"] == "yvraine"); + response.set_status_and_content(status_type::ok, "ok"); + }); + }); + + server.set_http_handler( + "/ai/robot/:messages", + [](coro_http_request &req, + coro_http_response &response) -> async_simple::coro::Lazy { + co_await coro_io::post([&]() { + CHECK(req.params_["messages"] == "android"); + response.set_status_and_content(status_type::ok, "ok"); + }); + }); + + server.async_start(); + std::this_thread::sleep_for(200ms); + + coro_http_client client; + client.get("http://127.0.0.1:9001/user/cinatra"); + client.get("http://127.0.0.1:9001/user/subid/subscriptions"); + client.get("http://127.0.0.1:9001/user/ultramarines/subscriptions/guilliman"); + client.get("http://127.0.0.1:9001/value/guilliman/cawl/yvraine"); + + client.post("http://127.0.0.1:9001/user/cinatra", "hello", + req_content_type::string); + client.post("http://127.0.0.1:9001/user/subid/subscriptions", "hello", + req_content_type::string); + client.post("http://127.0.0.1:9001/user/ultramarines/subscriptions/guilliman", + "hello", req_content_type::string); + client.post("http://127.0.0.1:9001/value/guilliman/cawl/yvraine", "hello", + req_content_type::string); + client.post("http://127.0.0.1:9001/ai/robot/android", "hello", + req_content_type::string); +} + +TEST_CASE("test reverse proxy") { + SUBCASE( + "exception tests: empty hosts, empty weights test or count not equal") { + cinatra::coro_http_server server(1, 9002); + CHECK_THROWS_AS(server.set_http_proxy_handler( + "/", {}, coro_io::load_blance_algorithm::WRR, {2, 1}), + std::invalid_argument); + + CHECK_THROWS_AS(server.set_http_proxy_handler( + "/", {"127.0.0.1:8801", "127.0.0.1:8802"}, + coro_io::load_blance_algorithm::WRR), + std::invalid_argument); + + CHECK_THROWS_AS(server.set_http_proxy_handler( + "/", {"127.0.0.1:8801", "127.0.0.1:8802"}, + coro_io::load_blance_algorithm::WRR, {1}), + std::invalid_argument); + + CHECK_THROWS_AS( + server.set_http_proxy_handler("/", {}), + std::invalid_argument); + } + + cinatra::coro_http_server web_one(1, 9001); + + web_one.set_http_handler( + "/", + [](coro_http_request &req, + coro_http_response &response) -> async_simple::coro::Lazy { + co_await coro_io::post([&]() { + response.set_status_and_content(status_type::ok, "web1"); + }); + }); + + web_one.async_start(); + + cinatra::coro_http_server web_two(1, 9002); + + web_two.set_http_handler( + "/", + [](coro_http_request &req, + coro_http_response &response) -> async_simple::coro::Lazy { + co_await coro_io::post([&]() { + response.set_status_and_content(status_type::ok, "web2"); + }); + }); + + web_two.async_start(); + + cinatra::coro_http_server web_three(1, 9003); + + web_three.set_http_handler( + "/", [](coro_http_request &req, coro_http_response &response) { + response.set_status_and_content(status_type::ok, "web3"); + }); + + web_three.async_start(); + + std::this_thread::sleep_for(200ms); + + coro_http_server proxy_wrr(2, 8090); + proxy_wrr.set_http_proxy_handler( + "/", {"127.0.0.1:9001", "127.0.0.1:9002", "127.0.0.1:9003"}, + coro_io::load_blance_algorithm::WRR, {10, 5, 5}, log_t{}, check_t{}); + + coro_http_server proxy_rr(2, 8091); + proxy_rr.set_http_proxy_handler( + "/", {"127.0.0.1:9001", "127.0.0.1:9002", "127.0.0.1:9003"}, + coro_io::load_blance_algorithm::RR, {}, log_t{}); + + coro_http_server proxy_random(2, 8092); + proxy_random.set_http_proxy_handler( + "/", {"127.0.0.1:9001", "127.0.0.1:9002", "127.0.0.1:9003"}); + + coro_http_server proxy_all(2, 8093); + proxy_all.set_http_proxy_handler( + "/", {"127.0.0.1:9001", "127.0.0.1:9002", "127.0.0.1:9003"}); + + proxy_wrr.async_start(); + proxy_rr.async_start(); + proxy_random.async_start(); + proxy_all.async_start(); + + std::this_thread::sleep_for(200ms); + + coro_http_client client_rr; + resp_data resp_rr = client_rr.get("http://127.0.0.1:8091/"); + CHECK(resp_rr.resp_body == "web1"); + resp_rr = client_rr.get("http://127.0.0.1:8091/"); + CHECK(resp_rr.resp_body == "web2"); + resp_rr = client_rr.get("http://127.0.0.1:8091/"); + CHECK(resp_rr.resp_body == "web3"); + resp_rr = client_rr.get("http://127.0.0.1:8091/"); + CHECK(resp_rr.resp_body == "web1"); + resp_rr = client_rr.get("http://127.0.0.1:8091/"); + CHECK(resp_rr.resp_body == "web2"); + resp_rr = client_rr.post("http://127.0.0.1:8091/", "test content", + req_content_type::text); + CHECK(resp_rr.resp_body == "web3"); + + coro_http_client client_wrr; + resp_data resp = client_wrr.get("http://127.0.0.1:8090/"); + CHECK(resp.resp_body == "web1"); + resp = client_wrr.get("http://127.0.0.1:8090/"); + CHECK(resp.resp_body == "web1"); + resp = client_wrr.get("http://127.0.0.1:8090/"); + CHECK(resp.resp_body == "web2"); + resp = client_wrr.get("http://127.0.0.1:8090/"); + CHECK(resp.resp_body == "web3"); + + coro_http_client client_random; + resp_data resp_random = client_random.get("http://127.0.0.1:8092/"); + std::cout << resp_random.resp_body << "\n"; + CHECK(!resp_random.resp_body.empty()); + + coro_http_client client_all; + resp_random = client_all.post("http://127.0.0.1:8093/", "test content", + req_content_type::text); + std::cout << resp_random.resp_body << "\n"; + CHECK(!resp_random.resp_body.empty()); +} + +TEST_CASE("test reverse proxy websocket") { + coro_http_server server(1, 9001); + server.set_http_handler( + "/ws_echo", + [](coro_http_request &req, + coro_http_response &resp) -> async_simple::coro::Lazy { + CHECK(req.get_content_type() == content_type::websocket); + websocket_result result{}; + while (true) { + result = co_await req.get_conn()->read_websocket(); + if (result.ec) { + break; + } + + auto ec = co_await req.get_conn()->write_websocket(result.data); + if (ec) { + break; + } + } + }); + server.async_start(); + + coro_http_server proxy_server(1, 9002); + proxy_server.set_websocket_proxy_handler("/ws_echo", + {"ws://127.0.0.1:9001/ws_echo"}); + proxy_server.async_start(); + std::this_thread::sleep_for(200ms); + + coro_http_client client{}; + auto r = async_simple::coro::syncAwait( + client.connect("ws://127.0.0.1:9002/ws_echo")); + CHECK(!r.net_err); + for (int i = 0; i < 10; i++) { + async_simple::coro::syncAwait(client.write_websocket("test websocket")); + auto data = async_simple::coro::syncAwait(client.read_websocket()); + std::cout << data.resp_body << "\n"; + CHECK(data.resp_body == "test websocket"); + } +} diff --git a/src/coro_http/tests/test_http_parse.cpp b/src/coro_http/tests/test_http_parse.cpp new file mode 100644 index 000000000..0f3947aec --- /dev/null +++ b/src/coro_http/tests/test_http_parse.cpp @@ -0,0 +1,101 @@ +#include + +#include "doctest.h" +#include "ylt/coro_http/coro_http_client.hpp" +#include "ylt/coro_http/coro_http_server.hpp" + +using namespace coro_http; + +std::string_view REQ = + "R(GET /wp-content/uploads/2010/03/hello-kitty-darth-vader-pink.jpg " + "HTTP/1.1\r\n" + "Host: www.kittyhell.com\r\n" + "User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; ja-JP-mac; " + "rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 " + "Pathtraq/0.9\r\n" + "Accept: " + "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" + "Accept-Language: ja,en-us;q=0.7,en;q=0.3\r\n" + "Accept-Encoding: gzip,deflate\r\n" + "Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7\r\n" + "Keep-Alive: 115\r\n" + "Connection: keep-alive\r\n" + "Cookie: wp_ozh_wsa_visits=2; wp_ozh_wsa_visit_lasttime=xxxxxxxxxx; " + "__utma=xxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.x; " + "__utmz=xxxxxxxxx.xxxxxxxxxx.x.x.utmccn=(referral)|utmcsr=reader.livedoor." + "com|utmcct=/reader/|utmcmd=referral\r\n" + "\r\n)"; + +std::string_view multipart_str = + "R(POST / HTTP/1.1\r\n" + "User-Agent: PostmanRuntime/7.39.0\r\n" + "Accept: */*\r\n" + "Cache-Control: no-cache\r\n" + "Postman-Token: 33c25732-1648-42ed-a467-cc9f1eb1e961\r\n" + "Host: purecpp.cn\r\n" + "Accept-Encoding: gzip, deflate, br\r\n" + "Connection: keep-alive\r\n" + "Content-Type: multipart/form-data; " + "boundary=--------------------------559980232503017651158362\r\n" + "Cookie: CSESSIONID=87343c8a24f34e28be05efea55315aab\r\n" + "\r\n" + "----------------------------559980232503017651158362\r\n" + "Content-Disposition: form-data; name=\"test\"\r\n" + "tom\r\n" + "----------------------------559980232503017651158362--\r\n"; + +std::string_view bad_multipart_str = + "R(POST / HTTP/1.1\r\n" + "User-Agent: PostmanRuntime/7.39.0\r\n" + "Accept: */*\r\n" + "Cache-Control: no-cache\r\n" + "Postman-Token: 33c25732-1648-42ed-a467-cc9f1eb1e961\r\n" + "Host: purecpp.cn\r\n" + "Accept-Encoding: gzip, deflate, br\r\n" + "Connection: keep-alive\r\n" + "Content-Type: multipart/form-data; boundary=559980232503017651158362\r\n" + "Cookie: CSESSIONID=87343c8a24f34e28be05efea55315aab\r\n" + "\r\n" + "559980232503017651158362\r\n" + "Content-Disposition: form-data; name=\"test\"\r\n" + "tom\r\n" + "559980232503017651158362--\r\n"; + +std::string_view resp_str = + "R(HTTP/1.1 400 Bad Request\r\n" + "Connection: keep-alive\r\n" + "Content-Length: 20\r\n" + "Host: cinatra\r\n" + "\r\n\r\n" + "the url is not right)"; + +TEST_CASE("http_parser test") { + http_parser parser{}; + parser.parse_request(REQ.data(), REQ.size(), 0); + CHECK(parser.body_len() == 0); + CHECK(parser.body_len() + parser.header_len() == parser.total_len()); + CHECK(parser.has_connection()); + + parser = {}; + std::string_view str(REQ.data(), 20); + int ret = parser.parse_request(str.data(), str.size(), 0); + CHECK(ret < 0); + + parser = {}; + ret = parser.parse_request(multipart_str.data(), multipart_str.size(), 0); + CHECK(ret > 0); + auto boundary = parser.get_boundary(); + CHECK(boundary == "--------------------------559980232503017651158362"); + + parser = {}; + ret = parser.parse_request(bad_multipart_str.data(), bad_multipart_str.size(), + 0); + CHECK(ret > 0); + auto bad_boundary = parser.get_boundary(); + CHECK(bad_boundary.empty()); + + parser = {}; + std::string_view part_resp(resp_str.data(), 20); + ret = parser.parse_response(part_resp.data(), part_resp.size(), 0); + CHECK(ret < 0); +} diff --git a/src/coro_http/tests/test_mixed.cpp b/src/coro_http/tests/test_mixed.cpp new file mode 100644 index 000000000..80f8945ad --- /dev/null +++ b/src/coro_http/tests/test_mixed.cpp @@ -0,0 +1,220 @@ +#include "doctest.h" +#include "ylt/coro_http/coro_http_client.hpp" +#include "ylt/coro_http/coro_http_server.hpp" + +using namespace coro_http; + +bool create_file2(std::string_view filename, size_t file_size = 1024) { + std::ofstream out(filename.data(), std::ios::binary); + if (!out.is_open()) { + return false; + } + + std::string str; + for (int i = 0; i < file_size; ++i) { + str.push_back(rand() % 26 + 'A'); + } + out.write(str.data(), str.size()); + return true; +} + +/* +loop ok/failed with outbuf/no outbuf +*/ +TEST_CASE("test mixed async_request") { + coro_http_server server(2, 9900); + server.set_http_handler("/", [](request& req, response& resp) { + resp.set_status_and_content(status_type::ok, "ok"); + }); + server.set_http_handler("/test", [](request& req, response& resp) { + resp.set_status_and_content(status_type::ok, "test"); + }); + server.set_http_handler( + "/upload", [](request& req, response& resp) { + CHECK(req.get_body().size() == 20); + resp.set_status_and_content(status_type::ok, "upload ok"); + }); + server.set_http_handler( + "/chunked", + [](coro_http_request& req, + coro_http_response& resp) -> async_simple::coro::Lazy { + assert(req.get_content_type() == content_type::chunked); + chunked_result result{}; + std::string content; + + while (true) { + result = co_await req.get_conn()->read_chunked(); + if (result.ec) { + co_return; + } + if (result.eof) { + break; + } + + content.append(result.data); + } + + std::cout << "content size: " << content.size() << "\n"; + resp.set_format_type(format_type::chunked); + resp.set_status_and_content(status_type::ok, "chunked ok"); + }); + + server.async_start(); + std::string uri = "http://127.0.0.1:9900/"; + std::string test_uri = "http://127.0.0.1:9900/test"; + std::string failed_uri = "http://127.0.0.1:9900/failed"; + + // loop ok-failed + auto lazy = [&]() -> async_simple::coro::Lazy { + coro_http_client client{}; + req_context<> ctx{}; + for (size_t i = 0; i < 6; i++) { + auto result = co_await client.async_request(uri, http_method::GET, ctx); + CHECK(result.status == 200); + result = co_await client.async_request(failed_uri, http_method::GET, ctx); + CHECK(result.status != 200); + } + }; + async_simple::coro::syncAwait(lazy()); + + // loop ok-failed with outbuf + auto lazy1 = [&]() -> async_simple::coro::Lazy { + coro_http_client client{}; + req_context<> ctx{}; + std::unordered_map headers; + for (size_t i = 0; i < 10; i++) { + std::vector buffer(10, 'a'); + auto result = co_await client.async_request(uri, http_method::GET, ctx, + headers, {buffer}); + CHECK(result.status == 200); + std::string_view body(buffer.data(), result.resp_body.size()); + CHECK(body == "ok"); + + if (i % 2 == 0) { + result = co_await client.async_request(failed_uri, http_method::GET, + ctx, headers, {buffer}); + } + else { + result = co_await client.async_request(failed_uri, http_method::GET, + ctx, headers); + } + + CHECK(result.status != 200); + + result = co_await client.async_request(test_uri, http_method::GET, ctx, + headers, {buffer}); + std::string_view body1(buffer.data(), result.resp_body.size()); + CHECK(body1 == "test"); + CHECK(result.status == 200); + } + }; + async_simple::coro::syncAwait(lazy1()); + + std::string filename = "mixed_file.txt"; + create_file2(filename, 20); + std::string upload_uri = "http://127.0.0.1:9900/upload"; + std::unordered_map headers; + + // loop request and sendfile ok-failed + auto lazy2 = [&]() -> async_simple::coro::Lazy { + coro_http_client client{}; + req_context<> ctx{}; + for (size_t i = 0; i < 6; i++) { + auto result = co_await client.async_request(uri, http_method::GET, ctx); + CHECK(result.status == 200); + result = + co_await client.async_upload(failed_uri, http_method::PUT, filename); + CHECK(result.status != 200); + result = + co_await client.async_upload(upload_uri, http_method::PUT, filename); + CHECK(result.status == 200); + } + }; + async_simple::coro::syncAwait(lazy2()); + + // loop request and sendfile ok-failed with outbuf + auto lazy3 = [&]() -> async_simple::coro::Lazy { + coro_http_client client{}; + req_context<> ctx{}; + for (size_t i = 0; i < 6; i++) { + std::vector buffer(10, 'a'); + auto result = co_await client.async_request(uri, http_method::GET, ctx); + CHECK(result.status == 200); + + if (i % 2 == 0) { + result = co_await client.async_request(uri, http_method::GET, ctx, + headers, {buffer}); + } + else { + result = + co_await client.async_request(uri, http_method::GET, ctx, headers); + } + CHECK(result.status == 200); + + if (i % 2 == 0) { + result = co_await client.async_request(failed_uri, http_method::GET, + ctx, headers, {buffer}); + CHECK(result.status != 200); + } + else { + result = + co_await client.async_request(uri, http_method::GET, ctx, headers); + CHECK(result.status == 200); + } + + result = + co_await client.async_upload(failed_uri, http_method::PUT, filename); + CHECK(result.status != 200); + result = + co_await client.async_upload(upload_uri, http_method::PUT, filename); + CHECK(result.status == 200); + } + }; + async_simple::coro::syncAwait(lazy3()); + + std::string chunked_uri = "http://127.0.0.1:9900/chunked"; + auto lazy4 = [&]() -> async_simple::coro::Lazy { + coro_http_client client{}; + req_context<> ctx{}; + for (size_t i = 0; i < 6; i++) { + std::vector buffer(10, 'a'); + auto result = co_await client.async_request(uri, http_method::GET, ctx); + CHECK(result.status == 200); + + if (i % 2 == 0) { + result = co_await client.async_request(uri, http_method::GET, ctx, + headers, {buffer}); + } + else { + result = + co_await client.async_request(uri, http_method::GET, ctx, headers); + } + CHECK(result.status == 200); + + if (i % 2 == 0) { + result = co_await client.async_request(failed_uri, http_method::GET, + ctx, headers, {buffer}); + CHECK(result.status != 200); + } + else { + result = + co_await client.async_request(uri, http_method::GET, ctx, headers); + CHECK(result.status == 200); + } + + result = co_await client.async_upload_chunked(failed_uri, + http_method::PUT, filename); + CHECK(result.status != 200); + client.reset(); + result = + co_await client.async_request(uri, http_method::GET, ctx, headers); + CHECK(result.status == 200); + result = co_await client.async_upload_chunked(chunked_uri, + http_method::PUT, filename); + CHECK(result.status == 200); + } + }; + async_simple::coro::syncAwait(lazy4()); + + std::filesystem::remove(filename); +} diff --git a/src/coro_rpc/tests/test_coro_rpc_client.cpp b/src/coro_rpc/tests/test_coro_rpc_client.cpp index 156dbbc5d..21412afed 100644 --- a/src/coro_rpc/tests/test_coro_rpc_client.cpp +++ b/src/coro_rpc/tests/test_coro_rpc_client.cpp @@ -480,14 +480,76 @@ TEST_CASE("testing client with shutdown") { g_action = {}; } TEST_CASE("testing client timeout") { - // SUBCASE("connect, 0ms timeout") { - // coro_rpc_client client; - // auto ret = client.connect("127.0.0.1", "8801", 0ms); - // auto val = syncAwait(ret); + coro_rpc_server server(2, 8801); + server.register_handler(); + auto res = server.async_start(); + CHECK_MESSAGE(!res.hasResult(), "server start timeout"); - // CHECK_MESSAGE(val == std::errc::timed_out, - // make_error_code(val).message()); - // } + SUBCASE("connect, 0ms connect timeout") { + coro_rpc_client client; + coro_rpc_client::config config{}; + config.connect_timeout_duration = 0ms; + bool r = client.init_config(config); + CHECK(r); + auto ret = client.connect( + "127.0.0.1", "8801", + 1000ms); // this arg won't update config connect timeout duration. + auto val = syncAwait(ret); + + if (val) { + CHECK_MESSAGE(val == coro_rpc::errc::timed_out, val.message()); + } + } + SUBCASE("connect, -1ms never timeout") { + coro_rpc_client client; + coro_rpc_client::config config{}; + config.connect_timeout_duration = -1ms; // less than 0, no timeout + // checking. + bool r = client.init_config(config); + CHECK(r); + auto ret = client.connect( + "127.0.0.1", "8801", + 1000ms); // this arg won't update config connect timeout duration. + auto val = syncAwait(ret); + + CHECK(!val); + } + + SUBCASE("connect, 0ms request timeout") { + coro_rpc_client client; + coro_rpc_client::config config{}; + config.connect_timeout_duration = 1000ms; + config.request_timeout_duration = 0ms; + bool r = client.init_config(config); + CHECK(r); + auto ret = client.connect( + "127.0.0.1", "8801", + 0ms); // 0ms won't cover config connect timeout duration. + auto val = syncAwait(ret); + + CHECK(!val); + auto result = syncAwait(client.call()); + + if (result.has_value()) { + std::cout << result.value() << std::endl; + } + else { + CHECK_MESSAGE(result.error().code == coro_rpc::errc::timed_out, + result.error().msg); + } + } + SUBCASE("connect, -1ms never request timeout") { + coro_rpc_client client; + client.get_config().request_timeout_duration = + -1ms; // less than 0, never timeout. + auto ret = client.connect("127.0.0.1", "8801"); + auto val = syncAwait(ret); + + CHECK(!val); + auto result = syncAwait(client.call()); + + CHECK(result.has_value()); + } SUBCASE("connect, ip timeout") { g_action = {}; // https://stackoverflow.com/questions/100841/artificially-create-a-connection-timeout-error diff --git a/src/metric/benchmark/bench.hpp b/src/metric/benchmark/bench.hpp new file mode 100644 index 000000000..b464df58d --- /dev/null +++ b/src/metric/benchmark/bench.hpp @@ -0,0 +1,303 @@ +#include +#include + +#include "ylt/metric.hpp" +#include "ylt/metric/counter.hpp" +#include "ylt/metric/summary.hpp" + +using namespace std::chrono_literals; +using namespace ylt::metric; + +inline auto get_random(size_t range = 10000) { + thread_local std::default_random_engine gen(std::time(nullptr)); + std::uniform_int_distribution<> distr(1, range); + return distr(gen); +} + +struct bench_clock_t { + bench_clock_t() : start_(std::chrono::steady_clock::now()) {} + template + unit duration() { + auto now = std::chrono::steady_clock::now(); + auto ret = now - start_; + return std::chrono::duration_cast(ret); + } + std::chrono::steady_clock::time_point start_; +}; + +template +void bench_mixed_impl(IMPL& impl, WRITE_OP&& op, size_t thd_num, + std::chrono::seconds duration) { + ylt::metric::summary_t lantency_summary( + "write latency(ms)", "", {0.99, 0.999, 0.9999, 0.99999, 0.999999, 1.0}); + std::atomic stop = false; + std::vector vec; + std::array arr{"/test", "200"}; + bench_clock_t clock; + std::string val(36, ' '); + for (size_t i = 0; i < thd_num; i++) { + vec.push_back(std::thread([&, i] { + bench_clock_t clock_loop; + auto dur = clock.duration(); + while (!stop && dur < duration + 1s) { + op(); + auto new_dur = clock.duration(); + lantency_summary.observe((new_dur - dur).count() / 1000.0f); + dur = new_dur; + } + })); + } + std::string s; + + bench_clock_t clock2; + int64_t serialze_cnt = 0; + do { + s.clear(); + impl.serialize(s); + ++serialze_cnt; + } while (clock2.duration() < duration); + auto total_ms = clock.duration(); + stop = true; + if constexpr (requires { impl.size(); }) { + std::cout << "size:" << impl.size() << "\n"; + } + + std::cout << "run " << total_ms.count() << "ms\n"; + uint64_t cnt; + double sum; + auto result = lantency_summary.get_rates(sum, cnt); + auto seconds = total_ms.count() / 1000.0; + auto qps = 1.0 * cnt / seconds; + std::cout << "write thd num: " << thd_num << ", write qps: " << (int64_t)qps + << "\n"; + std::cout << "serialize qps:" << 1000.0 * serialze_cnt / total_ms.count() + << ", str size=" << s.size() << "\n"; + s = ""; + lantency_summary.serialize(s); + std::cout << s; + for (auto& thd : vec) { + thd.join(); + } +} + +inline void bench_static_summary_mixed(size_t thd_num, + std::chrono::seconds duration, + std::chrono::seconds age = 1s) { + ylt::metric::summary_t summary("summary mixed test", "", + {0.5, 0.9, 0.95, 0.99, 0.995}, age); + bench_mixed_impl( + summary, + [&]() { + summary.observe(get_random(100)); + }, + thd_num, duration); +} + +inline void bench_static_counter_mixed(size_t thd_num, + std::chrono::seconds duration) { + ylt::metric::counter_t counter("counter mixed test", ""); + bench_mixed_impl( + counter, + [&]() { + counter.inc(1); + }, + thd_num, duration); +} + +inline void bench_dynamic_summary_mixed(size_t thd_num, + std::chrono::seconds duration, + std::chrono::seconds age = 1s, + int max_cnt = 1000000) { + ylt::metric::dynamic_summary summary("dynamic summary mixed test", "", + {0.5, 0.9, 0.95, 0.99, 0.995}, + {"a", "b"}, age); + bench_mixed_impl( + summary, + [&]() mutable { + summary.observe({"123e4567-e89b-12d3-a456-426614174000", + std::to_string(get_random(max_cnt))}, + get_random(100)); + }, + thd_num, duration); +} + +inline void bench_dynamic_counter_mixed_with_delete( + size_t thd_num, std::chrono::seconds duration, + std::chrono::seconds age = 1s, int max_cnt = 1000000) { + ylt::metric::dynamic_counter_2d counter("dynamic summary mixed test", "", + {"a", "b"}); + bench_mixed_impl( + counter, + [&, i = 0]() mutable { + ++i; + std::array label = { + "123e4567-e89b-12d3-a456-426614174000", + std::to_string(get_random(max_cnt))}; + counter.inc(label, 1); + counter.remove_label_value({{"a", label[0]}, {"b", label[1]}}); + }, + thd_num, duration); +} + +inline void bench_dynamic_counter_mixed(size_t thd_num, + std::chrono::seconds duration, + std::chrono::seconds age = 1s, + int max_cnt = 1000000) { + ylt::metric::dynamic_counter_2d counter("dynamic summary mixed test", "", + {"a", "b"}); + bench_mixed_impl( + counter, + [&, i = 0]() mutable { + ++i; + counter.inc({"123e4567-e89b-12d3-a456-426614174000", + std::to_string(get_random(max_cnt))}, + 1); + }, + thd_num, duration); +} + +template +void bench_serialize_impl(IMPL& impl, OP&& op, size_t COUNT, bool to_json) { + for (size_t i = 0; i < COUNT; i++) { + op(i); + } + std::string str; + bench_clock_t clock; + if (to_json) { + if constexpr (requires { impl[0]; }) { + str = manager_helper::serialize_to_json(impl); + } + else { + impl.serialize_to_json(str); + } + } + else { + if constexpr (requires { impl[0]; }) { + str = manager_helper::serialize(impl); + } + else { + impl.serialize(str); + } + } + std::cout << "COUNT:" << COUNT << ", string size: " << str.size() << ", " + << clock.duration().count() << "ms\n"; + if (str.size() < 1000) { + std::cout << str << std::endl; + } +} + +inline void bench_many_metric_serialize(size_t COUNT, size_t LABEL_COUNT, + bool to_json = false) { + std::vector> vec; + bench_serialize_impl( + vec, + [LABEL_COUNT, &vec](int) { + auto counter = std::make_shared( + std::string("qps"), "", std::array{"url", "code"}); + for (size_t j = 0; j < LABEL_COUNT; j++) { + counter->inc({"test_label_value", std::to_string(j)}); + } + vec.push_back(counter); + }, + COUNT, to_json); +} + +inline void bench_dynamic_counter_serialize(size_t COUNT, + bool to_json = false) { + dynamic_counter_t counter("qps2", "", {"url", "code"}); + bench_serialize_impl( + counter, + [&](int i) { + counter.inc( + {"123e4567-e89b-12d3-a456-426614174000", std::to_string(i)}); + }, + COUNT, to_json); +} + +inline void bench_dynamic_summary_serialize(size_t COUNT, + bool to_json = false) { + dynamic_summary_2 summary("qps2", "", {0.5, 0.9, 0.95, 0.995}, + std::array{"method", "url"}); + bench_serialize_impl( + summary, + [&](int i) { + summary.observe( + {"123e4567-e89b-12d3-a456-426614174000", std::to_string(i)}, i); + }, + COUNT, to_json); +} + +template +void bench_write_impl(IMPL& impl, OP&& op, size_t thd_num, + std::chrono::seconds duration) { + std::atomic stop = false; + std::vector> vec; + std::array arr{"/test", "200"}; + thread_local_value local_val(thd_num); + bench_clock_t clock; + for (size_t i = 0; i < thd_num; i++) { + vec.push_back(std::async([&] { + int64_t cnt = 0; + while (!stop) { + op(); + ++cnt; + } + return cnt; + })); + } + std::this_thread::sleep_for(duration); + stop = true; + std::cout << "run " << clock.duration().count() << "ms\n"; + double qps = 0; + for (auto& thd : vec) { + qps += thd.get(); + } + qps /= (clock.duration().count() / 1000.0); + std::cout << "thd num: " << thd_num << ", qps: " << (int64_t)qps << "\n"; +} + +inline void bench_static_counter_write(size_t thd_num, + std::chrono::seconds duration) { + counter_t counter("qps", ""); + bench_write_impl( + counter, + [&] { + counter.inc(1); + }, + thd_num, duration); +} + +inline void bench_dynamic_counter_write(size_t thd_num, + std::chrono::seconds duration) { + dynamic_counter_t counter("qps2", "", {"url", "code"}); + bench_write_impl( + counter, + [&] { + counter.inc({"/test", std::to_string(get_random())}, 1); + }, + thd_num, duration); +} + +inline void bench_dynamic_summary_write(size_t thd_num, + std::chrono::seconds duration) { + dynamic_summary_2 summary("qps2", "", {0.5, 0.9, 0.95, 0.99}, + std::array{"method", "url"}); + bench_write_impl( + summary, + [&]() { + summary.observe({"/test", std::to_string(get_random())}, + get_random(100)); + }, + thd_num, duration); +} + +inline void bench_static_summary_write(size_t thd_num, + std::chrono::seconds duration) { + ylt::metric::summary_t summary("qps2", "", {0.5, 0.9, 0.95, 0.99}); + bench_write_impl( + summary, + [&]() { + summary.observe(get_random(100)); + }, + thd_num, duration); +} \ No newline at end of file diff --git a/src/metric/benchmark/main.cpp b/src/metric/benchmark/main.cpp index 287efed0e..1ad87f2a1 100644 --- a/src/metric/benchmark/main.cpp +++ b/src/metric/benchmark/main.cpp @@ -1,255 +1,55 @@ -#include -#include -#include -#include -#include -#include - -#include "ylt/metric.hpp" - -using namespace std::chrono_literals; -using namespace ylt::metric; - -void bench_static_counter_qps(size_t thd_num, std::chrono::seconds duration, - size_t dupli_count = 2) { - counter_t counter("qps", "", dupli_count); - std::vector vec; - std::atomic stop = false; - auto start = std::chrono::system_clock::now(); - for (size_t i = 0; i < thd_num; i++) { - vec.push_back(std::thread([&] { - while (!stop) { - counter.inc(1); - } - })); - } - - std::this_thread::sleep_for(duration); - stop = true; - auto end = std::chrono::system_clock::now(); - - auto elaps = - std::chrono::duration_cast(end - start) - .count(); - - double seconds = double(elaps) / 1000; - - auto qps = counter.value() / seconds; - std::cout << "duplicate count: " << dupli_count << ", thd num: " << thd_num - << ", qps: " << (uint64_t)qps << "\n"; - - for (auto& thd : vec) { - thd.join(); - } -} - -auto get_random(size_t range = 10000) { - thread_local std::default_random_engine gen(std::time(nullptr)); - std::uniform_int_distribution<> distr(1, range); - return distr(gen); -} - -void bench_dynamic_counter_qps(size_t thd_num, std::chrono::seconds duration, - size_t dupli_count = 2) { - dynamic_counter_t counter("qps2", "", {"url", "code"}, dupli_count); - std::atomic stop = false; - std::vector vec; - std::array arr{"/test", "200"}; - auto start = std::chrono::system_clock::now(); - for (size_t i = 0; i < thd_num; i++) { - vec.push_back(std::thread([&, i] { - while (!stop) { - counter.inc({"/test", std::to_string(get_random())}, 1); - } - })); - } - std::this_thread::sleep_for(duration); - stop = true; - auto end = std::chrono::system_clock::now(); - - auto elaps = - std::chrono::duration_cast(end - start) - .count(); - - double seconds = double(elaps) / 1000; - std::cout << "run " << elaps << "ms, " << seconds << " seconds\n"; - - size_t total = 0; - for (size_t i = 0; i < 10000; i++) { - total += counter.value({"/test", std::to_string(i)}); - } - auto qps = total / seconds; - std::cout << "duplicate count: " << dupli_count << ", thd num: " << thd_num - << ", qps: " << (int64_t)qps << "\n"; - for (auto& thd : vec) { - thd.join(); - } -} - -void bench_many_metric_serialize(size_t COUNT, size_t LABEL_COUNT, - bool to_json = false) { - std::vector> vec; - for (size_t i = 0; i < COUNT; i++) { - auto counter = std::make_shared( - std::string("qps"), "", std::array{"url", "code"}); - for (size_t j = 0; j < LABEL_COUNT; j++) { - counter->inc({"test_label_value", std::to_string(j)}); - } - vec.push_back(counter); - } - - std::cout << "begin test\n"; - - std::string str; - - auto start = std::chrono::system_clock::now(); - if (to_json) { - str = manager_helper::serialize_to_json(vec); - } - else { - str = manager_helper::serialize(vec); - } - - auto end = std::chrono::system_clock::now(); - auto elaps = - std::chrono::duration_cast(end - start) - .count(); - std::cout << "string size: " << str.size() << ", " << elaps << "ms\n"; -} - -void bench_many_labels_serialize(size_t COUNT, bool to_json = false) { - dynamic_counter_t counter("qps2", "", {"url", "code"}); - std::string val(36, ' '); - for (size_t i = 0; i < COUNT; i++) { - strcpy(val.data(), std::to_string(i).data()); - counter.inc({"123e4567-e89b-12d3-a456-426614174000", val}); - } - - std::string str; - auto start = std::chrono::system_clock::now(); - if (to_json) { - counter.serialize_to_json(str); - } - else { - counter.serialize(str); - } - - auto end = std::chrono::system_clock::now(); - auto elaps = - std::chrono::duration_cast(end - start) - .count(); - std::cout << elaps << "ms\n"; - std::cout << "label value count: " << counter.label_value_count() - << " string size: " << str.size() << "\n"; -} - -void bench_many_labels_qps_summary(size_t thd_num, - std::chrono::seconds duration) { - dynamic_summary_2 summary("qps2", "", {0.5, 0.9, 0.95, 0.99}, - std::array{"method", "url"}); - std::atomic stop = false; - std::vector vec; - std::array arr{"/test", "200"}; - thread_local_value local_val(thd_num); - auto start = std::chrono::system_clock::now(); - std::string val(36, ' '); - for (size_t i = 0; i < thd_num; i++) { - vec.push_back(std::thread([&, i] { - while (!stop) { - strcpy(val.data(), std::to_string(i).data()); - summary.observe({"/test", std::to_string(get_random())}, - get_random(100)); - local_val.inc(); - } - })); - } - std::this_thread::sleep_for(duration); - stop = true; - auto end = std::chrono::system_clock::now(); - - auto elaps = - std::chrono::duration_cast(end - start) - .count(); - - double seconds = double(elaps) / 1000; - std::cout << "run " << elaps << "ms, " << seconds << " seconds\n"; - - auto qps = local_val.value() / seconds; - std::cout << "thd num: " << thd_num << ", qps: " << (int64_t)qps << "\n"; - for (auto& thd : vec) { - thd.join(); - } -} - -void bench_many_labels_serialize_summary(size_t COUNT, bool to_json = false) { - dynamic_summary_2 summary("qps2", "", {0.5, 0.9, 0.95, 0.005}, - std::array{"method", "url"}); - std::string val(36, ' '); - for (size_t i = 0; i < COUNT; i++) { - strcpy(val.data(), std::to_string(i).data()); - summary.observe({"123e4567-e89b-12d3-a456-426614174000", val}, - get_random(100)); - } - - std::string str; - auto start = std::chrono::system_clock::now(); - if (to_json) { - summary.serialize_to_json(str); - } - else { - summary.serialize(str); - } - - auto end = std::chrono::system_clock::now(); - auto elaps = - std::chrono::duration_cast(end - start) - .count(); - std::cout << elaps << "ms\n"; - std::cout << "label value count: " << summary.label_value_count() - << " string size: " << str.size() << "\n"; -} +#include "bench.hpp" int main() { - bench_many_labels_serialize_summary(100000); - bench_many_labels_serialize_summary(1000000); + std::cout << "start serialize bench" << std::endl; - bench_many_labels_serialize_summary(100000, true); - bench_many_labels_serialize_summary(1000000, true); + std::cout << "\ndynamic summary serialize:" << std::endl; + bench_dynamic_summary_serialize(100000); - bench_many_labels_qps_summary(1, 5s); - bench_many_labels_qps_summary(2, 5s); - bench_many_labels_qps_summary(8, 5s); - bench_many_labels_qps_summary(16, 5s); - bench_many_labels_qps_summary(32, 5s); - bench_many_labels_qps_summary(96, 5s); + std::cout << "\ndynamic summary serialize(json):" << std::endl; + bench_dynamic_summary_serialize(100000, true); - bench_many_labels_serialize(100000); - bench_many_labels_serialize(1000000); - bench_many_labels_serialize(10000000); - bench_many_labels_serialize(100000, true); - bench_many_labels_serialize(1000000, true); - bench_many_labels_serialize(10000000, true); + std::cout << "\ndynamic counter with many labels serialize:" << std::endl; + bench_dynamic_counter_serialize(100000); + std::cout << "\ndynamic counter with many labels serialize(json):" + << std::endl; + bench_dynamic_counter_serialize(100000, true); + + std::cout << "\nmulti dynamic counter serialize:" << std::endl; bench_many_metric_serialize(100000, 10); - bench_many_metric_serialize(1000000, 10); + std::cout << "\nmulti dynamic counter serialize(json):" << std::endl; bench_many_metric_serialize(100000, 10, true); - bench_many_metric_serialize(1000000, 10, true); - - bench_static_counter_qps(1, 5s); - bench_static_counter_qps(2, 5s); - bench_static_counter_qps(8, 5s); - bench_static_counter_qps(16, 5s); - bench_static_counter_qps(32, 5s); - bench_static_counter_qps(96, 5s); - bench_static_counter_qps(32, 5s, 32); - bench_static_counter_qps(96, 5s, 96); - bench_dynamic_counter_qps(1, 5s); - bench_dynamic_counter_qps(2, 5s); - bench_dynamic_counter_qps(8, 5s); - bench_dynamic_counter_qps(16, 5s); - bench_dynamic_counter_qps(32, 5s); - bench_dynamic_counter_qps(96, 10s); - bench_dynamic_counter_qps(32, 5s, 32); - bench_dynamic_counter_qps(96, 5s, 96); + std::cout << "\nstart write bench" << std::endl; + + std::cout << "\nstatic summary performance test:" << std::endl; + bench_static_summary_write(1, 5s); + bench_static_summary_write(std::thread::hardware_concurrency(), 5s); + + std::cout << "\ndynamic summary performance test:" << std::endl; + bench_dynamic_summary_write(1, 5s); + bench_dynamic_summary_write(std::thread::hardware_concurrency(), 5s); + + std::cout << "\nstatic counter performance test:" << std::endl; + bench_static_counter_write(1, 5s); + bench_static_counter_write(std::thread::hardware_concurrency(), 5s); + + std::cout << "\ndynamic counter performance test:" << std::endl; + bench_dynamic_counter_write(1, 5s); + bench_dynamic_counter_write(std::thread::hardware_concurrency(), 5s); + + std::cout << "\nstart write/seriailize mixed bench" << std::endl; + std::cout << "\nstatic summary mixed test:" << std::endl; + bench_static_summary_mixed(1, 5s); + bench_static_summary_mixed(std::thread::hardware_concurrency(), 5s); + std::cout << "\nstatic counter mixed test:" << std::endl; + bench_static_counter_mixed(1, 5s); + bench_static_counter_mixed(std::thread::hardware_concurrency(), 5s); + std::cout << "\ndynamic summary mixed test:" << std::endl; + bench_dynamic_summary_mixed(1, 5s); + bench_dynamic_summary_mixed(std::thread::hardware_concurrency(), 5s); + std::cout << "\ndynamic counter mixed test:" << std::endl; + bench_dynamic_counter_mixed(1, 5s); + bench_dynamic_counter_mixed(std::thread::hardware_concurrency(), 5s); } diff --git a/src/metric/tests/test_metric.cpp b/src/metric/tests/test_metric.cpp index cfa400c24..40b5bed21 100644 --- a/src/metric/tests/test_metric.cpp +++ b/src/metric/tests/test_metric.cpp @@ -15,40 +15,86 @@ struct metrc_tag {}; struct test_tag {}; TEST_CASE("serialize zero") { + { + std::string str; + counter_t c("test", ""); + c.inc(0); + c.serialize_to_json(str); + CHECK(!str.empty()); + str.clear(); + gauge_t g("test1", ""); + g.dec(0); + g.serialize_to_json(str); + CHECK(!str.empty()); + } + { + std::string str; + counter_t c("test", ""); + c.inc(-1); + c.serialize_to_json(str); + CHECK(str.empty()); + c.inc(0); + c.serialize_to_json(str); + CHECK(!str.empty()); + } + { + std::string str; + counter_t c("test", ""); + c.inc(2); + c.update(0); // now zero + c.serialize_to_json(str); + CHECK(!str.empty()); + str.clear(); + gauge_t g("test1", ""); + g.inc(2); + g.dec(2); // now zero + g.serialize_to_json(str); + CHECK(!str.empty()); + } + counter_t c("test", ""); gauge_t g("test1", ""); std::string str; - c.serialize(str); + c.serialize_to_json(str); CHECK(str.empty()); - g.serialize(str); + g.serialize_to_json(str); CHECK(str.empty()); c.inc(); - c.serialize(str); + c.serialize_to_json(str); CHECK(!str.empty()); str.clear(); g.inc(); - g.serialize(str); + g.serialize_to_json(str); CHECK(!str.empty()); + str.clear(); c.update(0); - c.serialize(str); + c.serialize_to_json(str); CHECK(!str.empty()); str.clear(); + // dec to zero g.dec(); - g.serialize(str); + g.serialize_to_json(str); CHECK(!str.empty()); str.clear(); dynamic_counter_1t c1("test", "", {"url"}); c1.serialize(str); CHECK(str.empty()); + c1.update({"/test"}, 0); + c1.serialize_to_json(str); + CHECK(!str.empty()); + str.clear(); + dynamic_gauge_1t g1("test", "", {"url"}); g1.serialize(str); CHECK(str.empty()); c1.inc({"/test"}); - c1.serialize(str); + c1.update({"/test"}, 0); + c1.serialize_to_json(str); CHECK(!str.empty()); str.clear(); g1.inc({"/test"}); + g1.dec({"/test"}); g1.serialize(str); CHECK(!str.empty()); str.clear(); @@ -109,6 +155,9 @@ TEST_CASE("test metric manager") { auto v2 = inst_s.get_metric_by_name("test1"); CHECK(v2 != nullptr); + v2 = inst_s.get_metric_by_name("test111"); + CHECK(v2 == nullptr); + c->inc(); g->inc(); @@ -138,6 +187,7 @@ TEST_CASE("test metric manager") { auto dc = std::make_shared( std::string("test3"), std::string(""), std::array{"url", "code"}); + dc->remove_label_value({{"url", "/"}, {"code", "200"}}); dynamic_metric_manager::instance().register_metric(dc); auto& inst_d = dynamic_metric_manager::instance(); auto pair1 = inst_d.create_metric_dynamic( @@ -237,6 +287,68 @@ TEST_CASE("test metric manager") { CHECK(v8.size() == 1); } +struct remove_tag {}; + +TEST_CASE("test metric manager remove") { + auto dc = std::make_shared( + std::string("test3"), std::string(""), + std::array{"url", "code"}); + dc->inc(std::array{"/", "200"}); + dynamic_metric_manager::instance().register_metric(dc); + auto& inst_d = dynamic_metric_manager::instance(); + std::map map{ + {"url", "/"}, {"code", "200"}, {"method", "GET"}}; + CHECK(inst_d.metric_count() == 1); + inst_d.remove_metric_by_label(map); + CHECK(inst_d.metric_count() == 1); + + std::vector> filtered_metrics; + metric_filter_options options; + options.name_regex = ".*counter.*"; + manager_helper::filter_by_label_name(filtered_metrics, dc, options); + CHECK(filtered_metrics.empty()); + + options.label_regex = ".*counter.*"; + manager_helper::filter_by_label_name(filtered_metrics, dc, options); + CHECK(filtered_metrics.empty()); + + options.label_regex = "url"; + manager_helper::filter_by_label_name(filtered_metrics, dc, options); + CHECK(filtered_metrics.size() == 1); + filtered_metrics.clear(); + options.label_regex = "code"; + manager_helper::filter_by_label_name(filtered_metrics, dc, options); + CHECK(filtered_metrics.size() == 1); + + std::map map1{{"url", "/test"}, {"code", "200"}}; + inst_d.remove_metric_by_label(map1); + CHECK(inst_d.metric_count() == 1); + std::map map2{{"url", "/"}, {"code", "400"}}; + inst_d.remove_metric_by_label(map2); + CHECK(inst_d.metric_count() == 1); + std::map map3{{"url1", "/"}, {"code", "200"}}; + inst_d.remove_metric_by_label(map3); + CHECK(inst_d.metric_count() == 1); + std::map map4{{"url", "/"}, {"code", "200"}}; + inst_d.remove_metric_by_label(map4); + CHECK(inst_d.metric_count() == 0); + + inst_d.register_metric(dc); + CHECK(inst_d.metric_count() == 1); + + std::map map5{{"url", "/get"}}; + inst_d.remove_metric_by_label(map5); + CHECK(inst_d.metric_count() == 1); + + std::map map6{{"url1", "/"}}; + inst_d.remove_metric_by_label(map6); + CHECK(inst_d.metric_count() == 1); + + std::map map7{{"url", "/"}}; + inst_d.remove_metric_by_label(map7); + CHECK(inst_d.metric_count() == 0); +} + TEST_CASE("test dynamic counter") { basic_dynamic_counter c("test", "", {"url", "code"}); c.inc({"/", "200"}); @@ -287,6 +399,14 @@ TEST_CASE("test dynamic counter") { dynamic_gauge_t g2("test_g2", "", {"url", "code"}); g2.inc({"/", "200"}); CHECK(g2.value({"/", "200"}) == 1); + + auto count = g2.label_value_count(); + g2.remove_label_value({{"url", "/"}, {"code", "200"}, {"method", "GET"}}); + auto count1 = g2.label_value_count(); + CHECK(count == count1); + g2.remove_label_value({{"url1", "/"}, {"code1", "200"}}); + count1 = g2.label_value_count(); + CHECK(count == count1); } TEST_CASE("test static counter") { @@ -459,6 +579,39 @@ TEST_CASE("test no label") { for (auto& e : result) { CHECK(e == 100); } + + double sum{}; + result = summary->get_rates(sum); + CHECK(sum == 100); + + uint64_t count{}; + result = summary->get_rates(count); + CHECK(count == 1); + } + + { + std::map customMap = {}; + auto summary = std::make_shared( + "test", "help", std::vector{0.95, 0.5, 0.99, 0.9}, customMap); + summary->observe(100); + auto result = summary->get_rates(); + for (auto& e : result) { + CHECK(e == 100); + } + } + { + std::map customMap = {}; + auto summary = std::make_shared( + "test", "help", std::vector{}, customMap); + std::string str; + summary->serialize(str); + CHECK(str.empty()); + +#ifdef CINATRA_ENABLE_METRIC_JSON + std::string str_json; + summary->serialize_to_json(str_json); + CHECK(str_json.empty()); +#endif } auto g_counter = g_pair.second; g_counter->inc(); @@ -490,9 +643,25 @@ TEST_CASE("test no label") { CHECK(r == 1); CHECK(g.value() == 10); } + { + metric_t m{}; + auto name = m.metric_name(); + CHECK(name == "unknown"); + + m.clean_expired_label(); + + std::string str; + m.serialize(str); + CHECK(str.empty()); +#ifdef CINATRA_ENABLE_METRIC_JSON + m.serialize_to_json(str); + CHECK(str.empty()); +#endif + } { counter_t c("get_count", "get counter"); CHECK(c.metric_type() == MetricType::Counter); + CHECK(c.help() == "get counter"); CHECK(c.labels_name().empty()); c.inc(); CHECK(c.value() == 1); @@ -521,6 +690,21 @@ TEST_CASE("test with atomic") { c.update(10); CHECK(c.value() == 10); + bool r = c.has_label_value("GET"); + CHECK(r); + r = c.has_label_value("POST"); + CHECK(!r); + + r = c.has_label_value(std::vector{"GET", "/"}); + CHECK(r); + r = c.has_label_value(std::vector{"GET"}); + CHECK(!r); + + c.remove_label_value( + std::map{{"method", "GET"}, {"url", "/"}}); + r = c.has_label_value(std::vector{"GET", "/"}); + CHECK(r); + gauge_t g( "get_qps", "get qps", std::map{{"method", "GET"}, {"url", "/"}}); @@ -575,11 +759,9 @@ TEST_CASE("test counter with dynamic labels value") { std::array{"method", "code"}); CHECK(c.labels_name() == std::vector{"method", "code"}); c.inc({"GET", "200"}, 1); - auto values = c.value_map(); - CHECK(values[{"GET", "200"}].value() == 1); + CHECK(c.value({"GET", "200"}) == 1); c.inc({"GET", "200"}, 2); - values = c.value_map(); - CHECK(values[{"GET", "200"}].value() == 3); + CHECK(c.value({"GET", "200"}) == 3); std::string str; c.serialize(str); @@ -590,8 +772,7 @@ TEST_CASE("test counter with dynamic labels value") { c.update({"GET", "200"}, 20); std::this_thread::sleep_for(std::chrono::milliseconds(10)); - values = c.value_map(); - CHECK(values[{"GET", "200"}].value() == 20); + CHECK(c.value({"GET", "200"}) == 20); } } @@ -624,11 +805,9 @@ TEST_CASE("test gauge") { CHECK(g.labels_name() == std::vector{"method", "code", "url"}); // method, status code, url g.inc({"GET", "200", "/"}, 1); - auto values = g.value_map(); - CHECK(values[{"GET", "200", "/"}].value() == 1); + CHECK(g.value({"GET", "200", "/"}) == 1); g.inc({"GET", "200", "/"}, 2); - values = g.value_map(); - CHECK(values[{"GET", "200", "/"}].value() == 3); + CHECK(g.value({"GET", "200", "/"}) == 3); g.inc({"POST", "200", "/"}, 4); @@ -636,7 +815,8 @@ TEST_CASE("test gauge") { std::string str_json; g.serialize_to_json(str_json); std::cout << str_json << "\n"; - CHECK(str_json.find("\"code\":\"200\"") != std::string::npos); + std::cout << str_json.size() << std::endl; + CHECK(str_json.size() == 185); #endif std::string str; @@ -647,11 +827,9 @@ TEST_CASE("test gauge") { std::string::npos); g.dec({"GET", "200", "/"}, 1); - values = g.value_map(); - CHECK(values[{"GET", "200", "/"}].value() == 2); + CHECK(g.value({"GET", "200", "/"}) == 2); g.dec({"GET", "200", "/"}, 2); - values = g.value_map(); - CHECK(values[{"GET", "200", "/"}].value() == 0); + CHECK(g.value({"GET", "200", "/"}) == 0); } } @@ -709,7 +887,6 @@ TEST_CASE("test summary") { std::string str_json; summary.serialize_to_json(str_json); std::cout << str_json << "\n"; - CHECK(str_json.find("\"0.9\":") != std::string::npos); #endif } @@ -738,7 +915,8 @@ TEST_CASE("test summary with INF") { std::string str_json; summary.serialize_to_json(str_json); std::cout << str_json << "\n"; - CHECK(str_json.find("\"0.9\":") != std::string::npos); + std::cout << str_json.size() << std::endl; + CHECK(str_json.size() == 238); #endif } @@ -767,7 +945,8 @@ TEST_CASE("test summary with NAN") { std::string str_json; summary.serialize_to_json(str_json); std::cout << str_json << "\n"; - CHECK(str_json.find("\"0.9\":") != std::string::npos); + std::cout << str_json.size() << std::endl; + CHECK(str_json.size() == 238); #endif } @@ -793,14 +972,15 @@ TEST_CASE("test summary with illegal quantities") { CHECK(str.find("test_summary_sum") != std::string::npos); CHECK(str.find("test_summary{quantile=\"") != std::string::npos); CHECK(result[0] < 0); - CHECK(result[1] < 0); + CHECK(result[1] == 0); CHECK(result[result.size() - 1] > result[result.size() - 2]); #ifdef CINATRA_ENABLE_METRIC_JSON std::string str_json; summary.serialize_to_json(str_json); std::cout << str_json << "\n"; - CHECK(str_json.find("\"0.9\":") != std::string::npos); + std::cout << str_json.size() << std::endl; + CHECK(str_json.size() == 222); #endif } @@ -818,7 +998,7 @@ TEST_CASE("test summary with many quantities") { } std::string str; summary.serialize(str); - std::cout << str; + // std::cout << str; double sum; uint64_t cnt; auto result = summary.get_rates(sum, cnt); @@ -835,10 +1015,43 @@ TEST_CASE("test summary with many quantities") { std::string str_json; summary.serialize_to_json(str_json); std::cout << str_json << "\n"; - CHECK(str_json.find("\"0.9\":") != std::string::npos); + std::cout << str_json.size() << std::endl; + CHECK(str_json.size() == 8857); #endif } +TEST_CASE("test summary refresh") { + summary_t summary{"test_summary", "summary help", {0.5, 0.9, 0.95, 1.1}, 1s}; + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution<> distr(1, 100); + for (int i = 0; i < 50; i++) { + summary.observe(i); + } + double sum; + uint64_t cnt; + summary.get_rates(sum, cnt); + CHECK(cnt == 50); + std::this_thread::sleep_for(1s); + summary.get_rates(sum, cnt); + CHECK(cnt == 0); + for (int i = 0; i < 50; i++) { + summary.observe(i); + } + std::this_thread::sleep_for(500ms); + for (int i = 0; i < 10; i++) { + summary.observe(i); + } + summary.get_rates(sum, cnt); + CHECK(cnt == 60); + std::this_thread::sleep_for(500ms); + summary.get_rates(sum, cnt); + CHECK(cnt == 10); + std::this_thread::sleep_for(500ms); + summary.get_rates(sum, cnt); + CHECK(cnt == 0); +} + TEST_CASE("test register metric") { auto c = std::make_shared(std::string("get_count"), std::string("get counter")); @@ -878,6 +1091,11 @@ TEST_CASE("test register metric") { default_static_metric_manager::instance().get_metric_static( "get_guage_count"); CHECK(m1->as()->value() == 1); + + auto m2 = + default_static_metric_manager::instance().get_metric_static( + "not_exist"); + CHECK(m2 == nullptr); } template @@ -898,6 +1116,20 @@ TEST_CASE("test remove metric and serialize metrics") { count = metric_mgr::instance().metric_count(); CHECK(count == 1); + metric_mgr::instance().remove_metric(std::vector{}); + count = metric_mgr::instance().metric_count(); + CHECK(count == 1); + + std::shared_ptr ptr = nullptr; + metric_mgr::instance().remove_metric(ptr); + count = metric_mgr::instance().metric_count(); + CHECK(count == 1); + + metric_mgr::instance().remove_metric( + std::vector>{}); + count = metric_mgr::instance().metric_count(); + CHECK(count == 1); + metric_mgr::instance().remove_metric("test_counter2"); count = metric_mgr::instance().metric_count(); CHECK(count == 0); @@ -966,6 +1198,19 @@ TEST_CASE("test filter metrics static") { CHECK(s.empty()); } + { +#ifdef CINATRA_ENABLE_METRIC_JSON + std::vector> vec{}; + auto s = manager_helper::serialize_to_json(vec); + CHECK(s == "[]"); + auto c = std::make_shared(std::string("get_count"), + std::string("get counter")); + vec.push_back(c); + s = manager_helper::serialize_to_json(vec); + CHECK(s == "[]"); +#endif + } + // don't filter options = {}; { @@ -1331,6 +1576,18 @@ TEST_CASE("test summary with dynamic labels") { summary.serialize_to_json(json_str); std::cout << json_str << "\n"; #endif + + { + basic_dynamic_summary<2> summary{"test_summary", + "summary help", + {0.9, 0.5, 0.95, 0.99}, + {"method", "url"}}; +#ifdef CINATRA_ENABLE_METRIC_JSON + std::string json_str1; + summary.serialize_to_json(json_str1); + CHECK(json_str1.empty()); +#endif + } } TEST_CASE("test summary with static labels") { @@ -1375,6 +1632,15 @@ TEST_CASE("test serialize with emptry metrics") { std::array{"method"}); h1->serialize(s1); CHECK(s1.empty()); + +#ifdef CINATRA_ENABLE_METRIC_JSON + h1->serialize_to_json(s1); + CHECK(s1.empty()); +#endif + + h1->observe({"GET"}, 0); + h1->serialize(s1); + CHECK(s1.empty()); #ifdef CINATRA_ENABLE_METRIC_JSON h1->serialize_to_json(s1); CHECK(s1.empty()); @@ -1432,6 +1698,14 @@ TEST_CASE("test serialize with emptry metrics") { { std::string str; h1->observe({"POST"}, 1); + bool r = h1->has_label_value("POST"); + CHECK(r); + r = h1->has_label_value("HEAD"); + CHECK(!r); + r = h1->has_label_value(std::vector{"POST"}); + CHECK(r); + r = h1->has_label_value(std::vector{"HEAD"}); + CHECK(!r); h1->serialize(str); CHECK(!str.empty()); str.clear(); @@ -1572,9 +1846,9 @@ TEST_CASE("test system metric") { } TEST_CASE("test metric capacity") { - std::cout << g_user_metric_count << "\n"; + std::cout << ylt::metric::metric_t::g_user_metric_count << "\n"; using test_metric_manager = dynamic_metric_manager>; - set_metric_capacity(g_user_metric_count + 2); + set_metric_capacity(ylt::metric::metric_t::g_user_metric_count + 2); auto c = test_metric_manager::instance().create_metric_dynamic( std::string("counter"), "", std::array{}); @@ -1719,14 +1993,74 @@ TEST_CASE("test metric manager clean expired label") { auto& inst = dynamic_metric_manager::instance(); auto pair = inst.create_metric_dynamic( std::string("some_counter"), "", std::array{"url"}); + auto summary = std::make_shared>( + std::string("test_summary"), std::string("summary help"), + std::vector{0.5, 0.9, 0.95, 0.99}, + std::array{"method", "url"}); + auto h = std::make_shared( + std::string("test"), std::string("help"), + std::vector{5.23, 10.54, 20.0, 50.0, 100.0}, + std::array{"method", "url"}); + inst.register_metric(summary); + inst.register_metric(h); auto c = pair.second; c->inc({"/"}); c->inc({"/test"}); + summary->observe({"GET", "test"}, 10); + h->observe({"GET", "test"}, 10); CHECK(c->label_value_count() == 2); + CHECK(summary->label_value_count() == 1); + CHECK(h->label_value_count() == 1); std::this_thread::sleep_for(std::chrono::seconds(2)); c->inc({"/index"}); size_t count = c->label_value_count(); CHECK(count == 1); + auto ct1 = summary->label_value_count(); + CHECK(ct1 == 0); + auto ct2 = h->label_value_count(); + CHECK(ct2 == 0); +} + +TEST_CASE("test remove label value") { + dynamic_counter_t counter("test", "", + std::array{"url", "code"}); + counter.inc({"/", "200"}); + counter.inc({"/", "400"}); + counter.inc({"/index", "200"}); + counter.inc({"/test", "404"}); + CHECK(counter.has_label_name("url")); + CHECK(counter.has_label_name(std::vector{"url", "code"})); + + CHECK(counter.has_label_value("/")); + CHECK(counter.has_label_value("/index")); + CHECK(counter.has_label_value("/test")); + + CHECK(!counter.has_label_value(std::vector{"/"})); + bool r = counter.has_label_value(std::vector{"/index"}); + CHECK(!counter.has_label_value(std::vector{"/index"})); + CHECK(!counter.has_label_value(std::vector{"/test"})); + + CHECK(!counter.has_label_value(std::vector{"/", "test"})); + CHECK(!counter.has_label_value(std::vector{"/", "200", "test"})); + CHECK(!counter.has_label_value(std::vector{})); +} + +TEST_CASE("test static summary with 0 and 1 quantiles") { + { + ylt::metric::summary_t s("test", "help", {0, 1}); + for (uint64_t i = 0; i < 100ull; ++i) { + s.observe(1); + } + auto result = s.get_rates(); + CHECK(result[0] == 1); + CHECK(result[1] == 1); + } + { + ylt::metric::summary_t s("test", "help", {0, 1}); + auto result = s.get_rates(); + CHECK(result[0] == 0); + CHECK(result[1] == 0); + } } DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4007)