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
|
||||
IApiCall.cpp
|
||||
ApiCall.cpp
|
||||
ApiBatch.cpp
|
||||
AuthService.cpp)
|
||||
target_include_directories(geopro_net PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
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
|
||||
# 并链接 OpenSSL(geopro_net 的 PUBLIC 链接通常已传递,这里显式以防头文件找不到)。
|
||||
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)
|
||||
# 端到端登录连通测试(ApiClient + AuthService),需 Qt6::Core/Network 与事件循环。
|
||||
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 等运行时
|
||||
# DLL 在旁。复用 app 同样的 TARGET_RUNTIME_DLLS POST_BUILD 拷贝。
|
||||
|
|
@ -87,6 +89,8 @@ if(TARGET qwt)
|
|||
endif()
|
||||
|
||||
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)
|
||||
# ColorMapService 测试(geopro_desktop 是可执行文件,直接把源加入测试目标)
|
||||
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