Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions autotests/dfm-search-tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ target_sources(dfm-search-test PRIVATE
${CMAKE_SOURCE_DIR}/src/dfm-search/dfm-search-client/size_parser.cpp
${CMAKE_SOURCE_DIR}/src/dfm-search/dfm-search-client/time_parser.cpp
${CMAKE_SOURCE_DIR}/src/dfm-search/dfm-search-client/cli_options.cpp
${CMAKE_SOURCE_DIR}/src/dfm-search/dfm-search-client/preview_output_utils.cpp
${CMAKE_SOURCE_DIR}/src/dfm-search/dfm-search-client/output/output_formatter.h
${CMAKE_SOURCE_DIR}/src/dfm-search/dfm-search-client/output/text_output.cpp
${CMAKE_SOURCE_DIR}/src/dfm-search/dfm-search-client/output/json_output.cpp
${CMAKE_SOURCE_DIR}/3rdparty/testutils/stub-ext/stub-shadow.cpp
)

Expand Down
5 changes: 5 additions & 0 deletions autotests/dfm-search-tests/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ extern QObject *create_tst_FileNameSearchEngine();
extern QObject *create_tst_RecentSearchEngine();
extern QObject *create_tst_CliOptions();
extern QObject *create_tst_DConfigParsing();
extern QObject *create_tst_SearchOutput();

int main(int argc, char *argv[])
{
Expand Down Expand Up @@ -116,5 +117,9 @@ int main(int argc, char *argv[])
result |= QTest::qExec(testObj20, argc, argv);
delete testObj20;

QObject *testObj21 = create_tst_SearchOutput();
result |= QTest::qExec(testObj21, argc, argv);
delete testObj21;

return result;
}
6 changes: 6 additions & 0 deletions autotests/dfm-search-tests/tst_content_retriever.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ void tst_ContentRetriever::fetchPreview_noKeyword()
SearchType::Content,
options);
QCOMPARE(result.content(), QString("world"));
QCOMPARE(result.charCount(), QString("hello world from content index").size());
QCOMPARE(result.keywordOffset(), -1);
}

Expand All @@ -279,6 +280,7 @@ void tst_ContentRetriever::fetchPreview_withKeyword()
SearchType::Content,
options);
QCOMPARE(result.content(), QString("world from"));
QCOMPARE(result.charCount(), QString("hello world from content index").size());
QCOMPARE(result.keywordOffset(), 6);
}

Expand All @@ -304,6 +306,7 @@ void tst_ContentRetriever::fetchPreview_keywordNotFound()
SearchType::Content,
options);
QVERIFY(result.content().isEmpty());
QCOMPARE(result.charCount(), QString("hello world from content index").size());
QCOMPARE(result.keywordOffset(), -1);
}

Expand All @@ -328,6 +331,7 @@ void tst_ContentRetriever::fetchPreview_offsetBeyondContent()
SearchType::Content,
options);
QVERIFY(result.content().isEmpty());
QCOMPARE(result.charCount(), QString("hello world from content index").size());
QCOMPARE(result.keywordOffset(), -1);
}

Expand All @@ -354,6 +358,7 @@ void tst_ContentRetriever::fetchPreview_unlimitedNoKeyword()
SearchType::Content,
options);
QCOMPARE(result.content(), QString("hello world from content index"));
QCOMPARE(result.charCount(), QString("hello world from content index").size());
QCOMPARE(result.keywordOffset(), -1);
}

Expand All @@ -368,6 +373,7 @@ void tst_ContentRetriever::fetchPreview_unlimitedNoKeyword()
SearchType::Content,
options);
QCOMPARE(result.content(), QString("world from content index"));
QCOMPARE(result.charCount(), QString("hello world from content index").size());
QCOMPARE(result.keywordOffset(), -1);
}
}
Expand Down
279 changes: 279 additions & 0 deletions autotests/dfm-search-tests/tst_search_output.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: GPL-3.0-or-later

