chromium/chrome/test/data/extensions/api_test/file_browser/filesystem_operations_test/test.js

// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * Component extension that tests the extensions with fileBrowesrPrivate
 * permission are able to perform file system operations on external file
 * systems. The test can be run for three different external file system types:
 * local native, restricted local native and drive. Depending on the type,
 * a file system with specific volumeId is acquired. C++ side of the test
 * (external_file_system_extension_apitest.cc) should make sure the file systems
 * are available, and the volume IDs are correct.
 *
 * The test files on file systems should be created before running the test
 * extension. The test extension expects following hierarchy:
 *   (root) - test_dir - subdir
 *                         |
 *                          - empty_dir
 *                         |
 *                          - test_file.xul
 *                         |
 *                          - test_file.xul.foo
 *                         |
 *                          - test_file.tiff
 *                         |
 *                          - test_file.tiff.foo
 *
 * 'root' exists only for Drive.
 * (root/)test_dir/subdir/ will be used as destination dir for copy and move
 * operations.
 * (root/)test_dir/empty_dir/ should be empty and will stay empty until
 * the end of the test.
 * (root/)test_dir/test_file.xul will not change during the test.
 *
 * All files should initially have content: kInitialFileContent.
 */

var kInitialFileContent = 'This is some test content.';
var kWriteOffset = 26;
var kWriteData = ' Yay!';
var kFileContentAfterWrite = 'This is some test content. Yay!';
var kTruncateShortLength = 4;
var kFileContentAfterTruncateShort = 'This';
var kTruncateLongLength = 6;
var kFileContentAfterTruncateLong = 'This\0\0';

function assertEqAndRunCallback(expectedValue, value, errorMessage,
                                callback, callbackArg) {
  chrome.test.assertEq(expectedValue, value, errorMessage);
  callback(callbackArg);
}

/**
 * Helper methods for performing file system operations during tests.
 * Each of them will call |callback| on success, or fail the current test
 * otherwise.
 *
 * callback for |getDirectory| and |getFile| functions expects the gotten entry
 * as an argument. For Other methods, the callback argument should be ignored.
 */

// Gets the path for operations. The path is relative to the volume for
// local entries and relative to the "My Drive" root for Drive entries.
function getPath(relativePath, isOnDrive) {
  return (isOnDrive ? 'root/' : '') + relativePath;
}

// Gets the directory entry.
function getDirectory(
    volumeId, entry, path, shouldCreate, expectSuccess, callback) {
  var messagePrefix = shouldCreate ? 'Creating ' : 'Getting ';
  var message = messagePrefix + 'directory: \'' + path +'\'.';
  var isOnDrive = volumeId == 'drive:drive-user';

  entry.getDirectory(
      getPath(path, isOnDrive), {create: shouldCreate},
      assertEqAndRunCallback.bind(null, expectSuccess, true, message, callback),
      assertEqAndRunCallback.bind(null, expectSuccess, false, message,
                                  callback, null));
}

// Gets the file entry.
function getFile(volumeId, entry, path, shouldCreate, expectSuccess, callback) {
  var messagePrefix = shouldCreate ? 'Creating ' : 'Getting ';
  var message = messagePrefix + 'file: \'' + path +'\'.';
  var isOnDrive = volumeId == 'drive:drive-user';

  entry.getFile(
      getPath(path, isOnDrive), {create: shouldCreate},
      assertEqAndRunCallback.bind(null, expectSuccess, true, message, callback),
      assertEqAndRunCallback.bind(null, expectSuccess, false, message,
                                  callback, null));
}

// Reads file entry/path and verifies its content. The read operation
// should always succeed.
function readFileAndExpectContent(
    volumeId, entry, path, expectedContent, callback) {
  var message = 'Content of the file \'' + path + '\'.';
  getFile(volumeId, entry, path, false, true, function(entry) {
    var reader = new FileReader();
    reader.onload = function() {
      assertEqAndRunCallback(expectedContent, reader.result, message, callback);
    };
    reader.onerror = function(event) {
      chrome.test.fail('Failed to read: ' + reader.error.name);
    };
    entry.file(reader.readAsText.bind(reader),
               function(error) {
                 chrome.test.fail('Failed to get file: ' + error.name);
               });
  });
}

