Refactor condition logic to enable testing

This commit is contained in:
WarmUpTill 2024-08-01 21:24:30 +02:00 committed by WarmUpTill
parent d80df57ef8
commit 24f33fb0d2
9 changed files with 262 additions and 193 deletions

View File

@ -155,6 +155,8 @@ target_sources(
lib/queue/action-queue-tab.hpp
lib/utils/backup.cpp
lib/utils/backup.hpp
lib/utils/condition-logic.cpp
lib/utils/condition-logic.hpp
lib/utils/curl-helper.cpp
lib/utils/curl-helper.hpp
lib/utils/cursor-shape-changer.cpp

View File

@ -1,5 +1,6 @@
#pragma once
#include "macro-segment-list.hpp"
#include "condition-logic.hpp"
#include "log-helper.hpp"
#include <ui_advanced-scene-switcher.h>
@ -12,7 +13,6 @@ class MacroActionEdit;
class MacroConditionEdit;
class Duration;
class SequenceWidget;
enum class LogicType;
struct SceneGroup;
/*******************************************************************************
@ -174,7 +174,7 @@ public slots:
void MacroConditionReorder(int to, int target);
void AddMacroCondition(int idx);
void AddMacroCondition(Macro *macro, int idx, const std::string &id,
obs_data_t *data, LogicType logic);
obs_data_t *data, Logic::Type logic);
void RemoveMacroCondition(int idx);
void MoveMacroConditionUp(int idx);
void MoveMacroConditionDown(int idx);

View File

@ -10,26 +10,6 @@
namespace advss {
static inline void populateLogicSelection(QComboBox *list, bool root = false)
{
if (root) {
for (const auto &entry : MacroCondition::logicTypes) {
if (static_cast<int>(entry.first) < logic_root_offset) {
list->addItem(obs_module_text(
entry.second._name.c_str()));
}
}
} else {
for (const auto &entry : MacroCondition::logicTypes) {
if (static_cast<int>(entry.first) >=
logic_root_offset) {
list->addItem(obs_module_text(
entry.second._name.c_str()));
}
}
}
}
static inline void populateConditionSelection(QComboBox *list)
{
for (const auto &[_, condition] :
@ -120,13 +100,13 @@ void DurationModifierEdit::Collapse(bool collapse)
MacroConditionEdit::MacroConditionEdit(
QWidget *parent, std::shared_ptr<MacroCondition> *entryData,
const std::string &id, bool root)
const std::string &id, bool isRootCondition)
: MacroSegmentEdit(parent),
_logicSelection(new QComboBox()),
_conditionSelection(new FilterComboBox()),
_dur(new DurationModifierEdit()),
_entryData(entryData),
_isRoot(root)
_isRoot(isRootCondition)
{
QWidget::connect(_logicSelection, SIGNAL(currentIndexChanged(int)),
this, SLOT(LogicSelectionChanged(int)));
@ -139,7 +119,7 @@ MacroConditionEdit::MacroConditionEdit(
this,
SLOT(DurationModifierChanged(DurationModifier::Type)));
populateLogicSelection(_logicSelection, root);
Logic::PopulateLogicTypeSelection(_logicSelection, isRootCondition);
populateConditionSelection(_conditionSelection);
_section->AddHeaderWidget(_logicSelection);
@ -168,15 +148,10 @@ void MacroConditionEdit::LogicSelectionChanged(int idx)
return;
}
LogicType type;
if (IsRootNode()) {
type = static_cast<LogicType>(idx);
} else {
type = static_cast<LogicType>(idx + logic_root_offset);
}
auto lock = LockContext();
(*_entryData)->SetLogicType(type);
const auto logic = static_cast<Logic::Type>(
_logicSelection->itemData(idx).toInt());
(*_entryData)->SetLogicType(logic);
}
bool MacroConditionEdit::IsRootNode()
@ -186,13 +161,9 @@ bool MacroConditionEdit::IsRootNode()
void MacroConditionEdit::SetLogicSelection()
{
auto logic = (*_entryData)->GetLogicType();
if (IsRootNode()) {
_logicSelection->setCurrentIndex(static_cast<int>(logic));
} else {
_logicSelection->setCurrentIndex(static_cast<int>(logic) -
logic_root_offset);
}
const auto logic = (*_entryData)->GetLogicType();
_logicSelection->setCurrentIndex(
_logicSelection->findData(static_cast<int>(logic)));
}
void MacroConditionEdit::SetRootNode(bool root)
@ -200,7 +171,7 @@ void MacroConditionEdit::SetRootNode(bool root)
_isRoot = root;
const QSignalBlocker blocker(_logicSelection);
_logicSelection->clear();
populateLogicSelection(_logicSelection, root);
Logic::PopulateLogicTypeSelection(_logicSelection, root);
SetLogicSelection();
}
@ -301,17 +272,17 @@ void AdvSceneSwitcher::AddMacroCondition(int idx)
}
std::string id;
LogicType logic;
Logic::Type logic;
if (idx >= 1) {
id = macro->Conditions().at(idx - 1)->GetId();
if (idx == 1) {
logic = LogicType::OR;
logic = Logic::Type::OR;
} else {
logic = macro->Conditions().at(idx - 1)->GetLogicType();
}
} else {
id = MacroCondition::GetDefaultID();
logic = LogicType::ROOT_NONE;
logic = Logic::Type::ROOT_NONE;
}
OBSDataAutoRelease data;
@ -324,7 +295,7 @@ void AdvSceneSwitcher::AddMacroCondition(int idx)
void AdvSceneSwitcher::AddMacroCondition(Macro *macro, int idx,
const std::string &id,
obs_data_t *data, LogicType logic)
obs_data_t *data, Logic::Type logic)
{
if (idx < 0 || idx > (int)macro->Conditions().size()) {
assert(false);
@ -389,7 +360,7 @@ void AdvSceneSwitcher::RemoveMacroCondition(int idx)
macro->UpdateConditionIndices();
if (idx == 0 && macro->Conditions().size() > 0) {
auto newRoot = macro->Conditions().at(0);
newRoot->SetLogicType(LogicType::ROOT_NONE);
newRoot->SetLogicType(Logic::Type::ROOT_NONE);
static_cast<MacroConditionEdit *>(
ui->conditionsList->WidgetAt(0))
->SetRootNode(true);
@ -540,22 +511,23 @@ void AdvSceneSwitcher::MacroConditionReorder(int to, int from)
auto lock = LockContext();
auto condition = macro->Conditions().at(from);
if (to == 0) {
condition->SetLogicType(LogicType::ROOT_NONE);
condition->SetLogicType(Logic::Type::ROOT_NONE);
static_cast<MacroConditionEdit *>(
ui->conditionsList->WidgetAt(from))
->SetRootNode(true);
macro->Conditions().at(0)->SetLogicType(LogicType::AND);
macro->Conditions().at(0)->SetLogicType(
Logic::Type::AND);
static_cast<MacroConditionEdit *>(
ui->conditionsList->WidgetAt(0))
->SetRootNode(false);
}
if (from == 0) {
condition->SetLogicType(LogicType::AND);
condition->SetLogicType(Logic::Type::AND);
static_cast<MacroConditionEdit *>(
ui->conditionsList->WidgetAt(from))
->SetRootNode(false);
macro->Conditions().at(1)->SetLogicType(
LogicType::ROOT_NONE);
Logic::Type::ROOT_NONE);
static_cast<MacroConditionEdit *>(
ui->conditionsList->WidgetAt(1))
->SetRootNode(true);

View File

@ -2,16 +2,6 @@
namespace advss {
const std::map<LogicType, LogicTypeInfo> MacroCondition::logicTypes = {
{LogicType::NONE, {"AdvSceneSwitcher.logic.none"}},
{LogicType::AND, {"AdvSceneSwitcher.logic.and"}},
{LogicType::OR, {"AdvSceneSwitcher.logic.or"}},
{LogicType::AND_NOT, {"AdvSceneSwitcher.logic.andNot"}},
{LogicType::OR_NOT, {"AdvSceneSwitcher.logic.orNot"}},
{LogicType::ROOT_NONE, {"AdvSceneSwitcher.logic.rootNone"}},
{LogicType::ROOT_NOT, {"AdvSceneSwitcher.logic.not"}},
};
void DurationModifier::Save(obs_data_t *obj, const char *condName,
const char *duration) const
{
@ -84,7 +74,7 @@ bool MacroCondition::Save(obs_data_t *obj) const
{
MacroSegment::Save(obj);
obs_data_set_string(obj, "id", GetId().c_str());
obs_data_set_int(obj, "logic", static_cast<int>(_logic));
_logic.Save(obj, "logic");
// To avoid conflicts with conditions which also use the Duration class
// save the duration modifier in a separate obj
@ -99,7 +89,7 @@ bool MacroCondition::Save(obs_data_t *obj) const
bool MacroCondition::Load(obs_data_t *obj)
{
MacroSegment::Load(obj);
_logic = static_cast<LogicType>(obs_data_get_int(obj, "logic"));
_logic.Load(obj, "logic");
if (obs_data_has_user_value(obj, "durationModifier")) {
auto durObj = obs_data_get_obj(obj, "durationModifier");
_duration.Load(durObj);
@ -111,6 +101,27 @@ bool MacroCondition::Load(obs_data_t *obj)
return true;
}
void MacroCondition::ValidateLogicSelection(bool isRootCondition,
const char *context)
{
if (_logic.IsValidSelection(isRootCondition)) {
return;
}
if (_logic.IsRootType()) {
_logic.SetType(Logic::Type::ROOT_NONE);
blog(LOG_WARNING,
"setting invalid logic selection to 'if' for macro %s",
context);
return;
}
_logic.SetType(Logic::Type::NONE);
blog(LOG_WARNING,
"setting invalid logic selection to 'ignore' for macro %s",
context);
}
void MacroCondition::ResetDuration()
{
_duration.Reset();

View File

@ -1,34 +1,11 @@
#pragma once
#include "macro-segment.hpp"
#include "condition-logic.hpp"
#include "duration-control.hpp"
#include "macro-ref.hpp"
#include <duration-control.hpp>
namespace advss {
constexpr auto logic_root_offset = 100;
enum class LogicType {
ROOT_NONE = 0,
ROOT_NOT,
ROOT_LAST,
// leave some space for potential expansion
NONE = 100,
AND,
OR,
AND_NOT,
OR_NOT,
LAST,
};
static inline bool isRootLogicType(LogicType l)
{
return static_cast<int>(l) < logic_root_offset;
}
struct LogicTypeInfo {
std::string _name;
};
class DurationModifier {
public:
enum class Type {
@ -65,9 +42,9 @@ public:
virtual bool CheckCondition() = 0;
virtual bool Save(obs_data_t *obj) const = 0;
virtual bool Load(obs_data_t *obj) = 0;
LogicType GetLogicType() { return _logic; }
void SetLogicType(LogicType logic) { _logic = logic; }
static const std::map<LogicType, LogicTypeInfo> logicTypes;
Logic::Type GetLogicType() const { return _logic.GetType(); }
void SetLogicType(const Logic::Type &logic) { _logic.SetType(logic); }
void ValidateLogicSelection(bool isRootCondition, const char *context);
void ResetDuration();
void CheckDurationModifier(bool &val);
DurationModifier GetDurationModifier() { return _duration; }
@ -77,7 +54,7 @@ public:
static std::string_view GetDefaultID();
private:
LogicType _logic = LogicType::ROOT_NONE;
Logic _logic = Logic(Logic::Type::ROOT_NONE);
DurationModifier _duration;
};

View File

@ -61,13 +61,13 @@ void AdvSceneSwitcher::PasteMacroSegment()
const auto condition = std::static_pointer_cast<MacroCondition>(
copyInfo.segment);
auto logic = condition->GetLogicType();
if (logic > LogicType::ROOT_LAST &&
if (logic > Logic::Type::ROOT_LAST &&
macro->Conditions().empty()) {
logic = LogicType::ROOT_NONE;
logic = Logic::Type::ROOT_NONE;
}
if (logic < LogicType::ROOT_LAST &&
if (logic < Logic::Type::ROOT_LAST &&
!macro->Conditions().empty()) {
logic = LogicType::OR;
logic = Logic::Type::OR;
}
AddMacroCondition(macro.get(), macro->Conditions().size(),
copyInfo.segment->GetId(), data.Get(), logic);

View File

@ -17,7 +17,6 @@
namespace advss {
static constexpr int perfLogThreshold = 300;
static std::deque<std::shared_ptr<Macro>> macros;
Macro::Macro(const std::string &name, const bool addHotkey)
@ -101,6 +100,30 @@ void Macro::PrepareMoveToGroup(std::shared_ptr<Macro> group,
}
}
static bool checkCondition(const std::shared_ptr<MacroCondition> &condition)
{
using namespace std::chrono_literals;
static constexpr auto perfLogThreshold = 300ms;
const auto startTime = std::chrono::high_resolution_clock::now();
const bool conditionMatched = condition->CheckCondition();
const auto endTime = std::chrono::high_resolution_clock::now();
const auto timeSpent = endTime - startTime;
if (timeSpent >= perfLogThreshold) {
const long int ms =
std::chrono::duration_cast<std::chrono::milliseconds>(
timeSpent)
.count();
blog(LOG_WARNING,
"spent %ld ms in %s condition check of macro '%s'!", ms,
condition->GetId().c_str(),
condition->GetMacro()->Name().c_str());
}
return conditionMatched;
}
bool Macro::CeckMatch(bool ignorePause)
{
if (_isGroup) {
@ -108,77 +131,35 @@ bool Macro::CeckMatch(bool ignorePause)
}
_matched = false;
for (auto &c : _conditions) {
for (auto &condition : _conditions) {
if (_paused && !ignorePause) {
vblog(LOG_INFO, "Macro %s is paused", _name.c_str());
return false;
}
auto startTime = std::chrono::high_resolution_clock::now();
bool cond = c->CheckCondition();
auto endTime = std::chrono::high_resolution_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
endTime - startTime);
if (ms.count() >= perfLogThreshold) {
blog(LOG_WARNING,
"spent %ld ms in %s condition check of macro '%s'!",
(long int)ms.count(), c->GetId().c_str(),
Name().c_str());
}
bool conditionMatched = checkCondition(condition);
condition->CheckDurationModifier(conditionMatched);
c->CheckDurationModifier(cond);
switch (c->GetLogicType()) {
case LogicType::NONE:
vblog(LOG_INFO,
"ignoring condition check 'none' for '%s'",
_name.c_str());
const auto logicType = condition->GetLogicType();
if (logicType == Logic::Type::NONE) {
vblog(LOG_INFO, "ignoring condition '%s' for '%s'",
condition->GetId().c_str(), _name.c_str());
continue;
case LogicType::AND:
_matched = _matched && cond;
if (cond) {
c->EnableHighlight();
}
break;
case LogicType::OR:
_matched = _matched || cond;
if (cond) {
c->EnableHighlight();
}
break;
case LogicType::AND_NOT:
_matched = _matched && !cond;
if (!cond) {
c->EnableHighlight();
}
break;
case LogicType::OR_NOT:
_matched = _matched || !cond;
if (!cond) {
c->EnableHighlight();
}
break;
case LogicType::ROOT_NONE:
_matched = cond;
if (cond) {
c->EnableHighlight();
}
break;
case LogicType::ROOT_NOT:
_matched = !cond;
if (!cond) {
c->EnableHighlight();
}
break;
default:
blog(LOG_WARNING,
"ignoring unknown condition check for '%s'",
_name.c_str());
break;
}
vblog(LOG_INFO, "condition %s returned %d", c->GetId().c_str(),
cond);
vblog(LOG_INFO, "condition %s returned %d",
condition->GetId().c_str(), conditionMatched);
const bool isNegativeLogicType =
Logic::IsNegationType(logicType);
if ((conditionMatched && !isNegativeLogicType) ||
(!conditionMatched && isNegativeLogicType)) {
condition->EnableHighlight();
}
_matched = Logic::ApplyConditionLogic(
logicType, _matched, conditionMatched, _name.c_str());
}
vblog(LOG_INFO, "Macro %s returned %d", _name.c_str(), _matched);
_conditionSateChanged = _lastMatched != _matched;
@ -631,40 +612,6 @@ bool Macro::Save(obs_data_t *obj) const
return true;
}
bool isValidLogic(LogicType t, bool root)
{
bool isRoot = isRootLogicType(t);
if (!isRoot == root) {
return false;
}
if (isRoot) {
if (t >= LogicType::ROOT_LAST) {
return false;
}
} else if (t >= LogicType::LAST) {
return false;
}
return true;
}
void setValidLogic(MacroCondition *c, bool root, std::string name)
{
if (isValidLogic(c->GetLogicType(), root)) {
return;
}
if (root) {
c->SetLogicType(LogicType::ROOT_NONE);
blog(LOG_WARNING,
"setting invalid logic selection to 'if' for macro %s",
name.c_str());
} else {
c->SetLogicType(LogicType::NONE);
blog(LOG_WARNING,
"setting invalid logic selection to 'ignore' for macro %s",
name.c_str());
}
}
bool Macro::Load(obs_data_t *obj)
{
_name = obs_data_get_string(obj, "name");
@ -720,7 +667,7 @@ bool Macro::Load(obs_data_t *obj)
_conditions.emplace_back(newEntry);
auto c = _conditions.back().get();
c->Load(arrayObj);
setValidLogic(c, root, _name);
c->ValidateLogicSelection(root, Name().c_str());
} else {
blog(LOG_WARNING,
"discarding condition entry with unknown id (%s) for macro %s",

View File

@ -0,0 +1,109 @@
#include "condition-logic.hpp"
#include "obs-module-helper.hpp"
#include "log-helper.hpp"
#include <cassert>
#include <QComboBox>
namespace advss {
const std::map<Logic::Type, const char *> Logic::localeMap = {
{Logic::Type::NONE, {"AdvSceneSwitcher.logic.none"}},
{Logic::Type::AND, {"AdvSceneSwitcher.logic.and"}},
{Logic::Type::OR, {"AdvSceneSwitcher.logic.or"}},
{Logic::Type::AND_NOT, {"AdvSceneSwitcher.logic.andNot"}},
{Logic::Type::OR_NOT, {"AdvSceneSwitcher.logic.orNot"}},
{Logic::Type::ROOT_NONE, {"AdvSceneSwitcher.logic.rootNone"}},
{Logic::Type::ROOT_NOT, {"AdvSceneSwitcher.logic.not"}},
};
bool Logic::ApplyConditionLogic(Type type, bool currentMatchResult,
bool conditionMatched, const char *context)
{
if (!context) {
context = "";
}
switch (type) {
case Type::ROOT_NONE:
return conditionMatched;
case Type::ROOT_NOT:
return !conditionMatched;
case Type::ROOT_LAST:
break;
case Type::NONE:
vblog(LOG_INFO, "skipping condition check for '%s'", context);
return currentMatchResult;
case Type::AND:
return currentMatchResult && conditionMatched;
case Type::OR:
return currentMatchResult || conditionMatched;
case Type::AND_NOT:
return currentMatchResult && !conditionMatched;
case Type::OR_NOT:
return currentMatchResult || !conditionMatched;
case Type::LAST:
default:
blog(LOG_WARNING, "ignoring invalid logic check (%s)", context);
return currentMatchResult;
}
return currentMatchResult;
}
void Logic::PopulateLogicTypeSelection(QComboBox *list, bool isRootCondition)
{
auto compare = isRootCondition
? std::function<bool(int)>{[](int typeValue) {
return typeValue < rootOffset;
}}
: std::function<bool(int)>{[](int typeValue) {
return typeValue >= rootOffset;
}};
for (const auto &[type, name] : localeMap) {
const int typeValue = static_cast<int>(type);
if (compare(typeValue)) {
list->addItem(obs_module_text(name), typeValue);
}
}
}
void Logic::Save(obs_data_t *obj, const char *name) const
{
obs_data_set_int(obj, name, static_cast<int>(_type));
}
void Logic::Load(obs_data_t *obj, const char *name)
{
_type = static_cast<Type>(obs_data_get_int(obj, name));
}
bool Logic::IsRootType() const
{
return _type < static_cast<Type>(rootOffset);
}
bool Logic::IsNegationType(Logic::Type type)
{
return type == Type::ROOT_NOT || type == Type::AND_NOT ||
type == Type::OR_NOT;
;
}
bool Logic::IsValidSelection(bool isRootCondition) const
{
if (!IsRootType() == isRootCondition) {
return false;
}
if (IsRootType() &&
(_type < Type::ROOT_NONE || _type >= Type::ROOT_LAST)) {
return false;
}
if (!IsRootType() &&
(_type <= Type::ROOT_LAST || _type >= Type::LAST)) {
return false;
}
return true;
}
} // namespace advss

View File

@ -0,0 +1,51 @@
#pragma once
#include <map>
#include <obs-data.h>
#include <string>
class QComboBox;
namespace advss {
class Logic {
public:
static constexpr auto rootOffset = 100;
enum class Type {
ROOT_NONE = 0,
ROOT_NOT,
ROOT_LAST,
// leave some space for potential expansion
NONE = rootOffset,
AND,
OR,
AND_NOT,
OR_NOT,
LAST,
};
Logic(Type type) { _type = type; }
void Save(obs_data_t *obj, const char *name) const;
void Load(obs_data_t *obj, const char *name);
Type GetType() const { return _type; }
void SetType(const Type &type) { _type = type; }
bool IsRootType() const;
static bool IsNegationType(Logic::Type);
bool IsValidSelection(bool isRootCondition) const;
static bool ApplyConditionLogic(Type, bool currentMatchResult,
bool conditionMatched,
const char *context);
static void PopulateLogicTypeSelection(QComboBox *list,
bool isRootCondition);
private:
Type _type = Type::NONE;
static const std::map<Type, const char *> localeMap;
};
} // namespace advss