refactor: Restructure project into sources/tests layout and dissolve PluginLLMCore

This commit is contained in:
Petr Mironychev
2026-07-14 02:50:43 +02:00
parent adef7972ed
commit 34d4f2c517
348 changed files with 809 additions and 852 deletions

30
tests/CMakeLists.txt Normal file
View 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)

View 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
View 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"

View 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
View 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
View 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
View 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();
}