// Writes |content| to the file entry/path  with offest |offest|.
function writeFile(
    volumeId, entry, path, offset, content, expectSuccess, callback) {
  var message = 'Writing to file: \'' + path + '\'.';

  getFile(volumeId, entry, path, false, true, function(entry) {
    entry.createWriter(function(writer) {
      writer.onwrite = assertEqAndRunCallback.bind(null,
          expectSuccess, true, message, callback);
      writer.onerror = assertEqAndRunCallback.bind(null,
          expectSuccess, false, message, callback);

      writer.seek(offset);
      writer.write(new Blob([content], {'type': 'text/plain'}));
    },
    assertEqAndRunCallback.bind(null, expectSuccess, false,
        'Creating writer for \'' + path + '\'.', callback));
  });
}

// Starts and aborts write operation to entry/path.
function abortWriteFile(volumeId, entry, path, callback) {
  getFile(volumeId, entry, path, false, true, function(entry) {
    entry.createWriter(function(writer) {
      var aborted = false;
      var failed = false;

      writer.onwritestart = function() { writer.abort(); };
      writer.onwrite = function() { failed = true; };
      writer.onerror = function() { failed = true; };
      writer.onabort = function() { aborted = true; };

      writer.onwriteend = function() {
        chrome.test.assertTrue(aborted);
        chrome.test.assertFalse(failed);
        callback();
      }

      writer.write(new Blob(['xxxxx'], {'type': 'text/plain'}));
    }, function(error) {
      chrome.test.fail('Error creating writer: ' + error.name);
    });
  });
}

// Truncates file entry/path to length |length|.
function truncateFile(volumeId, entry, path, length, expectSuccess, callback) {
  var message = 'Truncating file: \'' + path + '\' to length ' + length + '.';
  getFile(volumeId, entry, path, false, true, function(entry) {
    entry.createWriter(function(writer) {
      writer.onwrite = assertEqAndRunCallback.bind(null,
          expectSuccess, true, message, callback);
      writer.onerror = assertEqAndRunCallback.bind(null,
          expectSuccess, false, message, callback);

      writer.truncate(length);
    },
    assertEqAndRunCallback.bind(null, expectSuccess, false,
        'Creating writer for \'' + path + '\'.', callback));
  });
}

// Starts and aborts truncate operation on entry/path.
function abortTruncateFile(volumeId, entry, path, callback) {
  getFile(volumeId, entry, path, false, true, function(entry) {
    entry.createWriter(function(writer) {
      var aborted = false;
      var failed = false;

      writer.onwritestart = function() { writer.abort(); };
      writer.onwrite = function() { failed = true; };
      writer.onerror = function() { failed = true; };
      writer.onabort = function() { aborted = true; };

      writer.onwriteend = function() {
        chrome.test.assertTrue(aborted);
        chrome.test.assertFalse(failed);
        callback();
      }

      writer.truncate(10);
    }, function(error) {
      chrome.test.fail('Error creating writer: ' + error.name);
    });
  });
}

// Copies file entry/path from to entry/to/newName.
function copyFile(volumeId, entry, from, to, newName, expectSuccess, callback) {
  var message = 'Copying \'' + from + '\' to \'' + to + '/' + newName + '\'.';

  getFile(volumeId, entry, from, false, true, function(sourceEntry) {
    getDirectory(volumeId, entry, to, false, true, function(targetDir) {
      sourceEntry.copyTo(targetDir, newName,
          assertEqAndRunCallback.bind(null, expectSuccess, true, message,
                                      callback),
          assertEqAndRunCallback.bind(null, expectSuccess, false, message,
                                      callback));
    });
  });
}

