text: // Copyright (c) 2013 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.
  
  /**
   * 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_test_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_test_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 = chrome.test.fail.bind(null, 'Reading file.');
  
      entry.file(reader.readAsText.bind(reader),
                 chrome.test.fail.bind(null, 'Getting file.'));
    });
  }
  
  // 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'}));
      },
      chrome.test.fail.bind(null, 'Error creating writer.'));
    });
  }
  
  // Truncates file entry/path to lenght |lenght|.
  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);
      },
      chrome.test.fail.bind(null, 'Error creating writer.'));
    });
  }
  
  // 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_test_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_test_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.fileBrowserPrivate.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.fileBrowserPrivate.requestFileSystem(
          sortedVolumeMetadataList[0].volumeId,
          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);
  });
  