Skip to content

Commit 7353586

Browse files
authored
Tests: File Indexing and Search Functionality (#1503)
* Add improvements to code indexing and search operations This commit includes additions for the creation of files, directories, and unit tests related to file indexing and searching operations. * Shortened lines * Added doc for TempFolderManager
1 parent c276d03 commit 7353586

File tree

6 files changed

+491
-0
lines changed

6 files changed

+491
-0
lines changed

CodeEdit.xcodeproj/project.pbxproj

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,10 @@
265265
611192062B08CCF600D4459B /* SearchIndexer+Add.swift in Sources */ = {isa = PBXBuildFile; fileRef = 611192052B08CCF600D4459B /* SearchIndexer+Add.swift */; };
266266
611192082B08CCFD00D4459B /* SearchIndexer+Terms.swift in Sources */ = {isa = PBXBuildFile; fileRef = 611192072B08CCFD00D4459B /* SearchIndexer+Terms.swift */; };
267267
6111920C2B08CD0B00D4459B /* SearchIndexer+InternalMethods.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6111920B2B08CD0B00D4459B /* SearchIndexer+InternalMethods.swift */; };
268+
6130535C2B23933D00D767E3 /* MemoryIndexingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6130535B2B23933D00D767E3 /* MemoryIndexingTests.swift */; };
269+
6130535F2B23A31300D767E3 /* MemorySearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6130535E2B23A31300D767E3 /* MemorySearchTests.swift */; };
270+
613053652B23A49300D767E3 /* TemporaryFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613053642B23A49300D767E3 /* TemporaryFile.swift */; };
271+
6130536B2B24722C00D767E3 /* AsyncIndexingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6130536A2B24722C00D767E3 /* AsyncIndexingTests.swift */; };
268272
613DF55E2B08DD5D00E9D902 /* FileHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613DF55D2B08DD5D00E9D902 /* FileHelper.swift */; };
269273
61538B902B111FE800A88846 /* String+AppearancesOfSubstring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61538B8F2B111FE800A88846 /* String+AppearancesOfSubstring.swift */; };
270274
61538B932B11201900A88846 /* String+Character.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61538B922B11201900A88846 /* String+Character.swift */; };
@@ -787,6 +791,10 @@
787791
611192052B08CCF600D4459B /* SearchIndexer+Add.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchIndexer+Add.swift"; sourceTree = "<group>"; };
788792
611192072B08CCFD00D4459B /* SearchIndexer+Terms.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchIndexer+Terms.swift"; sourceTree = "<group>"; };
789793
6111920B2B08CD0B00D4459B /* SearchIndexer+InternalMethods.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchIndexer+InternalMethods.swift"; sourceTree = "<group>"; };
794+
6130535B2B23933D00D767E3 /* MemoryIndexingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryIndexingTests.swift; sourceTree = "<group>"; };
795+
6130535E2B23A31300D767E3 /* MemorySearchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemorySearchTests.swift; sourceTree = "<group>"; };
796+
613053642B23A49300D767E3 /* TemporaryFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryFile.swift; sourceTree = "<group>"; };
797+
6130536A2B24722C00D767E3 /* AsyncIndexingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncIndexingTests.swift; sourceTree = "<group>"; };
790798
613DF55D2B08DD5D00E9D902 /* FileHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileHelper.swift; sourceTree = "<group>"; };
791799
61538B8F2B111FE800A88846 /* String+AppearancesOfSubstring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+AppearancesOfSubstring.swift"; sourceTree = "<group>"; };
792800
61538B922B11201900A88846 /* String+Character.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Character.swift"; sourceTree = "<group>"; };
@@ -1245,6 +1253,7 @@
12451253
children = (
12461254
4EE96ECC296059D200FFBEA8 /* Mocks */,
12471255
4EE96ECA2960565E00FFBEA8 /* DocumentsUnitTests.swift */,
1256+
613053582B23916D00D767E3 /* Indexer */,
12481257
);
12491258
path = Documents;
12501259
sourceTree = "<group>";
@@ -2206,6 +2215,17 @@
22062215
path = Indexer;
22072216
sourceTree = "<group>";
22082217
};
2218+
613053582B23916D00D767E3 /* Indexer */ = {
2219+
isa = PBXGroup;
2220+
children = (
2221+
6130535B2B23933D00D767E3 /* MemoryIndexingTests.swift */,
2222+
6130535E2B23A31300D767E3 /* MemorySearchTests.swift */,
2223+
6130536A2B24722C00D767E3 /* AsyncIndexingTests.swift */,
2224+
613053642B23A49300D767E3 /* TemporaryFile.swift */,
2225+
);
2226+
path = Indexer;
2227+
sourceTree = "<group>";
2228+
};
22092229
6C092EDC2A53A63E00489202 /* Views */ = {
22102230
isa = PBXGroup;
22112231
children = (
@@ -3483,11 +3503,15 @@
34833503
buildActionMask = 2147483647;
34843504
files = (
34853505
583E528C29361B39001AB554 /* CodeEditUITests.swift in Sources */,
3506+
613053652B23A49300D767E3 /* TemporaryFile.swift in Sources */,
34863507
587B60F82934124200D5CD8F /* CEWorkspaceFileManagerTests.swift in Sources */,
3508+
6130535F2B23A31300D767E3 /* MemorySearchTests.swift in Sources */,
34873509
587B61012934170A00D5CD8F /* UnitTests_Extensions.swift in Sources */,
34883510
283BDCC52972F236002AFF81 /* AcknowledgementsTests.swift in Sources */,
34893511
4EE96ECB2960565E00FFBEA8 /* DocumentsUnitTests.swift in Sources */,
34903512
4EE96ECE296059E000FFBEA8 /* NSHapticFeedbackPerformerMock.swift in Sources */,
3513+
6130535C2B23933D00D767E3 /* MemoryIndexingTests.swift in Sources */,
3514+
6130536B2B24722C00D767E3 /* AsyncIndexingTests.swift in Sources */,
34913515
587B612E293419B700D5CD8F /* CodeFileTests.swift in Sources */,
34923516
);
34933517
runOnlyForDeploymentPostprocessing = 0;

CodeEdit/Features/Documents/Indexer/SearchIndexer+AsyncController.swift

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,32 @@ extension SearchIndexer {
3535
}
3636

3737
// MARK: - Search
38+
39+
/// Performs an asynchronous progressive search on the index for the specified query.
40+
///
41+
/// - Parameters:
42+
/// - query: The search query string.
43+
/// - maxResults: The maximum number of results to retrieve in each chunk.
44+
/// - timeout: The timeout duration for each search operation. Default is 1.0 second.
45+
///
46+
/// - Returns: An asynchronous stream (`AsyncStream`) of search results in chunks.
47+
/// The search results are returned in the form of a `SearchIndexer.ProgressivSearch.Results` object.
48+
///
49+
/// This function initiates a progressive search on the index for the specified query
50+
/// and asynchronously yields search results in chunks using an `AsyncStream`.
51+
/// The search continues until there are no more results or the specified timeout is reached.
52+
///
53+
/// - Warning: Prior to calling this function,
54+
/// ensure that the `index` has been flushed to search within the most up-to-date data.
55+
///
56+
/// Example usage:
57+
/// ```swift
58+
/// let searchStream = await asyncController.search(query: searchQuery, 20)
59+
/// for try await result in searchStream {
60+
/// // Process each result
61+
/// print(result)
62+
/// }
63+
/// ```
3864
func search(
3965
query: String,
4066
_ maxResults: Int,
@@ -55,16 +81,26 @@ extension SearchIndexer {
5581

5682
// MARK: - Add
5783

84+
/// Adds files from an array of TextFile objects to the index asynchronously.
85+
///
86+
/// - Parameters:
87+
/// - files: An array of TextFile objects containing the information about the files to be added.
88+
/// - flushWhenComplete: A boolean flag indicating whether to flush
89+
/// the index when the operation is complete. Default is `false`.
90+
///
91+
/// - Returns: An array of booleans indicating the success of adding each file to the index.
5892
func addText(
5993
files: [TextFile],
6094
flushWhenComplete: Bool = false
6195
) async -> [Bool] {
6296

6397
var addedFiles = [Bool]()
6498

99+
// Asynchronously iterate through the provided files using a task group
65100
await withTaskGroup(of: Bool.self) { taskGroup in
66101
for file in files {
67102
taskGroup.addTask {
103+
// Add the file to the index and return the success status
68104
return self.index.addFileWithText(file.url, text: file.text, canReplace: true)
69105
}
70106
}
@@ -73,12 +109,24 @@ extension SearchIndexer {
73109
addedFiles.append(result)
74110
}
75111
}
112+
76113
if flushWhenComplete {
77114
index.flush()
78115
}
116+
79117
return addedFiles
80118
}
81119

120+
/// Adds files from an array of URLs to the index asynchronously.
121+
///
122+
/// - Parameters:
123+
/// - urls: An array of URLs representing the file locations to be added to the index.
124+
/// - flushWhenComplete: A boolean flag indicating whether to flush
125+
/// the index when the operation is complete. Default is `false`.
126+
///
127+
/// - Returns: An array of booleans indicating the success of adding each file to the index.
128+
/// - Warning: Prefer using `addText` when possible as SearchKit does not have the ability
129+
/// to read every file type. For example, it is often not possible to read Swift files.
82130
func addFiles(
83131
urls: [URL],
84132
flushWhenComplete: Bool = false
@@ -100,6 +148,16 @@ extension SearchIndexer {
100148
return addedURLs
101149
}
102150

151+
/// Adds files from a folder specified by the given URL to the index asynchronously.
152+
///
153+
/// - Parameters:
154+
/// - url: The URL of the folder containing files to be added to the index.
155+
/// - flushWhenComplete: A boolean flag indicating whether to flush
156+
/// the index when the operation is complete. Default is `false`.
157+
///
158+
/// This function uses asynchronous processing to add files from the specified folder to the index.
159+
///
160+
/// - Note: Subfolders within the specified folder are also processed.
103161
func addFolder(
104162
url: URL,
105163
flushWhenComplete: Bool = false
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
//
2+
// AsyncIndexingTests.swift
3+
// CodeEditTests
4+
//
5+
// Created by Tommy Ludwig on 09.12.23.
6+
//
7+
8+
import XCTest
9+
@testable import CodeEdit
10+
11+
final class AsyncIndexingTests: XCTestCase {
12+
func testAddDocuments() {
13+
guard let index = SearchIndexer.Memory.create() else {
14+
XCTFail("Failed to create an index")
15+
return
16+
}
17+
18+
let asyncManager = SearchIndexer.AsyncManager(index: index)
19+
let expectation = XCTestExpectation(description: "Async operations completed")
20+
let tempFile1 = TemporaryFile().url
21+
let tempFile2 = TemporaryFile().url
22+
23+
Task {
24+
let results = await asyncManager.addFiles(urls: [tempFile1, tempFile2])
25+
XCTAssertEqual(results.count, 2, "Unexpected indexing results.")
26+
asyncManager.index.flush()
27+
let documents = asyncManager.index.documents()
28+
XCTAssertEqual(documents.count, 2)
29+
expectation.fulfill()
30+
}
31+
32+
wait(for: [expectation], timeout: 2)
33+
}
34+
35+
func testSearchDocuments() {
36+
guard let index = SearchIndexer.Memory.create() else {
37+
XCTFail("Failed to create an index")
38+
return
39+
}
40+
41+
let asyncManager = SearchIndexer.AsyncManager(index: index)
42+
let expectation = XCTestExpectation(description: "Async operations completed")
43+
44+
let tempFile1 = SearchIndexer.AsyncManager.TextFile(
45+
url: TemporaryFile().url,
46+
text: "Itaque ratione asperiores."
47+
)
48+
let tempFile2 = SearchIndexer.AsyncManager.TextFile(
49+
url: TemporaryFile().url,
50+
text: "Perspiciatis perspiciatis rerum ex asperiores."
51+
)
52+
53+
Task {
54+
var searchResults = [URL]()
55+
let results = await asyncManager.addText(files: [tempFile1, tempFile2])
56+
XCTAssertEqual(results.count, 2, "Unexpected indexing results.")
57+
asyncManager.index.flush()
58+
let searchStream = await asyncManager.search(query: "asperiores", 10)
59+
for try await result in searchStream {
60+
let urls: [(URL, Float)] = result.results.compactMap {
61+
($0.url, $0.score)
62+
}
63+
64+
for (url, _) in urls {
65+
searchResults.append(url)
66+
}
67+
}
68+
69+
XCTAssertEqual(searchResults.count, 2)
70+
expectation.fulfill()
71+
}
72+
73+
wait(for: [expectation], timeout: 2)
74+
}
75+
76+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
//
2+
// MemoryIndexing.swift
3+
// CodeEditTests
4+
//
5+
// Created by Tommy Ludwig on 08.12.23.
6+
//
7+
8+
import XCTest
9+
@testable import CodeEdit
10+
11+
final class MemoryIndexingTests: XCTestCase {
12+
func testIndexFile() {
13+
guard let index = SearchIndexer.Memory.create() else {
14+
XCTFail("Failed to create an index")
15+
return
16+
}
17+
18+
let filePath = TemporaryFile().url
19+
20+
let indexResults = index.addFileWithText(filePath, text: "Hello, World!")
21+
XCTAssert(indexResults)
22+
}
23+
24+
func testIndexFiles() {
25+
guard let index = SearchIndexer.Memory.create() else {
26+
XCTFail("Failed to create an index")
27+
return
28+
}
29+
30+
let document1 = TemporaryFile().url
31+
let document2 = TemporaryFile().url
32+
33+
var indexResults = index.addFileWithText(document1, text: "fileContent")
34+
XCTAssert(indexResults)
35+
indexResults = index.addFileWithText(document2, text: "")
36+
XCTAssert(indexResults)
37+
index.flush()
38+
let res = index.cleanUp()
39+
XCTAssertEqual(res, 1)
40+
}
41+
42+
func testIndexFolder() {
43+
guard let index = SearchIndexer.Memory.create() else {
44+
XCTFail("Failed to create an index")
45+
return
46+
}
47+
48+
let folder = TempFolderManager()
49+
folder.createCustomFolder()
50+
folder.createFiles()
51+
52+
let indexResults = index.addFolderContent(folderURL: folder.customFolderURL)
53+
XCTAssertEqual(indexResults.count, 2)
54+
55+
index.flush()
56+
57+
let searchResults = index.search("file")
58+
XCTAssertEqual(searchResults.count, 2, "Unexpected search results")
59+
}
60+
61+
func testIndexCleanUp() {
62+
guard let index = SearchIndexer.Memory.create() else {
63+
XCTFail("Failed to create an index")
64+
return
65+
}
66+
67+
let document1 = TemporaryFile().url
68+
let document2 = TemporaryFile().url
69+
70+
var indexResults = index.addFileWithText(document1, text: "fileContent")
71+
XCTAssert(indexResults)
72+
indexResults = index.addFileWithText(document2, text: "")
73+
XCTAssert(indexResults)
74+
index.flush()
75+
let res = index.cleanUp()
76+
XCTAssertEqual(res, 1)
77+
}
78+
79+
func testCloseIndex() {
80+
guard let index = SearchIndexer.Memory.create() else {
81+
XCTFail("Failed to create an index")
82+
return
83+
}
84+
85+
let filePath = TemporaryFile().url
86+
87+
let indexResults = index.addFileWithText(filePath, text: "Hello, World!")
88+
XCTAssert(indexResults)
89+
90+
index.close()
91+
92+
let closedIndexResults = index.addFileWithText(filePath, text: "Hello, World")
93+
XCTAssertEqual(closedIndexResults, false)
94+
}
95+
96+
func testDocumentIsIndex() {
97+
guard let index = SearchIndexer.Memory.create() else {
98+
XCTFail("Failed to create an index")
99+
return
100+
}
101+
102+
let filePath = TemporaryFile().url
103+
104+
let indexResults = index.addFileWithText(filePath, text: "Hello, World!")
105+
XCTAssert(indexResults)
106+
107+
let isIndexed = index.documentIndexed(filePath)
108+
XCTAssertEqual(isIndexed, false)
109+
110+
index.flush()
111+
let isIndexedAfterFlush = index.documentIndexed(filePath)
112+
XCTAssert(isIndexedAfterFlush)
113+
}
114+
115+
func testSaveAndLoad() {
116+
guard let index = SearchIndexer.Memory.create() else {
117+
XCTFail("Failed to create an index")
118+
return
119+
}
120+
121+
let textFilePath = TemporaryFile().url
122+
123+
XCTAssertTrue(index.addFileWithText(textFilePath, text: "Illum assumenda iure earum dolorum fugit."))
124+
125+
index.flush()
126+
127+
let searchResults = index.search("earum")
128+
XCTAssertEqual(1, searchResults.count)
129+
XCTAssertEqual(searchResults[0].url, textFilePath)
130+
131+
// Save the current index.
132+
let savedIndex = index.getAsData()
133+
XCTAssertNotNil(savedIndex, "Failed to save the index.")
134+
135+
// Close the index, i.e. the index gets deallocated form memory.
136+
index.close()
137+
138+
// Load the saved index
139+
guard let loadedIndex = SearchIndexer.Memory(data: savedIndex!) else {
140+
XCTFail("Failed to create an index")
141+
return
142+
}
143+
144+
let savedIndexResult = loadedIndex.search("earum")
145+
XCTAssertEqual(savedIndexResult.count, 1)
146+
}
147+
}

0 commit comments

Comments
 (0)