210 lines
9.1 KiB
C++
210 lines
9.1 KiB
C++
#include "zeroerr/assert.h"
|
|
#include "zeroerr/unittest.h"
|
|
|
|
#include "editor/EditorAuthority.h"
|
|
#include "editor/EditorCommandService.h"
|
|
#include "editor/EditorSession.h"
|
|
#include "editor/EditorTransactionService.h"
|
|
|
|
#include <cstdint>
|
|
#include <string>
|
|
|
|
using namespace eve::editor;
|
|
|
|
namespace {
|
|
|
|
class IntegerOperationTarget final : public IDomainOperationTarget {
|
|
public:
|
|
explicit IntegerOperationTarget(std::string id, int value = 0) : id_(std::move(id)), value_(value) {}
|
|
|
|
const std::string& targetId() const override { return id_; }
|
|
unsigned long long revision() const override { return revision_; }
|
|
EditRegion dirtyRegion() const override { return dirty_; }
|
|
void clearDirtyRegion() override { dirty_.clear(); }
|
|
|
|
EditorResult<void> applyDomainOperation(const DomainOperation& operation) override {
|
|
if (operation.type == "test.fail.v1")
|
|
return EditorResult<void>::error(EditorStatus::Rejected, RuleId("test.operation.failure"),
|
|
"Synthetic operation failure");
|
|
if (operation.type != "test.integer.set.v1")
|
|
return EditorResult<void>::error(EditorStatus::Unsupported, RuleId("test.operation.unsupported"),
|
|
"Unsupported operation type");
|
|
const auto* value = operation.payload.getIf<std::int64_t>();
|
|
if (!value)
|
|
return EditorResult<void>::error(EditorStatus::Rejected, RuleId("test.operation.payload"),
|
|
"Integer payload required");
|
|
value_ = static_cast<int>(*value);
|
|
++revision_;
|
|
dirty_.include(0, 0);
|
|
return EditorResult<void>::applied();
|
|
}
|
|
|
|
int value() const { return value_; }
|
|
|
|
private:
|
|
std::string id_;
|
|
int value_ = 0;
|
|
unsigned long long revision_ = 0;
|
|
EditRegion dirty_;
|
|
};
|
|
|
|
DomainOperation setInteger(const IntegerOperationTarget& target, int before, int after) {
|
|
DomainOperation operation;
|
|
operation.type = "test.integer.set.v1";
|
|
operation.target = TargetId(target.targetId());
|
|
operation.payload = EditorValue(after);
|
|
operation.inverse = EditorValue(before);
|
|
operation.hasInverse = true;
|
|
operation.affectedProperties.push_back("value");
|
|
return operation;
|
|
}
|
|
|
|
TransactionSpec transaction(const IntegerOperationTarget& target, const char* id) {
|
|
TransactionSpec specification;
|
|
specification.id = TransactionId(id);
|
|
specification.label = "Set integer";
|
|
specification.target = TargetId(target.targetId());
|
|
specification.baseRevision = target.revision();
|
|
return specification;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("editor.v2.authority_preflight_commit_and_compensate") {
|
|
IntegerOperationTarget target("integer", 1);
|
|
LocalWorldAuthority authority(&target);
|
|
DomainOperation operation = setInteger(target, 1, 5);
|
|
TransactionSpec specification = transaction(target, "transaction.one");
|
|
|
|
auto plan = authority.preflight(specification, std::span<const DomainOperation>(&operation, 1));
|
|
CHECK(plan.accepted());
|
|
CHECK_EQ(target.value(), 1);
|
|
auto committed = authority.commit(*plan.value);
|
|
CHECK(committed.accepted());
|
|
CHECK_EQ(target.value(), 5);
|
|
CHECK_EQ(committed.value->beforeRevision, static_cast<Revision>(0));
|
|
CHECK_EQ(committed.value->afterRevision, static_cast<Revision>(1));
|
|
|
|
auto compensated = authority.compensate(*committed.value);
|
|
CHECK(compensated.accepted());
|
|
CHECK_EQ(target.value(), 1);
|
|
CHECK_EQ(compensated.value->afterRevision, static_cast<Revision>(2));
|
|
|
|
auto conflict = authority.preflight(specification, std::span<const DomainOperation>(&operation, 1));
|
|
CHECK_EQ(static_cast<int>(conflict.status), static_cast<int>(EditorStatus::Conflict));
|
|
}
|
|
|
|
TEST_CASE("editor.v2.authority_rolls_back_partial_commit") {
|
|
IntegerOperationTarget target("integer");
|
|
LocalWorldAuthority authority(&target);
|
|
DomainOperation first = setInteger(target, 0, 9);
|
|
DomainOperation failure;
|
|
failure.type = "test.fail.v1";
|
|
failure.target = TargetId(target.targetId());
|
|
const DomainOperation operations[] = {first, failure};
|
|
|
|
auto plan = authority.preflight(transaction(target, "transaction.rollback"), operations);
|
|
CHECK(plan.accepted());
|
|
auto result = authority.commit(*plan.value);
|
|
CHECK(!result.accepted());
|
|
CHECK_EQ(target.value(), 0);
|
|
CHECK(result.value.has_value());
|
|
CHECK_EQ(static_cast<int>(result.value->state), static_cast<int>(TransactionState::Rejected));
|
|
}
|
|
|
|
TEST_CASE("editor.v2.local_transaction_backend_undo_and_redo") {
|
|
IntegerOperationTarget target("integer");
|
|
LocalWorldAuthority authority(&target);
|
|
LocalTransactionBackend backend(&authority);
|
|
|
|
CHECK(backend.begin(transaction(target, "transaction.history")).accepted());
|
|
CHECK(backend.append(setInteger(target, 0, 7)).accepted());
|
|
auto committed = backend.commit();
|
|
CHECK(committed.accepted());
|
|
CHECK_EQ(target.value(), 7);
|
|
CHECK(backend.canUndo());
|
|
|
|
auto undone = backend.undo();
|
|
CHECK(undone.accepted());
|
|
CHECK_EQ(target.value(), 0);
|
|
CHECK(backend.canRedo());
|
|
|
|
auto redone = backend.redo();
|
|
CHECK(redone.accepted());
|
|
CHECK_EQ(target.value(), 7);
|
|
}
|
|
|
|
TEST_CASE("editor.v2.planned_command_is_dry_run_and_revision_safe") {
|
|
IntegerOperationTarget target("integer", 2);
|
|
LocalWorldAuthority authority(&target);
|
|
LocalTransactionBackend backend(&authority);
|
|
EditorCommandService commands;
|
|
|
|
CommandDescriptor descriptor;
|
|
descriptor.id = CommandId("test.integer.set");
|
|
descriptor.ownerModule = "test";
|
|
CHECK(commands
|
|
.registerPlannedCommand(
|
|
descriptor,
|
|
[&](const CommandRequest& request) {
|
|
const auto* after = request.payload.getIf<std::int64_t>();
|
|
if (!after)
|
|
return EditorResult<CommandPlan>::error(
|
|
EditorStatus::Rejected, RuleId("test.command.payload"), "Integer payload required");
|
|
CommandPlan plan;
|
|
plan.operations.push_back(setInteger(target, target.value(), static_cast<int>(*after)));
|
|
return EditorResult<CommandPlan>::applied(std::move(plan));
|
|
},
|
|
[&](const CommandRequest&, const CommandPlan& plan) {
|
|
TransactionSpec specification;
|
|
specification.id = TransactionId(plan.id.value());
|
|
specification.label = "Planned integer change";
|
|
specification.target = plan.target;
|
|
specification.baseRevision = plan.baseRevision;
|
|
auto begun = backend.begin(std::move(specification));
|
|
if (!begun.accepted())
|
|
return EditorResult<TransactionReceipt>::error(begun.status, RuleId("test.command.begin"),
|
|
"Could not begin transaction");
|
|
for (const DomainOperation& operation : plan.operations) {
|
|
auto appended = backend.append(operation);
|
|
if (!appended.accepted()) {
|
|
backend.rollback();
|
|
return EditorResult<TransactionReceipt>::error(
|
|
appended.status, RuleId("test.command.append"), "Could not append operation");
|
|
}
|
|
}
|
|
return backend.commit();
|
|
})
|
|
.accepted());
|
|
|
|
EditorSession session;
|
|
session.setSessionId(SessionId("test-session"));
|
|
session.setCommandService(&commands);
|
|
session.bindTarget(&target);
|
|
|
|
auto plan = session.planCommand(descriptor.id, EditorValue(11), CommandSource::Script, target.revision());
|
|
CHECK(plan.accepted());
|
|
CHECK_EQ(target.value(), 2);
|
|
auto executed = session.executePlan(*plan.value, EditorValue(11), CommandSource::Script);
|
|
CHECK(executed.accepted());
|
|
CHECK_EQ(target.value(), 11);
|
|
CHECK_EQ(static_cast<int>(executed.value->state), static_cast<int>(TransactionState::Committed));
|
|
|
|
auto stalePlan = session.planCommand(descriptor.id, EditorValue(15));
|
|
CHECK(stalePlan.accepted());
|
|
DomainOperation external = setInteger(target, 11, 12);
|
|
CHECK(target.applyDomainOperation(external).accepted());
|
|
auto stale = session.executePlan(*stalePlan.value, EditorValue(15));
|
|
CHECK_EQ(static_cast<int>(stale.status), static_cast<int>(EditorStatus::Conflict));
|
|
CHECK_EQ(target.value(), 12);
|
|
}
|
|
|
|
TEST_CASE("editor.v2.read_only_authority_rejects_mutation") {
|
|
ReadOnlyAuthority authority;
|
|
TransactionSpec specification;
|
|
specification.id = TransactionId("read-only");
|
|
specification.target = TargetId("target");
|
|
auto result = authority.preflight(specification, {});
|
|
CHECK_EQ(static_cast<int>(result.status), static_cast<int>(EditorStatus::Rejected));
|
|
}
|