geopro/tests/controller/test_workbench_nav_controll...

222 lines
9.4 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include <gtest/gtest.h>
#include <QSignalSpy>
#include <QVariant>
#include <vector>
#include "WorkbenchNavController.hpp"
#include "api/NavLoads.hpp"
#include "api/NavRequest.hpp"
#include "repo/IAsyncProjectRepository.hpp"
using namespace geopro;
namespace {
// 桩句柄:不声明 Q_OBJECT —— 发射继承自 data::NavRequest 的 done/failed、override abort 记录。
struct StubNavRequest : data::NavRequest {
bool aborted = false;
void abort() override { aborted = true; }
void fireDone(const QVariant& v) { emit done(v); }
void fireFailed() { emit failed(QStringLiteral("x")); }
};
struct StubAsyncRepo : data::IAsyncProjectRepository {
StubNavRequest* lastWorkspaces = nullptr;
StubNavRequest* lastSwitchWs = nullptr;
StubNavRequest* lastProjects = nullptr;
StubNavRequest* lastStructure = nullptr;
StubNavRequest* lastData = nullptr;
StubNavRequest* lastFile = nullptr;
StubNavRequest* lastDetail = nullptr;
StubNavRequest* lastDataset = nullptr;
std::vector<StubNavRequest*> exceptions; // setCheckedTms 并发批
data::NavRequest* listWorkspacesAsync() override { return lastWorkspaces = new StubNavRequest; }
data::NavRequest* switchWorkspaceAsync(const std::string&) override {
return lastSwitchWs = new StubNavRequest;
}
data::NavRequest* pageProjectsAsync(const std::string&, const std::string&, int, int) override {
return lastProjects = new StubNavRequest;
}
data::NavRequest* listProjectTypesAsync() override { return new StubNavRequest; }
data::NavRequest* loadStructureAsync(const std::string&) override {
return lastStructure = new StubNavRequest;
}
data::NavRequest* loadRowsAsync(const std::string&, const std::string&, int, int classifyType,
int) override {
auto* r = new StubNavRequest;
if (classifyType == 1)
lastFile = r;
else
lastData = r;
return r;
}
data::NavRequest* loadObjectDetailAsync(const std::string&, int) override {
return lastDetail = new StubNavRequest;
}
data::NavRequest* loadDatasetFormAsync(const std::string&) override {
return lastDataset = new StubNavRequest;
}
data::NavRequest* loadExceptionsByTmAsync(const std::string&) override {
auto* r = new StubNavRequest;
exceptions.push_back(r);
return r;
}
};
QVariant wsVar() {
return QVariant::fromValue(std::vector<data::Workspace>{{"w1", "WS", 2, true}});
}
QVariant pageVar() {
data::ProjectSummary p;
p.id = "p1";
p.name = "P1";
return QVariant::fromValue(data::ProjectListPage{{p}, 1});
}
QVariant emptyPageVar() { return QVariant::fromValue(data::ProjectListPage{{}, 0}); }
QVariant nodesVar() { return QVariant::fromValue(std::vector<data::StructNode>{}); }
QVariant dsPageVar() { return QVariant::fromValue(data::DsPage{{}, 0}); }
QVariant formVar() { return QVariant::fromValue(data::DynamicForm{}); }
QVariant exVar() { return QVariant::fromValue(std::vector<data::ExceptionRow>{}); }
} // namespace
// start() 依赖链workspaces → projects → structure逐级 emit 既有信号。
TEST(WorkbenchNavController, StartChainEmitsWorkspacesThenProjectsThenStructure) {
StubAsyncRepo repo;
controller::WorkbenchNavController c(repo);
QSignalSpy wsSpy(&c, &controller::WorkbenchNavController::workspacesLoaded);
QSignalSpy psSpy(&c, &controller::WorkbenchNavController::projectsLoaded);
QSignalSpy stSpy(&c, &controller::WorkbenchNavController::structureLoaded);
c.start();
repo.lastWorkspaces->fireDone(wsVar());
EXPECT_EQ(wsSpy.count(), 1);
repo.lastProjects->fireDone(pageVar());
EXPECT_EQ(psSpy.count(), 1);
repo.lastStructure->fireDone(nodesVar());
EXPECT_EQ(stSpy.count(), 1);
}
// busyChanged 反映在飞发起→true最后完成→false。
TEST(WorkbenchNavController, BusyChangedReflectsInflight) {
StubAsyncRepo repo;
controller::WorkbenchNavController c(repo);
QSignalSpy busySpy(&c, &controller::WorkbenchNavController::busyChanged);
c.start();
ASSERT_GE(busySpy.count(), 1);
EXPECT_TRUE(busySpy.takeFirst().at(0).toBool()); // 首次 true
repo.lastWorkspaces->fireDone(wsVar());
repo.lastProjects->fireDone(pageVar());
repo.lastStructure->fireDone(nodesVar());
EXPECT_FALSE(busySpy.last().at(0).toBool()); // 末尾 false
}
// 空项目链projects 空 → structure 发空树 → busy 复位(不发结构请求)。
TEST(WorkbenchNavController, StartWithNoProjectsEmitsEmptyStructureAndClearsBusy) {
StubAsyncRepo repo;
controller::WorkbenchNavController c(repo);
QSignalSpy stSpy(&c, &controller::WorkbenchNavController::structureLoaded);
QSignalSpy busySpy(&c, &controller::WorkbenchNavController::busyChanged);
c.start();
repo.lastWorkspaces->fireDone(wsVar());
repo.lastProjects->fireDone(emptyPageVar());
EXPECT_EQ(stSpy.count(), 1);
EXPECT_FALSE(busySpy.last().at(0).toBool());
}
// setCheckedTms新勾选 abort 旧异常批(以最后一次为准)。
TEST(WorkbenchNavController, SetCheckedTmsAbortsPreviousBatch) {
StubAsyncRepo repo;
controller::WorkbenchNavController c(repo);
c.setCheckedTms({"tmA"});
StubNavRequest* a = repo.exceptions.back();
c.setCheckedTms({"tmB"}); // 覆盖
EXPECT_TRUE(a->aborted);
}
// setCheckedTms全命中缓存 → 不发新请求、直接组装 emit。
TEST(WorkbenchNavController, SetCheckedTmsUsesCacheWithoutRequest) {
StubAsyncRepo repo;
controller::WorkbenchNavController c(repo);
QSignalSpy exSpy(&c, &controller::WorkbenchNavController::exceptionTreeLoaded);
c.setCheckedTms({"tmA"}); // 首次未命中 → 发请求
ASSERT_EQ(repo.exceptions.size(), 1u);
repo.exceptions.back()->fireDone(exVar()); // 写缓存 + emit
EXPECT_EQ(exSpy.count(), 1);
c.setCheckedTms({"tmA"}); // 第二次命中缓存 → 不发新请求
EXPECT_EQ(repo.exceptions.size(), 1u);
EXPECT_EQ(exSpy.count(), 2); // 仍 emit
}
// 回灌防护abort 后旧句柄迟到 done 被身份比对丢弃。
TEST(WorkbenchNavController, DropsLateStructureAfterProjectSwitch) {
StubAsyncRepo repo;
controller::WorkbenchNavController c(repo);
QSignalSpy stSpy(&c, &controller::WorkbenchNavController::structureLoaded);
c.switchProject("pA");
StubNavRequest* a = repo.lastStructure;
c.switchProject("pB");
StubNavRequest* b = repo.lastStructure;
EXPECT_TRUE(a->aborted); // 旧句柄被 abort
a->fireDone(nodesVar()); // 旧 → 丢弃
EXPECT_EQ(stSpy.count(), 0);
b->fireDone(nodesVar()); // 新 → 正常
EXPECT_EQ(stSpy.count(), 1);
}
// selectObject 三并发data/file/detail 各自完成 → 各发对应信号。
TEST(WorkbenchNavController, SelectObjectConcurrentEmitsAllThree) {
StubAsyncRepo repo;
controller::WorkbenchNavController c(repo);
QSignalSpy dsSpy(&c, &controller::WorkbenchNavController::datasetsLoaded);
QSignalSpy flSpy(&c, &controller::WorkbenchNavController::filesLoaded);
QSignalSpy dtSpy(&c, &controller::WorkbenchNavController::objectDetailLoaded);
c.selectObject("obj1", 1);
repo.lastData->fireDone(dsPageVar());
repo.lastFile->fireDone(dsPageVar());
repo.lastDetail->fireDone(formVar());
EXPECT_EQ(dsSpy.count(), 1);
EXPECT_EQ(flSpy.count(), 1);
EXPECT_EQ(dtSpy.count(), 1);
}
// selectDataset 单请求 → datasetDetailLoaded。
TEST(WorkbenchNavController, SelectDatasetEmitsDetail) {
StubAsyncRepo repo;
controller::WorkbenchNavController c(repo);
QSignalSpy spy(&c, &controller::WorkbenchNavController::datasetDetailLoaded);
c.selectDataset("ds1");
repo.lastDataset->fireDone(formVar());
EXPECT_EQ(spy.count(), 1);
}
// selectObject 三并发部分失败data 失败、file/detail 成功 → loadFailed×1filesLoaded×1objectDetailLoaded×1datasetsLoaded×0。
TEST(WorkbenchNavController, SelectObjectOneFailureEmitsPartialResults) {
StubAsyncRepo repo;
controller::WorkbenchNavController c(repo);
QSignalSpy dsSpy(&c, &controller::WorkbenchNavController::datasetsLoaded);
QSignalSpy flSpy(&c, &controller::WorkbenchNavController::filesLoaded);
QSignalSpy dtSpy(&c, &controller::WorkbenchNavController::objectDetailLoaded);
QSignalSpy failSpy(&c, &controller::WorkbenchNavController::loadFailed);
c.selectObject("obj2", 1);
// data 路失败file/detail 路成功(三路独立,互不影响)
repo.lastData->fireFailed();
repo.lastFile->fireDone(dsPageVar());
repo.lastDetail->fireDone(formVar());
EXPECT_EQ(dsSpy.count(), 0); // data 失败,无 datasetsLoaded
EXPECT_EQ(flSpy.count(), 1); // file 成功,有 filesLoaded
EXPECT_EQ(dtSpy.count(), 1); // detail 成功,有 objectDetailLoaded
EXPECT_EQ(failSpy.count(), 1); // 只有 data 路触发 loadFailed
EXPECT_EQ(failSpy.first().at(0).toString(), QStringLiteral("datasets"));
}
// 失败路径start 首级失败 → loadFailed + busy 复位。
TEST(WorkbenchNavController, StartWorkspacesFailureEmitsLoadFailed) {
StubAsyncRepo repo;
controller::WorkbenchNavController c(repo);
QSignalSpy failSpy(&c, &controller::WorkbenchNavController::loadFailed);
QSignalSpy busySpy(&c, &controller::WorkbenchNavController::busyChanged);
c.start();
repo.lastWorkspaces->fireFailed();
EXPECT_EQ(failSpy.count(), 1);
EXPECT_FALSE(busySpy.last().at(0).toBool());
}