// Moves file entry/from to entry/to/newName.
function moveFile(volumeId, entry, from, to, newName, expectSuccess, callback) {
  var message = 'Moving \'' + from + '\' to \'' + to + '/' + newName + '\'.';

  getFile(volumeId, entry, from, false, true, function(sourceEntry) {
    getDirectory(volumeId, entry, to, false, true, function(targetDir) {
      sourceEntry.moveTo(targetDir, newName,
          assertEqAndRunCallback.bind(null, expectSuccess, true, message,
                                      callback),
          assertEqAndRunCallback.bind(null, expectSuccess, false, message,
                                      callback));
    });
  });
}

// Deletes file entry/path.
function deleteFile(volumeId, entry, path, expectSuccess, callback) {
  var message = 'Deleting file \'' + path + '\'.';

  getFile(volumeId, entry, path, false, true, function(entry) {
    entry.remove(
        assertEqAndRunCallback.bind(null, expectSuccess, true, message,
                                    callback),
        assertEqAndRunCallback.bind(null, expectSuccess, false, message,
                                    callback));
  });
}

// Deletes directory entry/path.
function deleteDirectory(volumeId, entry, path, expectSuccess, callback) {
  var message = 'Deleting directory \'' + path + '\'.';

  getDirectory(volumeId, entry, path, false, true, function(entry) {
    entry.remove(
        assertEqAndRunCallback.bind(null, expectSuccess, true, message,
                                    callback),
        assertEqAndRunCallback.bind(null, expectSuccess, false, message,
                                    callback));
  });
}

// Recursively deletes directory entry/path.
function deleteDirectoryRecursively(
    volumeId, entry, path, expectSuccess, callback) {
  var message = 'Recursively deleting directory \'' + path + '\'.';

  getDirectory(volumeId, entry, path, false, true, function(entry) {
    entry.removeRecursively(
        assertEqAndRunCallback.bind(null, expectSuccess, true, message,
                                    callback),
        assertEqAndRunCallback.bind(null, expectSuccess, false, message,
                                    callback));
  });
}

/**
 * Collects all tests that should be run for the test volume.
 *
 * @param {string} volumeId ID of the volume.
 * @param {DOMFileSystem} fileSystem File system of the volume.
 * @returns {Array<function()>} The list of tests that should be run.
 */
