// Copyright 2011 The Closure Library Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS-IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. goog.provide('goog.crypt.BlobHasherTest'); goog.setTestOnly('goog.crypt.BlobHasherTest'); goog.require('goog.crypt'); goog.require('goog.crypt.BlobHasher'); goog.require('goog.crypt.Md5'); goog.require('goog.events'); goog.require('goog.testing.PropertyReplacer'); goog.require('goog.testing.jsunit'); // A browser-independent mock of goog.fs.sliceBlob. The actual implementation // calls the underlying slice method differently based on browser version. // This mock does not support negative opt_end. var fsSliceBlobMock = function(blob, start, opt_end) { if (!goog.isNumber(opt_end)) { opt_end = blob.size; } return blob.slice(start, opt_end); }; // Mock out the Blob using a string. BlobMock = function(string) { this.data = string; this.size = this.data.length; }; BlobMock.prototype.slice = function(start, end) { return new BlobMock(this.data.substr(start, end - start)); }; // Mock out the FileReader to have control over the flow. FileReaderMock = function() { this.array_ = []; this.result = null; this.readyState = this.EMPTY; this.onload = null; this.onabort = null; this.onerror = null; }; FileReaderMock.prototype.EMPTY = 0; FileReaderMock.prototype.LOADING = 1; FileReaderMock.prototype.DONE = 2; FileReaderMock.prototype.mockLoad = function() { this.readyState = this.DONE; this.result = this.array_; if (this.onload) { this.onload.call(); } }; FileReaderMock.prototype.abort = function() { this.readyState = this.DONE; if (this.onabort) { this.onabort.call(); } }; FileReaderMock.prototype.mockError = function() { this.readyState = this.DONE; if (this.onerror) { this.onerror.call(); } }; FileReaderMock.prototype.readAsArrayBuffer = function(blobMock) { this.readyState = this.LOADING; this.array_ = []; for (var i = 0; i < blobMock.size; ++i) { this.array_[i] = blobMock.data.charCodeAt(i); } }; FileReaderMock.prototype.isLoading = function() { return this.readyState == this.LOADING; }; var stubs = new goog.testing.PropertyReplacer(); function setUp() { stubs.set(goog.global, 'FileReader', FileReaderMock); stubs.set(goog.fs, 'sliceBlob', fsSliceBlobMock); } function tearDown() { stubs.reset(); } /** * Makes the blobHasher read chunks from the blob and hash it. The number of * reads shall not exceed a pre-determined number (typically blob size / chunk * size) for computing hash. This function fails fast (after maxReads is * reached), assuming that the hasher failed to generate hashes. This prevents * the test suite from going into infinite loop. * @param {!goog.crypt.BlobHasher} blobHasher Hasher in action. * @param {number} maxReads Max number of read attempts. */ function readFromBlob(blobHasher, maxReads) { var counter = 0; while (blobHasher.fileReader_ && blobHasher.fileReader_.isLoading() && counter <= maxReads) { blobHasher.fileReader_.mockLoad(); counter++; } assertTrue(counter <= maxReads); return counter; } function testBasicOperations() { if (!window.Blob) { return; } // Test hashing with one chunk. var hashFn = new goog.crypt.Md5(); var blobHasher = new goog.crypt.BlobHasher(hashFn); var blob = new BlobMock('The quick brown fox jumps over the lazy dog'); blobHasher.hash(blob); readFromBlob(blobHasher, 1); assertEquals( '9e107d9d372bb6826bd81d3542a419d6', goog.crypt.byteArrayToHex(blobHasher.getHash())); // Test hashing with multiple chunks. blobHasher = new goog.crypt.BlobHasher(hashFn, 7); blobHasher.hash(blob); readFromBlob(blobHasher, Math.ceil(blob.size / 7)); assertEquals( '9e107d9d372bb6826bd81d3542a419d6', goog.crypt.byteArrayToHex(blobHasher.getHash())); // Test hashing with no chunks. blob = new BlobMock(''); blobHasher.hash(blob); readFromBlob(blobHasher, 1); assertEquals( 'd41d8cd98f00b204e9800998ecf8427e', goog.crypt.byteArrayToHex(blobHasher.getHash())); } function testNormalFlow() { if (!window.Blob) { return; } // Test the flow with one chunk. var hashFn = new goog.crypt.Md5(); var blobHasher = new goog.crypt.BlobHasher(hashFn, 13); var blob = new BlobMock('short'); var startedEvents = 0; var progressEvents = 0; var completeEvents = 0; goog.events.listen( blobHasher, goog.crypt.BlobHasher.EventType.STARTED, function() { ++startedEvents; }); goog.events.listen( blobHasher, goog.crypt.BlobHasher.EventType.PROGRESS, function() { ++progressEvents; }); goog.events.listen( blobHasher, goog.crypt.BlobHasher.EventType.COMPLETE, function() { ++completeEvents; }); blobHasher.hash(blob); assertEquals(1, startedEvents); assertEquals(0, progressEvents); assertEquals(0, completeEvents); readFromBlob(blobHasher, 1); assertEquals(1, startedEvents); assertEquals(1, progressEvents); assertEquals(1, completeEvents); // Test the flow with multiple chunks. blob = new BlobMock('The quick brown fox jumps over the lazy dog'); startedEvents = 0; progressEvents = 0; completeEvents = 0; var progressLoops = 0; blobHasher.hash(blob); assertEquals(1, startedEvents); assertEquals(0, progressEvents); assertEquals(0, completeEvents); progressLoops = readFromBlob(blobHasher, Math.ceil(blob.size / 13)); assertEquals(1, startedEvents); assertEquals(progressLoops, progressEvents); assertEquals(1, completeEvents); } function testAbortsAndErrors() { if (!window.Blob) { return; } var hashFn = new goog.crypt.Md5(); var blobHasher = new goog.crypt.BlobHasher(hashFn, 13); var blob = new BlobMock('The quick brown fox jumps over the lazy dog'); var abortEvents = 0; var errorEvents = 0; var completeEvents = 0; goog.events.listen( blobHasher, goog.crypt.BlobHasher.EventType.ABORT, function() { ++abortEvents; }); goog.events.listen( blobHasher, goog.crypt.BlobHasher.EventType.ERROR, function() { ++errorEvents; }); goog.events.listen( blobHasher, goog.crypt.BlobHasher.EventType.COMPLETE, function() { ++completeEvents; }); // Immediate abort. blobHasher.hash(blob); assertEquals(0, abortEvents); assertEquals(0, errorEvents); assertEquals(0, completeEvents); blobHasher.abort(); blobHasher.abort(); assertEquals(1, abortEvents); assertEquals(0, errorEvents); assertEquals(0, completeEvents); abortEvents = 0; // Delayed abort. blobHasher.hash(blob); blobHasher.fileReader_.mockLoad(); assertEquals(0, abortEvents); assertEquals(0, errorEvents); assertEquals(0, completeEvents); blobHasher.abort(); blobHasher.abort(); assertEquals(1, abortEvents); assertEquals(0, errorEvents); assertEquals(0, completeEvents); abortEvents = 0; // Immediate error. blobHasher.hash(blob); blobHasher.fileReader_.mockError(); assertEquals(0, abortEvents); assertEquals(1, errorEvents); assertEquals(0, completeEvents); errorEvents = 0; // Delayed error. blobHasher.hash(blob); blobHasher.fileReader_.mockLoad(); blobHasher.fileReader_.mockError(); assertEquals(0, abortEvents); assertEquals(1, errorEvents); assertEquals(0, completeEvents); abortEvents = 0; } function testBasicThrottling() { if (!window.Blob) { return; } var hashFn = new goog.crypt.Md5(); var blobHasher = new goog.crypt.BlobHasher(hashFn, 5); var blob = new BlobMock('The quick brown fox jumps over the lazy dog'); var throttledEvents = 0; var completeEvents = 0; goog.events.listen( blobHasher, goog.crypt.BlobHasher.EventType.THROTTLED, function() { ++throttledEvents; }); goog.events.listen( blobHasher, goog.crypt.BlobHasher.EventType.COMPLETE, function() { ++completeEvents; }); // Start a throttled hash. No chunks should be processed yet. blobHasher.setHashingLimit(0); assertEquals(0, throttledEvents); blobHasher.hash(blob); assertEquals(1, throttledEvents); assertEquals(0, blobHasher.getBytesProcessed()); assertNull(blobHasher.fileReader_); // One chunk should be processed. blobHasher.setHashingLimit(4); assertEquals(1, throttledEvents); assertEquals(1, readFromBlob(blobHasher, 1)); assertEquals(2, throttledEvents); assertEquals(4, blobHasher.getBytesProcessed()); // One more chunk should be processed. blobHasher.setHashingLimit(5); assertEquals(2, throttledEvents); assertEquals(1, readFromBlob(blobHasher, 1)); assertEquals(3, throttledEvents); assertEquals(5, blobHasher.getBytesProcessed()); // Two more chunks should be processed. blobHasher.setHashingLimit(15); assertEquals(3, throttledEvents); assertEquals(2, readFromBlob(blobHasher, 2)); assertEquals(4, throttledEvents); assertEquals(15, blobHasher.getBytesProcessed()); // The entire blob should be processed. blobHasher.setHashingLimit(Infinity); var expectedChunks = Math.ceil(blob.size / 5) - 3; assertEquals(expectedChunks, readFromBlob(blobHasher, expectedChunks)); assertEquals(4, throttledEvents); assertEquals(1, completeEvents); assertEquals( '9e107d9d372bb6826bd81d3542a419d6', goog.crypt.byteArrayToHex(blobHasher.getHash())); } function testLengthZeroThrottling() { if (!window.Blob) { return; } var hashFn = new goog.crypt.Md5(); var blobHasher = new goog.crypt.BlobHasher(hashFn); var throttledEvents = 0; var completeEvents = 0; goog.events.listen( blobHasher, goog.crypt.BlobHasher.EventType.THROTTLED, function() { ++throttledEvents; }); goog.events.listen( blobHasher, goog.crypt.BlobHasher.EventType.COMPLETE, function() { ++completeEvents; }); // Test throttling with length 0 blob. var blob = new BlobMock(''); blobHasher.setHashingLimit(0); blobHasher.hash(blob); assertEquals(0, throttledEvents); assertEquals(1, completeEvents); assertEquals( 'd41d8cd98f00b204e9800998ecf8427e', goog.crypt.byteArrayToHex(blobHasher.getHash())); } function testAbortsAndErrorsWhileThrottling() { if (!window.Blob) { return; } var hashFn = new goog.crypt.Md5(); var blobHasher = new goog.crypt.BlobHasher(hashFn, 5); var blob = new BlobMock('The quick brown fox jumps over the lazy dog'); var abortEvents = 0; var errorEvents = 0; var throttledEvents = 0; var completeEvents = 0; goog.events.listen( blobHasher, goog.crypt.BlobHasher.EventType.ABORT, function() { ++abortEvents; }); goog.events.listen( blobHasher, goog.crypt.BlobHasher.EventType.ERROR, function() { ++errorEvents; }); goog.events.listen( blobHasher, goog.crypt.BlobHasher.EventType.THROTTLED, function() { ++throttledEvents; }); goog.events.listen( blobHasher, goog.crypt.BlobHasher.EventType.COMPLETE, function() { ++completeEvents; }); // Test that processing cannot be continued after abort. blobHasher.setHashingLimit(0); blobHasher.hash(blob); assertEquals(1, throttledEvents); blobHasher.abort(); assertEquals(1, abortEvents); blobHasher.setHashingLimit(10); assertNull(blobHasher.fileReader_); assertEquals(1, throttledEvents); assertEquals(0, completeEvents); assertNull(blobHasher.getHash()); // Test that processing cannot be continued after error. blobHasher.hash(blob); assertEquals(1, throttledEvents); blobHasher.fileReader_.mockError(); assertEquals(1, errorEvents); blobHasher.setHashingLimit(100); assertNull(blobHasher.fileReader_); assertEquals(1, throttledEvents); assertEquals(0, completeEvents); assertNull(blobHasher.getHash()); }