#include <QDir>

Check warning on line 5 in autotests/dfm-search-tests/tst_search_output.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QDir> not found. Please note: Cppcheck does not need standard library headers to get proper results.

Check warning on line 5 in autotests/dfm-search-tests/tst_search_output.cpp

View workflow job for this annotation

GitHub Actions / static-check / static-check

Include file: <QDir> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QJsonArray>

Check warning on line 6 in autotests/dfm-search-tests/tst_search_output.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QJsonArray> not found. Please note: Cppcheck does not need standard library headers to get proper results.

Check warning on line 6 in autotests/dfm-search-tests/tst_search_output.cpp

View workflow job for this annotation

GitHub Actions / static-check / static-check

Include file: <QJsonArray> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QJsonDocument>

Check warning on line 7 in autotests/dfm-search-tests/tst_search_output.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QJsonDocument> not found. Please note: Cppcheck does not need standard library headers to get proper results.

Check warning on line 7 in autotests/dfm-search-tests/tst_search_output.cpp

View workflow job for this annotation

GitHub Actions / static-check / static-check

Include file: <QJsonDocument> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QJsonObject>

Check warning on line 8 in autotests/dfm-search-tests/tst_search_output.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QJsonObject> not found. Please note: Cppcheck does not need standard library headers to get proper results.

Check warning on line 8 in autotests/dfm-search-tests/tst_search_output.cpp

View workflow job for this annotation

GitHub Actions / static-check / static-check

Include file: <QJsonObject> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QTemporaryDir>

Check warning on line 9 in autotests/dfm-search-tests/tst_search_output.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QTemporaryDir> not found. Please note: Cppcheck does not need standard library headers to get proper results.

Check warning on line 9 in autotests/dfm-search-tests/tst_search_output.cpp

View workflow job for this annotation

GitHub Actions / static-check / static-check

Include file: <QTemporaryDir> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QTemporaryFile>

Check warning on line 10 in autotests/dfm-search-tests/tst_search_output.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QTemporaryFile> not found. Please note: Cppcheck does not need standard library headers to get proper results.

Check warning on line 10 in autotests/dfm-search-tests/tst_search_output.cpp

View workflow job for this annotation

GitHub Actions / static-check / static-check

Include file: <QTemporaryFile> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QTest>

Check warning on line 11 in autotests/dfm-search-tests/tst_search_output.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QTest> not found. Please note: Cppcheck does not need standard library headers to get proper results.

Check warning on line 11 in autotests/dfm-search-tests/tst_search_output.cpp

View workflow job for this annotation

GitHub Actions / static-check / static-check

Include file: <QTest> not found. Please note: Cppcheck does not need standard library headers to get proper results.

#include <cstdio>

Check warning on line 13 in autotests/dfm-search-tests/tst_search_output.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <cstdio> not found. Please note: Cppcheck does not need standard library headers to get proper results.

Check warning on line 13 in autotests/dfm-search-tests/tst_search_output.cpp

View workflow job for this annotation

GitHub Actions / static-check / static-check

Include file: <cstdio> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <functional>

Check warning on line 14 in autotests/dfm-search-tests/tst_search_output.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <functional> not found. Please note: Cppcheck does not need standard library headers to get proper results.

Check warning on line 14 in autotests/dfm-search-tests/tst_search_output.cpp

View workflow job for this annotation

GitHub Actions / static-check / static-check

Include file: <functional> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <iostream>

Check warning on line 15 in autotests/dfm-search-tests/tst_search_output.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <iostream> not found. Please note: Cppcheck does not need standard library headers to get proper results.

Check warning on line 15 in autotests/dfm-search-tests/tst_search_output.cpp

View workflow job for this annotation

GitHub Actions / static-check / static-check

Include file: <iostream> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <unistd.h>

