feat/dataset-detail-chart #5
|
|
@ -0,0 +1,41 @@
|
||||||
|
#include "ApiBatch.hpp"
|
||||||
|
|
||||||
|
namespace geopro::net {
|
||||||
|
|
||||||
|
ApiBatch::ApiBatch(QList<IApiCall*> calls, Predicate isFailure, QObject* parent)
|
||||||
|
: QObject(parent), isFailure_(std::move(isFailure)) {
|
||||||
|
responses_.resize(calls.size());
|
||||||
|
remaining_ = static_cast<int>(calls.size());
|
||||||
|
for (int i = 0; i < calls.size(); ++i) {
|
||||||
|
IApiCall* c = calls[i];
|
||||||
|
calls_.append(c);
|
||||||
|
QObject::connect(c, &IApiCall::finished, this, [this, i](const ApiResponse& resp) {
|
||||||
|
if (aborted_) return; // §5.0 入口守卫
|
||||||
|
if (isFailure_(resp)) {
|
||||||
|
aborted_ = true;
|
||||||
|
for (const auto& other : calls_) { // fail-fast:abort 其余在飞
|
||||||
|
if (other) other->abort();
|
||||||
|
}
|
||||||
|
emit failed(i, resp);
|
||||||
|
deleteLater();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
responses_[i] = resp;
|
||||||
|
if (--remaining_ == 0) {
|
||||||
|
emit succeeded(responses_);
|
||||||
|
deleteLater();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApiBatch::abort() {
|
||||||
|
if (aborted_) return;
|
||||||
|
aborted_ = true;
|
||||||
|
for (const auto& c : calls_) {
|
||||||
|
if (c) c->abort();
|
||||||
|
}
|
||||||
|
deleteLater();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::net
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
#pragma once
|
||||||
|
#include <functional>
|
||||||
|
#include <QList>
|
||||||
|
#include <QPointer>
|
||||||
|
#include <QObject>
|
||||||
|
#include "IApiCall.hpp"
|
||||||
|
|
||||||
|
namespace geopro::net {
|
||||||
|
|
||||||
|
// 并发汇聚 N 个 IApiCall:全成功 → succeeded(按下标对齐);任一失败 → fail-fast:
|
||||||
|
// failed(index,resp) + abort 其余在飞 call。安全不变量见 spec §5.0。
|
||||||
|
class ApiBatch : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
using Predicate = std::function<bool(const ApiResponse&)>;
|
||||||
|
ApiBatch(QList<IApiCall*> calls, Predicate isFailure, QObject* parent = nullptr); // 接管 calls
|
||||||
|
void abort();
|
||||||
|
signals:
|
||||||
|
void succeeded(const QList<geopro::net::ApiResponse>& responses);
|
||||||
|
void failed(int index, const geopro::net::ApiResponse& resp);
|
||||||
|
private:
|
||||||
|
QList<QPointer<IApiCall>> calls_;
|
||||||
|
QList<ApiResponse> responses_;
|
||||||
|
Predicate isFailure_;
|
||||||
|
int remaining_ = 0;
|
||||||
|
bool aborted_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::net
|
||||||
|
|
@ -6,6 +6,7 @@ add_library(geopro_net STATIC
|
||||||
ApiResponseParse.cpp
|
ApiResponseParse.cpp
|
||||||
IApiCall.cpp
|
IApiCall.cpp
|
||||||
ApiCall.cpp
|
ApiCall.cpp
|
||||||
|
ApiBatch.cpp
|
||||||
AuthService.cpp)
|
AuthService.cpp)
|
||||||
target_include_directories(geopro_net PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(geopro_net PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
target_link_libraries(geopro_net PUBLIC OpenSSL::SSL OpenSSL::Crypto Qt6::Core Qt6::Network)
|
target_link_libraries(geopro_net PUBLIC OpenSSL::SSL OpenSSL::Crypto Qt6::Core Qt6::Network)
|
||||||
|
|
|
||||||
|
|
@ -43,11 +43,13 @@ target_link_libraries(geopro_tests PRIVATE geopro_data)
|
||||||
# net 层:RSA 加密器。测试需直接用 OpenSSL 生成/解密密钥,故显式 find_package
|
# net 层:RSA 加密器。测试需直接用 OpenSSL 生成/解密密钥,故显式 find_package
|
||||||
# 并链接 OpenSSL(geopro_net 的 PUBLIC 链接通常已传递,这里显式以防头文件找不到)。
|
# 并链接 OpenSSL(geopro_net 的 PUBLIC 链接通常已传递,这里显式以防头文件找不到)。
|
||||||
find_package(OpenSSL REQUIRED)
|
find_package(OpenSSL REQUIRED)
|
||||||
find_package(Qt6 COMPONENTS Core Network REQUIRED)
|
find_package(Qt6 COMPONENTS Core Network Test REQUIRED)
|
||||||
target_sources(geopro_tests PRIVATE net/test_rsa.cpp)
|
target_sources(geopro_tests PRIVATE net/test_rsa.cpp)
|
||||||
# 端到端登录连通测试(ApiClient + AuthService),需 Qt6::Core/Network 与事件循环。
|
# 端到端登录连通测试(ApiClient + AuthService),需 Qt6::Core/Network 与事件循环。
|
||||||
target_sources(geopro_tests PRIVATE net/test_auth.cpp)
|
target_sources(geopro_tests PRIVATE net/test_auth.cpp)
|
||||||
target_link_libraries(geopro_tests PRIVATE geopro_net OpenSSL::SSL OpenSSL::Crypto Qt6::Core Qt6::Network)
|
# ApiBatch 离线单测(QSignalSpy 需 Qt6::Test)。
|
||||||
|
target_sources(geopro_tests PRIVATE net/test_api_batch.cpp)
|
||||||
|
target_link_libraries(geopro_tests PRIVATE geopro_net OpenSSL::SSL OpenSSL::Crypto Qt6::Core Qt6::Network Qt6::Test)
|
||||||
|
|
||||||
# geopro_data 链 Qt6::Core,测试 exe 运行(含 gtest 发现)需要 Qt6Core.dll 等运行时
|
# geopro_data 链 Qt6::Core,测试 exe 运行(含 gtest 发现)需要 Qt6Core.dll 等运行时
|
||||||
# DLL 在旁。复用 app 同样的 TARGET_RUNTIME_DLLS POST_BUILD 拷贝。
|
# DLL 在旁。复用 app 同样的 TARGET_RUNTIME_DLLS POST_BUILD 拷贝。
|
||||||
|
|
@ -87,6 +89,8 @@ if(TARGET qwt)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
target_include_directories(geopro_tests PRIVATE ${CMAKE_SOURCE_DIR}/src/app)
|
target_include_directories(geopro_tests PRIVATE ${CMAKE_SOURCE_DIR}/src/app)
|
||||||
|
# tests/ 在 include 路径:net/test_api_batch.cpp 用 #include "net/FakeApiCall.hpp"。
|
||||||
|
target_include_directories(geopro_tests PRIVATE ${CMAKE_SOURCE_DIR}/tests)
|
||||||
target_sources(geopro_tests PRIVATE app/test_chart_strategy_registry.cpp)
|
target_sources(geopro_tests PRIVATE app/test_chart_strategy_registry.cpp)
|
||||||
# ColorMapService 测试(geopro_desktop 是可执行文件,直接把源加入测试目标)
|
# ColorMapService 测试(geopro_desktop 是可执行文件,直接把源加入测试目标)
|
||||||
target_sources(geopro_tests PRIVATE
|
target_sources(geopro_tests PRIVATE
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
#pragma once
|
||||||
|
#include "IApiCall.hpp"
|
||||||
|
|
||||||
|
namespace geopro::net::test {
|
||||||
|
|
||||||
|
// 测试假 call。不加 Q_OBJECT —— 仅发射继承自 IApiCall 的 finished 信号、override abort。
|
||||||
|
class FakeApiCall : public IApiCall {
|
||||||
|
public:
|
||||||
|
using IApiCall::IApiCall;
|
||||||
|
bool aborted = false;
|
||||||
|
void abort() override { aborted = true; }
|
||||||
|
void fire(const ApiResponse& r) { emit finished(r); } // 发射继承的信号(无需自身 moc)
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::net::test
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <QSignalSpy>
|
||||||
|
#include "ApiBatch.hpp"
|
||||||
|
#include "net/FakeApiCall.hpp"
|
||||||
|
|
||||||
|
using namespace geopro::net;
|
||||||
|
using geopro::net::test::FakeApiCall;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
ApiResponse ok(int code = 200) { ApiResponse r; r.code = code; r.httpStatus = 200; return r; }
|
||||||
|
ApiResponse bad() { ApiResponse r; r.code = 500; r.httpStatus = 200; r.msg = QStringLiteral("boom"); return r; }
|
||||||
|
// repo 注入的失败谓词:业务码 != 200 或传输错误。
|
||||||
|
auto isFailure = [](const ApiResponse& r) { return r.code != 200 || !r.rawError.isEmpty(); };
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ApiBatch, SucceedsWhenAllOk) {
|
||||||
|
auto* a = new FakeApiCall;
|
||||||
|
auto* b = new FakeApiCall;
|
||||||
|
auto* batch = new ApiBatch({a, b}, isFailure);
|
||||||
|
QSignalSpy okSpy(batch, &ApiBatch::succeeded);
|
||||||
|
QSignalSpy failSpy(batch, &ApiBatch::failed);
|
||||||
|
a->fire(ok());
|
||||||
|
EXPECT_EQ(okSpy.count(), 0); // 还差 b
|
||||||
|
b->fire(ok());
|
||||||
|
EXPECT_EQ(okSpy.count(), 1); // 全到齐
|
||||||
|
EXPECT_EQ(failSpy.count(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ApiBatch, FailFastAbortsOthers) {
|
||||||
|
auto* a = new FakeApiCall;
|
||||||
|
auto* b = new FakeApiCall; // 慢的(永不 fire)
|
||||||
|
auto* batch = new ApiBatch({a, b}, isFailure);
|
||||||
|
QSignalSpy failSpy(batch, &ApiBatch::failed);
|
||||||
|
QSignalSpy okSpy(batch, &ApiBatch::succeeded);
|
||||||
|
a->fire(bad()); // 首个失败
|
||||||
|
EXPECT_EQ(failSpy.count(), 1);
|
||||||
|
EXPECT_EQ(okSpy.count(), 0);
|
||||||
|
EXPECT_TRUE(b->aborted); // 其余在飞被 abort
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ApiBatch, AbortGateSuppressesLateSignals) {
|
||||||
|
auto* a = new FakeApiCall;
|
||||||
|
auto* b = new FakeApiCall;
|
||||||
|
auto* batch = new ApiBatch({a, b}, isFailure);
|
||||||
|
QSignalSpy okSpy(batch, &ApiBatch::succeeded);
|
||||||
|
QSignalSpy failSpy(batch, &ApiBatch::failed);
|
||||||
|
batch->abort();
|
||||||
|
EXPECT_TRUE(a->aborted);
|
||||||
|
EXPECT_TRUE(b->aborted);
|
||||||
|
a->fire(ok()); // 迟到信号
|
||||||
|
b->fire(ok());
|
||||||
|
EXPECT_EQ(okSpy.count(), 0); // aborted_ 闸门:不 emit
|
||||||
|
EXPECT_EQ(failSpy.count(), 0);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue