blob: 9440ac9c45e642d7dc75e21925cd11da942a8883 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* Stub out the metrics package.
* @type {!Object<!string, !Function>}
*/
const metrics = {
recordTime: function() {},
recordValue: function() {}
};
/** @type {!importer.DefaultMediaScanner} */
let scanner;
/**
* @const {importer.ScanMode}
*/
const scanMode = importer.ScanMode.HISTORY;
/** @type {!importer.TestImportHistory} */
let importHistory;
/** @type {!TestDirectoryWatcher} */
let watcher;
/**
* @type {function(!FileEntry, !importer.Destination):
* !Promise<!importer.Disposition>}
*/
let dispositionChecker;
// Set up the test components.
function setUp() {
importHistory = new importer.TestImportHistory();
// Setup a default disposition checker. Tests can replace it at runtime
// if they need specialized disposition check behavior.
dispositionChecker = () => {
return Promise.resolve(importer.Disposition.ORIGINAL);
};
scanner = new importer.DefaultMediaScanner(
/** @param {!FileEntry} entry */
entry => {
return Promise.resolve(entry.name);
},
(entry, destination) => {
return dispositionChecker(entry, destination);
},
callback => {
watcher = new TestDirectoryWatcher(callback);
return watcher;
});
}
/**
* Verifies that scanning an empty filesystem produces an empty list.
*/
function testEmptySourceList() {
assertThrows(() => {
scanner.scanFiles([], scanMode);
});
}
function testIsScanning(callback) {
const filenames = [
'happy',
'thoughts',
];
reportPromise(
makeTestFileSystemRoot('testIsScanning')
.then(populateDir.bind(null, filenames))
.then(
/**
* Scans the directory.
* @param {!DirectoryEntry} root
*/
root => {
const results = scanner.scanDirectory(root, scanMode);
assertFalse(results.isFinal());
}),
callback);
}
function testObserverNotifiedOnScanFinish(callback) {
const filenames = [
'happy',
'thoughts',
];
makeTestFileSystemRoot('testObserverNotifiedOnScanFinish')
.then(populateDir.bind(null, filenames))
.then(
/**
* Scans the directory.
* @param {!DirectoryEntry} root
*/
root => {
// Kick off a scan so we can get notified of a scan being finished.
// We kick this off first so we can capture the result for
// use in an assert. Promises ensure the scan won't finish
// until after our function is fully processed.
const result = scanner.scanDirectory(root, scanMode);
scanner.addObserver((eventType, scanResult) => {
assertEquals(importer.ScanEvent.FINALIZED, eventType);
assertEquals(result, scanResult);
callback(false);
});
})
.catch(() => {
callback(true);
});
}
/**
* Verifies that scanFiles slurps up all specified files.
*/
function testScanFiles(callback) {
const filenames = [
'foo',
'foo.jpg',
'bar.gif',
'baz.avi',
];
const expectedFiles = [
'/testScanFiles/foo.jpg',
'/testScanFiles/bar.gif',
'/testScanFiles/baz.avi',
];
reportPromise(
makeTestFileSystemRoot('testScanFiles')
.then(populateDir.bind(null, filenames))
.then(fileOperationUtil.gatherEntriesRecursively)
.then(
/** @param {!Array<!FileEntry>} files */
files => {
return scanner.scanFiles(files, scanMode).whenFinal();
})
.then(assertFilesFound.bind(null, expectedFiles)),
callback);
}
/**
* Verifies that scanFiles skips duplicated files.
*/
function testScanFilesIgnoresPreviousImports(callback) {
const filenames = [
'oldimage1234.jpg', // a history duplicate
'driveimage1234.jpg', // a content duplicate
'foo.jpg',
'bar.gif',
'baz.avi',
];
// Replace the default dispositionChecker with a function
// that treats our dupes accordingly.
dispositionChecker = (entry, destination) => {
if (entry.name === filenames[0]) {
return Promise.resolve(importer.Disposition.HISTORY_DUPLICATE);
}
if (entry.name === filenames[1]) {
return Promise.resolve(importer.Disposition.CONTENT_DUPLICATE);
}
return Promise.resolve(importer.Disposition.ORIGINAL);
};
const expectedFiles = [
'/testScanFilesIgnoresPreviousImports/foo.jpg',
'/testScanFilesIgnoresPreviousImports/bar.gif',
'/testScanFilesIgnoresPreviousImports/baz.avi'
];
reportPromise(
makeTestFileSystemRoot('testScanFilesIgnoresPreviousImports')
.then(populateDir.bind(null, filenames))
.then(fileOperationUtil.gatherEntriesRecursively)
.then(
/** @param {!Array<!FileEntry>} files */
files => {
return scanner.scanFiles(files, scanMode).whenFinal();
})
.then(assertFilesFound.bind(null, expectedFiles)),
callback);
}
/**
* Verifies that scanning a simple single-level directory structure works.
*/
function testEmptyScanResults(callback) {
const filenames = [
'happy',
'thoughts',
];
reportPromise(
makeTestFileSystemRoot('testEmptyScanResults')
.then(populateDir.bind(null, filenames))
.then(
/**
* Scans the directory.
* @param {!DirectoryEntry} root
*/
root => {
return scanner.scanDirectory(root, scanMode).whenFinal();
})
.then(assertFilesFound.bind(null, [])),
callback);
}
/**
* Verifies that scanning a simple single-level directory structure works.
*/
function testSingleLevel(callback) {
const filenames = [
'foo',
'foo.jpg',
'bar.gif',
'baz.avi',
'foo.mp3',
'bar.txt',
];
const expectedFiles = [
'/testSingleLevel/foo.jpg',
'/testSingleLevel/bar.gif',
'/testSingleLevel/baz.avi',
];
reportPromise(
makeTestFileSystemRoot('testSingleLevel')
.then(populateDir.bind(null, filenames))
.then(
/**
* Scans the directory.
* @param {!DirectoryEntry} root
*/
root => {
return scanner.scanDirectory(root, scanMode).whenFinal();
})
.then(assertFilesFound.bind(null, expectedFiles)),
callback);
}
/**
* Verifies that scanning a simple single-level directory produces 100%
* progress at completion.
*/
function testProgress(callback) {
const filenames = [
'foo',
'foo.jpg',
'bar.gif',
'baz.avi',
'foo.mp3',
'bar.txt',
];
const expectedFiles = [
'/testProgress/foo.jpg',
'/testProgress/bar.gif',
'/testProgress/baz.avi',
];
reportPromise(
makeTestFileSystemRoot('testProgress')
.then(populateDir.bind(null, filenames))
.then(
/**
* Scans the directory.
* @param {!DirectoryEntry} root
*/
root => {
return scanner.scanDirectory(root, scanMode).whenFinal();
})
.then(assertProgress.bind(null, 100)),
callback);
}
/**
* Verifies that scanning ignores previously imported entries.
*/
function testIgnoresPreviousImports(callback) {
importHistory.importedPaths['/testIgnoresPreviousImports/oldimage1234.jpg'] =
[importer.Destination.GOOGLE_DRIVE];
const filenames = [
'oldimage1234.jpg', // a history duplicate
'driveimage1234.jpg', // a content duplicate
'foo.jpg',
'bar.gif',
'baz.avi',
];
// Replace the default dispositionChecker with a function
// that treats our dupes accordingly.
dispositionChecker = (entry, destination) => {
if (entry.name === filenames[0]) {
return Promise.resolve(importer.Disposition.HISTORY_DUPLICATE);
}
if (entry.name === filenames[1]) {
return Promise.resolve(importer.Disposition.CONTENT_DUPLICATE);
}
return Promise.resolve(importer.Disposition.ORIGINAL);
};
const expectedFiles = [
'/testIgnoresPreviousImports/foo.jpg',
'/testIgnoresPreviousImports/bar.gif',
'/testIgnoresPreviousImports/baz.avi',
];
const promise =
makeTestFileSystemRoot('testIgnoresPreviousImports')
.then(populateDir.bind(null, filenames))
.then(
/**
* Scans the directory.
* @param {!DirectoryEntry} root
*/
root => {
return scanner.scanDirectory(root, scanMode).whenFinal();
})
.then(assertFilesFound.bind(null, expectedFiles));
reportPromise(promise, callback);
}
function testTracksDuplicates(callback) {
importHistory.importedPaths['/testTracksDuplicates/oldimage1234.jpg'] =
[importer.Destination.GOOGLE_DRIVE];
const filenames = [
'oldimage1234.jpg', // a history duplicate
'driveimage1234.jpg', // a content duplicate
'driveimage9999.jpg', // a content duplicate
'bar.gif',
'baz.avi',
];
// Replace the default dispositionChecker with a function
// that treats our dupes accordingly.
dispositionChecker = (entry, destination) => {
if (entry.name === filenames[0]) {
return Promise.resolve(importer.Disposition.HISTORY_DUPLICATE);
}
if (entry.name === filenames[1]) {
return Promise.resolve(importer.Disposition.CONTENT_DUPLICATE);
}
if (entry.name === filenames[2]) {
return Promise.resolve(importer.Disposition.CONTENT_DUPLICATE);
}
return Promise.resolve(importer.Disposition.ORIGINAL);
};
const expectedDuplicates = [
'/testTracksDuplicates/driveimage1234.jpg',
'/testTracksDuplicates/driveimage9999.jpg',
];
const promise =
makeTestFileSystemRoot('testTracksDuplicates')
.then(populateDir.bind(null, filenames))
.then(
/**
* Scans the directory.
* @param {!DirectoryEntry} root
*/
root => {
return scanner.scanDirectory(root, scanMode).whenFinal();
})
.then(assertDuplicatesFound.bind(null, expectedDuplicates));
reportPromise(promise, callback);
}
function testMultiLevel(callback) {
const filenames = [
'foo.jpg', 'bar',
[
'dir1',
'bar.0.jpg',
],
[
'dir2',
'bar.1.gif',
[
'dir3',
'bar.1.0.avi',
],
]
];
const expectedFiles = [
'/testMultiLevel/foo.jpg',
'/testMultiLevel/dir1/bar.0.jpg',
'/testMultiLevel/dir2/bar.1.gif',
'/testMultiLevel/dir2/dir3/bar.1.0.avi',
];
reportPromise(
makeTestFileSystemRoot('testMultiLevel')
.then(populateDir.bind(null, filenames))
.then(
/**
* Scans the directory.
* @param {!DirectoryEntry} root
*/
root => {
return scanner.scanDirectory(root, scanMode).whenFinal();
})
.then(assertFilesFound.bind(null, expectedFiles)),
callback);
}
function testDedupesFilesInScanResult(callback) {
const filenames = [
'foo.jpg', 'bar.jpg',
[
'dir1',
'foo.jpg',
'bar.jpg',
],
[
'dir2',
'foo.jpg',
'bar.jpg',
[
'dir3',
'foo.jpg',
'bar.jpg',
],
]
];
const expectedFiles = [
'/testDedupesFilesInScanResult/foo.jpg',
'/testDedupesFilesInScanResult/bar.jpg',
];
reportPromise(
makeTestFileSystemRoot('testDedupesFilesInScanResult')
.then(populateDir.bind(null, filenames))
.then(
/**
* Scans the directory.
* @param {!DirectoryEntry} root
*/
root => {
return scanner.scanDirectory(root, scanMode).whenFinal();
})
.then(assertFilesFound.bind(null, expectedFiles)),
callback);
}
/**
* Verifies that scanning a simple single-level directory structure works.
*/
function testDefaultScanResult() {
const hashGenerator = file => {
return file.toURL();
};
const scan = new importer.DefaultScanResult(scanMode, hashGenerator);
// 0 before we set candidate count
assertProgress(0, scan);
scan.setCandidateCount(3);
assertProgress(0, scan);
scan.onCandidatesProcessed(1);
assertProgress(33, scan);
scan.onCandidatesProcessed(1);
assertProgress(66, scan);
scan.onCandidatesProcessed(1);
assertProgress(100, scan);
}
function testInvalidation(callback) {
const invalidatePromise = new Promise(fulfill => {
scanner.addObserver(fulfill);
});
reportPromise(
makeTestFileSystemRoot('testInvalidation')
.then(populateDir.bind(null, ['DCIM']))
.then(
/**
* Scans the directories.
* @param {!DirectoryEntry} root
*/
root => {
scanner.scanDirectory(root, scanMode);
watcher.callback();
return invalidatePromise;
}),
callback);
}
/**
* Verifies the results of the media scan are as expected.
* @param {number} expected, 0-100
* @param {!importer.ScanResult} scan
* @return {!importer.ScanResult}
*/
function assertProgress(expected, scan) {
assertEquals(expected, scan.getStatistics().progress);
return scan;
}
/**
* Verifies the results of the media scan are as expected.
* @param {!Array<string>} expected
* @param {!importer.ScanResult} scan
* @return {!importer.ScanResult}
*/
function assertFilesFound(expected, scan) {
assertFileEntryPathsEqual(expected, scan.getFileEntries());
return scan;
// TODO(smckay): Add coverage for getScanDurationMs && getTotalBytes.
}
/**
* Verifies the results of the media scan are as expected.
* @param {!Array<string>} expected
* @param {!importer.ScanResult} scan
* @return {!importer.ScanResult}
*/
function assertDuplicatesFound(expected, scan) {
assertFileEntryPathsEqual(expected, scan.getDuplicateFileEntries());
return scan;
}
/**
* Creates a subdirectory within a temporary file system for testing.
*
* @param {string} directoryName Name of the test directory to create. Must be
* unique within this test suite.
*/
function makeTestFileSystemRoot(directoryName) {
function makeTestFilesystem() {
return new Promise((resolve, reject) => {
window.webkitRequestFileSystem(
window.TEMPORARY, 1024 * 1024, resolve, reject);
});
}
return makeTestFilesystem().then(
// Create a directory, pretend that's the root.
fs => {
return new Promise((resolve, reject) => {
fs.root.getDirectory(
directoryName, {create: true, exclusive: true}, resolve, reject);
});
});
}
/**
* Creates a set of files in the given directory.
* @param {!Array<!Array|string>} filenames A (potentially nested) array of
* strings, reflecting a directory structure.
* @param {!DirectoryEntry} dir The root of the directory tree.
* @return {!Promise<!DirectoryEntry>} The root of the newly populated
* directory tree.
*/
function populateDir(filenames, dir) {
return Promise
.all(filenames.map(filename => {
if (filename instanceof Array) {
return new Promise((resolve, reject) => {
dir.getDirectory(
filename[0], {create: true}, resolve, reject);
})
.then(populateDir.bind(null, filename));
} else {
const name = /** @type {string} */ (filename);
return new Promise((resolve, reject) => {
dir.getFile(name, {create: true}, resolve, reject);
});
}
}))
.then(() => {
return dir;
});
}