#include <dfm-search/contentretriever.h>
#include <dfm-search/contentsearchapi.h>
#include <dfm-search/field_names.h>
#include <dfm-search/filenamesearchapi.h>
#include <dfm-search/ocrtextsearchapi.h>
#include <dfm-search/searchoptions.h>

#include <lucene++/Document.h>
#include <lucene++/Field.h>
#include <lucene++/FSDirectory.h>
#include <lucene++/IndexWriter.h>
#include <lucene++/KeywordAnalyzer.h>
#include <lucene++/LuceneHeaders.h>

#include "output/json_output.h"
#include "output/text_output.h"
#include "preview_output_utils.h"

using namespace DFMSEARCH;
using namespace Lucene;

namespace {

void addStoredDocument(const IndexWriterPtr &writer,
SearchType type,
const QString &path,
const QString &filename,
const QString &content)
{
DocumentPtr doc = newLucene<Document>();
if (type == SearchType::Ocr) {
doc->add(newLucene<Field>(LuceneFieldNames::OcrText::kPath, path.toStdWString(),
Field::STORE_YES, Field::INDEX_NOT_ANALYZED));
doc->add(newLucene<Field>(LuceneFieldNames::OcrText::kFilename, filename.toStdWString(),
Field::STORE_YES, Field::INDEX_NOT_ANALYZED));
doc->add(newLucene<Field>(LuceneFieldNames::OcrText::kOcrContents, content.toStdWString(),
Field::STORE_YES, Field::INDEX_NOT_ANALYZED));
} else {
doc->add(newLucene<Field>(LuceneFieldNames::Content::kPath, path.toStdWString(),
Field::STORE_YES, Field::INDEX_NOT_ANALYZED));
doc->add(newLucene<Field>(LuceneFieldNames::Content::kFilename, filename.toStdWString(),
Field::STORE_YES, Field::INDEX_NOT_ANALYZED));
doc->add(newLucene<Field>(LuceneFieldNames::Content::kContents, content.toStdWString(),
Field::STORE_YES, Field::INDEX_NOT_ANALYZED));
}
writer->addDocument(doc);
}

void createIndex(const QString &indexDir, SearchType type)
{
QDir().mkpath(indexDir);
IndexWriterPtr writer = newLucene<IndexWriter>(
FSDirectory::open(indexDir.toStdWString()),
newLucene<KeywordAnalyzer>(),
true,
IndexWriter::MaxFieldLengthLIMITED);

if (type == SearchType::Content) {
addStoredDocument(writer, type,
"/tmp/doc-a.txt",
"doc-a.txt",
"hello world from content index");
} else {
addStoredDocument(writer, type,
"/tmp/img-a.png",
"img-a.png",
"screenshot text from OCR");
}

writer->close();
}

QString captureStdout(const std::function<void()> &fn)
{
QTemporaryFile file;
if (!file.open()) {
return {};
}

fflush(stdout);
std::cout.flush();

const int savedStdout = dup(STDOUT_FILENO);
if (savedStdout < 0) {
return {};
}

dup2(file.handle(), STDOUT_FILENO);
fn();
fflush(stdout);
std::cout.flush();

dup2(savedStdout, STDOUT_FILENO);
close(savedStdout);

file.seek(0);
return QString::fromUtf8(file.readAll());
}

} // namespace

class tst_SearchOutput : public QObject
{
Q_OBJECT

private Q_SLOTS:
void previewOutputHelpers_includeCharCount();
void jsonOutput_contentAndFilenameCharCountContract();
void jsonOutput_ocrAndSemanticCharCountContract();
void textOutput_contentAndSemanticCharCountContract();
};