function collectTestsForVolumeId(volumeId, fileSystem) {
  console.log(volumeId);
  var isReadOnly = volumeId == 'testing:restricted';
  var isOnDrive = volumeId == 'drive:drive-user';

  var testsToRun = [];

  testsToRun.push(function getDirectoryTest() {
    getDirectory(volumeId, fileSystem.root, 'test_dir', false, true,
        chrome.test.succeed);
  });

  testsToRun.push(function createDirectoryTest() {
    // callback checks whether the new directory exists after create operation.
    // It should exists iff the file system is not read only.
    var callback = getDirectory.bind(null, volumeId, fileSystem.root,
        'new_test_dir', false, !isReadOnly, chrome.test.succeed);

    // Create operation should succeed only for non read-only file systems.
    getDirectory(volumeId, fileSystem.root, 'new_test_dir', true, !isReadOnly,
        callback);
  });

  testsToRun.push(function getFileTest() {
    getFile(volumeId, fileSystem.root, 'test_dir/test_file.xul', false, true,
            chrome.test.succeed);
  });

  testsToRun.push(function createFileTest() {
    // Checks whether the new file exists after create operation.
    // It should exists iff the file system is not read only.
    var callback = getFile.bind(null, volumeId, fileSystem.root,
        'test_dir/new_file', false, !isReadOnly, chrome.test.succeed);

    // Create operation should succeed only for non read-only file systems.
    getFile(volumeId, fileSystem.root, 'test_dir/new_file', true, !isReadOnly,
        callback);
  });

  testsToRun.push(function readFileTest() {
    readFileAndExpectContent(volumeId, fileSystem.root,
        'test_dir/test_file.xul', kInitialFileContent, chrome.test.succeed);
  });

  testsToRun.push(function writeFileTest() {
    var expectedFinalContent = isReadOnly ? kInitialFileContent :
                                            kFileContentAfterWrite;
    // Check file content after write operation. The content should not change
    // on read-only file system.
    var callback = readFileAndExpectContent.bind(null, volumeId,
        fileSystem.root, 'test_dir/test_file.tiff', expectedFinalContent,
        chrome.test.succeed);

    // Write should fail only on read-only file system.
    writeFile(volumeId, fileSystem.root, 'test_dir/test_file.tiff',
        kWriteOffset, kWriteData, !isReadOnly, callback);
  });

  testsToRun.push(function truncateFileShortTest() {
    var expectedFinalContent = isReadOnly ? kInitialFileContent :
                                            kFileContentAfterTruncateShort;
    // Check file content after truncate operation. The content should not
    // change on read-only file system.
    var callback = readFileAndExpectContent.bind(null, volumeId,
        fileSystem.root, 'test_dir/test_file.tiff', expectedFinalContent,
        chrome.test.succeed);

    // Truncate should fail only on read-only file system.
    truncateFile(volumeId, fileSystem.root, 'test_dir/test_file.tiff',
        kTruncateShortLength, !isReadOnly, callback);
  });

  testsToRun.push(function truncateFileLongTest() {
    var expectedFinalContent = isReadOnly ? kInitialFileContent :
                                            kFileContentAfterTruncateLong;
    // Check file content after truncate operation. The content should not
    // change on read-only file system.
    var callback = readFileAndExpectContent.bind(null, volumeId,
        fileSystem.root, 'test_dir/test_file.tiff', expectedFinalContent,
        chrome.test.succeed);

    // Truncate should fail only on read-only file system.
    truncateFile(volumeId, fileSystem.root, 'test_dir/test_file.tiff',
        kTruncateLongLength, !isReadOnly, callback);
  });

  // Skip abort tests for read-only file systems.
  if (!isReadOnly) {
    testsToRun.push(function abortWriteTest() {
      abortWriteFile(volumeId, fileSystem.root, 'test_dir/test_file.xul.foo',
                     chrome.test.succeed);
    });

    testsToRun.push(function abortTruncateTest() {
      abortTruncateFile(volumeId, fileSystem.root, 'test_dir/test_file.xul.foo',
                        chrome.test.succeed);
    });
  }

  testsToRun.push(function copyFileTest() {
    var verifyTarget = null;
    if (isReadOnly) {
      // If the file system is read-only, the target file should not exist after
      // copy operation.
      verifyTarget = getFile.bind(null, volumeId, fileSystem.root,
          'test_dir/subdir/copy', false, false, chrome.test.succeed);
    } else {
      // If the file system is not read-only, the target file should be created
      // during copy operation and its content should match the source file.
      verifyTarget = readFileAndExpectContent.bind(null, volumeId,
          fileSystem.root, 'test_dir/subdir/copy', kInitialFileContent,
          chrome.test.succeed);
    }

    // Verify the source file stil exists and its content hasn't changed.
    var verifySource = readFileAndExpectContent.bind(null, volumeId,
        fileSystem.root, 'test_dir/test_file.xul', kInitialFileContent,
        verifyTarget);

    // Copy file should fail on read-only file system.
    copyFile(volumeId, fileSystem.root, 'test_dir/test_file.xul',
        'test_dir/subdir', 'copy', !isReadOnly, chrome.test.succeed);
  });

  testsToRun.push(function moveFileTest() {
    var verifyTarget = null;
    if (isReadOnly) {
      // If the file system is read-only, the target file should not be created
      // during move.
      verifyTarget = getFile.bind(null, volumeId, fileSystem.root,
          'test_dir/subdir/move', false, false, chrome.test.succeed);
    } else {
      // If the file system is read-only, the target file should be created
      // during move and its content should match the source file.
      verifyTarget = readFileAndExpectContent.bind(null, volumeId,
          fileSystem.root, 'test_dir/subdir/move', kInitialFileContent,
          chrome.test.succeed);
    }

    // On read-only file system the source file should still exist. Otherwise
    // the source file should have been deleted during move operation.
    var verifySource = getFile.bind(null, volumeId, fileSystem.root,
        'test_dir/test_file.xul', false, isReadOnly, verifyTarget);

    // Copy file should fail on read-only file system.
    moveFile(volumeId, fileSystem.root, 'test_dir/test_file.xul',
        'test_dir/subdir', 'move', !isReadOnly, chrome.test.succeed);
  });

  testsToRun.push(function deleteFileTest() {
    // Verify that file exists after delete operation if and only if the file
    // system is read only.
    var callback = getFile.bind(null, volumeId, fileSystem.root,
        'test_dir/test_file.xul.foo', false, isReadOnly, chrome.test.succeed);

    // Delete operation should fail for read-only file systems.
    deleteFile(volumeId, fileSystem.root, 'test_dir/test_file.xul.foo',
        !isReadOnly, callback);
  });

  testsToRun.push(function deleteEmptyDirectoryTest() {
    // Verify that the directory exists after delete operation if and only if
    // the file system is read-only.
    var callback = getDirectory.bind(null, volumeId, fileSystem.root,
        'test_dir/empty_dir', false, isReadOnly, chrome.test.succeed);

    // Deleting empty directory should fail for read-only file systems, and
    // succeed otherwise.
    deleteDirectory(volumeId, fileSystem.root, 'test_dir/empty_dir',
        !isReadOnly, callback);
  });

  testsToRun.push(function deleteDirectoryTest() {
    // Verify that the directory still exists after the operation.
    var callback = getDirectory.bind(null, volumeId, fileSystem.root,
        'test_dir', false, true, chrome.test.succeed);

    // The directory should still contain some files, so non-recursive delete
    // should fail.
    deleteDirectory(volumeId, fileSystem.root, 'test_dir', false, callback);
  });

  // On drive, the directory was deleted in the previous test.
  if (!isOnDrive) {
    testsToRun.push(function deleteDirectoryRecursivelyTest() {
      // Verify that the directory exists after delete operation if and only if
      // the file system is read-only.
      var callback = getDirectory.bind(null, volumeId, fileSystem.root,
          'test_dir', false, isReadOnly, chrome.test.succeed);

      // Recursive delete dhouls fail only for read-only file system.
      deleteDirectoryRecursively(volumeId, fileSystem.root, 'test_dir',
          !isReadOnly, callback);
    });
  }

  return testsToRun;
}

