michael@0: /* Any copyright is dedicated to the Public Domain. michael@0: http://creativecommons.org/publicdomain/zero/1.0/ */ michael@0: michael@0: // This test ensures that the nsIUrlClassifierHashCompleter works as expected michael@0: // and simulates an HTTP server to provide completions. michael@0: // michael@0: // In order to test completions, each group of completions sent as one request michael@0: // to the HTTP server is called a completion set. There is currently not michael@0: // support for multiple requests being sent to the server at once, in this test. michael@0: // This tests makes a request for each element of |completionSets|, waits for michael@0: // a response and then moves to the next element. michael@0: // Each element of |completionSets| is an array of completions, and each michael@0: // completion is an object with the properties: michael@0: // hash: complete hash for the completion. Automatically right-padded michael@0: // to be COMPLETE_LENGTH. michael@0: // expectCompletion: boolean indicating whether the server should respond michael@0: // with a full hash. michael@0: // table: name of the table that the hash corresponds to. Only needs to be set michael@0: // if a completion is expected. michael@0: // chunkId: positive integer corresponding to the chunk that the hash belongs michael@0: // to. Only needs to be set if a completion is expected. michael@0: // multipleCompletions: boolean indicating whether the server should respond michael@0: // with more than one full hash. If this is set to true michael@0: // then |expectCompletion| must also be set to true and michael@0: // |hash| must have the same prefix as all |completions|. michael@0: // completions: an array of completions (objects with a hash, table and michael@0: // chunkId property as described above). This property is only michael@0: // used when |multipleCompletions| is set to true. michael@0: michael@0: // Basic prefixes with 2/3 completions. michael@0: let basicCompletionSet = [ michael@0: { michael@0: hash: "abcdefgh", michael@0: expectCompletion: true, michael@0: table: "test", michael@0: chunkId: 1234, michael@0: }, michael@0: { michael@0: hash: "1234", michael@0: expectCompletion: false, michael@0: }, michael@0: { michael@0: hash: "\u0000\u0000\u000012312", michael@0: expectCompletion: true, michael@0: table: "test", michael@0: chunkId: 1234, michael@0: } michael@0: ]; michael@0: michael@0: // 3 prefixes with 0 completions to test HashCompleter handling a 204 status. michael@0: let falseCompletionSet = [ michael@0: { michael@0: hash: "1234", michael@0: expectCompletion: false, michael@0: }, michael@0: { michael@0: hash: "", michael@0: expectCompletion: false, michael@0: }, michael@0: { michael@0: hash: "abc", michael@0: expectCompletion: false, michael@0: } michael@0: ]; michael@0: michael@0: // The current implementation (as of Mar 2011) sometimes sends duplicate michael@0: // entries to HashCompleter and even expects responses for duplicated entries. michael@0: let dupedCompletionSet = [ michael@0: { michael@0: hash: "1234", michael@0: expectCompletion: true, michael@0: table: "test", michael@0: chunkId: 1, michael@0: }, michael@0: { michael@0: hash: "5678", michael@0: expectCompletion: false, michael@0: table: "test2", michael@0: chunkId: 2, michael@0: }, michael@0: { michael@0: hash: "1234", michael@0: expectCompletion: true, michael@0: table: "test", michael@0: chunkId: 1, michael@0: }, michael@0: { michael@0: hash: "5678", michael@0: expectCompletion: false, michael@0: table: "test2", michael@0: chunkId: 2 michael@0: } michael@0: ]; michael@0: michael@0: // It is possible for a hash completion request to return with multiple michael@0: // completions, the HashCompleter should return all of these. michael@0: let multipleResponsesCompletionSet = [ michael@0: { michael@0: hash: "1234", michael@0: expectCompletion: true, michael@0: multipleCompletions: true, michael@0: completions: [ michael@0: { michael@0: hash: "123456", michael@0: table: "test1", michael@0: chunkId: 3, michael@0: }, michael@0: { michael@0: hash: "123478", michael@0: table: "test2", michael@0: chunkId: 4, michael@0: } michael@0: ], michael@0: } michael@0: ]; michael@0: michael@0: // The fifth completion set is added at runtime by addRandomCompletionSet. michael@0: // Each completion in the set only has one response and its purpose is to michael@0: // provide an easy way to test the HashCompleter handling an arbitrarily large michael@0: // completion set (determined by SIZE_OF_RANDOM_SET). michael@0: const SIZE_OF_RANDOM_SET = 16; michael@0: function addRandomCompletionSet() { michael@0: let completionSet = []; michael@0: let hashPrefixes = []; michael@0: michael@0: let seed = Math.floor(Math.random() * Math.pow(2, 32)); michael@0: dump("Using seed of " + seed + " for random completion set.\n"); michael@0: let rand = new LFSRgenerator(seed); michael@0: michael@0: for (let i = 0; i < SIZE_OF_RANDOM_SET; i++) { michael@0: let completion = {}; michael@0: michael@0: // Generate a random 256 bit hash. First we get a random number and then michael@0: // convert it to a string. michael@0: let hash; michael@0: let prefix; michael@0: do { michael@0: hash = ""; michael@0: let length = 1 + rand.nextNum(5); michael@0: for (let i = 0; i < length; i++) michael@0: hash += String.fromCharCode(rand.nextNum(8)); michael@0: prefix = hash.substring(0,4); michael@0: } while (hashPrefixes.indexOf(prefix) != -1); michael@0: michael@0: hashPrefixes.push(prefix); michael@0: completion.hash = hash; michael@0: michael@0: completion.expectCompletion = rand.nextNum(1) == 1; michael@0: if (completion.expectCompletion) { michael@0: // Generate a random alpha-numeric string of length at most 6 for the michael@0: // table name. michael@0: completion.table = (rand.nextNum(31)).toString(36); michael@0: michael@0: completion.chunkId = rand.nextNum(16); michael@0: } michael@0: michael@0: completionSet.push(completion); michael@0: } michael@0: michael@0: completionSets.push(completionSet); michael@0: } michael@0: michael@0: let completionSets = [basicCompletionSet, falseCompletionSet, michael@0: dupedCompletionSet, multipleResponsesCompletionSet]; michael@0: let currentCompletionSet = -1; michael@0: let finishedCompletions = 0; michael@0: michael@0: const SERVER_PORT = 8080; michael@0: const SERVER_PATH = "/hash-completer"; michael@0: let server; michael@0: michael@0: // Completion hashes are automatically right-padded with null chars to have a michael@0: // length of COMPLETE_LENGTH. michael@0: // Taken from nsUrlClassifierDBService.h michael@0: const COMPLETE_LENGTH = 32; michael@0: michael@0: let completer = Cc["@mozilla.org/url-classifier/hashcompleter;1"]. michael@0: getService(Ci.nsIUrlClassifierHashCompleter); michael@0: michael@0: function run_test() { michael@0: addRandomCompletionSet(); michael@0: michael@0: // Fix up the completions before running the test. michael@0: for each (let completionSet in completionSets) { michael@0: for each (let completion in completionSet) { michael@0: // Pad the right of each |hash| so that the length is COMPLETE_LENGTH. michael@0: if (completion.multipleCompletions) { michael@0: for each (let responseCompletion in completion.completions) { michael@0: let numChars = COMPLETE_LENGTH - responseCompletion.hash.length; michael@0: responseCompletion.hash += (new Array(numChars + 1)).join("\u0000"); michael@0: } michael@0: } michael@0: else { michael@0: let numChars = COMPLETE_LENGTH - completion.hash.length; michael@0: completion.hash += (new Array(numChars + 1)).join("\u0000"); michael@0: } michael@0: } michael@0: } michael@0: do_test_pending(); michael@0: michael@0: server = new HttpServer(); michael@0: server.registerPathHandler(SERVER_PATH, hashCompleterServer); michael@0: michael@0: const SERVER_PORT = 8080; michael@0: server.start(SERVER_PORT); michael@0: michael@0: completer.gethashUrl = "http://localhost:" + SERVER_PORT + SERVER_PATH; michael@0: michael@0: runNextCompletion(); michael@0: } michael@0: michael@0: function doneCompletionSet() { michael@0: do_check_eq(finishedCompletions, completionSets[currentCompletionSet].length); michael@0: michael@0: for each (let completion in completionSets[currentCompletionSet]) michael@0: do_check_true(completion._finished); michael@0: michael@0: runNextCompletion(); michael@0: } michael@0: michael@0: function runNextCompletion() { michael@0: currentCompletionSet++; michael@0: finishedCompletions = 0; michael@0: michael@0: if (currentCompletionSet >= completionSets.length) { michael@0: finish(); michael@0: return; michael@0: } michael@0: michael@0: dump("Now on completion set index " + currentCompletionSet + "\n"); michael@0: for each (let completion in completionSets[currentCompletionSet]) { michael@0: completer.complete(completion.hash.substring(0,4), michael@0: (new callback(completion))); michael@0: } michael@0: } michael@0: michael@0: function hashCompleterServer(aRequest, aResponse) { michael@0: let stream = aRequest.bodyInputStream; michael@0: let wrapperStream = Cc["@mozilla.org/binaryinputstream;1"]. michael@0: createInstance(Ci.nsIBinaryInputStream); michael@0: wrapperStream.setInputStream(stream); michael@0: michael@0: let len = stream.available(); michael@0: let data = wrapperStream.readBytes(len); michael@0: michael@0: // To avoid a response with duplicate hash completions, we keep track of all michael@0: // completed hash prefixes so far. michael@0: let completedHashes = []; michael@0: let responseText = ""; michael@0: michael@0: function responseForCompletion(x) { michael@0: return x.table + ":" + x.chunkId + ":" + x.hash.length + "\n" + x.hash; michael@0: } michael@0: for each (let completion in completionSets[currentCompletionSet]) { michael@0: if (completion.expectCompletion && michael@0: (completedHashes.indexOf(completion.hash) == -1)) { michael@0: completedHashes.push(completion.hash); michael@0: michael@0: if (completion.multipleCompletions) michael@0: responseText += completion.completions.map(responseForCompletion).join(""); michael@0: else michael@0: responseText += responseForCompletion(completion); michael@0: } michael@0: } michael@0: michael@0: // As per the spec, a server should response with a 204 if there are no michael@0: // full-length hashes that match the prefixes. michael@0: if (responseText) michael@0: aResponse.write(responseText); michael@0: else michael@0: aResponse.setStatusLine(null, 204, null); michael@0: } michael@0: michael@0: michael@0: function callback(completion) { michael@0: this._completion = completion; michael@0: } michael@0: callback.prototype = { michael@0: completion: function completion(hash, table, chunkId, trusted) { michael@0: do_check_true(this._completion.expectCompletion); michael@0: michael@0: if (this._completion.multipleCompletions) { michael@0: for each (let completion in this._completion.completions) { michael@0: if (completion.hash == hash) { michael@0: do_check_eq(JSON.stringify(hash), JSON.stringify(completion.hash)); michael@0: do_check_eq(table, completion.table); michael@0: do_check_eq(chunkId, completion.chunkId); michael@0: michael@0: completion._completed = true; michael@0: michael@0: if (this._completion.completions.every(function(x) x._completed)) michael@0: this._completed = true; michael@0: michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: else { michael@0: // Hashes are not actually strings and can contain arbitrary data. michael@0: do_check_eq(JSON.stringify(hash), JSON.stringify(this._completion.hash)); michael@0: do_check_eq(table, this._completion.table); michael@0: do_check_eq(chunkId, this._completion.chunkId); michael@0: michael@0: this._completed = true; michael@0: } michael@0: }, michael@0: michael@0: completionFinished: function completionFinished(status) { michael@0: do_check_eq(!!this._completion.expectCompletion, !!this._completed); michael@0: this._completion._finished = true; michael@0: michael@0: finishedCompletions++; michael@0: if (finishedCompletions == completionSets[currentCompletionSet].length) michael@0: doneCompletionSet(); michael@0: }, michael@0: }; michael@0: michael@0: function finish() { michael@0: server.stop(function() { michael@0: do_test_finished(); michael@0: }); michael@0: }