Revision control

1
/* This Source Code Form is subject to the terms of the Mozilla Public
2
* License, v. 2.0. If a copy of the MPL was not distributed with this
3
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5
@testable import Client
6
import Foundation
7
import Storage
8
9
import XCTest
10
11
class TestHistory: ProfileTest {
12
fileprivate func addSite(_ history: BrowserHistory, url: String, title: String, s: Bool = true) {
13
let site = Site(url: url, title: title)
14
let visit = SiteVisit(site: site, date: Date.nowMicroseconds())
15
XCTAssertEqual(s, history.addLocalVisit(visit).value.isSuccess, "Site added: \(url).")
16
}
17
18
fileprivate func innerCheckSites(_ history: BrowserHistory, callback: @escaping (_ cursor: Cursor<Site>) -> Void) {
19
// Retrieve the entry
20
history.getSitesByLastVisit(limit: 100, offset: 0).upon {
21
XCTAssertTrue($0.isSuccess)
22
callback($0.successValue!)
23
}
24
}
25
26
fileprivate func checkSites(_ history: BrowserHistory, urls: [String: String], s: Bool = true) {
27
// Retrieve the entry.
28
if let cursor = history.getSitesByLastVisit(limit: 100, offset: 0).value.successValue {
29
XCTAssertEqual(cursor.status, CursorStatus.success, "Returned success \(cursor.statusMessage).")
30
XCTAssertEqual(cursor.count, urls.count, "Cursor has \(urls.count) entries.")
31
32
for index in 0..<cursor.count {
33
let s = cursor[index]!
34
XCTAssertNotNil(s, "Cursor has a site for entry.")
35
let title = urls[s.url]
36
XCTAssertNotNil(title, "Found right URL.")
37
XCTAssertEqual(s.title, title!, "Found right title.")
38
}
39
} else {
40
XCTFail("Couldn't get cursor.")
41
}
42
}
43
44
fileprivate func clear(_ history: BrowserHistory) {
45
XCTAssertTrue(history.clearHistory().value.isSuccess, "History cleared.")
46
}
47
48
fileprivate func checkVisits(_ history: BrowserHistory, url: String) {
49
let expectation = self.expectation(description: "Wait for history")
50
history.getSitesByLastVisit(limit: 100, offset: 0).upon { result in
51
XCTAssertTrue(result.isSuccess)
52
history.getFrecentHistory().getSites(matchingSearchQuery: url, limit: 100).upon { result in
53
XCTAssertTrue(result.isSuccess)
54
let cursor = result.successValue!
55
XCTAssertEqual(cursor.status, CursorStatus.success, "returned success \(cursor.statusMessage)")
56
// XXX - We don't allow querying much info about visits here anymore, so there isn't a lot to do
57
expectation.fulfill()
58
}
59
}
60
self.waitForExpectations(timeout: 100, handler: nil)
61
}
62
63
// This is a very basic test. Adds an entry. Retrieves it, and then clears the database
64
func testHistory() {
65
withTestProfile { profile -> Void in
66
let h = profile.history
67
self.addSite(h, url: "http://url1/", title: "title")
68
self.addSite(h, url: "http://url1/", title: "title")
69
self.addSite(h, url: "http://url1/", title: "title 2")
70
self.addSite(h, url: "https://url2/", title: "title")
71
self.addSite(h, url: "https://url2/", title: "title")
72
self.checkSites(h, urls: ["http://url1/": "title 2", "https://url2/": "title"])
73
self.checkVisits(h, url: "http://url1/")
74
self.checkVisits(h, url: "https://url2/")
75
self.clear(h)
76
}
77
}
78
79
func testAboutUrls() {
80
withTestProfile { (profile) -> Void in
81
let h = profile.history
82
self.addSite(h, url: "about:home", title: "About Home", s: false)
83
self.clear(h)
84
}
85
}
86
87
let NumThreads = 5
88
let NumCmds = 10
89
90
func testInsertPerformance() {
91
withTestProfile { profile -> Void in
92
let h = profile.history
93
var j = 0
94
95
self.measure({ () -> Void in
96
for _ in 0...self.NumCmds {
97
self.addSite(h, url: "https://someurl\(j).com/", title: "title \(j)")
98
j += 1
99
}
100
self.clear(h)
101
})
102
}
103
}
104
105
func testGetPerformance() {
106
withTestProfile { profile -> Void in
107
let h = profile.history
108
var j = 0
109
var urls = [String: String]()
110
111
self.clear(h)
112
for _ in 0...self.NumCmds {
113
self.addSite(h, url: "https://someurl\(j).com/", title: "title \(j)")
114
urls["https://someurl\(j).com/"] = "title \(j)"
115
j += 1
116
}
117
118
self.measure({ () -> Void in
119
self.checkSites(h, urls: urls)
120
return
121
})
122
123
self.clear(h)
124
}
125
}
126
127
// Fuzzing tests. These fire random insert/query/clear commands into the history database from threads. The don't check
128
// the results. Just look for crashes.
129
func testRandomThreading() {
130
withTestProfile { profile -> Void in
131
let queue = DispatchQueue(label: "My Queue", qos: DispatchQoS.default, attributes: DispatchQueue.Attributes.concurrent, autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency.inherit, target: nil)
132
var counter = 0
133
134
let expectation = self.expectation(description: "Wait for history")
135
for _ in 0..<self.NumThreads {
136
var history = profile.history as BrowserHistory
137
self.runRandom(&history, queue: queue, cb: { () -> Void in
138
counter += 1
139
if counter == self.NumThreads {
140
self.clear(history)
141
expectation.fulfill()
142
}
143
})
144
}
145
self.waitForExpectations(timeout: 10, handler: nil)
146
}
147
}
148
149
// Same as testRandomThreading, but uses one history connection for all threads
150
func testRandomThreading2() {
151
withTestProfile { profile -> Void in
152
let queue = DispatchQueue(label: "My Queue", qos: DispatchQoS.default, attributes: DispatchQueue.Attributes.concurrent, autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency.inherit, target: nil)
153
var history = profile.history as BrowserHistory
154
var counter = 0
155
156
let expectation = self.expectation(description: "Wait for history")
157
for _ in 0..<self.NumThreads {
158
self.runRandom(&history, queue: queue, cb: { () -> Void in
159
counter += 1
160
if counter == self.NumThreads {
161
self.clear(history)
162
expectation.fulfill()
163
}
164
})
165
}
166
self.waitForExpectations(timeout: 10, handler: nil)
167
}
168
}
169
170
// Runs a random command on a database. Calls cb when finished.
171
fileprivate func runRandom(_ history: inout BrowserHistory, cmdIn: Int, cb: @escaping () -> Void) {
172
var cmd = cmdIn
173
if cmd < 0 {
174
cmd = Int(arc4random() % 5)
175
}
176
177
switch cmd {
178
case 0...1:
179
let url = "https://randomurl.com/\(arc4random() % 100)"
180
let title = "title \(arc4random() % 100)"
181
addSite(history, url: url, title: title)
182
cb()
183
case 2...3:
184
innerCheckSites(history) { cursor in
185
for site in cursor {
186
_ = site!
187
}
188
}
189
cb()
190
default:
191
history.clearHistory().upon() { success in cb() }
192
}
193
}
194
195
// Calls numCmds random methods on this database. val is a counter used by this interally (i.e. always pass zero for it).
196
// Calls cb when finished.
197
fileprivate func runMultiRandom(_ history: inout BrowserHistory, val: Int, numCmds: Int, cb: @escaping () -> Void) {
198
if val == numCmds {
199
cb()
200
return
201
} else {
202
runRandom(&history, cmdIn: -1) { [history] in
203
var history = history
204
self.runMultiRandom(&history, val: val+1, numCmds: numCmds, cb: cb)
205
}
206
}
207
}
208
209
// Helper for starting a new thread running NumCmds random methods on it. Calls cb when done.
210
fileprivate func runRandom(_ history: inout BrowserHistory, queue: DispatchQueue, cb: @escaping () -> Void) {
211
queue.async { [history] in
212
var history = history
213
// Each thread creates its own history provider
214
self.runMultiRandom(&history, val: 0, numCmds: self.NumCmds) {
215
DispatchQueue.main.async(execute: cb)
216
}
217
}
218
}
219
}