mudlet/test/TVariableEditorTest.cpp
Vadim Peretokin 1dfd5c4605
infrastructure: release resources in test fixture destructors (#9522)
#### Brief overview of PR changes/additions
- Convert the raw owning `LuaInterface*` member in the
`TLuaInterfaceTest` and `TVariableEditorTest` Qt-Test fixtures to
`std::unique_ptr<LuaInterface>` so the fixture's destructor frees it.
- `TLuaInterfaceTest` also stops allocating the interface (and a
`lua_State`) twice: members now init to `nullptr`/empty and are
allocated only in `init()`; `cleanup()` resets the interface before
closing the `lua_State`.

#### Motivation for adding to Mudlet
Keeps the test suite leak-clean and clears static-analysis warnings, per
CLAUDE.md's "smart pointers for non-Qt classes".

#### Other info (issues closed, discussion etc)
- Clears 2 CodeQL `cpp/resource-not-released-in-destructor` warnings
(`test/TVariableEditorTest.cpp`, `test/TLuaInterfaceTest.cpp`).
- Also removes a real runtime leak in `TLuaInterfaceTest`: the old
fixture never deleted the `interface` and double-allocated it
(construction + `init()`), leaking a `LuaInterface` per test plus a
construction-time `lua_State`/`LuaInterface`. Verified gone under
LeakSanitizer (old binary leaked, new binary is leak-clean;
`TVariableEditorTest` already deleted its interface so for it this is
modernization).

**Test case:** Build and run `ctest -R
'TLuaInterfaceTest|TVariableEditorTest'` - both pass (TLuaInterfaceTest
4/4, TVariableEditorTest 96 passed/13 skipped). Optionally run
`./test/TLuaInterfaceTest` with `ASAN_OPTIONS=detect_leaks=1` to confirm
no leaks are reported.
2026-07-29 13:45:22 +02:00

1488 lines
60 KiB
C++

/***************************************************************************
* Copyright (C) 2026 by Vadim Peretokin - vperetokin@gmail.com *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
***************************************************************************/
#include <LuaInterface.h>
#include <TVar.h>
#include <VarUnit.h>
#include <QtTest/QtTest>
#include <memory>
extern "C" {
#if defined(INCLUDE_VERSIONED_LUA_HEADERS)
#include <lua5.1/lauxlib.h>
#include <lua5.1/lua.h>
#include <lua5.1/lualib.h>
#else
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#endif
}
class TVariableEditorTest : public QObject
{
Q_OBJECT
private:
lua_State* L = nullptr;
std::unique_ptr<LuaInterface> interface;
void execLua(const QString& code)
{
int error = luaL_loadstring(L, code.toUtf8().constData());
QCOMPARE(error, 0);
error = lua_pcall(L, 0, 0, 0);
QCOMPARE(error, 0);
}
QString getLuaValue(const QString& expr)
{
const QString code = QStringLiteral("return %1").arg(expr);
luaL_loadstring(L, code.toUtf8().constData());
lua_pcall(L, 0, 1, 0);
QString result;
if (lua_isstring(L, -1)) {
result = QString::fromUtf8(lua_tostring(L, -1));
} else if (lua_isnumber(L, -1)) {
result = QString::number(lua_tonumber(L, -1));
} else if (lua_isboolean(L, -1)) {
result = lua_toboolean(L, -1) ? QStringLiteral("true") : QStringLiteral("false");
} else if (lua_isnil(L, -1)) {
result = QStringLiteral("nil");
} else if (lua_istable(L, -1)) {
result = QStringLiteral("table");
}
lua_pop(L, 1);
return result;
}
QString getLuaType(const QString& expr)
{
const QString code = QStringLiteral("return type(%1)").arg(expr);
luaL_loadstring(L, code.toUtf8().constData());
lua_pcall(L, 0, 1, 0);
QString result = QString::fromUtf8(lua_tostring(L, -1));
lua_pop(L, 1);
return result;
}
TVar* findChild(TVar* parent, const QString& name)
{
for (auto* child : parent->getChildren()) {
if (child->getName() == name) {
return child;
}
}
return nullptr;
}
TVar* createVar(TVar* parent, const QString& name, int keyType, const QString& value, int valueType)
{
auto* var = new TVar(parent);
var->setName(name, keyType);
var->setValue(value, valueType);
parent->addChild(var);
interface->createVar(var);
return var;
}
private slots:
void init()
{
L = luaL_newstate();
luaL_openlibs(L);
interface = std::make_unique<LuaInterface>(L);
}
void cleanup()
{
interface.reset();
lua_close(L);
}
// ========================================================================
// Reading variables from Lua — basic types
// ========================================================================
void testRetrieveStringVariable()
{
execLua(QStringLiteral("testVar = 'hello'"));
interface->getVars(false);
TVar* var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(var);
QCOMPARE(var->getValue(), QStringLiteral("hello"));
QCOMPARE(var->getValueType(), LUA_TSTRING);
QCOMPARE(var->getKeyType(), LUA_TSTRING);
}
void testRetrieveNumberVariable()
{
execLua(QStringLiteral("testVar = 42"));
interface->getVars(false);
TVar* var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(var);
QCOMPARE(var->getValue(), QStringLiteral("42"));
QCOMPARE(var->getValueType(), LUA_TNUMBER);
}
void testRetrieveBooleanTrue()
{
execLua(QStringLiteral("testVar = true"));
interface->getVars(false);
TVar* var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(var);
QCOMPARE(var->getValue(), QStringLiteral("true"));
QCOMPARE(var->getValueType(), LUA_TBOOLEAN);
}
void testRetrieveBooleanFalse()
{
execLua(QStringLiteral("testVar = false"));
interface->getVars(false);
TVar* var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(var);
QCOMPARE(var->getValue(), QStringLiteral("false"));
QCOMPARE(var->getValueType(), LUA_TBOOLEAN);
}
void testRetrieveEmptyTable()
{
execLua(QStringLiteral("testVar = {}"));
interface->getVars(false);
TVar* var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(var);
QCOMPARE(var->getValueType(), LUA_TTABLE);
QCOMPARE(var->getChildren().size(), 0);
}
void testRetrieveNumberValueZero()
{
execLua(QStringLiteral("testVar = 0"));
interface->getVars(false);
TVar* var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(var);
QCOMPARE(var->getValue(), QStringLiteral("0"));
QCOMPARE(var->getValueType(), LUA_TNUMBER);
}
void testRetrieveStringValueZero()
{
execLua(QStringLiteral("testVar = '0'"));
interface->getVars(false);
TVar* var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(var);
QCOMPARE(var->getValue(), QStringLiteral("0"));
QCOMPARE(var->getValueType(), LUA_TSTRING);
}
void testRetrieveEmptyStringValue()
{
execLua(QStringLiteral("testVar = ''"));
interface->getVars(false);
TVar* var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(var);
QCOMPARE(var->getValue(), QStringLiteral(""));
QCOMPARE(var->getValueType(), LUA_TSTRING);
}
void testRetrieveFloatValue()
{
execLua(QStringLiteral("testVar = 3.14"));
interface->getVars(false);
TVar* var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(var);
QCOMPARE(var->getValueType(), LUA_TNUMBER);
QVERIFY(var->getValue().startsWith(QStringLiteral("3.14")));
}
void testRetrieveNegativeNumber()
{
execLua(QStringLiteral("testVar = -42"));
interface->getVars(false);
TVar* var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(var);
QCOMPARE(var->getValue(), QStringLiteral("-42"));
QCOMPARE(var->getValueType(), LUA_TNUMBER);
}
void testRetrieveLargeNumber()
{
execLua(QStringLiteral("testVar = 999999999"));
interface->getVars(false);
TVar* var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(var);
QCOMPARE(var->getValueType(), LUA_TNUMBER);
}
// ========================================================================
// Reading variables — table structures
// ========================================================================
void testRetrieveTableWithStringKeys()
{
execLua(QStringLiteral("testVar = {a='one', b='two'}"));
interface->getVars(false);
TVar* tbl = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(tbl);
QCOMPARE(tbl->getValueType(), LUA_TTABLE);
TVar* a = findChild(tbl, QStringLiteral("a"));
QVERIFY(a);
QCOMPARE(a->getValue(), QStringLiteral("one"));
QCOMPARE(a->getKeyType(), LUA_TSTRING);
}
void testRetrieveTableWithNumericKeys()
{
execLua(QStringLiteral("testVar = {[1]='a', [2]='b'}"));
interface->getVars(false);
TVar* tbl = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(tbl);
TVar* one = findChild(tbl, QStringLiteral("1"));
QVERIFY(one);
QCOMPARE(one->getKeyType(), LUA_TNUMBER);
QCOMPARE(one->getValue(), QStringLiteral("a"));
}
void testRetrieveNestedTable()
{
execLua(QStringLiteral("testVar = {sub = {val = 'deep'}}"));
interface->getVars(false);
TVar* tbl = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(tbl);
TVar* sub = findChild(tbl, QStringLiteral("sub"));
QVERIFY(sub);
QCOMPARE(sub->getValueType(), LUA_TTABLE);
TVar* val = findChild(sub, QStringLiteral("val"));
QVERIFY(val);
QCOMPARE(val->getValue(), QStringLiteral("deep"));
}
void testRetrieveDeeplyNestedTable()
{
execLua(QStringLiteral("testVar = {a = {b = {c = 'bottom'}}}"));
interface->getVars(false);
TVar* tbl = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(tbl);
TVar* a = findChild(tbl, QStringLiteral("a"));
QVERIFY(a);
TVar* b = findChild(a, QStringLiteral("b"));
QVERIFY(b);
TVar* c = findChild(b, QStringLiteral("c"));
QVERIFY(c);
QCOMPARE(c->getValue(), QStringLiteral("bottom"));
}
void testRetrieveNumericKeyZero()
{
execLua(QStringLiteral("testVar = {[0] = 'zero'}"));
interface->getVars(false);
TVar* tbl = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(tbl);
TVar* zero = findChild(tbl, QStringLiteral("0"));
QVERIFY(zero);
QCOMPARE(zero->getKeyType(), LUA_TNUMBER);
QCOMPARE(zero->getValue(), QStringLiteral("zero"));
}
void testRetrieveNegativeNumericKey()
{
execLua(QStringLiteral("testVar = {[-1] = 'neg'}"));
interface->getVars(false);
TVar* tbl = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(tbl);
TVar* neg = findChild(tbl, QStringLiteral("-1"));
QVERIFY(neg);
QCOMPARE(neg->getKeyType(), LUA_TNUMBER);
}
void testRetrieveMixedKeyTable()
{
execLua(QStringLiteral("testVar = {[1]='a', x='b'}"));
interface->getVars(false);
TVar* tbl = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(tbl);
TVar* numKey = findChild(tbl, QStringLiteral("1"));
QVERIFY(numKey);
QCOMPARE(numKey->getKeyType(), LUA_TNUMBER);
TVar* strKey = findChild(tbl, QStringLiteral("x"));
QVERIFY(strKey);
QCOMPARE(strKey->getKeyType(), LUA_TSTRING);
}
void testRetrieveTableWithMixedValueTypes()
{
execLua(QStringLiteral("testVar = {s='str', n=42, b=true, t={}}"));
interface->getVars(false);
TVar* tbl = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(tbl);
TVar* s = findChild(tbl, QStringLiteral("s"));
QVERIFY(s);
QCOMPARE(s->getValueType(), LUA_TSTRING);
TVar* n = findChild(tbl, QStringLiteral("n"));
QVERIFY(n);
QCOMPARE(n->getValueType(), LUA_TNUMBER);
TVar* b = findChild(tbl, QStringLiteral("b"));
QVERIFY(b);
QCOMPARE(b->getValueType(), LUA_TBOOLEAN);
TVar* t = findChild(tbl, QStringLiteral("t"));
QVERIFY(t);
QCOMPARE(t->getValueType(), LUA_TTABLE);
}
void testRetrieveUnicodeStringValue()
{
execLua(QStringLiteral("testVar = '\\228\\184\\150\\231\\149\\140'"));
interface->getVars(false);
TVar* var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(var);
QCOMPARE(var->getValueType(), LUA_TSTRING);
QVERIFY(!var->getValue().isEmpty());
}
void testRetrieveUnicodeKeyName()
{
execLua(QStringLiteral("testVar = {}; testVar['caf\\195\\169'] = 'coffee'"));
interface->getVars(false);
TVar* tbl = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(tbl);
QVERIFY(tbl->getChildren().size() > 0);
}
void testGetVarsCalledTwice()
{
execLua(QStringLiteral("testVar = 'first'"));
interface->getVars(false);
TVar* var1 = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(var1);
QCOMPARE(var1->getValue(), QStringLiteral("first"));
execLua(QStringLiteral("testVar = 'second'"));
interface->getVars(false);
TVar* var2 = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(var2);
QCOMPARE(var2->getValue(), QStringLiteral("second"));
}
// ========================================================================
// Creating variables — basic types
// ========================================================================
void testCreateStringVarAtRoot()
{
interface->getVars(false);
createVar(interface->getVarUnit()->getBase(), QStringLiteral("newStr"), LUA_TSTRING, QStringLiteral("hello"), LUA_TSTRING);
QCOMPARE(getLuaValue(QStringLiteral("newStr")), QStringLiteral("hello"));
QCOMPARE(getLuaType(QStringLiteral("newStr")), QStringLiteral("string"));
}
void testCreateNumberVarAtRoot()
{
interface->getVars(false);
createVar(interface->getVarUnit()->getBase(), QStringLiteral("newNum"), LUA_TSTRING, QStringLiteral("42"), LUA_TNUMBER);
QCOMPARE(getLuaValue(QStringLiteral("newNum")), QStringLiteral("42"));
QCOMPARE(getLuaType(QStringLiteral("newNum")), QStringLiteral("number"));
}
void testCreateBooleanTrueAtRoot()
{
interface->getVars(false);
createVar(interface->getVarUnit()->getBase(), QStringLiteral("newBool"), LUA_TSTRING, QStringLiteral("true"), LUA_TBOOLEAN);
QCOMPARE(getLuaValue(QStringLiteral("newBool")), QStringLiteral("true"));
QCOMPARE(getLuaType(QStringLiteral("newBool")), QStringLiteral("boolean"));
}
void testCreateBooleanFalseAtRoot()
{
interface->getVars(false);
createVar(interface->getVarUnit()->getBase(), QStringLiteral("newBoolF"), LUA_TSTRING, QStringLiteral("false"), LUA_TBOOLEAN);
QCOMPARE(getLuaValue(QStringLiteral("newBoolF")), QStringLiteral("false"));
QCOMPARE(getLuaType(QStringLiteral("newBoolF")), QStringLiteral("boolean"));
}
void testCreateTableAtRoot()
{
interface->getVars(false);
createVar(interface->getVarUnit()->getBase(), QStringLiteral("newTbl"), LUA_TSTRING, QStringLiteral("{}"), LUA_TTABLE);
QCOMPARE(getLuaType(QStringLiteral("newTbl")), QStringLiteral("table"));
}
void testCreateVarWithValueZero()
{
interface->getVars(false);
createVar(interface->getVarUnit()->getBase(), QStringLiteral("zeroVal"), LUA_TSTRING, QStringLiteral("0"), LUA_TNUMBER);
QCOMPARE(getLuaValue(QStringLiteral("zeroVal")), QStringLiteral("0"));
QCOMPARE(getLuaType(QStringLiteral("zeroVal")), QStringLiteral("number"));
}
void testCreateVarWithNegativeValue()
{
interface->getVars(false);
createVar(interface->getVarUnit()->getBase(), QStringLiteral("negVal"), LUA_TSTRING, QStringLiteral("-5"), LUA_TNUMBER);
QCOMPARE(getLuaValue(QStringLiteral("negVal")), QStringLiteral("-5"));
QCOMPARE(getLuaType(QStringLiteral("negVal")), QStringLiteral("number"));
}
// ========================================================================
// Creating variables — numeric keys (the "0" bug family)
// ========================================================================
void testCreateVarWithNumericKeyZeroAtRoot()
{
QSKIP("PENDING: setValue generates invalid Lua for root numeric keys (needs _G[N] instead of bare N)");
interface->getVars(false);
createVar(interface->getVarUnit()->getBase(), QStringLiteral("0"), LUA_TNUMBER, QStringLiteral("zeroVal"), LUA_TSTRING);
QCOMPARE(getLuaValue(QStringLiteral("_G[0]")), QStringLiteral("zeroVal"));
}
void testCreateVarWithNumericKeyAtRoot()
{
QSKIP("PENDING: setValue generates invalid Lua for root numeric keys (needs _G[N] instead of bare N)");
interface->getVars(false);
createVar(interface->getVarUnit()->getBase(), QStringLiteral("5"), LUA_TNUMBER, QStringLiteral("fiveVal"), LUA_TSTRING);
QCOMPARE(getLuaValue(QStringLiteral("_G[5]")), QStringLiteral("fiveVal"));
}
void testCreateVarWithNegativeNumericKeyAtRoot()
{
QSKIP("PENDING: setValue generates invalid Lua for root numeric keys (needs _G[N] instead of bare N)");
interface->getVars(false);
createVar(interface->getVarUnit()->getBase(), QStringLiteral("-1"), LUA_TNUMBER, QStringLiteral("negVal"), LUA_TSTRING);
QCOMPARE(getLuaValue(QStringLiteral("_G[-1]")), QStringLiteral("negVal"));
}
void testCreateVarWithNumericKeyInsideTable()
{
execLua(QStringLiteral("testTbl = {}"));
interface->getVars(false);
TVar* tbl = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testTbl"));
QVERIFY(tbl);
createVar(tbl, QStringLiteral("3"), LUA_TNUMBER, QStringLiteral("three"), LUA_TSTRING);
QCOMPARE(getLuaValue(QStringLiteral("testTbl[3]")), QStringLiteral("three"));
}
void testCreateVarWithNumericKeyZeroInsideTable()
{
execLua(QStringLiteral("testTbl = {}"));
interface->getVars(false);
TVar* tbl = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testTbl"));
QVERIFY(tbl);
createVar(tbl, QStringLiteral("0"), LUA_TNUMBER, QStringLiteral("zeroChild"), LUA_TSTRING);
QCOMPARE(getLuaValue(QStringLiteral("testTbl[0]")), QStringLiteral("zeroChild"));
}
void testCreateVarInsideTable()
{
execLua(QStringLiteral("testTbl = {}"));
interface->getVars(false);
TVar* tbl = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testTbl"));
QVERIFY(tbl);
createVar(tbl, QStringLiteral("child"), LUA_TSTRING, QStringLiteral("childVal"), LUA_TSTRING);
QCOMPARE(getLuaValue(QStringLiteral("testTbl.child")), QStringLiteral("childVal"));
}
void testCreateVarDeeplyNested()
{
execLua(QStringLiteral("testTbl = {sub = {}}"));
interface->getVars(false);
TVar* tbl = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testTbl"));
QVERIFY(tbl);
TVar* sub = findChild(tbl, QStringLiteral("sub"));
QVERIFY(sub);
createVar(sub, QStringLiteral("deep"), LUA_TSTRING, QStringLiteral("deepVal"), LUA_TSTRING);
QCOMPARE(getLuaValue(QStringLiteral("testTbl.sub.deep")), QStringLiteral("deepVal"));
}
// ========================================================================
// Renaming variables
// ========================================================================
void testRenameStringVarAtRoot()
{
execLua(QStringLiteral("oldName = 'value'"));
interface->getVars(false);
TVar* var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("oldName"));
QVERIFY(var);
var->setNewName(QStringLiteral("newName"), LUA_TSTRING);
interface->renameVar(var);
QCOMPARE(getLuaValue(QStringLiteral("newName")), QStringLiteral("value"));
QCOMPARE(getLuaValue(QStringLiteral("oldName")), QStringLiteral("nil"));
}
void testRenameStringVarInsideTable()
{
execLua(QStringLiteral("testTbl = {old = 'val'}"));
interface->getVars(false);
TVar* tbl = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testTbl"));
QVERIFY(tbl);
TVar* var = findChild(tbl, QStringLiteral("old"));
QVERIFY(var);
var->setNewName(QStringLiteral("new"), LUA_TSTRING);
interface->renameVar(var);
QCOMPARE(getLuaValue(QStringLiteral("testTbl.new")), QStringLiteral("val"));
QCOMPARE(getLuaValue(QStringLiteral("testTbl.old")), QStringLiteral("nil"));
}
void testRenameNumericKeyInTable()
{
QSKIP("PENDING: renameVar missing else causes numeric keys to get both [N] and [\"N\"] appended");
execLua(QStringLiteral("testTbl = {[1] = 'one'}"));
interface->getVars(false);
TVar* var = findChild(findChild(interface->getVarUnit()->getBase(), QStringLiteral("testTbl")), QStringLiteral("1"));
QVERIFY(var);
var->setNewName(QStringLiteral("2"), LUA_TNUMBER);
interface->renameVar(var);
QCOMPARE(getLuaValue(QStringLiteral("testTbl[2]")), QStringLiteral("one"));
QCOMPARE(getLuaValue(QStringLiteral("testTbl[1]")), QStringLiteral("nil"));
}
void testRenameNumericKeyToZeroInTable()
{
QSKIP("PENDING: renameVar missing else causes numeric keys to get both [N] and [\"N\"] appended");
execLua(QStringLiteral("testTbl = {[1] = 'one'}"));
interface->getVars(false);
TVar* var = findChild(findChild(interface->getVarUnit()->getBase(), QStringLiteral("testTbl")), QStringLiteral("1"));
QVERIFY(var);
var->setNewName(QStringLiteral("0"), LUA_TNUMBER);
interface->renameVar(var);
QCOMPARE(getLuaValue(QStringLiteral("testTbl[0]")), QStringLiteral("one"));
QCOMPARE(getLuaValue(QStringLiteral("testTbl[1]")), QStringLiteral("nil"));
}
void testRenameNumericKeyToNegativeInTable()
{
QSKIP("PENDING: renameVar missing else causes numeric keys to get both [N] and [\"N\"] appended");
execLua(QStringLiteral("testTbl = {[1] = 'one'}"));
interface->getVars(false);
TVar* var = findChild(findChild(interface->getVarUnit()->getBase(), QStringLiteral("testTbl")), QStringLiteral("1"));
QVERIFY(var);
var->setNewName(QStringLiteral("-5"), LUA_TNUMBER);
interface->renameVar(var);
QCOMPARE(getLuaValue(QStringLiteral("testTbl[-5]")), QStringLiteral("one"));
QCOMPARE(getLuaValue(QStringLiteral("testTbl[1]")), QStringLiteral("nil"));
}
void testRenameStringToNumericKeyInTable()
{
execLua(QStringLiteral("testTbl = {mykey = 'val'}"));
interface->getVars(false);
TVar* var = findChild(findChild(interface->getVarUnit()->getBase(), QStringLiteral("testTbl")), QStringLiteral("mykey"));
QVERIFY(var);
var->setNewName(QStringLiteral("42"), LUA_TNUMBER);
interface->renameVar(var);
QCOMPARE(getLuaValue(QStringLiteral("testTbl[42]")), QStringLiteral("val"));
QCOMPARE(getLuaValue(QStringLiteral("testTbl.mykey")), QStringLiteral("nil"));
}
void testRenameNumericToStringKeyInTable()
{
QSKIP("PENDING: renameVar missing else causes numeric keys to get both [N] and [\"N\"] appended");
execLua(QStringLiteral("testTbl = {[42] = 'val'}"));
interface->getVars(false);
TVar* var = findChild(findChild(interface->getVarUnit()->getBase(), QStringLiteral("testTbl")), QStringLiteral("42"));
QVERIFY(var);
var->setNewName(QStringLiteral("mykey"), LUA_TSTRING);
interface->renameVar(var);
QCOMPARE(getLuaValue(QStringLiteral("testTbl.mykey")), QStringLiteral("val"));
QCOMPARE(getLuaValue(QStringLiteral("testTbl[42]")), QStringLiteral("nil"));
}
void testRenameToSameName()
{
QSKIP("PENDING: renameVar generates self-referencing code instead of no-op for same-name rename");
execLua(QStringLiteral("sameName = 'val'"));
interface->getVars(false);
TVar* var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("sameName"));
QVERIFY(var);
var->setNewName(QStringLiteral("sameName"), LUA_TSTRING);
interface->renameVar(var);
QCOMPARE(getLuaValue(QStringLiteral("sameName")), QStringLiteral("val"));
}
void testRenameDeeplyNested()
{
execLua(QStringLiteral("testTbl = {sub = {old = 'deep'}}"));
interface->getVars(false);
TVar* tbl = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testTbl"));
QVERIFY(tbl);
TVar* sub = findChild(tbl, QStringLiteral("sub"));
QVERIFY(sub);
TVar* var = findChild(sub, QStringLiteral("old"));
QVERIFY(var);
var->setNewName(QStringLiteral("new"), LUA_TSTRING);
interface->renameVar(var);
QCOMPARE(getLuaValue(QStringLiteral("testTbl.sub.new")), QStringLiteral("deep"));
QCOMPARE(getLuaValue(QStringLiteral("testTbl.sub.old")), QStringLiteral("nil"));
}
// ========================================================================
// Deleting variables
// ========================================================================
void testDeleteStringVarAtRoot()
{
execLua(QStringLiteral("toDelete = 'bye'"));
interface->getVars(false);
TVar* var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("toDelete"));
QVERIFY(var);
interface->deleteVar(var);
QCOMPARE(getLuaValue(QStringLiteral("toDelete")), QStringLiteral("nil"));
}
void testDeleteNumericKeyInTable()
{
execLua(QStringLiteral("testTbl = {[1]='a', [2]='b'}"));
interface->getVars(false);
TVar* var = findChild(findChild(interface->getVarUnit()->getBase(), QStringLiteral("testTbl")), QStringLiteral("1"));
QVERIFY(var);
interface->deleteVar(var);
QCOMPARE(getLuaValue(QStringLiteral("testTbl[1]")), QStringLiteral("nil"));
QCOMPARE(getLuaValue(QStringLiteral("testTbl[2]")), QStringLiteral("b"));
}
void testDeleteTable()
{
execLua(QStringLiteral("toDelete = {a=1}"));
interface->getVars(false);
TVar* var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("toDelete"));
QVERIFY(var);
interface->deleteVar(var);
QCOMPARE(getLuaValue(QStringLiteral("toDelete")), QStringLiteral("nil"));
}
void testDeleteDeeplyNested()
{
execLua(QStringLiteral("testTbl = {sub = {val = 1, keep = 2}}"));
interface->getVars(false);
TVar* tbl = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testTbl"));
QVERIFY(tbl);
TVar* sub = findChild(tbl, QStringLiteral("sub"));
QVERIFY(sub);
TVar* val = findChild(sub, QStringLiteral("val"));
QVERIFY(val);
interface->deleteVar(val);
QCOMPARE(getLuaValue(QStringLiteral("testTbl.sub.val")), QStringLiteral("nil"));
QCOMPARE(getLuaValue(QStringLiteral("testTbl.sub.keep")), QStringLiteral("2"));
}
void testDeleteNumericKeyAtRoot()
{
QSKIP("PENDING: deleteVar generates invalid Lua for root numeric keys (needs _G[N] instead of bare N)");
execLua(QStringLiteral("_G[99] = 'rootNum'"));
interface->getVars(false);
TVar* var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("99"));
if (!var) {
QSKIP("Root numeric keys may not be iterated by getVars");
}
interface->deleteVar(var);
QCOMPARE(getLuaValue(QStringLiteral("_G[99]")), QStringLiteral("nil"));
}
// ========================================================================
// getValue
// ========================================================================
void testGetValueString()
{
execLua(QStringLiteral("testVar = 'hello'"));
interface->getVars(false);
TVar* var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(var);
QCOMPARE(interface->getValue(var), QStringLiteral("hello"));
}
void testGetValueNumber()
{
execLua(QStringLiteral("testVar = 42"));
interface->getVars(false);
TVar* var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(var);
QCOMPARE(interface->getValue(var), QStringLiteral("42"));
}
void testGetValueBoolean()
{
execLua(QStringLiteral("testVar = true"));
interface->getVars(false);
TVar* var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(var);
QCOMPARE(interface->getValue(var), QStringLiteral("true"));
}
void testGetValueNestedVar()
{
execLua(QStringLiteral("testTbl = {sub = 'deep'}"));
interface->getVars(false);
TVar* sub = findChild(findChild(interface->getVarUnit()->getBase(), QStringLiteral("testTbl")), QStringLiteral("sub"));
QVERIFY(sub);
QCOMPARE(interface->getValue(sub), QStringLiteral("deep"));
}
void testGetValueAfterModification()
{
execLua(QStringLiteral("testVar = 'before'"));
interface->getVars(false);
TVar* var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(var);
execLua(QStringLiteral("testVar = 'after'"));
QCOMPARE(interface->getValue(var), QStringLiteral("after"));
}
// ========================================================================
// setValue — modifying existing variables
// ========================================================================
void testSetValueChangeString()
{
execLua(QStringLiteral("testVar = 'old'"));
interface->getVars(false);
TVar* var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(var);
var->setValue(QStringLiteral("new"), LUA_TSTRING);
interface->setValue(var);
QCOMPARE(getLuaValue(QStringLiteral("testVar")), QStringLiteral("new"));
}
void testSetValueChangeToNumber()
{
execLua(QStringLiteral("testVar = 'was string'"));
interface->getVars(false);
TVar* var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(var);
var->setValue(QStringLiteral("42"), LUA_TNUMBER);
interface->setValue(var);
QCOMPARE(getLuaValue(QStringLiteral("testVar")), QStringLiteral("42"));
QCOMPARE(getLuaType(QStringLiteral("testVar")), QStringLiteral("number"));
}
void testSetValueChangeToBoolean()
{
execLua(QStringLiteral("testVar = 'was string'"));
interface->getVars(false);
TVar* var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(var);
var->setValue(QStringLiteral("true"), LUA_TBOOLEAN);
interface->setValue(var);
QCOMPARE(getLuaValue(QStringLiteral("testVar")), QStringLiteral("true"));
QCOMPARE(getLuaType(QStringLiteral("testVar")), QStringLiteral("boolean"));
}
void testSetValueChangeToTable()
{
execLua(QStringLiteral("testVar = 'was string'"));
interface->getVars(false);
TVar* var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(var);
var->setValue(QStringLiteral("{}"), LUA_TTABLE);
interface->setValue(var);
QCOMPARE(getLuaType(QStringLiteral("testVar")), QStringLiteral("table"));
}
void testSetValueZeroAsNumber()
{
execLua(QStringLiteral("testVar = 99"));
interface->getVars(false);
TVar* var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(var);
var->setValue(QStringLiteral("0"), LUA_TNUMBER);
interface->setValue(var);
QCOMPARE(getLuaValue(QStringLiteral("testVar")), QStringLiteral("0"));
QCOMPARE(getLuaType(QStringLiteral("testVar")), QStringLiteral("number"));
}
// ========================================================================
// Round-trip tests (create → read back, modify → read back, etc.)
// ========================================================================
void testCreateAndReadBackString()
{
interface->getVars(false);
createVar(interface->getVarUnit()->getBase(), QStringLiteral("roundTrip"), LUA_TSTRING, QStringLiteral("testValue"), LUA_TSTRING);
interface->getVars(false);
TVar* found = findChild(interface->getVarUnit()->getBase(), QStringLiteral("roundTrip"));
QVERIFY(found);
QCOMPARE(found->getValue(), QStringLiteral("testValue"));
QCOMPARE(found->getValueType(), LUA_TSTRING);
}
void testCreateAndReadBackNumber()
{
interface->getVars(false);
createVar(interface->getVarUnit()->getBase(), QStringLiteral("roundTripNum"), LUA_TSTRING, QStringLiteral("42"), LUA_TNUMBER);
interface->getVars(false);
TVar* found = findChild(interface->getVarUnit()->getBase(), QStringLiteral("roundTripNum"));
QVERIFY(found);
QCOMPARE(found->getValue(), QStringLiteral("42"));
QCOMPARE(found->getValueType(), LUA_TNUMBER);
}
void testCreateAndReadBackBoolean()
{
interface->getVars(false);
createVar(interface->getVarUnit()->getBase(), QStringLiteral("roundTripBool"), LUA_TSTRING, QStringLiteral("false"), LUA_TBOOLEAN);
interface->getVars(false);
TVar* found = findChild(interface->getVarUnit()->getBase(), QStringLiteral("roundTripBool"));
QVERIFY(found);
QCOMPARE(found->getValue(), QStringLiteral("false"));
QCOMPARE(found->getValueType(), LUA_TBOOLEAN);
}
void testModifyAndReadBack()
{
execLua(QStringLiteral("roundTrip = 'original'"));
interface->getVars(false);
TVar* var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("roundTrip"));
QVERIFY(var);
var->setValue(QStringLiteral("modified"), LUA_TSTRING);
interface->setValue(var);
QCOMPARE(getLuaValue(QStringLiteral("roundTrip")), QStringLiteral("modified"));
}
void testRenameAndReadBack()
{
execLua(QStringLiteral("oldRT = 'value'"));
interface->getVars(false);
TVar* var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("oldRT"));
QVERIFY(var);
var->setNewName(QStringLiteral("newRT"), LUA_TSTRING);
interface->renameVar(var);
QCOMPARE(getLuaValue(QStringLiteral("newRT")), QStringLiteral("value"));
QCOMPARE(getLuaValue(QStringLiteral("oldRT")), QStringLiteral("nil"));
}
void testDeleteAndReadBack()
{
execLua(QStringLiteral("ephemeral = 'gone'"));
interface->getVars(false);
TVar* var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("ephemeral"));
QVERIFY(var);
interface->deleteVar(var);
QCOMPARE(getLuaValue(QStringLiteral("ephemeral")), QStringLiteral("nil"));
}
void testCreateTwoDeleteOne()
{
interface->getVars(false);
TVar* base = interface->getVarUnit()->getBase();
createVar(base, QStringLiteral("keepMe"), LUA_TSTRING, QStringLiteral("kept"), LUA_TSTRING);
createVar(base, QStringLiteral("deleteMe"), LUA_TSTRING, QStringLiteral("gone"), LUA_TSTRING);
QCOMPARE(getLuaValue(QStringLiteral("keepMe")), QStringLiteral("kept"));
QCOMPARE(getLuaValue(QStringLiteral("deleteMe")), QStringLiteral("gone"));
interface->getVars(false);
TVar* toDel = findChild(interface->getVarUnit()->getBase(), QStringLiteral("deleteMe"));
QVERIFY(toDel);
interface->deleteVar(toDel);
QCOMPARE(getLuaValue(QStringLiteral("keepMe")), QStringLiteral("kept"));
QCOMPARE(getLuaValue(QStringLiteral("deleteMe")), QStringLiteral("nil"));
}
// ========================================================================
// Numeric key zero — full round-trip
// ========================================================================
void testNumericKeyZeroRoundTrip()
{
QSKIP("PENDING: setValue generates invalid Lua for root numeric keys (needs _G[N] instead of bare N)");
interface->getVars(false);
createVar(interface->getVarUnit()->getBase(), QStringLiteral("0"), LUA_TNUMBER, QStringLiteral("zeroRT"), LUA_TSTRING);
QCOMPARE(getLuaValue(QStringLiteral("_G[0]")), QStringLiteral("zeroRT"));
interface->getVars(false);
TVar* found = findChild(interface->getVarUnit()->getBase(), QStringLiteral("0"));
if (found) {
QCOMPARE(found->getKeyType(), LUA_TNUMBER);
QCOMPARE(found->getValue(), QStringLiteral("zeroRT"));
}
}
void testNumericKeyZeroInsideTableRoundTrip()
{
execLua(QStringLiteral("testTbl = {}"));
interface->getVars(false);
TVar* tbl = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testTbl"));
QVERIFY(tbl);
createVar(tbl, QStringLiteral("0"), LUA_TNUMBER, QStringLiteral("zeroInTbl"), LUA_TSTRING);
QCOMPARE(getLuaValue(QStringLiteral("testTbl[0]")), QStringLiteral("zeroInTbl"));
interface->getVars(false);
tbl = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testTbl"));
QVERIFY(tbl);
TVar* zero = findChild(tbl, QStringLiteral("0"));
QVERIFY(zero);
QCOMPARE(zero->getKeyType(), LUA_TNUMBER);
QCOMPARE(zero->getValue(), QStringLiteral("zeroInTbl"));
}
// ========================================================================
// Edge cases and known limitations
// ========================================================================
void testStringValueContainingClosingBrackets()
{
interface->getVars(false);
auto* var = new TVar(interface->getVarUnit()->getBase());
var->setName(QStringLiteral("bracketVal"), LUA_TSTRING);
var->setValue(QStringLiteral("a]]b"), LUA_TSTRING);
interface->getVarUnit()->getBase()->addChild(var);
const bool result = interface->setValue(var);
if (!result) {
QSKIP("Known issue: setValue fails for strings containing ']]'");
}
QCOMPARE(getLuaValue(QStringLiteral("bracketVal")), QStringLiteral("a]]b"));
}
void testStringValueContainingNewlines()
{
interface->getVars(false);
createVar(interface->getVarUnit()->getBase(), QStringLiteral("nlVal"), LUA_TSTRING, QStringLiteral("line1\nline2"), LUA_TSTRING);
QCOMPARE(getLuaValue(QStringLiteral("nlVal")), QStringLiteral("line1\nline2"));
}
void testStringValueContainingQuotes()
{
interface->getVars(false);
createVar(interface->getVarUnit()->getBase(), QStringLiteral("quoteVal"), LUA_TSTRING, QStringLiteral("he said \"hi\""), LUA_TSTRING);
QCOMPARE(getLuaValue(QStringLiteral("quoteVal")), QStringLiteral("he said \"hi\""));
}
void testStringValueEmpty()
{
interface->getVars(false);
createVar(interface->getVarUnit()->getBase(), QStringLiteral("emptyVal"), LUA_TSTRING, QStringLiteral(""), LUA_TSTRING);
QCOMPARE(getLuaValue(QStringLiteral("emptyVal")), QStringLiteral(""));
QCOMPARE(getLuaType(QStringLiteral("emptyVal")), QStringLiteral("string"));
}
// ========================================================================
// TVar data model tests
// ========================================================================
void testTVarDefaultConstructor()
{
TVar var;
QCOMPARE(var.getName(), QStringLiteral(""));
QCOMPARE(var.getValue(), QStringLiteral(""));
QCOMPARE(var.getKeyType(), LUA_TNONE);
QCOMPARE(var.getValueType(), LUA_TNONE);
QVERIFY(!var.getParent());
QCOMPARE(var.getChildren().size(), 0);
}
void testTVarSetNameWithType()
{
TVar var;
var.setName(QStringLiteral("foo"), LUA_TSTRING);
QCOMPARE(var.getName(), QStringLiteral("foo"));
QCOMPARE(var.getKeyType(), LUA_TSTRING);
}
void testTVarSetValueWithType()
{
TVar var;
var.setValue(QStringLiteral("123"), LUA_TNUMBER);
QCOMPARE(var.getValue(), QStringLiteral("123"));
QCOMPARE(var.getValueType(), LUA_TNUMBER);
}
void testTVarNewNameCycle()
{
TVar var;
var.setName(QStringLiteral("old"), LUA_TSTRING);
var.setNewName(QStringLiteral("new"), LUA_TNUMBER);
QCOMPARE(var.getNewName(), QStringLiteral("new"));
QCOMPARE(var.getNewKeyType(), LUA_TNUMBER);
var.clearNewName();
QCOMPARE(var.getName(), QStringLiteral("new"));
QCOMPARE(var.getKeyType(), LUA_TNUMBER);
QCOMPARE(var.getNewName(), QStringLiteral(""));
}
void testTVarParentChild()
{
TVar parent;
auto* child1 = new TVar(&parent);
child1->setName(QStringLiteral("c1"), LUA_TSTRING);
parent.addChild(child1);
auto* child2 = new TVar(&parent);
child2->setName(QStringLiteral("c2"), LUA_TSTRING);
parent.addChild(child2);
QCOMPARE(parent.getChildren().size(), 2);
parent.removeChild(child1);
QCOMPARE(parent.getChildren().size(), 1);
QCOMPARE(parent.getChildren().first()->getName(), QStringLiteral("c2"));
}
void testTVarReference()
{
TVar var;
QVERIFY(!var.isReference());
var.setReference(true);
QVERIFY(var.isReference());
var.setReference(false);
QVERIFY(!var.isReference());
}
// ========================================================================
// VarUnit tests
// ========================================================================
void testVarUnitShouldSaveNormalVar()
{
interface->getVars(false);
VarUnit* vu = interface->getVarUnit();
TVar var;
var.setName(QStringLiteral("test"), LUA_TSTRING);
var.setValue(QStringLiteral("val"), LUA_TSTRING);
QVERIFY(vu->shouldSave(&var));
}
void testVarUnitShouldNotSaveFunction()
{
interface->getVars(false);
VarUnit* vu = interface->getVarUnit();
TVar var;
var.setName(QStringLiteral("test"), LUA_TSTRING);
var.setValue(QStringLiteral(""), LUA_TFUNCTION);
QVERIFY(!vu->shouldSave(&var));
}
void testVarUnitShouldNotSaveReference()
{
interface->getVars(false);
VarUnit* vu = interface->getVarUnit();
TVar var;
var.setName(QStringLiteral("test"), LUA_TSTRING);
var.setValue(QStringLiteral("val"), LUA_TSTRING);
var.setReference(true);
QVERIFY(!vu->shouldSave(&var));
}
void testVarUnitHiddenVar()
{
interface->getVars(false);
VarUnit* vu = interface->getVarUnit();
TVar var;
var.setName(QStringLiteral("hidden"), LUA_TSTRING);
vu->addHidden(&var, 0);
QVERIFY(vu->isHidden(&var));
vu->removeHidden(&var);
QVERIFY(!vu->isHidden(&var));
}
void testVarUnitSavedVar()
{
interface->getVars(false);
VarUnit* vu = interface->getVarUnit();
TVar var;
var.setName(QStringLiteral("saved"), LUA_TSTRING);
vu->addSavedVar(&var);
QVERIFY(vu->isSaved(&var));
vu->removeSavedVar(&var);
QVERIFY(!vu->isSaved(&var));
}
// ========================================================================
// Float keys and values
// ========================================================================
void testRetrieveFloatKey()
{
execLua(QStringLiteral("testVar = {[1.5] = 'half'}"));
interface->getVars(false);
TVar* tbl = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(tbl);
QVERIFY(tbl->getChildren().size() > 0);
TVar* child = tbl->getChildren().first();
QCOMPARE(child->getKeyType(), LUA_TNUMBER);
QCOMPARE(child->getValue(), QStringLiteral("half"));
}
void testCreateVarWithFloatValue()
{
interface->getVars(false);
createVar(interface->getVarUnit()->getBase(), QStringLiteral("floatVal"), LUA_TSTRING, QStringLiteral("3.14"), LUA_TNUMBER);
QCOMPARE(getLuaType(QStringLiteral("floatVal")), QStringLiteral("number"));
// setValue uses string interpolation so 3.14 should work
QString val = getLuaValue(QStringLiteral("floatVal"));
QVERIFY(val.startsWith(QStringLiteral("3.14")));
}
// ========================================================================
// String key that looks numeric (distinguish "42" string from 42 number)
// ========================================================================
void testRetrieveStringKeyThatLooksNumeric()
{
// In Lua, ["42"] (string key) and [42] (number key) are different
execLua(QStringLiteral("testVar = {}; testVar['42'] = 'strkey'; testVar[42] = 'numkey'"));
interface->getVars(false);
TVar* tbl = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(tbl);
// Should have two children with different key types
int numKeyCount = 0;
int strKeyCount = 0;
for (auto* child : tbl->getChildren()) {
if (child->getName() == QStringLiteral("42")) {
if (child->getKeyType() == LUA_TNUMBER) {
numKeyCount++;
QCOMPARE(child->getValue(), QStringLiteral("numkey"));
} else if (child->getKeyType() == LUA_TSTRING) {
strKeyCount++;
QCOMPARE(child->getValue(), QStringLiteral("strkey"));
}
}
}
QCOMPARE(numKeyCount, 1);
QCOMPARE(strKeyCount, 1);
}
// ========================================================================
// Table rename preserves children
// ========================================================================
void testRenameTablePreservesChildren()
{
execLua(QStringLiteral("oldTbl = {a=1, b=2, c=3}"));
interface->getVars(false);
TVar* var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("oldTbl"));
QVERIFY(var);
var->setNewName(QStringLiteral("newTbl"), LUA_TSTRING);
interface->renameVar(var);
QCOMPARE(getLuaType(QStringLiteral("newTbl")), QStringLiteral("table"));
QCOMPARE(getLuaValue(QStringLiteral("newTbl.a")), QStringLiteral("1"));
QCOMPARE(getLuaValue(QStringLiteral("newTbl.b")), QStringLiteral("2"));
QCOMPARE(getLuaValue(QStringLiteral("newTbl.c")), QStringLiteral("3"));
QCOMPARE(getLuaValue(QStringLiteral("oldTbl")), QStringLiteral("nil"));
}
// ========================================================================
// Root-level renames between string and numeric keys
// ========================================================================
void testRenameRootStringToNumericKey()
{
QSKIP("PENDING: renameVar always uses string format for root-level renames");
execLua(QStringLiteral("myvar = 'val'"));
interface->getVars(false);
TVar* var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("myvar"));
QVERIFY(var);
var->setNewName(QStringLiteral("42"), LUA_TNUMBER);
interface->renameVar(var);
QCOMPARE(getLuaValue(QStringLiteral("_G[42]")), QStringLiteral("val"));
QCOMPARE(getLuaValue(QStringLiteral("myvar")), QStringLiteral("nil"));
}
void testRenameRootNumericToStringKey()
{
QSKIP("PENDING: renameVar always uses string format for root-level renames");
execLua(QStringLiteral("_G[42] = 'val'"));
interface->getVars(false);
TVar* var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("42"));
if (!var) {
QSKIP("Root numeric keys may not be iterated by getVars");
}
var->setNewName(QStringLiteral("myvar"), LUA_TSTRING);
interface->renameVar(var);
QCOMPARE(getLuaValue(QStringLiteral("myvar")), QStringLiteral("val"));
QCOMPARE(getLuaValue(QStringLiteral("_G[42]")), QStringLiteral("nil"));
}
// ========================================================================
// Child inside numeric-keyed table
// ========================================================================
void testCreateChildInsideNumericKeyedTable()
{
execLua(QStringLiteral("testTbl = {[5] = {}}"));
interface->getVars(false);
TVar* tbl = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testTbl"));
QVERIFY(tbl);
TVar* five = findChild(tbl, QStringLiteral("5"));
QVERIFY(five);
QCOMPARE(five->getValueType(), LUA_TTABLE);
createVar(five, QStringLiteral("child"), LUA_TSTRING, QStringLiteral("inside"), LUA_TSTRING);
QCOMPARE(getLuaValue(QStringLiteral("testTbl[5].child")), QStringLiteral("inside"));
}
// ========================================================================
// Multiple numeric keys in same table
// ========================================================================
void testRetrieveMultipleNumericKeys()
{
execLua(QStringLiteral("testVar = {[0]='a', [1]='b', [2]='c'}"));
interface->getVars(false);
TVar* tbl = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(tbl);
QCOMPARE(tbl->getChildren().size(), 3);
TVar* z = findChild(tbl, QStringLiteral("0"));
QVERIFY(z);
QCOMPARE(z->getValue(), QStringLiteral("a"));
TVar* o = findChild(tbl, QStringLiteral("1"));
QVERIFY(o);
QCOMPARE(o->getValue(), QStringLiteral("b"));
TVar* t = findChild(tbl, QStringLiteral("2"));
QVERIFY(t);
QCOMPARE(t->getValue(), QStringLiteral("c"));
}
// ========================================================================
// Large numeric keys
// ========================================================================
void testRetrieveLargeNumericKey()
{
execLua(QStringLiteral("testVar = {[999999] = 'big'}"));
interface->getVars(false);
TVar* tbl = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(tbl);
TVar* big = findChild(tbl, QStringLiteral("999999"));
QVERIFY(big);
QCOMPARE(big->getKeyType(), LUA_TNUMBER);
QCOMPARE(big->getValue(), QStringLiteral("big"));
}
void testCreateVarWithLargeNumericKey()
{
execLua(QStringLiteral("testTbl = {}"));
interface->getVars(false);
TVar* tbl = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testTbl"));
QVERIFY(tbl);
createVar(tbl, QStringLiteral("100000"), LUA_TNUMBER, QStringLiteral("large"), LUA_TSTRING);
QCOMPARE(getLuaValue(QStringLiteral("testTbl[100000]")), QStringLiteral("large"));
}
// ========================================================================
// Deeply nested numeric keys — code generation path
// ========================================================================
void testDeeplyNestedNumericKeys()
{
execLua(QStringLiteral("testVar = {[1] = {[2] = {[3] = 'deep'}}}"));
interface->getVars(false);
TVar* tbl = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(tbl);
TVar* one = findChild(tbl, QStringLiteral("1"));
QVERIFY(one);
TVar* two = findChild(one, QStringLiteral("2"));
QVERIFY(two);
TVar* three = findChild(two, QStringLiteral("3"));
QVERIFY(three);
QCOMPARE(three->getValue(), QStringLiteral("deep"));
QCOMPARE(three->getKeyType(), LUA_TNUMBER);
}
void testCreateVarInDeeplyNestedNumericKeyTable()
{
execLua(QStringLiteral("testVar = {[1] = {[2] = {}}}"));
interface->getVars(false);
TVar* tbl = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(tbl);
TVar* one = findChild(tbl, QStringLiteral("1"));
QVERIFY(one);
TVar* two = findChild(one, QStringLiteral("2"));
QVERIFY(two);
createVar(two, QStringLiteral("newkey"), LUA_TSTRING, QStringLiteral("nested"), LUA_TSTRING);
QCOMPARE(getLuaValue(QStringLiteral("testVar[1][2].newkey")), QStringLiteral("nested"));
}
// ========================================================================
// String values with special characters
// ========================================================================
void testStringValueWithBackslash()
{
interface->getVars(false);
createVar(interface->getVarUnit()->getBase(), QStringLiteral("bsVal"), LUA_TSTRING, QStringLiteral("path\\to\\file"), LUA_TSTRING);
QCOMPARE(getLuaValue(QStringLiteral("bsVal")), QStringLiteral("path\\to\\file"));
}
void testStringValueWithSingleQuotes()
{
interface->getVars(false);
createVar(interface->getVarUnit()->getBase(), QStringLiteral("sqVal"), LUA_TSTRING, QStringLiteral("it's a test"), LUA_TSTRING);
QCOMPARE(getLuaValue(QStringLiteral("sqVal")), QStringLiteral("it's a test"));
}
void testStringValueVeryLong()
{
interface->getVars(false);
QString longStr;
longStr.fill(QLatin1Char('x'), 10000);
createVar(interface->getVarUnit()->getBase(), QStringLiteral("longVal"), LUA_TSTRING, longStr, LUA_TSTRING);
QCOMPARE(getLuaValue(QStringLiteral("longVal")).size(), 10000);
}
// ========================================================================
// Self-referencing table (no infinite recursion)
// ========================================================================
void testSelfReferencingTable()
{
execLua(QStringLiteral("testVar = {}; testVar.self = testVar"));
// getVars should not hang or crash on self-referencing tables
interface->getVars(false);
TVar* tbl = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(tbl);
// The self-reference should be detected and handled (marked as reference or skipped)
}
// ========================================================================
// getValue on nonexistent/deleted path
// ========================================================================
void testGetValueAfterExternalDeletion()
{
execLua(QStringLiteral("testVar = 'exists'"));
interface->getVars(false);
TVar* var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(var);
QCOMPARE(interface->getValue(var), QStringLiteral("exists"));
// Delete externally via Lua
execLua(QStringLiteral("testVar = nil"));
// getValue should handle this gracefully (return empty or nil, not crash)
QString val = interface->getValue(var);
Q_UNUSED(val); // Just verifying no crash
}
// ========================================================================
// getVars standard library filtering
// ========================================================================
void testGetVarsFiltersStandardLibraries()
{
interface->getVars(false);
TVar* base = interface->getVarUnit()->getBase();
// Standard Lua libraries should not appear as editable children
// (they may be hidden or filtered by iterateTable's depth/hidden logic)
// At minimum, getVars should complete without error
QVERIFY(base);
QVERIFY(base->getChildren().size() >= 0);
}
// ========================================================================
// Mixed numeric/string path in deeply nested code generation
// ========================================================================
void testMixedNumericStringNestedPath()
{
execLua(QStringLiteral("testVar = {sub = {[3] = {}}}"));
interface->getVars(false);
TVar* tbl = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(tbl);
TVar* sub = findChild(tbl, QStringLiteral("sub"));
QVERIFY(sub);
TVar* three = findChild(sub, QStringLiteral("3"));
QVERIFY(three);
createVar(three, QStringLiteral("leaf"), LUA_TSTRING, QStringLiteral("mixed"), LUA_TSTRING);
QCOMPARE(getLuaValue(QStringLiteral("testVar.sub[3].leaf")), QStringLiteral("mixed"));
}
void testNumericThenStringNestedPath()
{
execLua(QStringLiteral("testVar = {[1] = {name = 'first'}}"));
interface->getVars(false);
TVar* tbl = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testVar"));
QVERIFY(tbl);
TVar* one = findChild(tbl, QStringLiteral("1"));
QVERIFY(one);
TVar* name = findChild(one, QStringLiteral("name"));
QVERIFY(name);
QCOMPARE(interface->getValue(name), QStringLiteral("first"));
}
// ========================================================================
// Delete numeric-keyed child, verify sibling survives
// ========================================================================
void testDeleteNumericKeyChildSiblingSurvives()
{
execLua(QStringLiteral("testTbl = {[0]='zero', [1]='one', [2]='two'}"));
interface->getVars(false);
TVar* tbl = findChild(interface->getVarUnit()->getBase(), QStringLiteral("testTbl"));
QVERIFY(tbl);
TVar* one = findChild(tbl, QStringLiteral("1"));
QVERIFY(one);
interface->deleteVar(one);
QCOMPARE(getLuaValue(QStringLiteral("testTbl[0]")), QStringLiteral("zero"));
QCOMPARE(getLuaValue(QStringLiteral("testTbl[1]")), QStringLiteral("nil"));
QCOMPARE(getLuaValue(QStringLiteral("testTbl[2]")), QStringLiteral("two"));
}
// ========================================================================
// Create boolean false (ensure it doesn't get treated as nil)
// ========================================================================
void testCreateBooleanFalseRoundTrip()
{
interface->getVars(false);
createVar(interface->getVarUnit()->getBase(), QStringLiteral("boolFalseRT"), LUA_TSTRING, QStringLiteral("false"), LUA_TBOOLEAN);
QCOMPARE(getLuaType(QStringLiteral("boolFalseRT")), QStringLiteral("boolean"));
QCOMPARE(getLuaValue(QStringLiteral("boolFalseRT")), QStringLiteral("false"));
// Read back
interface->getVars(false);
TVar* found = findChild(interface->getVarUnit()->getBase(), QStringLiteral("boolFalseRT"));
QVERIFY(found);
QCOMPARE(found->getValueType(), LUA_TBOOLEAN);
QCOMPARE(found->getValue(), QStringLiteral("false"));
}
// ========================================================================
// Multiple operations on same variable
// ========================================================================
void testCreateModifyRenameDelete()
{
interface->getVars(false);
TVar* base = interface->getVarUnit()->getBase();
createVar(base, QStringLiteral("lifecycle"), LUA_TSTRING, QStringLiteral("v1"), LUA_TSTRING);
QCOMPARE(getLuaValue(QStringLiteral("lifecycle")), QStringLiteral("v1"));
// Modify
interface->getVars(false);
TVar* var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("lifecycle"));
QVERIFY(var);
var->setValue(QStringLiteral("v2"), LUA_TSTRING);
interface->setValue(var);
QCOMPARE(getLuaValue(QStringLiteral("lifecycle")), QStringLiteral("v2"));
// Rename
var->setNewName(QStringLiteral("renamed"), LUA_TSTRING);
interface->renameVar(var);
QCOMPARE(getLuaValue(QStringLiteral("renamed")), QStringLiteral("v2"));
QCOMPARE(getLuaValue(QStringLiteral("lifecycle")), QStringLiteral("nil"));
// Delete
interface->getVars(false);
var = findChild(interface->getVarUnit()->getBase(), QStringLiteral("renamed"));
QVERIFY(var);
interface->deleteVar(var);
QCOMPARE(getLuaValue(QStringLiteral("renamed")), QStringLiteral("nil"));
}
};
#include "TVariableEditorTest.moc"
QTEST_MAIN(TVariableEditorTest)