void tst_SearchOutput::previewOutputHelpers_includeCharCount()
{
QTemporaryDir tempDir;
QVERIFY(tempDir.isValid());

const QString contentIndexDir = tempDir.path() + "/content-index";
createIndex(contentIndexDir, SearchType::Content);

ContentRetriever retriever;
retriever.setIndexDirectory(SearchType::Content, contentIndexDir);

PreviewOptions options;
options.setKeyword("world");
options.setOffset(0);
options.setMaxLength(10);

const PreviewResult result = retriever.fetchPreview("/tmp/doc-a.txt", SearchType::Content, options);
const QJsonObject json = dfmsearch::previewResultToJson("/tmp/doc-a.txt", result);
QCOMPARE(json.value("content").toString(), QString("world from"));
QCOMPARE(json.value("charCount").toInt(), QString("hello world from content index").size());
QCOMPARE(json.value("keywordOffset").toInt(), 6);

QString text;
QTextStream stream(&text);
dfmsearch::writePreviewResultText(stream, "/tmp/doc-a.txt", result);
QVERIFY(text.contains("Char count: 30"));
QVERIFY(text.contains("world from"));
}

void tst_SearchOutput::jsonOutput_contentAndFilenameCharCountContract()
{
SearchOptions options;
options.setDetailedResultsEnabled(true);

SearchResult contentResult("/tmp/doc-a.txt");
ContentResultAPI contentApi(contentResult);
contentApi.setCharCount(30);
contentApi.setFilename("doc-a.txt");
contentResult.setCustomAttribute("plainContentMatch", QString("world from"));
contentResult.setCustomAttribute("snippetOffset", 6);

dfmsearch::JsonOutput contentOutput(false);
contentOutput.setSearchOptions(options);
contentOutput.setSearchContext("world", "/tmp", SearchType::Content, SearchMethod::Indexed);
const QString contentJsonText = captureStdout([&]() {
contentOutput.outputSearchStarted();
contentOutput.outputSearchFinished({ contentResult });
});
const QJsonObject contentRoot = QJsonDocument::fromJson(contentJsonText.toUtf8()).object();
const QJsonObject contentItem = contentRoot.value("results").toArray().first().toObject();
QCOMPARE(contentItem.value("charCount").toInt(), 30);

SearchResult filenameResult("/tmp/doc-a.txt");
FileNameResultAPI fileApi(filenameResult);
fileApi.setFilename("doc-a.txt");
fileApi.setFileType("txt");
fileApi.setSize("123");
filenameResult.setCustomAttribute("charCount", 30);

dfmsearch::JsonOutput filenameOutput(false);
filenameOutput.setSearchOptions(options);
filenameOutput.setSearchContext("doc", "/tmp", SearchType::FileName, SearchMethod::Indexed);
const QString filenameJsonText = captureStdout([&]() {
filenameOutput.outputSearchStarted();
filenameOutput.outputSearchFinished({ filenameResult });
});
const QJsonObject filenameRoot = QJsonDocument::fromJson(filenameJsonText.toUtf8()).object();
const QJsonObject filenameItem = filenameRoot.value("results").toArray().first().toObject();
QVERIFY(!filenameItem.contains("charCount"));
}

void tst_SearchOutput::jsonOutput_ocrAndSemanticCharCountContract()
{
SearchOptions options;
options.setDetailedResultsEnabled(true);

SearchResult ocrResult("/tmp/img-a.png");
OcrTextResultAPI ocrApi(ocrResult);
ocrApi.setCharCount(24);
ocrApi.setOcrContent("screenshot text from OCR");
ocrResult.setCustomAttribute("plainContentMatch", QString("text from OCR"));
ocrResult.setCustomAttribute("snippetOffset", 11);

dfmsearch::JsonOutput ocrOutput(false);
ocrOutput.setSearchOptions(options);
ocrOutput.setSearchContext("text", "/tmp", SearchType::Ocr, SearchMethod::Indexed);
const QString ocrJsonText = captureStdout([&]() {
ocrOutput.outputSearchStarted();
ocrOutput.outputSearchFinished({ ocrResult });
});
const QJsonObject ocrRoot = QJsonDocument::fromJson(ocrJsonText.toUtf8()).object();
const QJsonObject ocrItem = ocrRoot.value("results").toArray().first().toObject();
QCOMPARE(ocrItem.value("charCount").toInt(), 24);

SearchResult semanticResult("/tmp/doc-a.txt");
semanticResult.setCustomAttribute("charCount", 30);
semanticResult.setCustomAttribute("contentMatch", QString("world from"));

dfmsearch::JsonOutput semanticOutput(false);
semanticOutput.setSearchOptions(options);
semanticOutput.setSearchContext("world", "/tmp", SearchType::Semantic, SearchMethod::Indexed);
const QString semanticJsonText = captureStdout([&]() {
semanticOutput.outputSearchStarted();
semanticOutput.outputSearchFinished({ semanticResult });
});
const QJsonObject semanticRoot = QJsonDocument::fromJson(semanticJsonText.toUtf8()).object();
const QJsonObject semanticItem = semanticRoot.value("results").toArray().first().toObject();
QCOMPARE(semanticItem.value("charCount").toInt(), 30);
}

