feat(net): ApiChain 顺序依赖链原语(fail-fast+abort闸门+工厂可抛) + 离线单测
This commit is contained in:
parent
751b486254
commit
22a7f2339e
|
|
@ -0,0 +1,53 @@
|
||||||
|
#include "ApiChain.hpp"
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
namespace geopro::net {
|
||||||
|
|
||||||
|
ApiChain::ApiChain(QList<StepFactory> steps, Predicate isFailure, QObject* parent)
|
||||||
|
: QObject(parent), steps_(std::move(steps)), isFailure_(std::move(isFailure)) {
|
||||||
|
Q_ASSERT(!steps_.isEmpty()); // 契约:至少一步(空链永不发 succeeded)
|
||||||
|
Q_ASSERT(isFailure_);
|
||||||
|
runNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApiChain::runNext() {
|
||||||
|
if (aborted_) return;
|
||||||
|
if (index_ >= steps_.size()) { // 全部完成
|
||||||
|
emit succeeded(responses_);
|
||||||
|
deleteLater();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IApiCall* call = nullptr;
|
||||||
|
try {
|
||||||
|
call = steps_[index_](responses_); // 工厂可抛(如 RSA 失败):转 failed
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
ApiResponse r;
|
||||||
|
r.rawError = QString::fromUtf8(e.what()); // 保留原因;登录 RSA 文案在 AuthService 层包装
|
||||||
|
aborted_ = true;
|
||||||
|
emit failed(index_, r);
|
||||||
|
deleteLater();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
current_ = call;
|
||||||
|
QObject::connect(call, &IApiCall::finished, this, [this](const ApiResponse& resp) {
|
||||||
|
if (aborted_) return; // §5.0 入口守卫
|
||||||
|
if (isFailure_(resp)) {
|
||||||
|
aborted_ = true;
|
||||||
|
emit failed(index_, resp);
|
||||||
|
deleteLater();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
responses_.append(resp);
|
||||||
|
++index_;
|
||||||
|
runNext(); // 链式推进
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApiChain::abort() {
|
||||||
|
if (aborted_) return;
|
||||||
|
aborted_ = true;
|
||||||
|
if (current_) current_->abort(); // abort 当前在飞步骤
|
||||||
|
deleteLater();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::net
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
#pragma once
|
||||||
|
#include <functional>
|
||||||
|
#include <QList>
|
||||||
|
#include <QPointer>
|
||||||
|
#include <QObject>
|
||||||
|
#include "IApiCall.hpp"
|
||||||
|
|
||||||
|
namespace geopro::net {
|
||||||
|
|
||||||
|
// 顺序执行 N 个步骤(依赖链):每步工厂用既往响应构造下一 IApiCall(工厂可抛 std::exception)。
|
||||||
|
// 任一步失败 → fail-fast:failed(index,resp) + abort 当前在飞 + deleteLater。
|
||||||
|
// 全部成功 → succeeded(按序响应)。安全不变量见 spec §5.0(aborted_ 闸门 + 一律 deleteLater)。
|
||||||
|
// 与 ApiBatch 对称:同 Predicate、同信号面、同安全约束。
|
||||||
|
class ApiChain : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
// 工厂:入参为已完成步骤的响应(按序),返回本步 IApiCall(接管所有权)。可抛 std::exception。
|
||||||
|
using StepFactory = std::function<IApiCall*(const QList<ApiResponse>& prior)>;
|
||||||
|
using Predicate = std::function<bool(const ApiResponse&)>;
|
||||||
|
ApiChain(QList<StepFactory> steps, Predicate isFailure, QObject* parent = nullptr);
|
||||||
|
void abort();
|
||||||
|
signals:
|
||||||
|
void succeeded(const QList<geopro::net::ApiResponse>& responses);
|
||||||
|
void failed(int index, const geopro::net::ApiResponse& resp);
|
||||||
|
private:
|
||||||
|
void runNext(); // 构造并连接下一步(工厂抛出 → emit failed)
|
||||||
|
QList<StepFactory> steps_;
|
||||||
|
Predicate isFailure_;
|
||||||
|
QList<ApiResponse> responses_;
|
||||||
|
QPointer<IApiCall> current_;
|
||||||
|
int index_ = 0;
|
||||||
|
bool aborted_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::net
|
||||||
|
|
@ -7,6 +7,7 @@ add_library(geopro_net STATIC
|
||||||
IApiCall.cpp
|
IApiCall.cpp
|
||||||
ApiCall.cpp
|
ApiCall.cpp
|
||||||
ApiBatch.cpp
|
ApiBatch.cpp
|
||||||
|
ApiChain.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)
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,8 @@ target_sources(geopro_tests PRIVATE net/test_rsa.cpp)
|
||||||
target_sources(geopro_tests PRIVATE net/test_auth.cpp)
|
target_sources(geopro_tests PRIVATE net/test_auth.cpp)
|
||||||
# ApiBatch 离线单测(QSignalSpy 需 Qt6::Test)。
|
# ApiBatch 离线单测(QSignalSpy 需 Qt6::Test)。
|
||||||
target_sources(geopro_tests PRIVATE net/test_api_batch.cpp)
|
target_sources(geopro_tests PRIVATE net/test_api_batch.cpp)
|
||||||
|
# ApiChain 离线单测(顺序依赖链:顺序/失败短路/abort闸门/工厂抛异常)。
|
||||||
|
target_sources(geopro_tests PRIVATE net/test_api_chain.cpp)
|
||||||
target_link_libraries(geopro_tests PRIVATE geopro_net OpenSSL::SSL OpenSSL::Crypto Qt6::Core Qt6::Network Qt6::Test)
|
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 等运行时
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <QSignalSpy>
|
||||||
|
#include "ApiChain.hpp"
|
||||||
|
#include "net/FakeApiCall.hpp"
|
||||||
|
|
||||||
|
using namespace geopro::net;
|
||||||
|
using geopro::net::test::FakeApiCall;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
ApiResponse ok(int v = 0) { ApiResponse r; r.code = 200; r.httpStatus = 200; r.data = QJsonObject{{"v", v}}; return r; }
|
||||||
|
ApiResponse bad() { ApiResponse r; r.code = 500; r.httpStatus = 200; r.msg = QStringLiteral("boom"); return r; }
|
||||||
|
auto isFailure = [](const ApiResponse& r) { return r.code != 200 || !r.rawError.isEmpty(); };
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(ApiChain, RunsStepsInOrderAndPassesPriorResponses) {
|
||||||
|
auto* s1 = new FakeApiCall;
|
||||||
|
auto* s2 = new FakeApiCall;
|
||||||
|
int seenPrior = -1;
|
||||||
|
QList<ApiChain::StepFactory> steps{
|
||||||
|
[&](const QList<ApiResponse>&) -> IApiCall* { return s1; },
|
||||||
|
[&](const QList<ApiResponse>& prior) -> IApiCall* {
|
||||||
|
seenPrior = prior.size(); // 第二步能看到第一步响应
|
||||||
|
return s2;
|
||||||
|
}};
|
||||||
|
auto* chain = new ApiChain(steps, isFailure);
|
||||||
|
QSignalSpy okSpy(chain, &ApiChain::succeeded);
|
||||||
|
s1->fire(ok(11)); // 第一步完成 → 触发第二步工厂
|
||||||
|
EXPECT_EQ(seenPrior, 1);
|
||||||
|
EXPECT_EQ(okSpy.count(), 0); // 还差第二步
|
||||||
|
s2->fire(ok(22));
|
||||||
|
EXPECT_EQ(okSpy.count(), 1);
|
||||||
|
const auto resps = okSpy.takeFirst().at(0).value<QList<ApiResponse>>();
|
||||||
|
EXPECT_EQ(resps.size(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ApiChain, FailFastShortCircuitsRemainingSteps) {
|
||||||
|
auto* s1 = new FakeApiCall;
|
||||||
|
bool secondBuilt = false;
|
||||||
|
QList<ApiChain::StepFactory> steps{
|
||||||
|
[&](const QList<ApiResponse>&) -> IApiCall* { return s1; },
|
||||||
|
[&](const QList<ApiResponse>&) -> IApiCall* { secondBuilt = true; return new FakeApiCall; }};
|
||||||
|
auto* chain = new ApiChain(steps, isFailure);
|
||||||
|
QSignalSpy failSpy(chain, &ApiChain::failed);
|
||||||
|
s1->fire(bad()); // 第一步失败
|
||||||
|
EXPECT_EQ(failSpy.count(), 1);
|
||||||
|
EXPECT_FALSE(secondBuilt); // 后续步骤不再构造
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ApiChain, AbortGateSuppressesLateSignals) {
|
||||||
|
auto* s1 = new FakeApiCall;
|
||||||
|
QList<ApiChain::StepFactory> steps{[&](const QList<ApiResponse>&) -> IApiCall* { return s1; }};
|
||||||
|
auto* chain = new ApiChain(steps, isFailure);
|
||||||
|
QSignalSpy okSpy(chain, &ApiChain::succeeded);
|
||||||
|
chain->abort();
|
||||||
|
EXPECT_TRUE(s1->aborted); // 在飞步骤被 abort
|
||||||
|
s1->fire(ok()); // 迟到
|
||||||
|
EXPECT_EQ(okSpy.count(), 0); // aborted_ 闸门
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ApiChain, StepFactoryThrowBecomesFailed) {
|
||||||
|
auto* s1 = new FakeApiCall;
|
||||||
|
QList<ApiChain::StepFactory> steps{
|
||||||
|
[&](const QList<ApiResponse>&) -> IApiCall* { return s1; },
|
||||||
|
[&](const QList<ApiResponse>&) -> IApiCall* { throw std::runtime_error("rsa fail"); }};
|
||||||
|
auto* chain = new ApiChain(steps, isFailure);
|
||||||
|
QSignalSpy failSpy(chain, &ApiChain::failed);
|
||||||
|
s1->fire(ok()); // 触发第二步工厂 → 抛 → failed
|
||||||
|
EXPECT_EQ(failSpy.count(), 1);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue