feat/dataset-detail-chart #5
|
|
@ -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
|
||||
ApiCall.cpp
|
||||
ApiBatch.cpp
|
||||
ApiChain.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)
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@ target_sources(geopro_tests PRIVATE net/test_rsa.cpp)
|
|||
target_sources(geopro_tests PRIVATE net/test_auth.cpp)
|
||||
# ApiBatch 离线单测(QSignalSpy 需 Qt6::Test)。
|
||||
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)
|
||||
|
||||
# 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