/**
 * Initializes testParams.
 * Gets test volume and creates list of tests that should be run for it.
 *
 * @param {function(Array, string)} callback. Called with an array containing
 *     the list of the tests to run and an error message. On error list of tests
 *     to run will be null.
 */
function initTests(callback) {
  chrome.fileManagerPrivate.getVolumeMetadataList(function(volumeMetadataList) {
    var possibleVolumeTypes = ['testing', 'drive'];

    var sortedVolumeMetadataList = volumeMetadataList.filter(function(volume) {
      return possibleVolumeTypes.indexOf(volume.volumeType) != -1;
    }).sort(function(volumeA, volumeB) {
      return possibleVolumeTypes.indexOf(volumeA.volumeType) -
             possibleVolumeTypes.indexOf(volumeB.volumeType);
    });

    if (sortedVolumeMetadataList.length == 0) {
      callback(null, 'No volumes available, which could be used for testing.');
      return;
    }

    chrome.fileSystem.requestFileSystem(
        {
          volumeId: sortedVolumeMetadataList[0].volumeId,
          writable: !sortedVolumeMetadataList[0].isReadOnly
        },
        function(fileSystem) {
          if (!fileSystem) {
            callback(null, 'Failed to acquire the testing volume.');
            return;
          }

          var testsToRun = collectTestsForVolumeId(
              sortedVolumeMetadataList[0].volumeId, fileSystem);
          callback(testsToRun, 'Success.');
        });
  });
}

// Trigger the tests.
initTests(function(testsToRun, errorMessage) {
  if (!testsToRun) {
    chrome.test.notifyFail('Failed to initialize tests: ' + errorMessage);
    return;
  }

  chrome.test.runTests(testsToRun);
});