mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-07-19 09:40:56 -04:00
refactor: Restructure project into sources/tests layout and dissolve PluginLLMCore
This commit is contained in:
30
tests/CMakeLists.txt
Normal file
30
tests/CMakeLists.txt
Normal file
@@ -0,0 +1,30 @@
|
||||
add_executable(QodeAssistTest
|
||||
../sources/completion/CodeHandler.cpp
|
||||
../sources/completion/LLMClientInterface.cpp
|
||||
../sources/completion/LLMSuggestion.cpp
|
||||
../sources/providers/Provider.cpp
|
||||
CodeHandlerTest.cpp
|
||||
ClaudeCacheControlTest.cpp
|
||||
DocumentContextReaderTest.cpp
|
||||
LLMSuggestionTest.cpp
|
||||
# LLMClientInterfaceTests.cpp
|
||||
unittest_main.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(QodeAssistTest PRIVATE
|
||||
Qt::Core
|
||||
Qt::Test
|
||||
GTest::GTest
|
||||
GTest::gmock
|
||||
GTest::Main
|
||||
QtCreator::LanguageClient
|
||||
Context
|
||||
QodeAssistLogger
|
||||
LLMQore
|
||||
)
|
||||
|
||||
target_include_directories(QodeAssistTest PRIVATE ${CMAKE_SOURCE_DIR}/sources)
|
||||
|
||||
target_compile_definitions(QodeAssistTest PRIVATE CMAKE_CURRENT_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
|
||||
add_test(NAME QodeAssistTest COMMAND QodeAssistTest)
|
||||
182
tests/ClaudeCacheControlTest.cpp
Normal file
182
tests/ClaudeCacheControlTest.cpp
Normal file
@@ -0,0 +1,182 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "providers/ClaudeCacheControl.hpp"
|
||||
|
||||
using namespace QodeAssist::Providers::ClaudeCacheControl;
|
||||
|
||||
namespace {
|
||||
|
||||
QJsonObject expectedEphemeral(bool extendedTtl)
|
||||
{
|
||||
QJsonObject obj{{"type", "ephemeral"}};
|
||||
if (extendedTtl)
|
||||
obj["ttl"] = "1h";
|
||||
return obj;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(ClaudeCacheControlTest, BreakpointWithoutExtendedTTL)
|
||||
{
|
||||
const QJsonObject cc = buildBreakpoint(false);
|
||||
EXPECT_EQ(cc.value("type").toString(), "ephemeral");
|
||||
EXPECT_FALSE(cc.contains("ttl"));
|
||||
}
|
||||
|
||||
TEST(ClaudeCacheControlTest, BreakpointWithExtendedTTL)
|
||||
{
|
||||
const QJsonObject cc = buildBreakpoint(true);
|
||||
EXPECT_EQ(cc.value("type").toString(), "ephemeral");
|
||||
EXPECT_EQ(cc.value("ttl").toString(), "1h");
|
||||
}
|
||||
|
||||
TEST(ClaudeCacheControlTest, SystemAsStringWrappedIntoArray)
|
||||
{
|
||||
QJsonObject request;
|
||||
request["system"] = "you are a helpful agent";
|
||||
|
||||
apply(request, false);
|
||||
|
||||
ASSERT_TRUE(request.value("system").isArray());
|
||||
const QJsonArray sys = request.value("system").toArray();
|
||||
ASSERT_EQ(sys.size(), 1);
|
||||
|
||||
const QJsonObject block = sys.first().toObject();
|
||||
EXPECT_EQ(block.value("type").toString(), "text");
|
||||
EXPECT_EQ(block.value("text").toString(), "you are a helpful agent");
|
||||
EXPECT_EQ(block.value("cache_control").toObject(), expectedEphemeral(false));
|
||||
}
|
||||
|
||||
TEST(ClaudeCacheControlTest, EmptySystemStringIsNotWrapped)
|
||||
{
|
||||
QJsonObject request;
|
||||
request["system"] = "";
|
||||
|
||||
apply(request, false);
|
||||
|
||||
EXPECT_TRUE(request.value("system").isString());
|
||||
}
|
||||
|
||||
TEST(ClaudeCacheControlTest, SystemAsArrayMarksLastBlock)
|
||||
{
|
||||
QJsonObject request;
|
||||
request["system"] = QJsonArray{
|
||||
QJsonObject{{"type", "text"}, {"text", "a"}},
|
||||
QJsonObject{{"type", "text"}, {"text", "b"}}};
|
||||
|
||||
apply(request, false);
|
||||
|
||||
const QJsonArray sys = request.value("system").toArray();
|
||||
ASSERT_EQ(sys.size(), 2);
|
||||
EXPECT_FALSE(sys[0].toObject().contains("cache_control"));
|
||||
EXPECT_EQ(sys[1].toObject().value("cache_control").toObject(), expectedEphemeral(false));
|
||||
}
|
||||
|
||||
TEST(ClaudeCacheControlTest, ToolsLastEntryGetsCacheControl)
|
||||
{
|
||||
QJsonObject request;
|
||||
request["tools"] = QJsonArray{
|
||||
QJsonObject{{"name", "read_file"}},
|
||||
QJsonObject{{"name", "edit_file"}},
|
||||
QJsonObject{{"name", "search"}}};
|
||||
|
||||
apply(request, true);
|
||||
|
||||
const QJsonArray tools = request.value("tools").toArray();
|
||||
ASSERT_EQ(tools.size(), 3);
|
||||
EXPECT_FALSE(tools[0].toObject().contains("cache_control"));
|
||||
EXPECT_FALSE(tools[1].toObject().contains("cache_control"));
|
||||
EXPECT_EQ(tools[2].toObject().value("cache_control").toObject(), expectedEphemeral(true));
|
||||
}
|
||||
|
||||
TEST(ClaudeCacheControlTest, SingleMessageHistorySkipped)
|
||||
{
|
||||
QJsonObject request;
|
||||
request["messages"]
|
||||
= QJsonArray{QJsonObject{{"role", "user"}, {"content", "first message"}}};
|
||||
|
||||
apply(request, false);
|
||||
|
||||
const QJsonArray msgs = request.value("messages").toArray();
|
||||
ASSERT_EQ(msgs.size(), 1);
|
||||
EXPECT_TRUE(msgs[0].toObject().value("content").isString());
|
||||
}
|
||||
|
||||
TEST(ClaudeCacheControlTest, HistoryBreakpointOnSecondToLastMessage)
|
||||
{
|
||||
QJsonObject request;
|
||||
request["messages"] = QJsonArray{
|
||||
QJsonObject{{"role", "user"}, {"content", "u1"}},
|
||||
QJsonObject{{"role", "assistant"}, {"content", "a1"}},
|
||||
QJsonObject{{"role", "user"}, {"content", "u2-current"}}};
|
||||
|
||||
apply(request, false);
|
||||
|
||||
const QJsonArray msgs = request.value("messages").toArray();
|
||||
ASSERT_EQ(msgs.size(), 3);
|
||||
|
||||
EXPECT_TRUE(msgs[0].toObject().value("content").isString());
|
||||
|
||||
const QJsonArray a1Content = msgs[1].toObject().value("content").toArray();
|
||||
ASSERT_EQ(a1Content.size(), 1);
|
||||
EXPECT_EQ(a1Content.first().toObject().value("text").toString(), "a1");
|
||||
EXPECT_EQ(
|
||||
a1Content.first().toObject().value("cache_control").toObject(),
|
||||
expectedEphemeral(false));
|
||||
|
||||
EXPECT_TRUE(msgs[2].toObject().value("content").isString());
|
||||
}
|
||||
|
||||
TEST(ClaudeCacheControlTest, HistoryArrayContentMarksLastBlock)
|
||||
{
|
||||
QJsonObject request;
|
||||
request["messages"] = QJsonArray{
|
||||
QJsonObject{
|
||||
{"role", "user"},
|
||||
{"content",
|
||||
QJsonArray{
|
||||
QJsonObject{{"type", "text"}, {"text", "describe this"}},
|
||||
QJsonObject{{"type", "image"}}}}},
|
||||
QJsonObject{{"role", "assistant"}, {"content", "ok"}}};
|
||||
|
||||
apply(request, false);
|
||||
|
||||
const QJsonArray msgs = request.value("messages").toArray();
|
||||
const QJsonArray content = msgs[0].toObject().value("content").toArray();
|
||||
ASSERT_EQ(content.size(), 2);
|
||||
EXPECT_FALSE(content[0].toObject().contains("cache_control"));
|
||||
EXPECT_EQ(content[1].toObject().value("cache_control").toObject(), expectedEphemeral(false));
|
||||
}
|
||||
|
||||
TEST(ClaudeCacheControlTest, NoSystemNoToolsNoMessagesIsNoop)
|
||||
{
|
||||
QJsonObject request;
|
||||
request["model"] = "claude-sonnet-4-5";
|
||||
request["max_tokens"] = 1024;
|
||||
|
||||
apply(request, false);
|
||||
|
||||
EXPECT_EQ(request.value("model").toString(), "claude-sonnet-4-5");
|
||||
EXPECT_EQ(request.value("max_tokens").toInt(), 1024);
|
||||
EXPECT_FALSE(request.contains("system"));
|
||||
EXPECT_FALSE(request.contains("tools"));
|
||||
EXPECT_FALSE(request.contains("messages"));
|
||||
}
|
||||
|
||||
TEST(ClaudeCacheControlTest, EmptyToolsArrayIsNoop)
|
||||
{
|
||||
QJsonObject request;
|
||||
request["tools"] = QJsonArray{};
|
||||
|
||||
apply(request, false);
|
||||
|
||||
EXPECT_TRUE(request.value("tools").isArray());
|
||||
EXPECT_TRUE(request.value("tools").toArray().isEmpty());
|
||||
}
|
||||
218
tests/CodeHandlerTest.cpp
Normal file
218
tests/CodeHandlerTest.cpp
Normal file
@@ -0,0 +1,218 @@
|
||||
// Copyright (C) 2025 Povilas Kanapickas
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "completion/CodeHandler.hpp"
|
||||
#include "TestUtils.hpp"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
using namespace QodeAssist;
|
||||
|
||||
class CodeHandlerTest : public QObject, public testing::Test
|
||||
{
|
||||
// Tests are written in the fixture format with the expectation that CodeHandler will
|
||||
// be expanded.
|
||||
Q_OBJECT
|
||||
};
|
||||
|
||||
TEST_F(CodeHandlerTest, testProcessTextEmpty)
|
||||
{
|
||||
EXPECT_EQ(CodeHandler::processText("", "/file.py"), "\n\n");
|
||||
}
|
||||
|
||||
TEST_F(CodeHandlerTest, testProcessTextWithLanguageCodeBlock)
|
||||
{
|
||||
QString input = "This is a comment\n"
|
||||
"```python\nprint('Hello, world!')\n```\n"
|
||||
"Another comment";
|
||||
|
||||
EXPECT_EQ(
|
||||
CodeHandler::processText(input, "/file.py"),
|
||||
"# This is a comment\n\nprint('Hello, world!')\n# Another comment\n\n");
|
||||
}
|
||||
|
||||
TEST_F(CodeHandlerTest, testProcessTextWithPlainCodeBlockNoNewline)
|
||||
{
|
||||
QString input = "This is a comment\n"
|
||||
"```print('Hello, world!')\n```\n"
|
||||
"Another comment";
|
||||
|
||||
EXPECT_EQ(
|
||||
CodeHandler::processText(input, "/file.py"),
|
||||
"# This is a comment\n\nprint('Hello, world!')\n# Another comment\n\n");
|
||||
}
|
||||
|
||||
TEST_F(CodeHandlerTest, testProcessTextWithPlainCodeBlockWithNewline)
|
||||
{
|
||||
QString input = "This is a comment\n"
|
||||
"```\nprint('Hello, world!')\n```\n"
|
||||
"Another comment";
|
||||
|
||||
EXPECT_EQ(
|
||||
CodeHandler::processText(input, "/file.py"),
|
||||
"# This is a comment\n\n\nprint('Hello, world!')\n# Another comment\n\n");
|
||||
}
|
||||
|
||||
TEST_F(CodeHandlerTest, testProcessTextNoCommentsWithLanguageCodeBlock)
|
||||
{
|
||||
QString input = "```python\nprint('Hello, world!')\n```";
|
||||
|
||||
EXPECT_EQ(CodeHandler::processText(input, "/file.py"), "print('Hello, world!')\n");
|
||||
}
|
||||
|
||||
TEST_F(CodeHandlerTest, testProcessTextNoCommentsWithPlainCodeBlockNoNewline)
|
||||
{
|
||||
QString input = "```print('Hello, world!')\n```";
|
||||
|
||||
EXPECT_EQ(CodeHandler::processText(input, "/file.py"), "print('Hello, world!')\n");
|
||||
}
|
||||
|
||||
TEST_F(CodeHandlerTest, testProcessTextNoCommentsWithPlainCodeBlockWithNewline)
|
||||
{
|
||||
QString input = "```\nprint('Hello, world!')\n```";
|
||||
|
||||
EXPECT_EQ(CodeHandler::processText(input, "/file.py"), "\nprint('Hello, world!')\n");
|
||||
}
|
||||
|
||||
TEST_F(CodeHandlerTest, testProcessTextWithMultipleCodeBlocksDifferentLanguages)
|
||||
{
|
||||
QString input = "First comment\n```python\nprint('Block 1')\n"
|
||||
"```\nMiddle comment\n"
|
||||
"```cpp\ncout << \"Block 2\";\n```\n"
|
||||
"Last comment";
|
||||
|
||||
EXPECT_EQ(
|
||||
CodeHandler::processText(input, "/file.py"),
|
||||
"# First comment\n\n"
|
||||
"print('Block 1')\n"
|
||||
"// Middle comment\n\n"
|
||||
"cout << \"Block 2\";\n"
|
||||
"// Last comment\n\n");
|
||||
}
|
||||
|
||||
TEST_F(CodeHandlerTest, testProcessTextWithMultipleCodeBlocksSameLanguage)
|
||||
{
|
||||
QString input = "First comment\n```python\nprint('Block 1')\n"
|
||||
"```\nMiddle comment\n"
|
||||
"```python\nprint('Block 2')\n```\n"
|
||||
"Last comment";
|
||||
|
||||
EXPECT_EQ(
|
||||
CodeHandler::processText(input, "/file.py"),
|
||||
"# First comment\n\n"
|
||||
"print('Block 1')\n"
|
||||
"# Middle comment\n\n"
|
||||
"print('Block 2')\n"
|
||||
"# Last comment\n\n");
|
||||
}
|
||||
|
||||
TEST_F(CodeHandlerTest, testProcessTextWithMultiplePlainCodeBlocksWithNewline)
|
||||
{
|
||||
QString input = "First comment\n```\nprint('Block 1')\n"
|
||||
"```\nMiddle comment\n"
|
||||
"```\ncout << \"Block 2\";\n```\n"
|
||||
"Last comment";
|
||||
|
||||
EXPECT_EQ(
|
||||
CodeHandler::processText(input, "/file.py"),
|
||||
"# First comment\n\n\n"
|
||||
"print('Block 1')\n"
|
||||
"# Middle comment\n\n\n"
|
||||
"cout << \"Block 2\";\n"
|
||||
"# Last comment\n\n");
|
||||
}
|
||||
|
||||
TEST_F(CodeHandlerTest, testProcessTextWithMultiplePlainCodeBlocksWithoutNewline)
|
||||
{
|
||||
QString input = "First comment\n```print('Block 1')\n"
|
||||
"```\nMiddle comment\n"
|
||||
"```cout << \"Block 2\";\n```\n"
|
||||
"Last comment";
|
||||
|
||||
EXPECT_EQ(
|
||||
CodeHandler::processText(input, "/file.py"),
|
||||
"# First comment\n\n"
|
||||
"print('Block 1')\n"
|
||||
"# Middle comment\n\n"
|
||||
"cout << \"Block 2\";\n"
|
||||
"# Last comment\n\n");
|
||||
}
|
||||
|
||||
TEST_F(CodeHandlerTest, testProcessTextWithEmptyLines)
|
||||
{
|
||||
QString input = "Comment with empty line\n\n```python\nprint('Hello')\n```\n\nAnother comment";
|
||||
|
||||
EXPECT_EQ(
|
||||
CodeHandler::processText(input, "/file.py"),
|
||||
"# Comment with empty line\n\n\n"
|
||||
"print('Hello')\n\n"
|
||||
"# Another comment\n\n");
|
||||
}
|
||||
|
||||
TEST_F(CodeHandlerTest, testProcessTextPlainCodeBlockWithNewlineWithEmptyLines)
|
||||
{
|
||||
QString input = "Comment with empty line\n\n```\nprint('Hello')\n```\n\nAnother comment";
|
||||
|
||||
EXPECT_EQ(
|
||||
CodeHandler::processText(input, "/file.py"),
|
||||
"# Comment with empty line\n\n\n\n"
|
||||
"print('Hello')\n\n"
|
||||
"# Another comment\n\n");
|
||||
}
|
||||
|
||||
TEST_F(CodeHandlerTest, testProcessTextWithoutCodeBlock)
|
||||
{
|
||||
QString input = "This is just a comment\nwith multiple lines";
|
||||
|
||||
EXPECT_EQ(
|
||||
CodeHandler::processText(input, "/file.py"),
|
||||
"# This is just a comment\n# with multiple lines\n\n");
|
||||
}
|
||||
|
||||
TEST_F(CodeHandlerTest, testDetectLanguageFromLine)
|
||||
{
|
||||
EXPECT_EQ(CodeHandler::detectLanguageFromLine("```python"), "python");
|
||||
EXPECT_EQ(CodeHandler::detectLanguageFromLine("```javascript"), "js");
|
||||
EXPECT_EQ(CodeHandler::detectLanguageFromLine("```cpp"), "c-like");
|
||||
EXPECT_EQ(CodeHandler::detectLanguageFromLine("``` ruby "), "ruby");
|
||||
EXPECT_EQ(CodeHandler::detectLanguageFromLine("```"), "");
|
||||
EXPECT_EQ(CodeHandler::detectLanguageFromLine("``` "), "");
|
||||
}
|
||||
|
||||
TEST_F(CodeHandlerTest, testDetectLanguageFromExtension)
|
||||
{
|
||||
EXPECT_EQ(CodeHandler::detectLanguageFromExtension("py"), "python");
|
||||
EXPECT_EQ(CodeHandler::detectLanguageFromExtension("js"), "js");
|
||||
EXPECT_EQ(CodeHandler::detectLanguageFromExtension("cpp"), "c-like");
|
||||
EXPECT_EQ(CodeHandler::detectLanguageFromExtension("hpp"), "c-like");
|
||||
EXPECT_EQ(CodeHandler::detectLanguageFromExtension("rb"), "ruby");
|
||||
EXPECT_EQ(CodeHandler::detectLanguageFromExtension("sh"), "shell");
|
||||
EXPECT_EQ(CodeHandler::detectLanguageFromExtension("unknown"), "");
|
||||
EXPECT_EQ(CodeHandler::detectLanguageFromExtension(""), "");
|
||||
}
|
||||
|
||||
TEST_F(CodeHandlerTest, testCommentPrefixForDifferentLanguages)
|
||||
{
|
||||
struct TestCase
|
||||
{
|
||||
std::string language;
|
||||
QString input;
|
||||
QString expected;
|
||||
};
|
||||
|
||||
std::vector<TestCase> testCases
|
||||
= {{"python", "Comment\n```python\ncode\n```", "# Comment\n\ncode\n"},
|
||||
{"cpp", "Comment\n```cpp\ncode\n```", "// Comment\n\ncode\n"},
|
||||
{"ruby", "Comment\n```ruby\ncode\n```", "# Comment\n\ncode\n"},
|
||||
{"lua", "Comment\n```lua\ncode\n```", "-- Comment\n\ncode\n"}};
|
||||
|
||||
for (const auto &testCase : testCases) {
|
||||
EXPECT_EQ(CodeHandler::processText(testCase.input, ""), testCase.expected)
|
||||
<< "Failed for language: " << testCase.language;
|
||||
}
|
||||
}
|
||||
|
||||
#include "CodeHandlerTest.moc"
|
||||
393
tests/DocumentContextReaderTest.cpp
Normal file
393
tests/DocumentContextReaderTest.cpp
Normal file
@@ -0,0 +1,393 @@
|
||||
// Copyright (C) 2025 Povilas Kanapickas
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "context/DocumentContextReader.hpp"
|
||||
#include "TestUtils.hpp"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <QSharedPointer>
|
||||
#include <QTextDocument>
|
||||
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
void PrintTo(const ContextData &data, std::ostream *os)
|
||||
{
|
||||
*os << "ContextData{prefix="
|
||||
<< (data.prefix ? data.prefix->toStdString() : "<nullopt>")
|
||||
<< ", suffix=" << (data.suffix ? data.suffix->toStdString() : "<nullopt>")
|
||||
<< ", fileContext=" << (data.fileContext ? data.fileContext->toStdString() : "<nullopt>")
|
||||
<< "}";
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::LLMCore
|
||||
|
||||
using namespace QodeAssist::Context;
|
||||
using namespace QodeAssist::LLMCore;
|
||||
using namespace QodeAssist::Settings;
|
||||
|
||||
class DocumentContextReaderTest : public QObject, public testing::Test
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
protected:
|
||||
QTextDocument *createTestDocument(const QString &text)
|
||||
{
|
||||
auto *doc = new QTextDocument(this);
|
||||
doc->setPlainText(text);
|
||||
return doc;
|
||||
}
|
||||
|
||||
DocumentContextReader createTestReader(const QString &text)
|
||||
{
|
||||
return DocumentContextReader(createTestDocument(text), "text/python", "/path/to/file");
|
||||
}
|
||||
|
||||
QSharedPointer<CodeCompletionSettings> createSettingsForWholeFile()
|
||||
{
|
||||
// CodeCompletionSettings is noncopyable
|
||||
auto settings = QSharedPointer<CodeCompletionSettings>::create();
|
||||
settings->readFullFile.setValue(true);
|
||||
return settings;
|
||||
}
|
||||
|
||||
QSharedPointer<CodeCompletionSettings> createSettingsForLines(int linesBefore, int linesAfter)
|
||||
{
|
||||
// CodeCompletionSettings is noncopyable
|
||||
auto settings = QSharedPointer<CodeCompletionSettings>::create();
|
||||
settings->readFullFile.setValue(false);
|
||||
settings->readStringsBeforeCursor.setValue(linesBefore);
|
||||
settings->readStringsAfterCursor.setValue(linesAfter);
|
||||
return settings;
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(DocumentContextReaderTest, testGetLineText)
|
||||
{
|
||||
auto reader = createTestReader("Line 0\nLine 1\nLine 2");
|
||||
|
||||
EXPECT_EQ(reader.getLineText(0), "Line 0");
|
||||
EXPECT_EQ(reader.getLineText(1), "Line 1");
|
||||
EXPECT_EQ(reader.getLineText(2), "Line 2");
|
||||
EXPECT_EQ(reader.getLineText(0, 4), "Line");
|
||||
}
|
||||
|
||||
TEST_F(DocumentContextReaderTest, testGetContext)
|
||||
{
|
||||
auto reader = createTestReader("Line 0\nLine 1\nLine 2\nLine 3\nLine 4");
|
||||
|
||||
// Unknown cursor position
|
||||
EXPECT_EQ(reader.getContextBefore(0, -1, 2), "Line 0");
|
||||
EXPECT_EQ(reader.getContextAfter(0, -1, 2), "Line 0\nLine 1");
|
||||
|
||||
EXPECT_EQ(reader.getContextBefore(1, -1, 2), "Line 0\nLine 1");
|
||||
EXPECT_EQ(reader.getContextAfter(1, -1, 2), "Line 1\nLine 2");
|
||||
|
||||
EXPECT_EQ(reader.getContextBefore(2, -1, 2), "Line 1\nLine 2");
|
||||
EXPECT_EQ(reader.getContextAfter(2, -1, 2), "Line 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.getContextBefore(3, -1, 2), "Line 2\nLine 3");
|
||||
EXPECT_EQ(reader.getContextAfter(3, -1, 2), "Line 3\nLine 4");
|
||||
|
||||
// Known cursor position
|
||||
EXPECT_EQ(reader.getContextBefore(0, 1, 2), "L");
|
||||
EXPECT_EQ(reader.getContextAfter(0, 1, 2), "ine 0\nLine 1");
|
||||
|
||||
EXPECT_EQ(reader.getContextBefore(1, 1, 2), "Line 0\nL");
|
||||
EXPECT_EQ(reader.getContextAfter(1, 1, 2), "ine 1\nLine 2");
|
||||
|
||||
EXPECT_EQ(reader.getContextBefore(2, 1, 2), "Line 1\nL");
|
||||
EXPECT_EQ(reader.getContextAfter(2, 1, 2), "ine 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.getContextBefore(3, 1, 2), "Line 2\nL");
|
||||
EXPECT_EQ(reader.getContextAfter(3, 1, 2), "ine 3\nLine 4");
|
||||
}
|
||||
|
||||
TEST_F(DocumentContextReaderTest, testGetContextWithCopyright)
|
||||
{
|
||||
auto reader = createTestReader("/* Copyright (C) 2024 */\nLine 0\nLine 1\nLine 2\nLine 3");
|
||||
|
||||
// Unknown cursor position
|
||||
EXPECT_EQ(reader.getContextBefore(0, -1, 2), "");
|
||||
EXPECT_EQ(reader.getContextAfter(0, -1, 2), "Line 0");
|
||||
|
||||
EXPECT_EQ(reader.getContextBefore(1, -1, 2), "Line 0");
|
||||
EXPECT_EQ(reader.getContextAfter(1, -1, 2), "Line 0\nLine 1");
|
||||
|
||||
EXPECT_EQ(reader.getContextBefore(2, -1, 2), "Line 0\nLine 1");
|
||||
EXPECT_EQ(reader.getContextAfter(2, -1, 2), "Line 1\nLine 2");
|
||||
|
||||
EXPECT_EQ(reader.getContextBefore(3, -1, 2), "Line 1\nLine 2");
|
||||
EXPECT_EQ(reader.getContextAfter(3, -1, 2), "Line 2\nLine 3");
|
||||
|
||||
// Known cursor position
|
||||
EXPECT_EQ(reader.getContextBefore(0, 1, 2), "");
|
||||
EXPECT_EQ(reader.getContextAfter(0, 1, 2), "Line 0");
|
||||
|
||||
EXPECT_EQ(reader.getContextBefore(1, 1, 2), "L");
|
||||
EXPECT_EQ(reader.getContextAfter(1, 1, 2), "ine 0\nLine 1");
|
||||
|
||||
EXPECT_EQ(reader.getContextBefore(2, 1, 2), "Line 0\nL");
|
||||
EXPECT_EQ(reader.getContextAfter(2, 1, 2), "ine 1\nLine 2");
|
||||
|
||||
EXPECT_EQ(reader.getContextBefore(3, 1, 2), "Line 1\nL");
|
||||
EXPECT_EQ(reader.getContextAfter(3, 1, 2), "ine 2\nLine 3");
|
||||
}
|
||||
|
||||
TEST_F(DocumentContextReaderTest, testReadWholeFile)
|
||||
{
|
||||
auto reader = createTestReader("Line 0\nLine 1\nLine 2\nLine 3\nLine 4");
|
||||
|
||||
// Unknown cursor position
|
||||
EXPECT_EQ(reader.readWholeFileBefore(0, -1), "Line 0");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(0, -1), "Line 0\nLine 1\nLine 2\nLine 3\nLine 4");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(1, -1), "Line 0\nLine 1");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(1, -1), "Line 1\nLine 2\nLine 3\nLine 4");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(2, -1), "Line 0\nLine 1\nLine 2");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(2, -1), "Line 2\nLine 3\nLine 4");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(3, -1), "Line 0\nLine 1\nLine 2\nLine 3");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(3, -1), "Line 3\nLine 4");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(4, -1), "Line 0\nLine 1\nLine 2\nLine 3\nLine 4");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(4, -1), "Line 4");
|
||||
|
||||
// Known cursor position
|
||||
EXPECT_EQ(reader.readWholeFileBefore(0, 1), "L");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(0, 1), "ine 0\nLine 1\nLine 2\nLine 3\nLine 4");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(1, 1), "Line 0\nL");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(1, 1), "ine 1\nLine 2\nLine 3\nLine 4");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(2, 1), "Line 0\nLine 1\nL");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(2, 1), "ine 2\nLine 3\nLine 4");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(3, 1), "Line 0\nLine 1\nLine 2\nL");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(3, 1), "ine 3\nLine 4");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(4, 1), "Line 0\nLine 1\nLine 2\nLine 3\nL");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(4, 1), "ine 4");
|
||||
}
|
||||
|
||||
TEST_F(DocumentContextReaderTest, testReadWholeFileWithCopyright)
|
||||
{
|
||||
auto reader = createTestReader("/* Copyright (C) 2024 */\nLine 0\nLine 1\nLine 2\nLine 3");
|
||||
// Unknown cursor position
|
||||
EXPECT_EQ(reader.readWholeFileBefore(0, -1), "");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(0, -1), "Line 0\nLine 1\nLine 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(1, -1), "Line 0");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(1, -1), "Line 0\nLine 1\nLine 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(2, -1), "Line 0\nLine 1");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(2, -1), "Line 1\nLine 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(3, -1), "Line 0\nLine 1\nLine 2");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(3, -1), "Line 2\nLine 3");
|
||||
|
||||
// Known cursor position
|
||||
EXPECT_EQ(reader.readWholeFileBefore(0, 0), "");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(0, 0), "Line 0\nLine 1\nLine 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(1, 0), "");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(1, 0), "Line 0\nLine 1\nLine 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(2, 0), "Line 0\n");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(2, 0), "Line 1\nLine 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(3, 0), "Line 0\nLine 1\n");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(3, 0), "Line 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(0, 1), "");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(0, 1), "Line 0\nLine 1\nLine 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(1, 1), "L");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(1, 1), "ine 0\nLine 1\nLine 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(2, 1), "Line 0\nL");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(2, 1), "ine 1\nLine 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(3, 1), "Line 0\nLine 1\nL");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(3, 1), "ine 2\nLine 3");
|
||||
}
|
||||
|
||||
TEST_F(DocumentContextReaderTest, testReadWholeFileWithMultilineCopyright)
|
||||
{
|
||||
auto reader = createTestReader(
|
||||
"/*\n * Copyright (C) 2024\n * \n * This file is part of QodeAssist.\n */\n"
|
||||
"Line 0\nLine 1\nLine 2\nLine 3");
|
||||
|
||||
// Unknown cursor position
|
||||
EXPECT_EQ(reader.readWholeFileBefore(0, -1), "");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(0, -1), "Line 0\nLine 1\nLine 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(1, -1), "");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(1, -1), "Line 0\nLine 1\nLine 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(2, -1), "");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(2, -1), "Line 0\nLine 1\nLine 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(3, -1), "");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(3, -1), "Line 0\nLine 1\nLine 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(4, -1), "");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(4, -1), "Line 0\nLine 1\nLine 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(5, -1), "Line 0");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(5, -1), "Line 0\nLine 1\nLine 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(6, -1), "Line 0\nLine 1");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(6, -1), "Line 1\nLine 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(7, -1), "Line 0\nLine 1\nLine 2");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(7, -1), "Line 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(8, -1), "Line 0\nLine 1\nLine 2\nLine 3");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(8, -1), "Line 3");
|
||||
|
||||
// Known cursor position
|
||||
EXPECT_EQ(reader.readWholeFileBefore(0, 0), "");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(0, 0), "Line 0\nLine 1\nLine 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(1, 0), "");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(1, 0), "Line 0\nLine 1\nLine 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(2, 0), "");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(2, 0), "Line 0\nLine 1\nLine 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(3, 0), "");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(3, 0), "Line 0\nLine 1\nLine 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(4, 0), "");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(4, 0), "Line 0\nLine 1\nLine 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(5, 0), "");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(5, 0), "Line 0\nLine 1\nLine 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(6, 0), "Line 0\n");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(6, 0), "Line 1\nLine 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(7, 0), "Line 0\nLine 1\n");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(7, 0), "Line 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(8, 0), "Line 0\nLine 1\nLine 2\n");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(8, 0), "Line 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(0, 1), "");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(0, 1), "Line 0\nLine 1\nLine 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(1, 1), "");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(1, 1), "Line 0\nLine 1\nLine 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(2, 1), "");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(2, 1), "Line 0\nLine 1\nLine 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(3, 1), "");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(3, 1), "Line 0\nLine 1\nLine 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(4, 1), "");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(4, 1), "Line 0\nLine 1\nLine 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(5, 1), "L");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(5, 1), "ine 0\nLine 1\nLine 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(6, 1), "Line 0\nL");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(6, 1), "ine 1\nLine 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(7, 1), "Line 0\nLine 1\nL");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(7, 1), "ine 2\nLine 3");
|
||||
|
||||
EXPECT_EQ(reader.readWholeFileBefore(8, 1), "Line 0\nLine 1\nLine 2\nL");
|
||||
EXPECT_EQ(reader.readWholeFileAfter(8, 1), "ine 3");
|
||||
}
|
||||
|
||||
TEST_F(DocumentContextReaderTest, testFindCopyrightSingleLine)
|
||||
{
|
||||
auto reader = createTestReader("/* Copyright (C) 2024 */\nCode line 0\nCode line 1");
|
||||
|
||||
auto info = reader.findCopyright();
|
||||
ASSERT_TRUE(info.found);
|
||||
EXPECT_EQ(info.startLine, 0);
|
||||
EXPECT_EQ(info.endLine, 0);
|
||||
}
|
||||
|
||||
TEST_F(DocumentContextReaderTest, testFindCopyrightMultiLine)
|
||||
{
|
||||
auto reader = createTestReader(
|
||||
"/*\n * Copyright (C) 2024\n * \n * This file is part of QodeAssist.\n */\nCode line 0");
|
||||
|
||||
auto info = reader.findCopyright();
|
||||
ASSERT_TRUE(info.found);
|
||||
EXPECT_EQ(info.startLine, 0);
|
||||
EXPECT_EQ(info.endLine, 4);
|
||||
}
|
||||
|
||||
TEST_F(DocumentContextReaderTest, testFindCopyrightMultipleBlocks)
|
||||
{
|
||||
auto reader = createTestReader("/* Copyright 2023 */\n\n/* Copyright 2024 */\nCode");
|
||||
|
||||
auto info = reader.findCopyright();
|
||||
ASSERT_TRUE(info.found);
|
||||
EXPECT_EQ(info.startLine, 0);
|
||||
EXPECT_EQ(info.endLine, 0);
|
||||
}
|
||||
|
||||
TEST_F(DocumentContextReaderTest, testFindCopyrightNoCopyright)
|
||||
{
|
||||
auto reader = createTestReader("/* Just a comment */\nCode line 0");
|
||||
|
||||
auto info = reader.findCopyright();
|
||||
ASSERT_TRUE(!info.found);
|
||||
EXPECT_EQ(info.startLine, -1);
|
||||
EXPECT_EQ(info.endLine, -1);
|
||||
}
|
||||
|
||||
TEST_F(DocumentContextReaderTest, testGetContextBetween)
|
||||
{
|
||||
auto reader = createTestReader("Line 0\nLine 1\nLine 2\nLine 3\nLine 4");
|
||||
|
||||
EXPECT_EQ(reader.getContextBetween(2, -1, 0, -1), "");
|
||||
EXPECT_EQ(reader.getContextBetween(0, -1, 0, -1), "Line 0");
|
||||
EXPECT_EQ(reader.getContextBetween(1, -1, 1, -1), "Line 1");
|
||||
EXPECT_EQ(reader.getContextBetween(1, 3, 1, 1), "");
|
||||
EXPECT_EQ(reader.getContextBetween(1, 3, 1, 3), "");
|
||||
EXPECT_EQ(reader.getContextBetween(1, 3, 1, 4), "e");
|
||||
|
||||
EXPECT_EQ(reader.getContextBetween(1, -1, 3, -1), "Line 1\nLine 2\nLine 3");
|
||||
EXPECT_EQ(reader.getContextBetween(1, 2, 3, -1), "ne 1\nLine 2\nLine 3");
|
||||
EXPECT_EQ(reader.getContextBetween(1, -1, 3, 2), "Line 1\nLine 2\nLi");
|
||||
EXPECT_EQ(reader.getContextBetween(1, 2, 3, 2), "ne 1\nLine 2\nLi");
|
||||
}
|
||||
|
||||
TEST_F(DocumentContextReaderTest, testPrepareContext)
|
||||
{
|
||||
auto reader = createTestReader("Line 0\nLine 1\nLine 2\nLine 3\nLine 4");
|
||||
|
||||
EXPECT_EQ(
|
||||
reader.prepareContext(2, 3, *createSettingsForWholeFile()),
|
||||
(QodeAssist::LLMCore::ContextData{
|
||||
.prefix = "Line 0\nLine 1\nLin",
|
||||
.suffix = "e 2\nLine 3\nLine 4",
|
||||
.fileContext = "\n Language: (MIME: text/python) filepath: /path/to/file()\n\n"
|
||||
"Recent Project Changes Context:\n "}));
|
||||
|
||||
EXPECT_EQ(
|
||||
reader.prepareContext(2, 3, *createSettingsForLines(1, 1)),
|
||||
(QodeAssist::LLMCore::ContextData{
|
||||
.prefix = "Line 1\nLin",
|
||||
.suffix = "e 2\nLine 3",
|
||||
.fileContext = "\n Language: (MIME: text/python) filepath: /path/to/file()\n\n"
|
||||
"Recent Project Changes Context:\n "}));
|
||||
|
||||
EXPECT_EQ(
|
||||
reader.prepareContext(2, 3, *createSettingsForLines(2, 2)),
|
||||
(QodeAssist::LLMCore::ContextData{
|
||||
.prefix = "Line 0\nLine 1\nLin",
|
||||
.suffix = "e 2\nLine 3\nLine 4",
|
||||
.fileContext = "\n Language: (MIME: text/python) filepath: /path/to/file()\n\n"
|
||||
"Recent Project Changes Context:\n "}));
|
||||
}
|
||||
|
||||
#include "DocumentContextReaderTest.moc"
|
||||
112
tests/LLMSuggestionTest.cpp
Normal file
112
tests/LLMSuggestionTest.cpp
Normal file
@@ -0,0 +1,112 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "completion/LLMSuggestion.hpp"
|
||||
#include "TestUtils.hpp"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
using namespace QodeAssist;
|
||||
|
||||
class LLMSuggestionTest : public QObject, public testing::Test
|
||||
{
|
||||
Q_OBJECT
|
||||
};
|
||||
|
||||
// Degenerate / no-op cases
|
||||
TEST_F(LLMSuggestionTest, emptyRight)
|
||||
{
|
||||
EXPECT_EQ(LLMSuggestion::calculateReplaceLength("foo", ""), 0);
|
||||
}
|
||||
|
||||
TEST_F(LLMSuggestionTest, noOverlap)
|
||||
{
|
||||
EXPECT_EQ(LLMSuggestion::calculateReplaceLength("foo", "bar"), 0);
|
||||
}
|
||||
|
||||
TEST_F(LLMSuggestionTest, singleCharNoOverlap)
|
||||
{
|
||||
EXPECT_EQ(LLMSuggestion::calculateReplaceLength("a", "b"), 0);
|
||||
}
|
||||
|
||||
// LCP: model echoed the existing right side as its own prefix
|
||||
TEST_F(LLMSuggestionTest, lcpExtendsRight)
|
||||
{
|
||||
// cursor: int |myVariable ; suggestion echoes "myVar" then adds nothing.
|
||||
// LCP=5, remainder "iable" not a closing tail -> replace 5.
|
||||
EXPECT_EQ(LLMSuggestion::calculateReplaceLength("myVar", "myVariable"), 5);
|
||||
}
|
||||
|
||||
TEST_F(LLMSuggestionTest, lcpPartialEngineCall)
|
||||
{
|
||||
// cursor: |engine.rootContext() ; suggestion: engine.load()
|
||||
// LCP="engine." (7); remainder "rootContext()" is not closing-only -> keep LCP
|
||||
EXPECT_EQ(LLMSuggestion::calculateReplaceLength("engine.load()", "engine.rootContext()"), 7);
|
||||
}
|
||||
|
||||
// LCP + closing-only tail: extend to full right
|
||||
TEST_F(LLMSuggestionTest, lcpThenClosingTailExtendsFull)
|
||||
{
|
||||
// cursor: QStringList |colors{}; ; suggestion: colors << "red"
|
||||
// LCP=6 ("colors"); remainder "{};" is punctuation -> replace all (9)
|
||||
EXPECT_EQ(LLMSuggestion::calculateReplaceLength("colors << \"red\"", "colors{};"), 9);
|
||||
}
|
||||
|
||||
// LCP=0, right is closing-only, suggestion ends with a matching closer -> replace full right
|
||||
TEST_F(LLMSuggestionTest, closingTailReplaceSemicolon)
|
||||
{
|
||||
EXPECT_EQ(LLMSuggestion::calculateReplaceLength("x;", ";"), 1);
|
||||
}
|
||||
|
||||
TEST_F(LLMSuggestionTest, closingTailReplaceParen)
|
||||
{
|
||||
EXPECT_EQ(LLMSuggestion::calculateReplaceLength("arg1, arg2)", ")"), 1);
|
||||
}
|
||||
|
||||
TEST_F(LLMSuggestionTest, closingTailReplaceBrackets)
|
||||
{
|
||||
EXPECT_EQ(LLMSuggestion::calculateReplaceLength("[0]", "];"), 2);
|
||||
}
|
||||
|
||||
TEST_F(LLMSuggestionTest, closingTailReplaceBracesAndSemi)
|
||||
{
|
||||
// cursor: colors|{}; ; suggestion: = {"red"}
|
||||
// LCP=0; right "{};" is closing-only; suggestion ends on "}" which is in right -> replace 3
|
||||
EXPECT_EQ(LLMSuggestion::calculateReplaceLength("= {\"red\"}", "{};"), 3);
|
||||
}
|
||||
|
||||
TEST_F(LLMSuggestionTest, closingTailWithLeadingSpace)
|
||||
{
|
||||
EXPECT_EQ(LLMSuggestion::calculateReplaceLength(" = {\"red\"}", "{};"), 3);
|
||||
}
|
||||
|
||||
TEST_F(LLMSuggestionTest, closingTailEqualsSemi)
|
||||
{
|
||||
EXPECT_EQ(LLMSuggestion::calculateReplaceLength("= 5;", ";"), 1);
|
||||
}
|
||||
|
||||
// LCP=0, right is closing-only but suggestion does not end on any of its chars -> leave right alone
|
||||
TEST_F(LLMSuggestionTest, closingTailNoMatchingClose)
|
||||
{
|
||||
// right "};" closes; suggestion ends on '"' (not in close set) -> 0
|
||||
EXPECT_EQ(LLMSuggestion::calculateReplaceLength("\"red\", \"green\"", "};"), 0);
|
||||
}
|
||||
|
||||
// Right side has real code (not closing-only) and no LCP -> leave it alone
|
||||
TEST_F(LLMSuggestionTest, realCodeRightNoLcp)
|
||||
{
|
||||
// cursor: int |myVar = 5; ; suggestion: myVar
|
||||
// LCP=0, right " = 5;" is not closing-only (has '5') -> 0
|
||||
EXPECT_EQ(LLMSuggestion::calculateReplaceLength("myVar", " = 5;"), 0);
|
||||
}
|
||||
|
||||
// Trailing whitespace is not treated as a closer, suggestion has no closer -> leave alone
|
||||
TEST_F(LLMSuggestionTest, trailingWhitespaceOnlyLeftAlone)
|
||||
{
|
||||
EXPECT_EQ(LLMSuggestion::calculateReplaceLength("code", " "), 0);
|
||||
}
|
||||
|
||||
#include "LLMSuggestionTest.moc"
|
||||
65
tests/TestUtils.hpp
Normal file
65
tests/TestUtils.hpp
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright (C) 2025 Povilas Kanapickas
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include <iostream>
|
||||
#include "llmcore/ContextData.hpp"
|
||||
#include <QString>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
// gtest can't pick pretty printer when comparing QString
|
||||
inline void PrintTo(const QString &value, ::std::ostream *out)
|
||||
{
|
||||
*out << '"' << value.toStdString() << '"';
|
||||
}
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
||||
inline std::ostream &operator<<(std::ostream &out, const QString &value)
|
||||
{
|
||||
out << '"' << value.toStdString() << '"';
|
||||
return out;
|
||||
}
|
||||
|
||||
template<class T>
|
||||
std::ostream &operator<<(std::ostream &out, const QVector<T> &value)
|
||||
{
|
||||
out << "[";
|
||||
for (const auto &el : value) {
|
||||
out << value << ", ";
|
||||
}
|
||||
out << "]";
|
||||
return out;
|
||||
}
|
||||
|
||||
template<class T>
|
||||
std::ostream &operator<<(std::ostream &out, const std::optional<T> &value)
|
||||
{
|
||||
if (value.has_value()) {
|
||||
out << value.value();
|
||||
} else {
|
||||
out << "(no value)";
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
inline std::ostream &operator<<(std::ostream &out, const Message &value)
|
||||
{
|
||||
out << "Message{"
|
||||
<< "role=" << value.role << "content=" << value.content << "}";
|
||||
return out;
|
||||
}
|
||||
|
||||
inline std::ostream &operator<<(std::ostream &out, const ContextData &value)
|
||||
{
|
||||
out << "ContextData{"
|
||||
<< "\n systemPrompt=" << value.systemPrompt << "\n prefix=" << value.prefix
|
||||
<< "\n suffix=" << value.suffix << "\n fileContext=" << value.fileContext
|
||||
<< "\n history=" << value.history << "\n}";
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::LLMCore
|
||||
23
tests/unittest_main.cpp
Normal file
23
tests/unittest_main.cpp
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright (C) 2025 Povilas Kanapickas
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <QGuiApplication>
|
||||
#include <QtGlobal>
|
||||
|
||||
void silenceQtWarningNoise(QtMsgType type, const QMessageLogContext &context, const QString &msg)
|
||||
{
|
||||
if (msg.startsWith("SOFT ASSERT") || msg.startsWith("Accessing MimeDatabase")) {
|
||||
return;
|
||||
}
|
||||
qt_message_output(type, context, msg);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
qInstallMessageHandler(silenceQtWarningNoise);
|
||||
QGuiApplication application(argc, argv);
|
||||
testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
Reference in New Issue
Block a user