Skip to content

Commit b57be17

Browse files
committed
Add infrastructure for testing IndexStoreDB
The goal of this change is to make it convenient to add tests for the index that test the whole process of producing the raw index data using the compilers in the toolchain, importing that data, and producing/updating an IndexStoreDB. The core infrastructure is also designed to be shared by the SourceKitLSP project. All of this is built for tests written in Swift. This change can be broken down into some inter-dependent high-level components: 1. Tibs (Test Index Build System) 2. ISDBTestSupport 3. Improvements to IndexStoreDB to make it testable 4. Tests --- Tibs ---- Tibs, or the "Test Index Build System" provides a simple and flexible build system for test projects. A test case can use a fixture from the `Tests/INPUTS` directory containing a `project.json` describing any targets of the test. Targets may be Swift modules containing any number of source files, C translations units, or a mix of both, including mixed language targets (subject to the capabilities of the platform - Linux cannot use Objective-C interop, for example). The test can then programatically build index raw data, make modifications to source files, and incrementally rebuild. Tibs is implemented using `ninja`, which introduces a new dependency in IndexStoreDB *only when running tests*. ISDBTestSupport --------------- This module contains a number of useful types and functions for working with test fixtures. In particular, * `TestSources` provides a one-stop shop for working with the source files inside a test project, including modifying them and scanning them for named `TestLocation`s, which can then be used to locate positions within source code during a test. * `TibsTestWorkspace` provides a convenient way to work with a tibs test project, its `TestSources`, and the resulting `IndexStoreDB.. Mutable projects are created with sources and build products copied into a temporary location (and cleared on deinit), and can be edited and rebuilt during the test. An immutable (static) test workspace can use the sources directly from the INPUTS directory, and its build products are stored in the build directory for IndexStoreDB itself, allowing faster incremental builds (or null builds) during development. Improvements to IndexStoreDB ---------------------------- The biggest change here is that the `Symbol` and `SymbolOccurrence` types are now simple struct types rather than wrapping the underlying C API types. This makes it much more convenient to work with them during testing, used along with the new `checkOccurrences` function. There is also a new API to `pollForUnitChangesAndWait`, intended only for testing. This replaces watching for file system changes during testing by polling and comparing to the last known state of the unit directory. Watching for file system changes is unnecessarily asynchronous during testing, and this lets us side step any performance issues with that, as well as determining when an index update is "done". Tests ----- To prove the usefulness of the new testing APIs, several new tests for indexing functionality, were added. These are the first real tests for the indexing code. There is lots of room to improve testing coverage further.
1 parent 97de184 commit b57be17

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+3702
-239
lines changed

Package.swift

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ let package = Package(
1111
.library(
1212
name: "IndexStoreDB_CXX",
1313
targets: ["IndexStoreDB_Index"]),
14+
.library(
15+
name: "ISDBTestSupport",
16+
targets: ["ISDBTestSupport"]),
17+
.executable(
18+
name: "tibs",
19+
targets: ["tibs"])
1420
],
1521
dependencies: [],
1622
targets: [
@@ -23,7 +29,28 @@ let package = Package(
2329

2430
.testTarget(
2531
name: "IndexStoreDBTests",
26-
dependencies: ["IndexStoreDB"]),
32+
dependencies: ["IndexStoreDB", "ISDBTestSupport"]),
33+
34+
// MARK: Swift Test Infrastructure
35+
36+
// The Test Index Build System (tibs) library.
37+
.target(
38+
name: "ISDBTibs",
39+
dependencies: []),
40+
41+
.testTarget(
42+
name: "ISDBTibsTests",
43+
dependencies: ["ISDBTibs"]),
44+
45+
// Commandline tool for working with tibs projects.
46+
.target(
47+
name: "tibs",
48+
dependencies: ["ISDBTibs"]),
49+
50+
// Test support library, built on top of tibs.
51+
.target(
52+
name: "ISDBTestSupport",
53+
dependencies: ["IndexStoreDB", "ISDBTibs"]),
2754

2855
// MARK: C++ interface
2956

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Foundation
14+
15+
/// Reads and caches file contents by URL.
16+
///
17+
/// Use `cache.get(url)` to read a file, or get its cached contents. The contents can be overridden
18+
/// or removed from the cache by calling `cache.set(url, to: "new contents")`
19+
public final class SourceFileCache {
20+
var cache: [URL: String] = [:]
21+
22+
public init(_ cache: [URL: String] = [:]) {
23+
self.cache = cache
24+
}
25+
26+
/// Read the contents of `file`, or retrieve them from the cache if available.
27+
///
28+
/// * parameter file: The file to read.
29+
/// * returns: The file contents as a String.
30+
/// * throws: If there are any errors reading the file.
31+
public func get(_ file: URL) throws -> String {
32+
if let content = cache[file] {
33+
return content
34+
}
35+
let content = try String(contentsOfFile: file.path, encoding: .utf8)
36+
cache[file] = content
37+
return content
38+
}
39+
40+
/// Set the cached contents of `file` to `content`.
41+
///
42+
/// * parameters
43+
/// * file: The file to read.
44+
/// * content: The new file content.
45+
public func set(_ file: URL, to content: String?) {
46+
cache[file] = content
47+
}
48+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Foundation
14+
import IndexStoreDB
15+
16+
/// A source location (file:line:column) in a test project, for use with the TestLocationScanner.
17+
public struct TestLocation: Hashable {
18+
19+
/// The path/url of the source file.
20+
public var url: URL
21+
22+
/// The one-based line number.
23+
public var line: Int
24+
25+
/// The one-based column number.
26+
///
27+
/// FIXME: define utf8 vs. utf16 column index.
28+
public var column: Int
29+
30+
public init(url: URL, line: Int, column: Int) {
31+
self.url = url
32+
self.line = line
33+
self.column = column
34+
}
35+
}
36+
37+
extension TestLocation: Comparable {
38+
public static func <(a: TestLocation, b: TestLocation) -> Bool {
39+
return (a.url.path, a.line, a.column) < (b.url.path, b.line, b.column)
40+
}
41+
}
42+
43+
extension SymbolLocation {
44+
45+
/// Constructs a SymbolLocation from a TestLocation, using a non-system path by default.
46+
public init(_ loc: TestLocation, isSystem: Bool = false) {
47+
self.init(
48+
path: loc.url.path,
49+
isSystem: isSystem,
50+
line: loc.line,
51+
utf8Column: loc.column)
52+
}
53+
}
54+
55+
extension Symbol {
56+
57+
/// Returns a SymbolOccurrence with the given location and roles.
58+
public func at(_ location: TestLocation, roles: SymbolRole) -> SymbolOccurrence {
59+
return self.at(SymbolLocation(location), roles: roles)
60+
}
61+
}
62+
63+
extension TestLocation: CustomStringConvertible {
64+
public var description: String { "\(url.path):\(line):\(column)" }
65+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Foundation
14+
15+
/// A builder object for scanning source files for TestLocations specified in /*inline comments*/.
16+
///
17+
/// The scanner searches source files for inline comments /*foo*/ and builds up a mapping from name
18+
/// (the contents of the inline comment) to the location in the source file that it was found.
19+
///
20+
/// For example:
21+
///
22+
/// ```
23+
/// var scanner = TestLocationScanner()
24+
/// scanner.scan("""
25+
/// func /*foo:def*/foo() {}
26+
/// """, url: myURL)
27+
/// scanner.result == ["foo:def": TestLocation(url: myURL, line: 1, column: 17)]
28+
/// ```
29+
public struct TestLocationScanner {
30+
31+
/// The result of the scan (so far), mapping name to test location.
32+
public var result: [String: TestLocation] = [:]
33+
34+
public init() {}
35+
36+
public enum Error: Swift.Error {
37+
case noSuchFileOrDirectory(URL)
38+
39+
/// The sources contained a `/*/*nested*/*/` inline comment, which is not supported.
40+
case nestedComment(TestLocation)
41+
42+
/// The same test location name was used in multiple places.
43+
case duplicateKey(String, TestLocation, TestLocation)
44+
}
45+
46+
public mutating func scan(_ str: String, url: URL) throws {
47+
if str.count < 4 {
48+
return
49+
}
50+
51+
enum State {
52+
/// Outside any comment.
53+
case normal(prev: Character)
54+
55+
/// Inside a comment. The payload contains the previous character and the index of the first
56+
/// character after the '*' (i.e. the start of the comment body).
57+
///
58+
/// bodyStart
59+
/// |
60+
/// /*XXX*/
61+
/// ^^^
62+
case comment(bodyStart: String.Index, prev: Character)
63+
}
64+
65+
var state = State.normal(prev: "_")
66+
var i = str.startIndex
67+
var line = 1
68+
var column = 1
69+
70+
while i != str.endIndex {
71+
let c = str[i]
72+
73+
switch (state, c) {
74+
case (.normal("/"), "*"):
75+
state = .comment(bodyStart: str.index(after: i), prev: "_")
76+
case (.normal(_), _):
77+
state = .normal(prev: c)
78+
79+
case (.comment(let start, "*"), "/"):
80+
let name = String(str[start..<str.index(before: i)])
81+
let loc = TestLocation(url: url, line: line, column: column + 1)
82+
if let prevLoc = result.updateValue(loc, forKey: name) {
83+
throw Error.duplicateKey(name, prevLoc, loc)
84+
}
85+
state = .normal(prev: "_")
86+
87+
case (.comment(_, "/"), "*"):
88+
throw Error.nestedComment(TestLocation(url: url, line: line, column: column))
89+
90+
case (.comment(let start, _), _):
91+
state = .comment(bodyStart: start, prev: c)
92+
}
93+
94+
if c == "\n" {
95+
line += 1
96+
column = 1
97+
} else {
98+
column += 1
99+
}
100+
101+
i = str.index(after: i)
102+
}
103+
}
104+
105+
public mutating func scan(file: URL, sourceCache: SourceFileCache) throws {
106+
let content = try sourceCache.get(file)
107+
try scan(content, url: file)
108+
}
109+
110+
public mutating func scan(rootDirectory: URL, sourceCache: SourceFileCache) throws {
111+
let fm = FileManager.default
112+
113+
guard let generator = fm.enumerator(at: rootDirectory, includingPropertiesForKeys: []) else {
114+
throw Error.noSuchFileOrDirectory(rootDirectory)
115+
}
116+
117+
while let url = generator.nextObject() as? URL {
118+
if isSourceFileExtension(url.pathExtension) {
119+
try scan(file: url, sourceCache: sourceCache)
120+
}
121+
}
122+
}
123+
}
124+
125+
/// Scans `rootDirectory` for test locations, returning a mapping from name to location.
126+
///
127+
/// See TestLocationScanner.
128+
public func scanLocations(
129+
rootDirectory: URL,
130+
sourceCache: SourceFileCache
131+
) throws -> [String: TestLocation] {
132+
var scanner = TestLocationScanner()
133+
try scanner.scan(rootDirectory: rootDirectory, sourceCache: sourceCache)
134+
return scanner.result
135+
}
136+
137+
func isSourceFileExtension(_ ext: String) -> Bool {
138+
switch ext {
139+
case "swift", "c", "cpp", "m", "mm", "h", "hpp":
140+
return true
141+
default:
142+
return false
143+
}
144+
}

0 commit comments

Comments
 (0)