summaryrefslogtreecommitdiffstats
path: root/testing/modules/FileTestUtils.jsm
blob: 2bfc8bca47e2042ad3f823ca7d6df941ee89f4f1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/**
 * Provides testing functions dealing with local files and their contents.
 */

"use strict";

var EXPORTED_SYMBOLS = ["FileTestUtils"];

ChromeUtils.import("resource://gre/modules/AsyncShutdown.jsm", this);
ChromeUtils.import("resource://gre/modules/DownloadPaths.jsm", this);
ChromeUtils.import("resource://gre/modules/FileUtils.jsm", this);
ChromeUtils.import("resource://gre/modules/osfile.jsm", this);
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", this);
ChromeUtils.import("resource://testing-common/Assert.jsm", this);

let gFileCounter = 1;
let gPathsToRemove = [];

var FileTestUtils = {
  /**
   * Returns a reference to a temporary file that is guaranteed not to exist and
   * to have never been created before. If a file or a directory with this name
   * is created by the test, it will be deleted when all tests terminate.
   *
   * @param suggestedName [optional]
   *        Any extension on this template file name will be preserved. If this
   *        is unspecified, the returned file name will have the generic ".dat"
   *        extension, which may indicate either a binary or a text data file.
   *
   * @return nsIFile pointing to a non-existent file in a temporary directory.
   *
   * @note It is not enough to delete the file if it exists, or to delete the
   *       file after calling nsIFile.createUnique, because on Windows the
   *       delete operation in the file system may still be pending, preventing
   *       a new file with the same name to be created.
   */
  getTempFile(suggestedName = "test.dat") {
    // Prepend a serial number to the extension in the suggested leaf name.
    let [base, ext] = DownloadPaths.splitBaseNameAndExtension(suggestedName);
    let leafName = base + "-" + gFileCounter + ext;
    gFileCounter++;

    // Get a file reference under the temporary directory for this test file.
    let file = this._globalTemporaryDirectory.clone();
    file.append(leafName);
    Assert.ok(!file.exists(), "Sanity check the temporary file doesn't exist.");

    // Since directory iteration on Windows may not see files that have just
    // been created, keep track of the known file names to be removed.
    gPathsToRemove.push(file.path);
    return file;
  },

  /**
   * Attemps to remove the given file or directory recursively, in a way that
   * works even on Windows, where race conditions may occur in the file system
   * when creating and removing files at the pace of the test suites.
   *
   * The function may fail silently if access is denied. This means that it
   * should only be used to clean up temporary files, rather than for cases
   * where the removal is part of a test and must be guaranteed.
   *
   * @param path
   *        String representing the path to remove.
   * @param isDir [optional]
   *        Indicates whether this is a directory. If not specified, this will
   *        be determined by querying the file system.
   */
  async tolerantRemove(path, isDir) {
    try {
      if (isDir === undefined) {
        isDir = (await OS.File.stat(path)).isDir;
      }
      if (isDir) {
        // The test created a directory, remove its contents recursively. The
        // deletion may still fail with an access denied error on Windows if any
        // of the files in the folder were recently deleted.
        await OS.File.removeDir(path);
      } else {
        // This is the usual case of a test file that has to be removed.
        await OS.File.remove(path);
      }
    } catch (ex) {
      // On Windows, we may get an access denied error instead of a no such file
      // error if the file existed before, and was recently deleted. There is no
      // way to distinguish this from an access list issue because checking for
      // the file existence would also result in the same error.
      if (
        !(ex instanceof OS.File.Error) ||
        !(ex.becauseNoSuchFile || ex.becauseAccessDenied)
      ) {
        throw ex;
      }
    }
  },
};

/**
 * Returns a reference to a global temporary directory that will be deleted
 * when all tests terminate.
 */
XPCOMUtils.defineLazyGetter(
  FileTestUtils,
  "_globalTemporaryDirectory",
  function() {
    // While previous test runs should have deleted their temporary directories,
    // on Windows they might still be pending deletion on the physical file
    // system. This makes a simple nsIFile.createUnique call unreliable, and we
    // have to use a random number to make a collision unlikely.
    let randomNumber = Math.floor(Math.random() * 1000000);
    let dir = FileUtils.getFile("TmpD", ["testdir-" + randomNumber]);
    dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
    AsyncShutdown.profileBeforeChange.addBlocker(
      "Removing test files",
      async () => {
        // Remove the files we know about first.
        for (let path of gPathsToRemove) {
          await this.tolerantRemove(path);
        }

        if (!(await OS.File.exists(dir.path))) {
          return;
        }

        // Detect any extra files, like the ".part" files of downloads.
        let iterator = new OS.File.DirectoryIterator(dir.path);
        try {
          await iterator.forEach(entry =>
            this.tolerantRemove(entry.path, entry.isDir)
          );
        } finally {
          iterator.close();
        }
        // This will fail if any test leaves inaccessible files behind.
        await OS.File.removeEmptyDir(dir.path);
      }
    );
    return dir;
  }
);