void tst_SearchOutput::textOutput_contentAndSemanticCharCountContract()
{
SearchOptions options;
options.setDetailedResultsEnabled(true);

SearchResult contentResult("/tmp/doc-a.txt");
ContentResultAPI contentApi(contentResult);
contentApi.setCharCount(30);
contentResult.setCustomAttribute("plainContentMatch", QString("world from"));
contentResult.setCustomAttribute("snippetOffset", 6);

dfmsearch::TextOutput contentOutput;
contentOutput.setSearchOptions(options);
contentOutput.setVerbose(true);
contentOutput.setSearchContext("world", "/tmp", SearchType::Content, SearchMethod::Indexed);
const QString contentText = captureStdout([&]() {
contentOutput.outputSearchFinished({ contentResult });
});
QVERIFY(contentText.contains("Char count: 30"));

SearchResult semanticResult("/tmp/doc-a.txt");
semanticResult.setCustomAttribute("charCount", 30);
semanticResult.setCustomAttribute("contentMatch", QString("world from"));

dfmsearch::TextOutput semanticOutput;
semanticOutput.setSearchOptions(options);
semanticOutput.setVerbose(true);
semanticOutput.setSearchContext("world", "/tmp", SearchType::Semantic, SearchMethod::Indexed);
const QString semanticText = captureStdout([&]() {
semanticOutput.outputSearchFinished({ semanticResult });
});
QVERIFY(semanticText.contains("charCount: 30"));
}

QObject *create_tst_SearchOutput()
{
return new tst_SearchOutput();
}

#include "tst_search_output.moc"
15 changes: 15 additions & 0 deletions autotests/dfm-search-tests/tst_textsearch_api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ private Q_SLOTS:
void textSearchResult_isHidden();
void textSearchResult_modifyTimestamp();
void textSearchResult_birthTimestamp();
void textSearchResult_charCount();
void textSearchResult_plainSnippetAttributes();

// ContentOptionsAPI tests
Expand Down Expand Up @@ -185,6 +186,20 @@ void tst_TextSearchAPI::textSearchResult_birthTimestamp()
QCOMPARE(api.birthTimestamp(), 0);
}

void tst_TextSearchAPI::textSearchResult_charCount()
{
SearchResult result("/test/path");
TextSearchResultAPI api(result);

QCOMPARE(api.charCount(), 0);

api.setCharCount(128);
QCOMPARE(api.charCount(), 128);

api.setCharCount(0);
QCOMPARE(api.charCount(), 0);
}

void tst_TextSearchAPI::textSearchResult_plainSnippetAttributes()
{
SearchResult result("/test/path");
Expand Down
1 change: 1 addition & 0 deletions include/dfm-search/dfm-search/contentretriever.h
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ class PreviewResult
PreviewResult &operator=(PreviewResult &&other) noexcept = default;

QString content() const;
int charCount() const;
int keywordOffset() const;

private:
Expand Down
Loading
Loading