Compare commits
No commits in common. "8b3bc50f5802c9d0caed424ac819607c9fc81bb3" and "8f944433232dbaa1c427742d9e26c52c46e7715a" have entirely different histories.
8b3bc50f58
...
8f94443323
|
|
@ -35,7 +35,6 @@ add_executable(geopro_desktop WIN32
|
|||
panels/chart/ContourPlotItem.cpp
|
||||
panels/chart/LivePanner.cpp
|
||||
panels/AnomalyTablePanel.cpp
|
||||
panels/LoadingOverlay.cpp
|
||||
panels/DatasetDetailPage.cpp
|
||||
panels/DatasetDetailPanel.cpp
|
||||
CentralScene.cpp
|
||||
|
|
|
|||
|
|
@ -520,19 +520,8 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
|
|||
[detailPanel](const geopro::controller::DatasetDetailController::GridData& d) {
|
||||
detailPanel->setGridData(d);
|
||||
});
|
||||
QObject::connect(
|
||||
&detailCtrl, &geopro::controller::DatasetDetailController::loadStarted, detailPanel,
|
||||
[detailPanel](const QString& dsId,
|
||||
geopro::controller::DatasetDetailController::LoadPhase phase) {
|
||||
if (phase == geopro::controller::DatasetDetailController::LoadPhase::Grid)
|
||||
detailPanel->setGridLoading(dsId, true);
|
||||
});
|
||||
// context 用 detailPanel(与 loadStarted 一致):detailPanel 析构即自动断连,避免野指针。
|
||||
// window 比 detailPanel 活得久,lambda 捕 &window 取状态栏安全。loadFailed 无 phase:
|
||||
// Chart 失败时 gridOverlay 本已隐藏,setGridLoading(false) 幂等无害;Grid 失败时正确清遮罩。
|
||||
QObject::connect(&detailCtrl, &geopro::controller::DatasetDetailController::loadFailed, detailPanel,
|
||||
[&window, detailPanel](const QString& dsId, const QString& msg) {
|
||||
detailPanel->setGridLoading(dsId, false);
|
||||
QObject::connect(&detailCtrl, &geopro::controller::DatasetDetailController::loadFailed, &window,
|
||||
[&window](const QString& dsId, const QString& msg) {
|
||||
window.statusBar()->showMessage(
|
||||
QStringLiteral("数据集 %1 加载失败:%2").arg(dsId, msg), 5000);
|
||||
});
|
||||
|
|
@ -799,10 +788,6 @@ int main(int argc, char* argv[])
|
|||
QSurfaceFormat::setDefaultFormat(QVTKOpenGLStereoWidget::defaultFormat());
|
||||
QApplication app(argc, argv);
|
||||
|
||||
// 异步 ApiCall::finished 等信号携带 ApiResponse;注册元类型以支持跨 QueuedConnection 传递
|
||||
// (当前详情链路为同线程 DirectConnection,非严格必需,但作防御性注册,见 spec §5.1)。
|
||||
qRegisterMetaType<geopro::net::ApiResponse>();
|
||||
|
||||
// 组织/应用名:QSettings 持久化(dock 布局、登录记忆等)按此定位存储位置。
|
||||
QCoreApplication::setOrganizationName(QStringLiteral("Geomative"));
|
||||
QCoreApplication::setApplicationName(QStringLiteral("Geopro3"));
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
|
||||
#include "Glyphs.hpp"
|
||||
#include "PanelHeader.hpp"
|
||||
#include "panels/LoadingOverlay.hpp"
|
||||
#include "panels/chart/GridDataChartView.hpp"
|
||||
#include "panels/chart/RawDataChartView.hpp"
|
||||
|
||||
|
|
@ -22,7 +21,6 @@ DatasetDetailPage::DatasetDetailPage(QWidget* parent) : QWidget(parent) {
|
|||
|
||||
rawView_ = new RawDataChartView(this);
|
||||
gridView_ = new GridDataChartView(this);
|
||||
gridOverlay_ = new LoadingOverlay(gridView_); // 父为 gridView_,随其尺寸覆盖网格图区
|
||||
|
||||
const QVector<PanelTab> tabs = {
|
||||
{Glyph::Detail, QStringLiteral("原数据"), rawView_, false},
|
||||
|
|
@ -51,18 +49,12 @@ void DatasetDetailPage::setData(const geopro::controller::DatasetDetailControlle
|
|||
gridRequested_ = false; // 新数据集 → 网格数据需重新按需加载
|
||||
rawView_->setData(d);
|
||||
gridView_->setData(d);
|
||||
setGridLoading(false); // 重开/换 ds:重置遮罩状态,避免上次的「加载中」残留
|
||||
}
|
||||
|
||||
void DatasetDetailPage::setGridData(
|
||||
const geopro::controller::DatasetDetailController::GridData& d) {
|
||||
gridRequested_ = true; // 已加载,切回网格页不再重复请求
|
||||
gridView_->setGridData(d.grid, d.gridScale, d.anomalies);
|
||||
setGridLoading(false); // 数据到达,隐藏遮罩
|
||||
}
|
||||
|
||||
void DatasetDetailPage::setGridLoading(bool on) {
|
||||
if (on) gridOverlay_->showOver(); else gridOverlay_->hide();
|
||||
}
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ namespace geopro::app {
|
|||
|
||||
class RawDataChartView;
|
||||
class GridDataChartView;
|
||||
class LoadingOverlay;
|
||||
|
||||
// 单个数据集详情页:下划线页签「原数据 / 网格数据」+ 右侧「导出」操作。
|
||||
// 内部分别由 RawDataChartView / GridDataChartView 实现各自三层布局。
|
||||
|
|
@ -17,8 +16,6 @@ public:
|
|||
void setData(const geopro::controller::DatasetDetailController::ChartData& d);
|
||||
// 网格数据到达(懒加载结果)→ 下发给 GridDataChartView 并标记已加载。
|
||||
void setGridData(const geopro::controller::DatasetDetailController::GridData& d);
|
||||
// 网格懒加载进行中(true)/结束(false)时切换遮罩显隐。
|
||||
void setGridLoading(bool on);
|
||||
QString dsId() const { return dsId_; }
|
||||
signals:
|
||||
// 「网格数据」页签首次激活且本页网格数据未加载 → 请求懒加载。
|
||||
|
|
@ -29,7 +26,6 @@ private:
|
|||
bool gridRequested_ = false; // 已请求过(避免重复发信号)
|
||||
RawDataChartView* rawView_;
|
||||
GridDataChartView* gridView_;
|
||||
LoadingOverlay* gridOverlay_; // 网格懒加载期间覆盖 gridView_ 的遮罩
|
||||
};
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
|
|||
|
|
@ -33,9 +33,6 @@ void DatasetDetailPanel::openOrUpdate(const geopro::controller::DatasetDetailCon
|
|||
void DatasetDetailPanel::setGridData(const geopro::controller::DatasetDetailController::GridData& d) {
|
||||
if (auto* p = pageFor(d.dsId)) p->setGridData(d);
|
||||
}
|
||||
void DatasetDetailPanel::setGridLoading(const QString& dsId, bool on) {
|
||||
if (auto* p = pageFor(dsId)) p->setGridLoading(on);
|
||||
}
|
||||
void DatasetDetailPanel::focusDataset(const QString& dsId) {
|
||||
if (auto* p = pageFor(dsId)) setCurrentWidget(p);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ public:
|
|||
explicit DatasetDetailPanel(QWidget* parent = nullptr);
|
||||
void openOrUpdate(const geopro::controller::DatasetDetailController::ChartData& d); // 双击/数据到达
|
||||
void setGridData(const geopro::controller::DatasetDetailController::GridData& d); // 网格数据懒加载到达
|
||||
void setGridLoading(const QString& dsId, bool on); // 网格懒加载进行中/结束 → 转发给对应页
|
||||
void focusDataset(const QString& dsId); // 单击聚焦已开页
|
||||
signals:
|
||||
void activeDatasetChanged(const QString& dsId); // 反向联动数据集列表
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
#include "panels/LoadingOverlay.hpp"
|
||||
#include <QEvent>
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
LoadingOverlay::LoadingOverlay(QWidget* parent) : QWidget(parent), label_(new QLabel(this)) {
|
||||
Q_ASSERT(parent); // 契约:必须有父(遮罩几何跟随父,无父无法工作)
|
||||
setAttribute(Qt::WA_StyledBackground, true);
|
||||
setStyleSheet(QStringLiteral("background: rgba(255,255,255,160);"));
|
||||
label_->setText(QStringLiteral("加载中…"));
|
||||
label_->setAlignment(Qt::AlignCenter);
|
||||
auto* lay = new QVBoxLayout(this);
|
||||
lay->addWidget(label_);
|
||||
if (parent) parent->installEventFilter(this);
|
||||
hide();
|
||||
}
|
||||
|
||||
void LoadingOverlay::showOver() {
|
||||
if (parentWidget()) setGeometry(parentWidget()->rect());
|
||||
raise();
|
||||
show();
|
||||
}
|
||||
|
||||
bool LoadingOverlay::eventFilter(QObject* obj, QEvent* ev) {
|
||||
if (obj == parentWidget() && ev->type() == QEvent::Resize && isVisible()) {
|
||||
setGeometry(parentWidget()->rect());
|
||||
}
|
||||
return QWidget::eventFilter(obj, ev);
|
||||
}
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
#pragma once
|
||||
#include <QWidget>
|
||||
class QLabel;
|
||||
namespace geopro::app {
|
||||
|
||||
// 半透明「加载中…」遮罩。贴在目标视图上层,showOver()/hide() 切换,几何随父 resize 跟随。
|
||||
class LoadingOverlay : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit LoadingOverlay(QWidget* parent);
|
||||
void showOver(); // 铺满父尺寸、置顶、显示
|
||||
protected:
|
||||
bool eventFilter(QObject* obj, QEvent* ev) override; // 跟随父 resize
|
||||
private:
|
||||
QLabel* label_;
|
||||
};
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
@ -1,67 +1,60 @@
|
|||
#include "DatasetDetailController.hpp"
|
||||
#include "repo/IAsyncDatasetRepository.hpp"
|
||||
#include "api/DatasetLoadHandles.hpp"
|
||||
#include <stdexcept>
|
||||
#include "repo/IDatasetRepository.hpp"
|
||||
namespace geopro::controller {
|
||||
|
||||
DatasetDetailController::DatasetDetailController(data::IAsyncDatasetRepository& repo, QObject* parent)
|
||||
DatasetDetailController::DatasetDetailController(data::IDatasetRepository& repo, QObject* parent)
|
||||
: QObject(parent), repo_(repo) {}
|
||||
|
||||
DatasetDetailController::~DatasetDetailController() {
|
||||
if (chartLoad_) chartLoad_->abort(); // 退出契约:abort 在飞句柄,不依赖外部析构顺序兜底
|
||||
if (gridLoad_) gridLoad_->abort();
|
||||
}
|
||||
|
||||
void DatasetDetailController::openDataset(const QString& dsId, const QString& ddCode) {
|
||||
if (busy_) return; // 防重入(同步网络期间 QEventLoop 可重入)
|
||||
busy_ = true;
|
||||
if (ddCode != QLatin1String("dd_inversion_data")) { // 首版仅支持 ERT 反演
|
||||
busy_ = false;
|
||||
emit loadFailed(dsId, QStringLiteral("暂不支持该数据类型的预览"));
|
||||
return;
|
||||
}
|
||||
if (chartLoad_) chartLoad_->abort(); // abort-and-replace
|
||||
data::ChartLoad* load = repo_.loadChartAsync(dsId.toStdString());
|
||||
chartLoad_ = load;
|
||||
emit loadStarted(dsId, LoadPhase::Chart);
|
||||
QObject::connect(load, &data::ChartLoad::done, this,
|
||||
[this, load, dsId, ddCode](const data::ChartParts& parts) {
|
||||
if (load != chartLoad_) return; // §5.0 句柄身份比对:丢弃迟到信号
|
||||
chartLoad_.clear();
|
||||
const std::string id = dsId.toStdString();
|
||||
try {
|
||||
ChartData d;
|
||||
d.dsId = dsId;
|
||||
d.ddCode = ddCode;
|
||||
d.scatter = parts.scatter;
|
||||
d.scatterScale = parts.scatterScale;
|
||||
// 严格对齐原版「原数据」页:只拉 scatter + 散点色阶(type1) 两个。
|
||||
// 网格数据(inversion/rows)与异常(queryException)随「网格数据」页按需懒加载。
|
||||
d.scatter = repo_.loadScatter(id);
|
||||
d.scatterScale = repo_.loadScatterColorScale(id);
|
||||
busy_ = false;
|
||||
emit chartReady(d);
|
||||
});
|
||||
QObject::connect(load, &data::ChartLoad::failed, this,
|
||||
[this, load, dsId](const QString& msg) {
|
||||
if (load != chartLoad_) return;
|
||||
chartLoad_.clear();
|
||||
emit loadFailed(dsId, msg);
|
||||
});
|
||||
} catch (const std::exception& e) {
|
||||
busy_ = false;
|
||||
emit loadFailed(dsId, QString::fromStdString(e.what()));
|
||||
} catch (...) { // 非 std 异常(VTK/Qt)也必须复位 busy_,否则永久拒绝后续加载
|
||||
busy_ = false;
|
||||
emit loadFailed(dsId, QStringLiteral("未知错误"));
|
||||
}
|
||||
}
|
||||
|
||||
void DatasetDetailController::loadGridData(const QString& dsId, const QString& ddCode) {
|
||||
if (busy_) return; // 防重入(同步网络期间 QEventLoop 可重入)
|
||||
if (ddCode != QLatin1String("dd_inversion_data")) return; // 仅 ERT 反演有网格数据
|
||||
if (gridLoad_) gridLoad_->abort(); // abort-and-replace
|
||||
data::GridLoad* load = repo_.loadGridAsync(dsId.toStdString());
|
||||
gridLoad_ = load;
|
||||
emit loadStarted(dsId, LoadPhase::Grid);
|
||||
QObject::connect(load, &data::GridLoad::done, this,
|
||||
[this, load, dsId](const data::GridParts& parts) {
|
||||
if (load != gridLoad_) return; // §5.0 句柄身份比对:丢弃迟到信号
|
||||
gridLoad_.clear();
|
||||
busy_ = true;
|
||||
const std::string id = dsId.toStdString();
|
||||
try {
|
||||
GridData d;
|
||||
d.dsId = dsId;
|
||||
d.grid = parts.grid;
|
||||
d.gridScale = parts.gridScale;
|
||||
d.anomalies = parts.anomalies;
|
||||
// 网格数据:rows(服务端网格化,慢) + 色阶 type2 + 异常 queryException。
|
||||
d.grid = repo_.loadGrid(id);
|
||||
d.gridScale = repo_.loadColorScale(id);
|
||||
d.anomalies = repo_.loadAnomalies(id);
|
||||
busy_ = false;
|
||||
emit gridReady(d);
|
||||
});
|
||||
QObject::connect(load, &data::GridLoad::failed, this,
|
||||
[this, load, dsId](const QString& msg) {
|
||||
if (load != gridLoad_) return;
|
||||
gridLoad_.clear();
|
||||
emit loadFailed(dsId, msg);
|
||||
});
|
||||
} catch (const std::exception& e) {
|
||||
busy_ = false;
|
||||
emit loadFailed(dsId, QString::fromStdString(e.what()));
|
||||
} catch (...) { // 非 std 异常(VTK/Qt)也必须复位 busy_,否则永久拒绝后续加载
|
||||
busy_ = false;
|
||||
emit loadFailed(dsId, QStringLiteral("未知错误"));
|
||||
}
|
||||
}
|
||||
|
||||
void DatasetDetailController::focusDataset(const QString& dsId) { emit focusRequested(dsId); }
|
||||
|
|
|
|||
|
|
@ -1,21 +1,18 @@
|
|||
#pragma once
|
||||
#include <string>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QString>
|
||||
#include "model/Field.hpp"
|
||||
#include "model/ColorScale.hpp"
|
||||
#include "model/Anomaly.hpp"
|
||||
namespace geopro::data { class IAsyncDatasetRepository; class ChartLoad; class GridLoad; }
|
||||
namespace geopro::data { class IDatasetRepository; }
|
||||
namespace geopro::controller {
|
||||
|
||||
// 数据详情编排:双击/网格页签 → 异步拉 散点/网格/色阶/异常 → 发信号给详情面板。被动视图。
|
||||
// 数据详情编排:单击/双击数据集 → 拉 散点/网格/色阶/异常 → 发信号给详情面板。被动视图。
|
||||
// 仅服务图表,不与 WorkbenchNavController(项目/结构导航)耦合。
|
||||
class DatasetDetailController : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum class LoadPhase { Chart, Grid };
|
||||
Q_ENUM(LoadPhase)
|
||||
|
||||
struct ChartData {
|
||||
QString dsId, ddCode;
|
||||
geopro::core::ScatterField scatter;
|
||||
|
|
@ -32,21 +29,19 @@ public:
|
|||
std::vector<geopro::core::Anomaly> anomalies;
|
||||
};
|
||||
|
||||
explicit DatasetDetailController(data::IAsyncDatasetRepository& repo, QObject* parent = nullptr);
|
||||
~DatasetDetailController() override; // 退出契约(spec §7):abort 在飞句柄,避免迟到信号打到已析构 this
|
||||
explicit DatasetDetailController(data::IDatasetRepository& repo, QObject* parent = nullptr);
|
||||
public slots:
|
||||
void openDataset(const QString& dsId, const QString& ddCode);
|
||||
void focusDataset(const QString& dsId);
|
||||
void openDataset(const QString& dsId, const QString& ddCode); // 双击=新建/聚焦页
|
||||
void focusDataset(const QString& dsId); // 单击=聚焦已开页
|
||||
// 网格数据懒加载:网格页首次激活时调用(rows 服务端网格化 1-4s,故不随 openDataset 拉)。
|
||||
void loadGridData(const QString& dsId, const QString& ddCode);
|
||||
signals:
|
||||
void loadStarted(const QString& dsId, LoadPhase phase);
|
||||
void chartReady(const ChartData& data);
|
||||
void gridReady(const GridData& data);
|
||||
void focusRequested(const QString& dsId);
|
||||
void loadFailed(const QString& dsId, const QString& message);
|
||||
private:
|
||||
data::IAsyncDatasetRepository& repo_;
|
||||
QPointer<data::ChartLoad> chartLoad_;
|
||||
QPointer<data::GridLoad> gridLoad_;
|
||||
data::IDatasetRepository& repo_;
|
||||
bool busy_ = false;
|
||||
};
|
||||
} // namespace geopro::controller
|
||||
|
|
|
|||
|
|
@ -6,9 +6,8 @@ add_library(geopro_data STATIC
|
|||
dto/NavDto.cpp
|
||||
dto/DatasetChartDto.cpp
|
||||
api/ApiProjectRepository.cpp
|
||||
api/ApiDatasetRepository.cpp
|
||||
api/DatasetLoadHandles.cpp)
|
||||
api/ApiDatasetRepository.cpp)
|
||||
target_include_directories(geopro_data PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(geopro_data PUBLIC geopro_core geopro_net Qt6::Core PRIVATE nlohmann_json::nlohmann_json)
|
||||
target_compile_features(geopro_data PUBLIC cxx_std_17)
|
||||
set_target_properties(geopro_data PROPERTIES AUTOMOC ON AUTOUIC OFF AUTORCC OFF)
|
||||
set_target_properties(geopro_data PROPERTIES AUTOMOC OFF AUTOUIC OFF AUTORCC OFF)
|
||||
|
|
|
|||
|
|
@ -1,62 +1,51 @@
|
|||
#include "api/ApiDatasetRepository.hpp"
|
||||
#include <stdexcept>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include "ApiClient.hpp"
|
||||
#include "ApiBatch.hpp"
|
||||
#include "api/DatasetLoadHandles.hpp"
|
||||
#include "dto/DatasetChartDto.hpp"
|
||||
|
||||
namespace geopro::data {
|
||||
namespace {
|
||||
|
||||
QString enc(const std::string& s) {
|
||||
return QString::fromUtf8(QUrl::toPercentEncoding(QString::fromStdString(s)));
|
||||
}
|
||||
|
||||
// 失败判定(原 must() 口径):业务码 != 200 或传输错误。
|
||||
bool isFailure(const geopro::net::ApiResponse& r) {
|
||||
return r.code != 200 || !r.rawError.isEmpty();
|
||||
void must(const net::ApiResponse& r, const char* what) {
|
||||
if (r.code != 200) throw std::runtime_error(std::string(what) + " failed: " +
|
||||
(r.msg.isEmpty() ? r.rawError.toStdString() : r.msg.toStdString()));
|
||||
}
|
||||
|
||||
QJsonObject colorBody(const std::string& dsId, int type) {
|
||||
return QJsonObject{{"dsObjectId", QString::fromStdString(dsId)}, {"businessCode", ""}, {"type", type}};
|
||||
geopro::core::ColorScale colorScale(net::ApiClient& api, const std::string& dsId, int type) {
|
||||
QJsonObject body{{"dsObjectId", QString::fromStdString(dsId)}, {"businessCode", ""}, {"type", type}};
|
||||
const net::ApiResponse r = api.postJson(QStringLiteral("/business/lvl/colorGradation/getDetail"), body);
|
||||
must(r, "colorGradation");
|
||||
return dto::parseColorBar(r.data);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ApiDatasetRepository::ApiDatasetRepository(net::ApiClient& api) : api_(api) {}
|
||||
|
||||
ChartLoad* ApiDatasetRepository::loadChartAsync(const std::string& dsId) {
|
||||
// index 0 = scatter(GET),index 1 = 散点色阶 type1(POST)
|
||||
QList<net::IApiCall*> calls{
|
||||
api_.getAsync(QStringLiteral("/business/dd/ert/inversion/getErtRawDataScatterGraph/%1").arg(enc(dsId))),
|
||||
api_.postJsonAsync(QStringLiteral("/business/lvl/colorGradation/getDetail"), colorBody(dsId, 1)),
|
||||
};
|
||||
auto* batch = new net::ApiBatch(calls, &isFailure);
|
||||
return new ApiChartLoad(batch, [](const QList<net::ApiResponse>& r) {
|
||||
ChartParts p;
|
||||
p.scatter = dto::parseScatterGraph(r[0].data);
|
||||
p.scatterScale = dto::parseColorBar(r[1].data);
|
||||
return p;
|
||||
});
|
||||
geopro::core::Grid ApiDatasetRepository::loadGrid(const std::string& dsId) {
|
||||
const net::ApiResponse r = api_.get(
|
||||
QStringLiteral("/business/dd/ert/inversion/rows/%1").arg(enc(dsId)));
|
||||
must(r, "inversion/rows");
|
||||
return dto::parseInversionGrid(r.data);
|
||||
}
|
||||
|
||||
GridLoad* ApiDatasetRepository::loadGridAsync(const std::string& dsId) {
|
||||
// index 0 = rows(GET,慢),1 = 色阶 type2(POST),2 = 异常(GET)
|
||||
QList<net::IApiCall*> calls{
|
||||
api_.getAsync(QStringLiteral("/business/dd/ert/inversion/rows/%1").arg(enc(dsId))),
|
||||
api_.postJsonAsync(QStringLiteral("/business/lvl/colorGradation/getDetail"), colorBody(dsId, 2)),
|
||||
api_.getAsync(QStringLiteral("/business/exception/queryException/%1").arg(enc(dsId))),
|
||||
};
|
||||
auto* batch = new net::ApiBatch(calls, &isFailure);
|
||||
return new ApiGridLoad(batch, [](const QList<net::ApiResponse>& r) {
|
||||
GridParts p;
|
||||
p.grid = dto::parseInversionGrid(r[0].data);
|
||||
p.gridScale = dto::parseColorBar(r[1].data);
|
||||
p.anomalies = dto::parseDatasetAnomalies(r[2].data.value(QStringLiteral("value")).toArray());
|
||||
return p;
|
||||
});
|
||||
geopro::core::ScatterField ApiDatasetRepository::loadScatter(const std::string& dsId) {
|
||||
const net::ApiResponse r = api_.get(
|
||||
QStringLiteral("/business/dd/ert/inversion/getErtRawDataScatterGraph/%1").arg(enc(dsId)));
|
||||
must(r, "scatterGraph");
|
||||
return dto::parseScatterGraph(r.data);
|
||||
}
|
||||
geopro::core::ColorScale ApiDatasetRepository::loadColorScale(const std::string& dsId) {
|
||||
return colorScale(api_, dsId, 2);
|
||||
}
|
||||
geopro::core::ColorScale ApiDatasetRepository::loadScatterColorScale(const std::string& dsId) {
|
||||
return colorScale(api_, dsId, 1);
|
||||
}
|
||||
std::vector<geopro::core::Anomaly> ApiDatasetRepository::loadAnomalies(const std::string& dsId) {
|
||||
const net::ApiResponse r = api_.get(
|
||||
QStringLiteral("/business/exception/queryException/%1").arg(enc(dsId)));
|
||||
must(r, "queryException");
|
||||
return dto::parseDatasetAnomalies(r.data.value(QStringLiteral("value")).toArray());
|
||||
}
|
||||
|
||||
} // namespace geopro::data
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
#pragma once
|
||||
#include "repo/IAsyncDatasetRepository.hpp"
|
||||
#include "repo/IDatasetRepository.hpp"
|
||||
namespace geopro::net { class ApiClient; }
|
||||
namespace geopro::data {
|
||||
|
||||
// 真实 API 实现 IAsyncDatasetRepository(ERT 反演)。每次加载返回自管理句柄。
|
||||
class ApiDatasetRepository : public IAsyncDatasetRepository {
|
||||
// 真实 API 实现 IDatasetRepository(ERT 反演相关)。失败抛 std::runtime_error。
|
||||
class ApiDatasetRepository : public IDatasetRepository {
|
||||
public:
|
||||
explicit ApiDatasetRepository(net::ApiClient& api);
|
||||
ChartLoad* loadChartAsync(const std::string& dsId) override;
|
||||
GridLoad* loadGridAsync(const std::string& dsId) override;
|
||||
std::vector<GsNode> loadStructure() override { return {}; } // 不经此仓储取结构
|
||||
geopro::core::Grid loadGrid(const std::string& dsId) override;
|
||||
geopro::core::ScatterField loadScatter(const std::string& dsId) override;
|
||||
geopro::core::ColorScale loadColorScale(const std::string& dsId) override; // type2 网格
|
||||
geopro::core::ColorScale loadScatterColorScale(const std::string& dsId) override; // type1 原数据
|
||||
std::vector<geopro::core::Anomaly> loadAnomalies(const std::string& dsId) override;
|
||||
private:
|
||||
net::ApiClient& api_;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,82 +0,0 @@
|
|||
#include "api/DatasetLoadHandles.hpp"
|
||||
#include <stdexcept>
|
||||
|
||||
namespace geopro::data {
|
||||
|
||||
namespace {
|
||||
QString reasonOf(const geopro::net::ApiResponse& r) {
|
||||
return r.msg.isEmpty() ? r.rawError : r.msg;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ApiChartLoad::ApiChartLoad(geopro::net::ApiBatch* batch, Parser parse, QObject* parent)
|
||||
: ChartLoad(parent), batch_(batch), parse_(std::move(parse)) {
|
||||
QObject::connect(batch, &geopro::net::ApiBatch::succeeded, this,
|
||||
[this](const QList<geopro::net::ApiResponse>& resps) {
|
||||
if (aborted_) return; // §5.0
|
||||
ChartParts parts;
|
||||
try {
|
||||
parts = parse_(resps); // 仅解析在 try 内:下游 done 处理器抛出不应误报为解析失败
|
||||
} catch (const std::exception& e) {
|
||||
emit failed(QString::fromUtf8(e.what()));
|
||||
deleteLater();
|
||||
return;
|
||||
} catch (...) { // 非 std 异常跨信号槽会 terminate,兜底转 failed
|
||||
emit failed(QStringLiteral("解析失败:未知异常"));
|
||||
deleteLater();
|
||||
return;
|
||||
}
|
||||
emit done(parts);
|
||||
deleteLater();
|
||||
});
|
||||
QObject::connect(batch, &geopro::net::ApiBatch::failed, this,
|
||||
[this](int, const geopro::net::ApiResponse& r) {
|
||||
if (aborted_) return;
|
||||
emit failed(reasonOf(r));
|
||||
deleteLater();
|
||||
});
|
||||
}
|
||||
|
||||
void ApiChartLoad::abort() {
|
||||
if (aborted_) return;
|
||||
aborted_ = true;
|
||||
if (batch_) batch_->abort();
|
||||
deleteLater();
|
||||
}
|
||||
|
||||
ApiGridLoad::ApiGridLoad(geopro::net::ApiBatch* batch, Parser parse, QObject* parent)
|
||||
: GridLoad(parent), batch_(batch), parse_(std::move(parse)) {
|
||||
QObject::connect(batch, &geopro::net::ApiBatch::succeeded, this,
|
||||
[this](const QList<geopro::net::ApiResponse>& resps) {
|
||||
if (aborted_) return;
|
||||
GridParts parts;
|
||||
try {
|
||||
parts = parse_(resps); // 仅解析在 try 内:下游 done 处理器抛出不应误报为解析失败
|
||||
} catch (const std::exception& e) {
|
||||
emit failed(QString::fromUtf8(e.what()));
|
||||
deleteLater();
|
||||
return;
|
||||
} catch (...) {
|
||||
emit failed(QStringLiteral("解析失败:未知异常"));
|
||||
deleteLater();
|
||||
return;
|
||||
}
|
||||
emit done(parts);
|
||||
deleteLater();
|
||||
});
|
||||
QObject::connect(batch, &geopro::net::ApiBatch::failed, this,
|
||||
[this](int, const geopro::net::ApiResponse& r) {
|
||||
if (aborted_) return;
|
||||
emit failed(reasonOf(r));
|
||||
deleteLater();
|
||||
});
|
||||
}
|
||||
|
||||
void ApiGridLoad::abort() {
|
||||
if (aborted_) return;
|
||||
aborted_ = true;
|
||||
if (batch_) batch_->abort();
|
||||
deleteLater();
|
||||
}
|
||||
|
||||
} // namespace geopro::data
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
#pragma once
|
||||
#include <functional>
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QString>
|
||||
#include "ApiBatch.hpp"
|
||||
#include "DatasetLoads.hpp"
|
||||
|
||||
namespace geopro::data {
|
||||
|
||||
// ── 抽象句柄(可测试缝,类比 IApiCall):仓储返回基类指针,控制器/测试只依赖它 ──
|
||||
class ChartLoad : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
using QObject::QObject;
|
||||
~ChartLoad() override = default;
|
||||
virtual void abort() = 0;
|
||||
signals:
|
||||
void done(const geopro::data::ChartParts& parts);
|
||||
void failed(const QString& message);
|
||||
};
|
||||
|
||||
class GridLoad : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
using QObject::QObject;
|
||||
~GridLoad() override = default;
|
||||
virtual void abort() = 0;
|
||||
signals:
|
||||
void done(const geopro::data::GridParts& parts);
|
||||
void failed(const QString& message);
|
||||
};
|
||||
|
||||
// ── Api 实现:包一个 ApiBatch + 注入的解析函数。batch.succeeded→解析→done;failed→failed ──
|
||||
class ApiChartLoad : public ChartLoad {
|
||||
Q_OBJECT
|
||||
public:
|
||||
using Parser = std::function<ChartParts(const QList<geopro::net::ApiResponse>&)>;
|
||||
ApiChartLoad(geopro::net::ApiBatch* batch, Parser parse, QObject* parent = nullptr);
|
||||
void abort() override;
|
||||
private:
|
||||
QPointer<geopro::net::ApiBatch> batch_;
|
||||
Parser parse_;
|
||||
bool aborted_ = false;
|
||||
};
|
||||
|
||||
class ApiGridLoad : public GridLoad {
|
||||
Q_OBJECT
|
||||
public:
|
||||
using Parser = std::function<GridParts(const QList<geopro::net::ApiResponse>&)>;
|
||||
ApiGridLoad(geopro::net::ApiBatch* batch, Parser parse, QObject* parent = nullptr);
|
||||
void abort() override;
|
||||
private:
|
||||
QPointer<geopro::net::ApiBatch> batch_;
|
||||
Parser parse_;
|
||||
bool aborted_ = false;
|
||||
};
|
||||
|
||||
} // namespace geopro::data
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
#pragma once
|
||||
#include <vector>
|
||||
#include "model/Field.hpp"
|
||||
#include "model/ColorScale.hpp"
|
||||
#include "model/Anomaly.hpp"
|
||||
|
||||
namespace geopro::data {
|
||||
|
||||
// 原数据加载结果:scatter + 散点色阶(type1)。
|
||||
struct ChartParts {
|
||||
geopro::core::ScatterField scatter;
|
||||
geopro::core::ColorScale scatterScale;
|
||||
};
|
||||
// 网格数据加载结果:grid(rows) + 网格色阶(type2) + 异常。
|
||||
struct GridParts {
|
||||
geopro::core::Grid grid{1, 1}; // Grid 无默认构造;占位
|
||||
geopro::core::ColorScale gridScale;
|
||||
std::vector<geopro::core::Anomaly> anomalies;
|
||||
};
|
||||
|
||||
} // namespace geopro::data
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
#pragma once
|
||||
#include <string>
|
||||
|
||||
namespace geopro::data {
|
||||
|
||||
class ChartLoad;
|
||||
class GridLoad;
|
||||
|
||||
// 数据集详情异步仓储抽象。返回自管理句柄(完成/失败后 deleteLater)。
|
||||
class IAsyncDatasetRepository {
|
||||
public:
|
||||
virtual ~IAsyncDatasetRepository() = default;
|
||||
virtual ChartLoad* loadChartAsync(const std::string& dsId) = 0; // scatter + 散点色阶(type1)
|
||||
virtual GridLoad* loadGridAsync(const std::string& dsId) = 0; // grid(rows) + 色阶(type2) + 异常
|
||||
};
|
||||
|
||||
} // namespace geopro::data
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
#include "ApiBatch.hpp"
|
||||
|
||||
namespace geopro::net {
|
||||
|
||||
ApiBatch::ApiBatch(QList<IApiCall*> calls, Predicate isFailure, QObject* parent)
|
||||
: QObject(parent), isFailure_(std::move(isFailure)) {
|
||||
Q_ASSERT(!calls.isEmpty()); // 契约:至少一个 call(空 batch 永不发 succeeded,调用方会挂起)
|
||||
Q_ASSERT(isFailure_); // 契约:必须提供失败谓词(否则首个 finished 即 bad_function_call)
|
||||
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; // 此后 remaining_ 不再维护:迟到 finished 全被入口守卫挡掉
|
||||
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
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
#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,16 +6,13 @@
|
|||
namespace geopro::net {
|
||||
|
||||
ApiCall::ApiCall(QNetworkReply* reply, QObject* parent) : IApiCall(parent), reply_(reply) {
|
||||
Q_ASSERT(reply); // 契约:不接受 null reply(NAM get/post 正常不返回 null)
|
||||
QObject::connect(reply_, &QNetworkReply::finished, this, &ApiCall::onFinished);
|
||||
}
|
||||
|
||||
void ApiCall::onFinished() {
|
||||
if (aborted_) return; // §5.0 入口守卫:迟到信号闸门
|
||||
QNetworkReply* reply = reply_.data(); // 快照:意图明确 + 防御 reply_ 中途被置空
|
||||
if (!reply) return;
|
||||
ApiResponse resp = buildResponse(reply);
|
||||
reply->deleteLater();
|
||||
ApiResponse resp = buildResponse(reply_);
|
||||
if (reply_) reply_->deleteLater();
|
||||
emit finished(resp);
|
||||
deleteLater();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ 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)
|
||||
|
|
|
|||
|
|
@ -38,19 +38,16 @@ target_sources(geopro_tests PRIVATE data/test_parsers.cpp)
|
|||
target_sources(geopro_tests PRIVATE data/test_local_repo.cpp)
|
||||
target_sources(geopro_tests PRIVATE data/test_nav_dto.cpp)
|
||||
target_sources(geopro_tests PRIVATE data/test_dataset_chart_dto.cpp)
|
||||
target_sources(geopro_tests PRIVATE data/test_dataset_load_handles.cpp)
|
||||
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 Test REQUIRED)
|
||||
find_package(Qt6 COMPONENTS Core Network REQUIRED)
|
||||
target_sources(geopro_tests PRIVATE net/test_rsa.cpp)
|
||||
# 端到端登录连通测试(ApiClient + AuthService),需 Qt6::Core/Network 与事件循环。
|
||||
target_sources(geopro_tests PRIVATE net/test_auth.cpp)
|
||||
# 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)
|
||||
target_link_libraries(geopro_tests PRIVATE geopro_net OpenSSL::SSL OpenSSL::Crypto Qt6::Core Qt6::Network)
|
||||
|
||||
# geopro_data 链 Qt6::Core,测试 exe 运行(含 gtest 发现)需要 Qt6Core.dll 等运行时
|
||||
# DLL 在旁。复用 app 同样的 TARGET_RUNTIME_DLLS POST_BUILD 拷贝。
|
||||
|
|
@ -90,8 +87,6 @@ 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
|
||||
|
|
|
|||
|
|
@ -1,115 +1,31 @@
|
|||
#include <gtest/gtest.h>
|
||||
#include <QSignalSpy>
|
||||
#include "DatasetDetailController.hpp"
|
||||
#include "repo/IAsyncDatasetRepository.hpp"
|
||||
#include "api/DatasetLoadHandles.hpp"
|
||||
#include "repo/IDatasetRepository.hpp"
|
||||
using namespace geopro;
|
||||
|
||||
namespace {
|
||||
// 桩句柄:不声明 Q_OBJECT —— 发射继承自 data::ChartLoad/GridLoad 的信号、override abort。
|
||||
struct StubChartLoad : data::ChartLoad {
|
||||
bool aborted = false;
|
||||
void abort() override { aborted = true; }
|
||||
void fireDone() { emit done(data::ChartParts{}); }
|
||||
void fireFailed() { emit failed(QStringLiteral("x")); }
|
||||
};
|
||||
struct StubGridLoad : data::GridLoad {
|
||||
bool aborted = false;
|
||||
void abort() override { aborted = true; }
|
||||
void fireDone() { emit done(data::GridParts{}); }
|
||||
void fireFailed() { emit failed(QStringLiteral("x")); }
|
||||
};
|
||||
struct StubAsyncRepo : data::IAsyncDatasetRepository {
|
||||
StubChartLoad* lastChart = nullptr;
|
||||
StubGridLoad* lastGrid = nullptr;
|
||||
data::ChartLoad* loadChartAsync(const std::string&) override {
|
||||
lastChart = new StubChartLoad; return lastChart;
|
||||
}
|
||||
data::GridLoad* loadGridAsync(const std::string&) override {
|
||||
lastGrid = new StubGridLoad; return lastGrid;
|
||||
}
|
||||
struct StubRepo : data::IDatasetRepository {
|
||||
bool fail = false;
|
||||
std::vector<data::GsNode> loadStructure() override { return {}; }
|
||||
core::Grid loadGrid(const std::string&) override { core::Grid g(2,2); g.x={0,1}; g.y={0,1}; return g; }
|
||||
// openDataset 现只拉 scatter/scatterScale/anomalies(网格懒加载),失败路径由 loadScatter 抛出触发。
|
||||
core::ScatterField loadScatter(const std::string&) override { if (fail) throw std::runtime_error("x"); return {}; }
|
||||
core::ColorScale loadColorScale(const std::string&) override { return {}; }
|
||||
core::ColorScale loadScatterColorScale(const std::string&) override { return {}; }
|
||||
std::vector<core::Anomaly> loadAnomalies(const std::string&) override { return {}; }
|
||||
};
|
||||
}
|
||||
|
||||
TEST(DatasetDetailController, EmitsChartReadyOnDone) {
|
||||
StubAsyncRepo repo;
|
||||
TEST(DatasetDetailController, EmitsChartReadyOnSuccess) {
|
||||
StubRepo repo;
|
||||
controller::DatasetDetailController c(repo);
|
||||
QSignalSpy spy(&c, &controller::DatasetDetailController::chartReady);
|
||||
c.openDataset("ds1", "dd_inversion_data");
|
||||
repo.lastChart->fireDone();
|
||||
EXPECT_EQ(spy.count(), 1);
|
||||
}
|
||||
|
||||
TEST(DatasetDetailController, EmitsLoadFailedOnFailed) {
|
||||
StubAsyncRepo repo;
|
||||
TEST(DatasetDetailController, EmitsLoadFailedOnThrow) {
|
||||
StubRepo repo; repo.fail = true;
|
||||
controller::DatasetDetailController c(repo);
|
||||
QSignalSpy spy(&c, &controller::DatasetDetailController::loadFailed);
|
||||
c.openDataset("ds1", "dd_inversion_data");
|
||||
repo.lastChart->fireFailed();
|
||||
EXPECT_EQ(spy.count(), 1);
|
||||
}
|
||||
|
||||
TEST(DatasetDetailController, UnsupportedTypeFailsImmediately) {
|
||||
StubAsyncRepo repo;
|
||||
controller::DatasetDetailController c(repo);
|
||||
QSignalSpy spy(&c, &controller::DatasetDetailController::loadFailed);
|
||||
c.openDataset("ds1", "dd_other");
|
||||
EXPECT_EQ(spy.count(), 1);
|
||||
EXPECT_EQ(repo.lastChart, nullptr); // 未发起加载
|
||||
}
|
||||
|
||||
TEST(DatasetDetailController, AbortsPreviousOnReopen) {
|
||||
StubAsyncRepo repo;
|
||||
controller::DatasetDetailController c(repo);
|
||||
c.openDataset("dsA", "dd_inversion_data");
|
||||
StubChartLoad* a = repo.lastChart;
|
||||
c.openDataset("dsB", "dd_inversion_data"); // 替换
|
||||
EXPECT_TRUE(a->aborted); // 旧句柄被 abort
|
||||
}
|
||||
|
||||
TEST(DatasetDetailController, DropsLateSignalFromAbortedLoad) {
|
||||
StubAsyncRepo repo;
|
||||
controller::DatasetDetailController c(repo);
|
||||
QSignalSpy spy(&c, &controller::DatasetDetailController::chartReady);
|
||||
c.openDataset("dsA", "dd_inversion_data");
|
||||
StubChartLoad* a = repo.lastChart;
|
||||
c.openDataset("dsB", "dd_inversion_data");
|
||||
StubChartLoad* b = repo.lastChart;
|
||||
a->fireDone(); // 旧句柄迟到 → 身份比对丢弃
|
||||
EXPECT_EQ(spy.count(), 0);
|
||||
b->fireDone(); // 当前句柄 → 正常
|
||||
EXPECT_EQ(spy.count(), 1);
|
||||
}
|
||||
|
||||
TEST(DatasetDetailController, EmitsGridReadyOnDone) {
|
||||
StubAsyncRepo repo;
|
||||
controller::DatasetDetailController c(repo);
|
||||
QSignalSpy spy(&c, &controller::DatasetDetailController::gridReady);
|
||||
c.loadGridData("ds1", "dd_inversion_data");
|
||||
repo.lastGrid->fireDone();
|
||||
EXPECT_EQ(spy.count(), 1);
|
||||
}
|
||||
|
||||
TEST(DatasetDetailController, EmitsLoadFailedOnGridFailed) {
|
||||
StubAsyncRepo repo;
|
||||
controller::DatasetDetailController c(repo);
|
||||
QSignalSpy spy(&c, &controller::DatasetDetailController::loadFailed);
|
||||
c.loadGridData("ds1", "dd_inversion_data");
|
||||
repo.lastGrid->fireFailed();
|
||||
EXPECT_EQ(spy.count(), 1);
|
||||
}
|
||||
|
||||
TEST(DatasetDetailController, DropsLateGridSignalFromAbortedLoad) {
|
||||
StubAsyncRepo repo;
|
||||
controller::DatasetDetailController c(repo);
|
||||
QSignalSpy spy(&c, &controller::DatasetDetailController::gridReady);
|
||||
c.loadGridData("dsA", "dd_inversion_data");
|
||||
StubGridLoad* a = repo.lastGrid;
|
||||
c.loadGridData("dsB", "dd_inversion_data"); // 替换 → 旧句柄被 abort
|
||||
StubGridLoad* b = repo.lastGrid;
|
||||
EXPECT_TRUE(a->aborted);
|
||||
a->fireDone(); // 旧句柄迟到 → 身份比对丢弃
|
||||
EXPECT_EQ(spy.count(), 0);
|
||||
b->fireDone(); // 当前句柄 → 正常
|
||||
EXPECT_EQ(spy.count(), 1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,65 +0,0 @@
|
|||
#include <gtest/gtest.h>
|
||||
#include <stdexcept>
|
||||
#include <QSignalSpy>
|
||||
#include "api/DatasetLoadHandles.hpp"
|
||||
#include "net/FakeApiCall.hpp"
|
||||
|
||||
using namespace geopro::data;
|
||||
using geopro::net::ApiBatch;
|
||||
using geopro::net::ApiResponse;
|
||||
using geopro::net::test::FakeApiCall;
|
||||
|
||||
namespace {
|
||||
ApiResponse ok() { ApiResponse r; r.code = 200; r.httpStatus = 200; 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(); };
|
||||
}
|
||||
|
||||
TEST(DatasetLoadHandles, ChartLoadEmitsDoneOnSuccess) {
|
||||
auto* a = new FakeApiCall;
|
||||
auto* b = new FakeApiCall;
|
||||
auto* batch = new ApiBatch({a, b}, isFailure);
|
||||
bool parsed = false;
|
||||
auto* load = new ApiChartLoad(batch, [&](const QList<ApiResponse>& resps) {
|
||||
parsed = (resps.size() == 2);
|
||||
return ChartParts{};
|
||||
});
|
||||
QSignalSpy doneSpy(load, &ChartLoad::done);
|
||||
a->fire(ok());
|
||||
b->fire(ok());
|
||||
EXPECT_EQ(doneSpy.count(), 1);
|
||||
EXPECT_TRUE(parsed);
|
||||
}
|
||||
|
||||
TEST(DatasetLoadHandles, ChartLoadEmitsFailedOnBatchFailure) {
|
||||
auto* a = new FakeApiCall;
|
||||
auto* b = new FakeApiCall;
|
||||
auto* batch = new ApiBatch({a, b}, isFailure);
|
||||
auto* load = new ApiChartLoad(batch, [](const QList<ApiResponse>&) { return ChartParts{}; });
|
||||
QSignalSpy failSpy(load, &ChartLoad::failed);
|
||||
a->fire(bad());
|
||||
EXPECT_EQ(failSpy.count(), 1);
|
||||
}
|
||||
|
||||
TEST(DatasetLoadHandles, ChartLoadEmitsFailedWhenParseThrows) {
|
||||
auto* a = new FakeApiCall;
|
||||
auto* batch = new ApiBatch({a}, isFailure);
|
||||
auto* load = new ApiChartLoad(batch, [](const QList<ApiResponse>&) -> ChartParts {
|
||||
throw std::runtime_error("parse boom");
|
||||
});
|
||||
QSignalSpy doneSpy(load, &ChartLoad::done);
|
||||
QSignalSpy failSpy(load, &ChartLoad::failed);
|
||||
a->fire(ok()); // batch 成功 → parse 抛异常 → failed(emit done 已移出 try)
|
||||
EXPECT_EQ(doneSpy.count(), 0);
|
||||
EXPECT_EQ(failSpy.count(), 1);
|
||||
}
|
||||
|
||||
TEST(DatasetLoadHandles, GridLoadAbortSuppressesLateDone) {
|
||||
auto* a = new FakeApiCall;
|
||||
auto* batch = new ApiBatch({a}, isFailure);
|
||||
auto* load = new ApiGridLoad(batch, [](const QList<ApiResponse>&) { return GridParts{}; });
|
||||
QSignalSpy doneSpy(load, &GridLoad::done);
|
||||
load->abort();
|
||||
a->fire(ok()); // 迟到
|
||||
EXPECT_EQ(doneSpy.count(), 0); // batch.aborted_ + load.aborted_ 双闸门
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
#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
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
#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, SucceedsWithSingleCall) {
|
||||
auto* a = new FakeApiCall;
|
||||
auto* batch = new ApiBatch({a}, isFailure);
|
||||
QSignalSpy okSpy(batch, &ApiBatch::succeeded);
|
||||
a->fire(ok()); // N=1:一次 fire 即触发 --remaining_==0
|
||||
EXPECT_EQ(okSpy.count(), 1);
|
||||
}
|
||||
|
||||
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