diff --git a/lib/AbstractReaderWriter.js b/lib/AbstractReaderWriter.js index 9b8f75ac..11ba90a0 100644 --- a/lib/AbstractReaderWriter.js +++ b/lib/AbstractReaderWriter.js @@ -25,11 +25,22 @@ class AbstractReaderWriter extends AbstractReader { * Writes the content of a resource to a path. * * @public - * @param {module:@ui5/fs.Resource} resource The Resource to write + * @param {module:@ui5/fs.Resource} resource Resource to write + * @param {Object} [options] + * @param {boolean} [options.readOnly=false] Whether the resource content shall be written read-only + * Do not use in conjunction with the drain option. + * The written file will be used as the new source of this resources content. + * Therefore the written file should not be altered by any means. + * Activating this option might improve overall memory consumption. + * @param {boolean} [options.drain=false] Whether the resource content shall be emptied during the write process. + * Do not use in conjunction with the readOnly option. + * Activating this option might improve overall memory consumption. + * This should be used in cases where this is the last access to the resource. + * E.g. the final write of a resource after all processing is finished. * @returns {Promise} Promise resolving once data has been written */ - write(resource) { - return this._write(resource); + write(resource, options = {drain: false, readOnly: false}) { + return this._write(resource, options); } /** @@ -37,10 +48,11 @@ class AbstractReaderWriter extends AbstractReader { * * @abstract * @protected - * @param {module:@ui5/fs.Resource} resource The Resource to write + * @param {module:@ui5/fs.Resource} resource Resource to write + * @param {Object} [options] Write options, see above * @returns {Promise} Promise resolving once data has been written */ - _write(resource) { + _write(resource, options) { throw new Error("Not implemented"); } } diff --git a/lib/Resource.js b/lib/Resource.js index 69043da1..599a6bf4 100644 --- a/lib/Resource.js +++ b/lib/Resource.js @@ -12,21 +12,32 @@ const fnFalse = () => false; * @memberof module:@ui5/fs */ class Resource { + /** + * Function for dynamic creation of content streams + * + * @public + * @callback module:@ui5/fs.Resource~createStream + * @returns {stream.Readable} A readable stream of a resources content + */ + /** * The constructor. * * @public * @param {Object} parameters Parameters * @param {string} parameters.path Virtual path - * @param {Object} [parameters.statInfo] File stat information + * @param {fs.Stats|Object} [parameters.statInfo] File information. Instance of + * [fs.Stats]{@link https://nodejs.org/api/fs.html#fs_class_fs_stats} or similar object * @param {Buffer} [parameters.buffer] Content of this resources as a Buffer instance * (cannot be used in conjunction with parameters string, stream or createStream) * @param {string} [parameters.string] Content of this resources as a string * (cannot be used in conjunction with parameters buffer, stream or createStream) * @param {Stream} [parameters.stream] Readable stream of the content of this resource * (cannot be used in conjunction with parameters buffer, string or createStream) - * @param {Function} [parameters.createStream] Function callback that returns a readable stream of the content - * of this resource (cannot be used in conjunction with parameters buffer, string or stream) + * @param {module:@ui5/fs.Resource~createStream} [parameters.createStream] Function callback that returns a readable + * stream of the content of this resource (cannot be used in conjunction with parameters buffer, + * string or stream). + * In some cases this is the most memory-efficient way to supply resource content */ constructor({path, statInfo, buffer, string, createStream, stream, project}) { if (!path) { @@ -74,25 +85,28 @@ class Resource { * Gets a buffer with the resource content. * * @public - * @returns {Promise} A Promise resolving with a buffer of the resource content. + * @returns {Promise} Promise resolving with a buffer of the resource content. */ - getBuffer() { - return new Promise((resolve, reject) => { - if (this._buffer) { - resolve(this._buffer); - } else if (this._createStream || this._stream) { - resolve(this._getBufferFromStream()); - } else { - reject(new Error(`Resource ${this._path} has no content`)); - } - }); + async getBuffer() { + if (this._contentDrained) { + throw new Error(`Content of Resource ${this._path} has been drained. ` + + "This might be caused by requesting resource content after a content stream has been " + + "requested and no new content (e.g. a new stream) has been set."); + } + if (this._buffer) { + return this._buffer; + } else if (this._createStream || this._stream) { + return this._getBufferFromStream(); + } else { + throw new Error(`Resource ${this._path} has no content`); + } } /** * Sets a Buffer as content. * * @public - * @param {Buffer} buffer A buffer instance + * @param {Buffer} buffer Buffer instance */ setBuffer(buffer) { this._createStream = null; @@ -101,15 +115,22 @@ class Resource { // } this._stream = null; this._buffer = buffer; + this._contentDrained = false; + this._streamDrained = false; } /** * Gets a string with the resource content. * * @public - * @returns {Promise} A Promise resolving with a string of the resource content. + * @returns {Promise} Promise resolving with the resource content. */ getString() { + if (this._contentDrained) { + return Promise.reject(new Error(`Content of Resource ${this._path} has been drained. ` + + "This might be caused by requesting resource content after a content stream has been " + + "requested and no new content (e.g. a new stream) has been set.")); + } return this.getBuffer().then((buffer) => buffer.toString()); } @@ -117,7 +138,7 @@ class Resource { * Sets a String as content * * @public - * @param {string} string A string + * @param {string} string Resource content */ setString(string) { this.setBuffer(Buffer.from(string, "utf8")); @@ -126,34 +147,64 @@ class Resource { /** * Gets a readable stream for the resource content. * + * Repetitive calls of this function are only possible if new content has been set in the meantime (through + * [setStream]{@link module:@ui5/fs.Resource#setStream}, [setBuffer]{@link module:@ui5/fs.Resource#setBuffer} + * or [setString]{@link module:@ui5/fs.Resource#setString}). This + * is to prevent consumers from accessing drained streams. + * * @public - * @returns {stream.Readable} A readable stream for the resource content. + * @returns {stream.Readable} Readable stream for the resource content. */ getStream() { + if (this._contentDrained) { + throw new Error(`Content of Resource ${this._path} has been drained. ` + + "This might be caused by requesting resource content after a content stream has been " + + "requested and no new content (e.g. a new stream) has been set."); + } + let contentStream; if (this._buffer) { const bufferStream = new stream.PassThrough(); bufferStream.end(this._buffer); - return bufferStream; + contentStream = bufferStream; } else if (this._createStream || this._stream) { - return this._getStream(); - } else { + contentStream = this._getStream(); + } + if (!contentStream) { throw new Error(`Resource ${this._path} has no content`); } + // If a stream instance is being returned, it will typically get drained be the consumer. + // In that case, further content access will result in a "Content stream has been drained" error. + // However, depending on the execution environment, a resources content stream might have been + // transformed into a buffer. In that case further content access is possible as a buffer can't be + // drained. + // To prevent unexpected "Content stream has been drained" errors caused by changing environments, we flag + // the resource content as "drained" every time a stream is requested. Even if actually a buffer or + // createStream callback is being used. + this._contentDrained = true; + return contentStream; } /** * Sets a readable stream as content. * * @public - * @param {stream.Readable} stream readable stream + * @param {stream.Readable|module:@ui5/fs.Resource~createStream} stream Readable stream of the resource content or + callback for dynamic creation of a readable stream */ setStream(stream) { this._buffer = null; - this._createStream = null; // if (this._stream) { // TODO this may cause strange issues // this._stream.destroy(); // } - this._stream = stream; + if (typeof stream === "function") { + this._createStream = stream; + this._stream = null; + } else { + this._stream = stream; + this._createStream = null; + } + this._contentDrained = false; + this._streamDrained = false; } /** @@ -181,7 +232,8 @@ class Resource { * Gets the resources stat info. * * @public - * @returns {fs.Stats} An object representing an fs.Stats instance + * @returns {fs.Stats|Object} Instance of [fs.Stats]{@link https://nodejs.org/api/fs.html#fs_class_fs_stats} + * or similar object */ getStatInfo() { return this._statInfo; @@ -201,10 +253,10 @@ class Resource { } /** - * Returns a clone of the resource. + * Returns a clone of the resource. The clones content is independent from that of the original resource * * @public - * @returns {Promise} A promise resolving the resource. + * @returns {Promise} Promise resolving with the clone */ clone() { const options = { @@ -235,7 +287,7 @@ class Resource { /** * Tracing: Get tree for printing out trace * - * @returns {Object} + * @returns {Object} Trace tree */ getPathTree() { const tree = {}; @@ -253,12 +305,16 @@ class Resource { * Returns the content as stream. * * @private - * @returns {Function} The stream + * @returns {stream.Readable} Readable stream */ _getStream() { + if (this._streamDrained) { + throw new Error(`Content stream of Resource ${this._path} is flagged as drained.`); + } if (this._createStream) { return this._createStream(); } + this._streamDrained = true; return this._stream; } @@ -269,7 +325,10 @@ class Resource { * @returns {Promise} Promise resolving with buffer. */ _getBufferFromStream() { - return new Promise((resolve, reject) => { + if (this._buffering) { // Prevent simultaneous buffering, causing unexpected access to drained stream + return this._buffering; + } + return this._buffering = new Promise((resolve, reject) => { const contentStream = this._getStream(); const buffers = []; contentStream.on("data", (data) => { @@ -281,6 +340,7 @@ class Resource { contentStream.on("end", () => { const buffer = Buffer.concat(buffers); this.setBuffer(buffer); + this._buffering = null; resolve(buffer); }); }); diff --git a/lib/adapters/FileSystem.js b/lib/adapters/FileSystem.js index 3f60dd43..98c076aa 100644 --- a/lib/adapters/FileSystem.js +++ b/lib/adapters/FileSystem.js @@ -3,6 +3,7 @@ const path = require("path"); const fs = require("graceful-fs"); const glob = require("globby"); const makeDir = require("make-dir"); +const {PassThrough} = require("stream"); const Resource = require("../Resource"); const AbstractAdapter = require("./AbstractAdapter"); @@ -153,7 +154,7 @@ class FileSystem extends AbstractAdapter { if (!stat.isDirectory()) { // Add content - options.createStream = () => { + options.createStream = function() { return fs.createReadStream(fsPath); }; } @@ -168,33 +169,79 @@ class FileSystem extends AbstractAdapter { * Writes the content of a resource to a path. * * @private - * @param {module:@ui5/fs.Resource} resource The Resource + * @param {module:@ui5/fs.Resource} resource Resource to write + * @param {Object} [options] + * @param {boolean} [options.readOnly] Whether the resource content shall be written read-only + * Do not use in conjunction with the drain option. + * The written file will be used as the new source of this resources content. + * Therefore the written file should not be altered by any means. + * Activating this option might improve overall memory consumption. + * @param {boolean} [options.drain] Whether the resource content shall be emptied during the write process. + * Do not use in conjunction with the readOnly option. + * Activating this option might improve overall memory consumption. + * This should be used in cases where this is the last access to the resource. + * E.g. the final write of a resource after all processing is finished. * @returns {Promise} Promise resolving once data has been written */ - _write(resource) { + async _write(resource, {drain, readOnly}) { + if (drain && readOnly) { + throw new Error(`Error while writing resource ${resource.getPath()}: ` + + "Do not use options 'drain' and 'readOnly' at the same time."); + } + const relPath = resource.getPath().substr(this._virBasePath.length); const fsPath = path.join(this._fsBasePath, relPath); const dirPath = path.dirname(fsPath); log.verbose("Writing to %s", fsPath); - return makeDir(dirPath, { - fs - }).then(() => { - return new Promise((resolve, reject) => { - const contentStream = resource.getStream(); - contentStream.on("error", function(err) { + await makeDir(dirPath, {fs}); + return new Promise((resolve, reject) => { + let contentStream; + + if (drain || readOnly) { + // Stream will be drained + contentStream = resource.getStream(); + + contentStream.on("error", (err) => { reject(err); }); - const write = fs.createWriteStream(fsPath); - write.on("error", function(err) { + } else { + // Transform stream into buffer before writing + contentStream = new PassThrough(); + const buffers = []; + contentStream.on("error", (err) => { reject(err); }); - write.on("close", function(ex) { - resolve(); + contentStream.on("data", (data) => { + buffers.push(data); + }); + contentStream.on("end", () => { + const buffer = Buffer.concat(buffers); + resource.setBuffer(buffer); }); - contentStream.pipe(write); + resource.getStream().pipe(contentStream); + } + + const writeOptions = {}; + if (readOnly) { + writeOptions.mode = 0o444; // read only + } + + const write = fs.createWriteStream(fsPath, writeOptions); + write.on("error", (err) => { + reject(err); + }); + write.on("close", (ex) => { + if (readOnly) { + // Create new stream from written file + resource.setStream(function() { + return fs.createReadStream(fsPath); + }); + } + resolve(); }); + contentStream.pipe(write); }); } } diff --git a/test/lib/Resource.js b/test/lib/Resource.js index d7e7f5a5..193efb89 100644 --- a/test/lib/Resource.js +++ b/test/lib/Resource.js @@ -1,7 +1,23 @@ const {test} = require("ava"); const Stream = require("stream"); +const fs = require("fs"); +const path = require("path"); const Resource = require("../../lib/Resource"); +function createBasicResource() { + const fsPath = path.join("test", "fixtures", "application.a", "webapp", "index.html"); + const resource = new Resource({ + path: "/app/index.html", + createStream: function() { + return fs.createReadStream(fsPath); + }, + project: {}, + statInfo: {}, + fsPath + }); + return resource; +} + test("Resource: constructor with missing path parameter", (t) => { const error = t.throws(() => { new Resource({}); @@ -140,3 +156,72 @@ test("Resource: clone resource with stream", (t) => { }); }); }); + +test("getStream with createStream callback content: Subsequent content requests should throw error due " + + "to drained content", async (t) => { + const resource = createBasicResource(); + resource.getStream(); + t.throws(() => { + resource.getStream(); + }, /Content of Resource \/app\/index.html has been drained/); + await t.throws(resource.getBuffer(), /Content of Resource \/app\/index.html has been drained/); + await t.throws(resource.getString(), /Content of Resource \/app\/index.html has been drained/); +}); + +test("getStream with Buffer content: Subsequent content requests should throw error due to drained " + + "content", async (t) => { + const resource = createBasicResource(); + await resource.getBuffer(); + resource.getStream(); + t.throws(() => { + resource.getStream(); + }, /Content of Resource \/app\/index.html has been drained/); + await t.throws(resource.getBuffer(), /Content of Resource \/app\/index.html has been drained/); + await t.throws(resource.getString(), /Content of Resource \/app\/index.html has been drained/); +}); + +test("getStream with Stream content: Subsequent content requests should throw error due to drained " + + "content", async (t) => { + const resource = createBasicResource(); + const {Transform} = require("stream"); + const tStream = new Transform({ + transform(chunk, encoding, callback) { + this.push(chunk.toString()); + callback(); + } + }); + const stream = resource.getStream(); + stream.pipe(tStream); + resource.setStream(tStream); + + resource.getStream(); + t.throws(() => { + resource.getStream(); + }, /Content of Resource \/app\/index.html has been drained/); + await t.throws(resource.getBuffer(), /Content of Resource \/app\/index.html has been drained/); + await t.throws(resource.getString(), /Content of Resource \/app\/index.html has been drained/); +}); + +test("getStream with Stream content: Subsequent content requests should throw error due to drained " + + "content", async (t) => { + const resource = createBasicResource(); + const {Transform} = require("stream"); + const tStream = new Transform({ + transform(chunk, encoding, callback) { + this.push(chunk.toString()); + callback(); + } + }); + const stream = resource.getStream(); + stream.pipe(tStream); + resource.setStream(tStream); + + const p1 = resource.getBuffer(); + const p2 = resource.getBuffer(); + + await t.notThrows(p1); + + // Race condition in _getBufferFromStream used to cause p2 + // to throw "Content stream of Resource /app/index.html is flagged as drained." + await t.notThrows(p2); +}); diff --git a/test/lib/adapters/FileSystem.js b/test/lib/adapters/FileSystem_read.js similarity index 75% rename from test/lib/adapters/FileSystem.js rename to test/lib/adapters/FileSystem_read.js index 779573ea..b29925b3 100644 --- a/test/lib/adapters/FileSystem.js +++ b/test/lib/adapters/FileSystem_read.js @@ -1,6 +1,28 @@ const {test} = require("ava"); const {resourceFactory} = require("../../../"); +test("GLOB resources from application.a w/ virtual base path prefix", async (t) => { + const readerWriter = resourceFactory.createAdapter({ + fsBasePath: "./test/fixtures/application.a/webapp", + virBasePath: "/app/" + }); + + await readerWriter.byGlob("/app/**/*.html").then(function(resources) { + t.deepEqual(resources.length, 1, "Found exactly one resource"); + }); +}); + +test("GLOB resources from application.a w/o virtual base path prefix", async (t) => { + const readerWriter = resourceFactory.createAdapter({ + fsBasePath: "./test/fixtures/application.a/webapp", + virBasePath: "/app/" + }); + + await readerWriter.byGlob("/**/*.html").then(function(resources) { + t.deepEqual(resources.length, 1, "Found exactly one resource"); + }); +}); + test("GLOB resources from application.a w/ virtual base path prefix", async (t) => { const readerWriter = resourceFactory.createAdapter({ fsBasePath: "./test/fixtures/application.a/webapp", @@ -21,7 +43,6 @@ test("GLOB resources from application.a w/o virtual base path prefix", async (t) t.deepEqual(resources.length, 1, "Found exactly one resource"); }); - test("GLOB virtual directory w/o virtual base path prefix", async (t) => { const readerWriter = resourceFactory.createAdapter({ fsBasePath: "./test/fixtures/application.a/webapp", diff --git a/test/lib/adapters/FileSystem_write.js b/test/lib/adapters/FileSystem_write.js new file mode 100644 index 00000000..a5f3a180 --- /dev/null +++ b/test/lib/adapters/FileSystem_write.js @@ -0,0 +1,95 @@ +const path = require("path"); +const {promisify} = require("util"); +const fs = require("fs"); +const fsAccess = promisify(fs.access); +const {test} = require("ava"); +const rimraf = promisify(require("rimraf")); +const chai = require("chai"); +chai.use(require("chai-fs")); +const assert = chai.assert; + +const ui5Fs = require("../../../"); + +test.beforeEach((t) => { + const tmpDirName = t.title.replace(/[^a-zA-Z0-9]/g, "_").toLowerCase(); // Generate tmp dir name from test name + + // Create a tmp directory for every test + t.context.tmpDirPath = path.join(__dirname, "..", "..", "tmp", "adapters", "FileSystemWrite", tmpDirName); + + t.context.readerWriters = { + source: ui5Fs.resourceFactory.createAdapter({ + fsBasePath: "./test/fixtures/application.a/webapp", + virBasePath: "/app/" + }), + dest: ui5Fs.resourceFactory.createAdapter({ + fsBasePath: "./test/tmp/adapters/FileSystemWrite/" + tmpDirName, + virBasePath: "/app/" + }) + }; +}); + +test.afterEach.always((t) => { + // Cleanup tmp directory + return rimraf(t.context.tmpDirPath); +}); + + +test("Write resource", async (t) => { + const readerWriters = t.context.readerWriters; + const destFsPath = path.join(t.context.tmpDirPath, "index.html"); + + // Get resource from one readerWriter + const resource = await readerWriters.source.byPath("/app/index.html"); + // Write resource content to another readerWriter + + await readerWriters.dest.write(resource); + t.notThrows(() => { + assert.fileEqual(destFsPath, "./test/fixtures/application.a/webapp/index.html"); + }); + await t.notThrows(resource.getBuffer(), "Resource content can still be accessed"); +}); + +test("Write resource in readOnly mode", async (t) => { + const readerWriters = t.context.readerWriters; + const destFsPath = path.join(t.context.tmpDirPath, "index.html"); + + // Get resource from one readerWriter + const resource = await readerWriters.source.byPath("/app/index.html"); + // Write resource content to another readerWriter + await readerWriters.dest.write(resource, {readOnly: true}); + + await t.notThrows(fsAccess(destFsPath, fs.constants.R_OK), "File can be read"); + await t.throws(fsAccess(destFsPath, fs.constants.W_OK), /EACCES: permission denied|EPERM: operation not permitted/, + "File can not be written"); + + t.notThrows(() => { + assert.fileEqual(destFsPath, "./test/fixtures/application.a/webapp/index.html"); + }); + await t.notThrows(resource.getBuffer(), "Resource content can still be accessed"); +}); + +test("Write resource in drain mode", async (t) => { + const readerWriters = t.context.readerWriters; + const destFsPath = path.join(t.context.tmpDirPath, "index.html"); + + // Get resource from one readerWriter + const resource = await readerWriters.source.byPath("/app/index.html"); + // Write resource content to another readerWriter + await readerWriters.dest.write(resource, {drain: true}); + + t.notThrows(() => { + assert.fileEqual(destFsPath, "./test/fixtures/application.a/webapp/index.html"); + }); + return t.throws(resource.getBuffer(), + /Content of Resource \/app\/index.html has been drained/); +}); + +test("Writing with readOnly and drain options set should fail", async (t) => { + const readerWriters = t.context.readerWriters; + + // Get resource from one readerWriter + const resource = await readerWriters.source.byPath("/app/index.html"); + // Write resource content to another readerWriter + await t.throws(readerWriters.dest.write(resource, {readOnly: true, drain: true}), + "Error while writing resource /app/index.html: Do not use options 'drain' and 'readOnly' at the same time."); +}); diff --git a/test/lib/adapters/Memory.js b/test/lib/adapters/Memory_read.js similarity index 72% rename from test/lib/adapters/Memory.js rename to test/lib/adapters/Memory_read.js index f2a5f3df..150b45cd 100644 --- a/test/lib/adapters/Memory.js +++ b/test/lib/adapters/Memory_read.js @@ -197,7 +197,7 @@ test("GLOB (normalized) root directory (=> fs root)", async (t) => { await fillFromFs(readerWriter); const resources = await readerWriter.byGlob([ - "/*/", + "/*", ], {nodir: false}); resources.forEach((res) => { t.deepEqual(res._name, "app"); @@ -231,91 +231,3 @@ test("GLOB subdirectory", async (t) => { t.deepEqual(resources[0].getPath(), "/app/application.a"); t.deepEqual(resources[0].getStatInfo().isDirectory(), true); }); - -test("Write resource w/ virtual base path", async (t) => { - const readerWriter = resourceFactory.createAdapter({ - virBasePath: "/app/" - }); - - const res = resourceFactory.createResource({ - path: "/app/test.html" - }); - await readerWriter.write(res); - - t.deepEqual(readerWriter._virFiles, { - "test.html": res - }, "Adapter added resource with correct path"); - - t.deepEqual(Object.keys(readerWriter._virDirs), [], "Adapter added correct virtual directories"); -}); - -test("Write resource w/o virtual base path", async (t) => { - const readerWriter = resourceFactory.createAdapter({ - virBasePath: "/" - }); - - const res = resourceFactory.createResource({ - path: "/one/two/three/test.html" - }); - await readerWriter.write(res); - - t.deepEqual(readerWriter._virFiles, { - "one/two/three/test.html": res - }, "Adapter added resource with correct path"); - - t.deepEqual(Object.keys(readerWriter._virDirs), [ - "one/two/three", - "one/two", - "one" - ], "Adapter added correct virtual directories"); - - const dirRes = readerWriter._virDirs["one/two/three"]; - t.deepEqual(dirRes.getStatInfo().isDirectory(), true, "Directory resource is a directory"); - t.deepEqual(dirRes.getPath(), "/one/two/three", "Directory resource has correct path"); -}); - -test("Write resource w/ deep virtual base path", async (t) => { - const readerWriter = resourceFactory.createAdapter({ - virBasePath: "/app/a/" - }); - - const res = resourceFactory.createResource({ - path: "/app/a/one/two/three/test.html" - }); - await readerWriter.write(res); - - t.deepEqual(readerWriter._virFiles, { - "one/two/three/test.html": res - }, "Adapter added resource with correct path"); - - t.deepEqual(Object.keys(readerWriter._virDirs), [ - "one/two/three", - "one/two", - "one" - ], "Adapter added correct virtual directories"); - - const dirRes = readerWriter._virDirs["one/two/three"]; - t.deepEqual(dirRes.getStatInfo().isDirectory(), true, "Directory resource is a directory"); - t.deepEqual(dirRes.getPath(), "/app/a/one/two/three", "Directory resource has correct path"); -}); - -test("Write resource w/ crazy virtual base path", async (t) => { - const readerWriter = resourceFactory.createAdapter({ - virBasePath: "/app/🐛/" - }); - - const res = resourceFactory.createResource({ - path: "/app/🐛/one\\/2/3️⃣/test" - }); - await readerWriter.write(res); - - t.deepEqual(readerWriter._virFiles, { - "one\\/2/3️⃣/test": res - }, "Adapter added resource with correct path"); - - t.deepEqual(Object.keys(readerWriter._virDirs), [ - "one\\/2/3️⃣", - "one\\/2", - "one\\" - ], "Adapter added correct virtual directories"); -}); diff --git a/test/lib/adapters/Memory_write.js b/test/lib/adapters/Memory_write.js new file mode 100644 index 00000000..ae63652e --- /dev/null +++ b/test/lib/adapters/Memory_write.js @@ -0,0 +1,120 @@ +const {test} = require("ava"); +const {resourceFactory} = require("../../../"); + +test("GLOB resources from application.a w/ virtual base path prefix", async (t) => { + const dest = resourceFactory.createAdapter({ + virBasePath: "/app/" + }); + + const res = resourceFactory.createResource({ + path: "/app/index.html" + }); + await dest.write(res) + .then(() => dest.byGlob("/app/*.html")) + .then((resources) => { + t.deepEqual(resources.length, 1, "Found exactly one resource"); + }); +}); + +test("GLOB resources from application.a w/o virtual base path prefix", async (t) => { + const dest = resourceFactory.createAdapter({ + virBasePath: "/app/" + }); + + const res = resourceFactory.createResource({ + path: "/app/index.html" + }); + await dest.write(res) + .then(() => dest.byGlob("/**/*.html")) + .then((resources) => { + t.deepEqual(resources.length, 1, "Found exactly one resource"); + }); +}); + +test("Write resource w/ virtual base path", async (t) => { + const readerWriter = resourceFactory.createAdapter({ + virBasePath: "/app/" + }); + + const res = resourceFactory.createResource({ + path: "/app/test.html" + }); + await readerWriter.write(res); + + t.deepEqual(readerWriter._virFiles, { + "test.html": res + }, "Adapter added resource with correct path"); + + t.deepEqual(Object.keys(readerWriter._virDirs), [], "Adapter added correct virtual directories"); +}); + +test("Write resource w/o virtual base path", async (t) => { + const readerWriter = resourceFactory.createAdapter({ + virBasePath: "/" + }); + + const res = resourceFactory.createResource({ + path: "/one/two/three/test.html" + }); + await readerWriter.write(res); + + t.deepEqual(readerWriter._virFiles, { + "one/two/three/test.html": res + }, "Adapter added resource with correct path"); + + t.deepEqual(Object.keys(readerWriter._virDirs), [ + "one/two/three", + "one/two", + "one" + ], "Adapter added correct virtual directories"); + + const dirRes = readerWriter._virDirs["one/two/three"]; + t.deepEqual(dirRes.getStatInfo().isDirectory(), true, "Directory resource is a directory"); + t.deepEqual(dirRes.getPath(), "/one/two/three", "Directory resource has correct path"); +}); + +test("Write resource w/ deep virtual base path", async (t) => { + const readerWriter = resourceFactory.createAdapter({ + virBasePath: "/app/a/" + }); + + const res = resourceFactory.createResource({ + path: "/app/a/one/two/three/test.html" + }); + await readerWriter.write(res); + + t.deepEqual(readerWriter._virFiles, { + "one/two/three/test.html": res + }, "Adapter added resource with correct path"); + + t.deepEqual(Object.keys(readerWriter._virDirs), [ + "one/two/three", + "one/two", + "one" + ], "Adapter added correct virtual directories"); + + const dirRes = readerWriter._virDirs["one/two/three"]; + t.deepEqual(dirRes.getStatInfo().isDirectory(), true, "Directory resource is a directory"); + t.deepEqual(dirRes.getPath(), "/app/a/one/two/three", "Directory resource has correct path"); +}); + +test("Write resource w/ crazy virtual base path", async (t) => { + const readerWriter = resourceFactory.createAdapter({ + virBasePath: "/app/🐛/" + }); + + const res = resourceFactory.createResource({ + path: "/app/🐛/one\\/2/3️⃣/test" + }); + await readerWriter.write(res); + + t.deepEqual(readerWriter._virFiles, { + "one\\/2/3️⃣/test": res + }, "Adapter added resource with correct path"); + + t.deepEqual(Object.keys(readerWriter._virDirs), [ + "one\\/2/3️⃣", + "one\\/2", + "one\\" + ], "Adapter added correct virtual directories"); +});