diff --git a/lib/Resource.js b/lib/Resource.js index 1edca7f9..b1598de6 100644 --- a/lib/Resource.js +++ b/lib/Resource.js @@ -58,7 +58,7 @@ class Resource { this._source = source; // Experimental, internal parameter if (this._source) { // Indicator for adapters like FileSystem to detect whether a resource has been changed - this._source.modified = false; + this._source.modified = this._source.modified || false; } this.__project = project; // Two underscores since "_project" was widely used in UI5 Tooling 2.0 @@ -314,7 +314,7 @@ class Resource { const options = { path: this._path, statInfo: clone(this._statInfo), - source: this._source + source: clone(this._source) }; if (this._stream) { @@ -325,6 +325,10 @@ class Resource { options.buffer = this._buffer; } + if (this.__project) { + options.project = this.__project; + } + return options; } diff --git a/lib/adapters/Memory.js b/lib/adapters/Memory.js index 92b8bdf1..afe507b2 100644 --- a/lib/adapters/Memory.js +++ b/lib/adapters/Memory.js @@ -27,6 +27,24 @@ class Memory extends AbstractAdapter { this._virDirs = Object.create(null); // map full of directories } + /** + * Matches and returns resources from a given map (either _virFiles or _virDirs). + * + * @private + * @param {string[]} patterns + * @param {object} resourceMap + * @returns {Promise} + */ + async _matchPatterns(patterns, resourceMap) { + const resourcePaths = Object.keys(resourceMap); + const matchedPaths = micromatch(resourcePaths, patterns, { + dot: true + }); + return Promise.all(matchedPaths.map((virPath) => { + return resourceMap[virPath] && resourceMap[virPath].clone(); + })); + } + /** * Locate resources by glob. * @@ -55,22 +73,11 @@ class Memory extends AbstractAdapter { ]; } - const filePaths = Object.keys(this._virFiles); - const matchedFilePaths = micromatch(filePaths, patterns, { - dot: true - }); - let matchedResources = matchedFilePaths.map((virPath) => { - return this._virFiles[virPath]; - }); + let matchedResources = await this._matchPatterns(patterns, this._virFiles); if (!options.nodir) { - const dirPaths = Object.keys(this._virDirs); - const matchedDirs = micromatch(dirPaths, patterns, { - dot: true - }); - matchedResources = matchedResources.concat(matchedDirs.map((virPath) => { - return this._virDirs[virPath]; - })); + const matchedDirs = await this._matchPatterns(patterns, this._virDirs); + matchedResources = matchedResources.concat(matchedDirs); } return matchedResources; @@ -85,28 +92,25 @@ class Memory extends AbstractAdapter { * @param {@ui5/fs/tracing.Trace} trace Trace instance * @returns {Promise<@ui5/fs/Resource>} Promise resolving to a single resource */ - _byPath(virPath, options, trace) { + async _byPath(virPath, options, trace) { if (this.isPathExcluded(virPath)) { - return Promise.resolve(null); + return null; + } + if (!virPath.startsWith(this._virBasePath) && virPath !== this._virBaseDir) { + // Neither starts with basePath, nor equals baseDirectory + return null; } - return new Promise((resolve, reject) => { - if (!virPath.startsWith(this._virBasePath) && virPath !== this._virBaseDir) { - // Neither starts with basePath, nor equals baseDirectory - resolve(null); - return; - } - const relPath = virPath.substr(this._virBasePath.length); - trace.pathCall(); + const relPath = virPath.substr(this._virBasePath.length); + trace.pathCall(); - const resource = this._virFiles[relPath]; + const resource = this._virFiles[relPath]; - if (!resource || (options.nodir && resource.getStatInfo().isDirectory())) { - resolve(null); - } else { - resolve(resource); - } - }); + if (!resource || (options.nodir && resource.getStatInfo().isDirectory())) { + return null; + } else { + return await resource.clone(); + } } /** @@ -119,42 +123,39 @@ class Memory extends AbstractAdapter { async _write(resource) { resource = await this._migrateResource(resource); super._write(resource); - return new Promise((resolve, reject) => { - const relPath = resource.getPath().substr(this._virBasePath.length); - log.silly("Writing to virtual path %s", resource.getPath()); - this._virFiles[relPath] = resource; - - // Add virtual directories for all path segments of the written resource - // TODO: Add tests for all this - const pathSegments = relPath.split("/"); - pathSegments.pop(); // Remove last segment representing the resource itself + const relPath = resource.getPath().substr(this._virBasePath.length); + log.silly("Writing to virtual path %s", resource.getPath()); + this._virFiles[relPath] = await resource.clone(); - pathSegments.forEach((segment, i) => { - if (i >= 1) { - segment = pathSegments[i - 1] + "/" + segment; - } - pathSegments[i] = segment; - }); + // Add virtual directories for all path segments of the written resource + // TODO: Add tests for all this + const pathSegments = relPath.split("/"); + pathSegments.pop(); // Remove last segment representing the resource itself - for (let i = pathSegments.length - 1; i >= 0; i--) { - const segment = pathSegments[i]; - if (!this._virDirs[segment]) { - this._virDirs[segment] = this._createResource({ - project: this._project, - source: { - adapter: "Memory" - }, - statInfo: { // TODO: make closer to fs stat info - isDirectory: function() { - return true; - } - }, - path: this._virBasePath + segment - }); - } + pathSegments.forEach((segment, i) => { + if (i >= 1) { + segment = pathSegments[i - 1] + "/" + segment; } - resolve(); + pathSegments[i] = segment; }); + + for (let i = pathSegments.length - 1; i >= 0; i--) { + const segment = pathSegments[i]; + if (!this._virDirs[segment]) { + this._virDirs[segment] = this._createResource({ + project: this._project, + source: { + adapter: "Memory" + }, + statInfo: { // TODO: make closer to fs stat info + isDirectory: function() { + return true; + } + }, + path: this._virBasePath + segment + }); + } + } } } diff --git a/test/lib/Resource.js b/test/lib/Resource.js index a3f6cb44..78e68312 100644 --- a/test/lib/Resource.js +++ b/test/lib/Resource.js @@ -316,6 +316,71 @@ test("Resource: clone resource with stream", async (t) => { t.is(clonedResourceContent, "Content", "Cloned resource has correct content string"); }); +test("Resource: clone resource with source", async (t) => { + t.plan(4); + + const resource = new Resource({ + path: "my/path/to/resource", + source: { + adapter: "FileSystem", + fsPath: "/resources/my.js" + } + }); + + const clonedResource = await resource.clone(); + + t.not(resource.getSource(), clonedResource.getSource()); + t.deepEqual(clonedResource.getSource(), resource.getSource()); + + // Change existing resource and clone + resource.setString("New Content"); + + const clonedResource2 = await resource.clone(); + + t.not(clonedResource.getSource(), resource.getSource()); + t.deepEqual(clonedResource2.getSource(), resource.getSource()); +}); + +test("Resource: clone resource with project", async (t) => { + t.plan(2); + + const myProject = { + name: "my project" + }; + + const resourceOptions = { + path: "my/path/to/resource", + project: myProject + }; + + const resource = new Resource({ + path: "my/path/to/resource", + project: myProject + }); + + const clonedResource = await resource.clone(); + t.pass("Resource cloned"); + + const clonedResourceProject = await clonedResource.getProject(); + t.is(clonedResourceProject, resourceOptions.project, "Cloned resource should have same " + + "project reference as the original resource"); +}); + +test("Resource: create resource with modified source", (t) => { + t.plan(1); + + const resource = new Resource({ + path: "my/path/to/resource", + source: { + adapter: "FileSystem", + fsPath: "/resources/my.js", + modified: true + } + }); + + t.true(resource.getSource().modified, "Modified flag is still true"); +}); + test("getStream with createStream callback content: Subsequent content requests should throw error due " + "to drained content", async (t) => { const resource = createBasicResource(); diff --git a/test/lib/adapters/AbstractAdapter.js b/test/lib/adapters/AbstractAdapter.js index 6ac92267..d0c4713e 100644 --- a/test/lib/adapters/AbstractAdapter.js +++ b/test/lib/adapters/AbstractAdapter.js @@ -1,9 +1,11 @@ import test from "ava"; import AbstractAdapter from "../../../lib/adapters/AbstractAdapter.js"; +import {createResource} from "../../../lib/resourceFactory.js"; -class MyAbstractAdapter extends AbstractAdapter {} +class MyAbstractAdapter extends AbstractAdapter { } test("_migrateResource", async (t) => { + // Any JS object which might be a kind of resource const resource = { _path: "/test.js" }; @@ -16,3 +18,25 @@ test("_migrateResource", async (t) => { t.is(migratedResource.getPath(), "/test.js"); }); + +test("Write resource with another project than provided in the adapter", (t) => { + const resource = createResource({ + path: "/test.js", + project: { + getName: () => "test.lib", + getVersion: () => "2.0.0" + } + }); + + const writer = new MyAbstractAdapter({ + virBasePath: "/", + project: { + getName: () => "test.lib1", + getVersion: () => "2.0.0" + } + }); + + const error = t.throws(() => writer._write(resource)); + t.is(error.message, + "Unable to write resource associated with project test.lib into adapter of project test.lib1: /test.js"); +}); diff --git a/test/lib/adapters/Memory_read.js b/test/lib/adapters/Memory_read.js index 09b99159..444116df 100644 --- a/test/lib/adapters/Memory_read.js +++ b/test/lib/adapters/Memory_read.js @@ -605,3 +605,59 @@ test("static excludes: glob with negated directory exclude, not excluding resour t.is(resources.length, 4, "Found two resources and two directories"); }); + +test("byPath returns new resource", async (t) => { + const originalResource = createResource({ + path: "/app/index.html", + string: "test" + }); + + const memoryAdapter = createAdapter({virBasePath: "/"}); + + await memoryAdapter.write(originalResource); + + const returnedResource = await memoryAdapter.byPath("/app/index.html"); + + t.deepEqual(returnedResource, originalResource, + "Returned resource should be deep equal to original resource"); + t.not(returnedResource, originalResource, + "Returned resource should not have same reference as original resource"); + + const anotherReturnedResource = await memoryAdapter.byPath("/app/index.html"); + + t.deepEqual(anotherReturnedResource, originalResource, + "Returned resource should be deep equal to original resource"); + t.not(anotherReturnedResource, originalResource, + "Returned resource should not have same reference as original resource"); + + t.not(returnedResource, anotherReturnedResource, + "Both returned resources should not have same reference"); +}); + +test("byGlob returns new resources", async (t) => { + const originalResource = createResource({ + path: "/app/index.html", + string: "test" + }); + + const memoryAdapter = createAdapter({virBasePath: "/"}); + + await memoryAdapter.write(originalResource); + + const [returnedResource] = await memoryAdapter.byGlob("/**"); + + t.deepEqual(returnedResource, originalResource, + "Returned resource should be deep equal to the original resource"); + t.not(returnedResource, originalResource, + "Returned resource should not have same reference as the original resource"); + + const [anotherReturnedResource] = await memoryAdapter.byGlob("/**"); + + t.deepEqual(anotherReturnedResource, originalResource, + "Another returned resource should be deep equal to the original resource"); + t.not(anotherReturnedResource, originalResource, + "Another returned resource should not have same reference as the original resource"); + + t.not(returnedResource, anotherReturnedResource, + "Both returned resources should not have same reference"); +}); diff --git a/test/lib/adapters/Memory_write.js b/test/lib/adapters/Memory_write.js index ee4ace41..d1cbad8a 100644 --- a/test/lib/adapters/Memory_write.js +++ b/test/lib/adapters/Memory_write.js @@ -10,11 +10,10 @@ test("glob resources from application.a w/ virtual base path prefix", async (t) const res = createResource({ path: "/app/index.html" }); - await dest.write(res) - .then(() => dest.byGlob("/app/*.html")) - .then((resources) => { - t.is(resources.length, 1, "Found exactly one resource"); - }); + await dest.write(res); + const resources = await dest.byGlob("/app/*.html"); + t.is(resources.length, 1, "Found exactly one resource"); + t.not(resources[0], res, "Not the same resource instance"); }); test("glob resources from application.a w/o virtual base path prefix", async (t) => { @@ -25,11 +24,9 @@ test("glob resources from application.a w/o virtual base path prefix", async (t) const res = createResource({ path: "/app/index.html" }); - await dest.write(res) - .then(() => dest.byGlob("/**/*.html")) - .then((resources) => { - t.is(resources.length, 1, "Found exactly one resource"); - }); + await dest.write(res); + const resources = await dest.byGlob("/**/*.html"); + t.is(resources.length, 1, "Found exactly one resource"); }); test("Write resource w/ virtual base path", async (t) => { @@ -47,6 +44,7 @@ test("Write resource w/ virtual base path", async (t) => { }, "Adapter added resource with correct path"); t.deepEqual(Object.keys(readerWriter._virDirs), [], "Adapter added correct virtual directories"); + t.not(readerWriter._virFiles["test.html"], res, "Not the same resource instance"); }); test("Write resource w/o virtual base path", async (t) => { @@ -133,3 +131,30 @@ test("Migration of resource is executed", async (t) => { await writer.write(resource); t.is(migrateResourceWriterSpy.callCount, 1); }); + +test("Resource: Change instance after write", async (t) => { + const writer = createAdapter({ + virBasePath: "/" + }); + + const resource = createResource({ + path: "/test.js", + string: "MyInitialContent" + }); + + await writer.write(resource); + + resource.setString("MyNewContent"); + + const resource1 = await writer.byPath("/test.js"); + + t.is(await resource.getString(), "MyNewContent"); + t.is(await resource1.getString(), "MyInitialContent"); + + await writer.write(resource); + + const resource2 = await writer.byPath("/test.js"); + t.is(await resource.getString(), "MyNewContent"); + t.is(await resource1.getString(), "MyInitialContent"); + t.is(await resource2.getString(), "MyNewContent"); +}); diff --git a/test/lib/resources.js b/test/lib/resources.js index 2b0be219..e448c762 100644 --- a/test/lib/resources.js +++ b/test/lib/resources.js @@ -4,88 +4,181 @@ import chaifs from "chai-fs"; chai.use(chaifs); const assert = chai.assert; import sinon from "sinon"; -import {createAdapter, createFilterReader, createFlatReader, createLinkReader} from "../../lib/resourceFactory.js"; +import {createAdapter, createFilterReader, + createFlatReader, createLinkReader, createResource} from "../../lib/resourceFactory.js"; test.afterEach.always((t) => { sinon.restore(); }); -/* BEWARE: - Always make sure that every test writes to a separate file! By default, tests are running concurrent. -*/ -test("Get resource from application.a (/index.html) and write it to /dest/ using a ReadableStream", async (t) => { - const source = createAdapter({ - fsBasePath: "./test/fixtures/application.a/webapp", - virBasePath: "/app/" - }); - const dest = createAdapter({ - fsBasePath: "./test/tmp/readerWriters/application.a/simple-read-write", - virBasePath: "/dest/" - }); - // Get resource from one readerWriter - const resource = await source.byPath("/app/index.html"); +const adapters = ["FileSystem", "Memory"]; - const clonedResource = await resource.clone(); +async function getAdapter(config, adapter) { + if (adapter === "Memory") { + const fsAdapter = createAdapter(config); + const fsResources = await fsAdapter.byGlob("**/*"); + // By removing the fsBasePath a MemAdapter will be created + delete config.fsBasePath; + const memAdapter = createAdapter(config); + for (const resource of fsResources) { + await memAdapter.write(resource); + } + return memAdapter; + } else { + return createAdapter(config); + } +} - // Write resource content to another readerWriter - clonedResource.setPath("/dest/index_readableStreamTest.html"); - await dest.write(clonedResource); +for (const adapter of adapters) { + /* BEWARE: + Always make sure that every test writes to a separate file! By default, tests are running concurrent. + */ + test(adapter + + ": Get resource from application.a (/index.html) and write it to /dest/ using a ReadableStream", async (t) => { + const source = await getAdapter({ + fsBasePath: "./test/fixtures/application.a/webapp", + virBasePath: "/app/" + }, adapter); + const dest = await getAdapter({ + fsBasePath: "./test/tmp/readerWriters/application.a/simple-read-write", + virBasePath: "/dest/" + }, adapter); - t.notThrows(() => { - assert.fileEqual("./test/tmp/readerWriters/application.a/simple-read-write/index_readableStreamTest.html", - "./test/fixtures/application.a/webapp/index.html"); - }); -}); + // Get resource from one readerWriter + const resource = await source.byPath("/app/index.html"); -test("Filter resources", async (t) => { - const source = createAdapter({ - fsBasePath: "./test/fixtures/application.a/webapp", - virBasePath: "/app/" - }); - const filteredSource = createFilterReader({ - reader: source, - callback: (resource) => { - return resource.getPath().endsWith(".js"); - } + // Write resource content to another readerWriter + resource.setPath("/dest/index_readableStreamTest.html"); + await dest.write(resource); + + t.notThrows(async () => { + if (adapter === "FileSystem") { + assert.fileEqual( + "./test/tmp/readerWriters/application.a/simple-read-write/index_readableStreamTest.html", + "./test/fixtures/application.a/webapp/index.html"); + } else { + const destResource = await dest.byPath("/dest/index_readableStreamTest.html"); + t.deepEqual(await destResource.getString(), await resource.getString()); + } + }); }); - const sourceResources = await source.byGlob("**"); - t.is(sourceResources.length, 2, "Found two resources in source"); - const resources = await filteredSource.byGlob("**"); + test(adapter + ": Create resource, write and change content", async (t) => { + const dest = await getAdapter({ + fsBasePath: "./test/tmp/writer/", + virBasePath: "/dest/writer/" + }); - t.is(resources.length, 1, "Found exactly one resource via filter"); - t.is(resources[0].getPath(), "/app/test.js", "Found correct resource"); -}); + const resource = createResource({ + path: "/dest/writer/test.js", + string: "MyInitialContent" + }); + + await dest.write(resource); + + resource.setString("MyNewContent"); + + const resource1 = await dest.byPath("/dest/writer/test.js"); -test("Flatten resources", async (t) => { - const source = createAdapter({ - fsBasePath: "./test/fixtures/application.a/webapp", - virBasePath: "/resources/app/" + t.is(await resource.getString(), "MyNewContent"); + t.is(await resource1.getString(), "MyInitialContent"); + + t.is(await resource.getString(), "MyNewContent"); + t.is(await resource1.getString(), "MyInitialContent"); + + await dest.write(resource); + + const resource2 = await dest.byPath("/dest/writer/test.js"); + t.is(await resource.getString(), "MyNewContent"); + t.is(await resource2.getString(), "MyNewContent"); }); - const transformedSource = createFlatReader({ - reader: source, - namespace: "app" + + test(adapter + ": Create resource, write and change path", async (t) => { + const dest = await getAdapter({ + fsBasePath: "./test/tmp/writer/", + virBasePath: "/dest/writer/" + }); + + const resource = createResource({ + path: "/dest/writer/test.js", + string: "MyInitialContent" + }); + + await dest.write(resource); + + resource.setPath("/dest/writer/test2.js"); + + const resourceOldPath = await dest.byPath("/dest/writer/test.js"); + const resourceNewPath = await dest.byPath("/dest/writer/test2.js"); + + t.is(await resource.getPath(), "/dest/writer/test2.js"); + t.truthy(resourceOldPath); + t.is(await resourceOldPath.getString(), await resource.getString()); + t.is(await resourceOldPath.getPath(), "/dest/writer/test.js"); + t.not(resourceNewPath); + + await dest.write(resource); + + const resourceOldPath1 = await dest.byPath("/dest/writer/test.js"); + const resourceNewPath1 = await dest.byPath("/dest/writer/test2.js"); + + t.is(await resource.getPath(), "/dest/writer/test2.js"); + t.truthy(resourceNewPath1); + t.is(await resourceNewPath1.getString(), await resource.getString()); + t.is(await resourceNewPath1.getPath(), "/dest/writer/test2.js"); + t.not(resourceOldPath1); }); - const resources = await transformedSource.byGlob("**/*.js"); - t.is(resources.length, 1, "Found one resource via transformer"); - t.is(resources[0].getPath(), "/test.js", "Found correct resource"); -}); + test(adapter + ": Filter resources", async (t) => { + const source = createAdapter({ + fsBasePath: "./test/fixtures/application.a/webapp", + virBasePath: "/app/" + }); + const filteredSource = createFilterReader({ + reader: source, + callback: (resource) => { + return resource.getPath().endsWith(".js"); + } + }); + const sourceResources = await source.byGlob("**"); + t.is(sourceResources.length, 2, "Found two resources in source"); + + const resources = await filteredSource.byGlob("**"); -test("Link resources", async (t) => { - const source = createAdapter({ - fsBasePath: "./test/fixtures/application.a/webapp", - virBasePath: "/resources/app/" + t.is(resources.length, 1, "Found exactly one resource via filter"); + t.is(resources[0].getPath(), "/app/test.js", "Found correct resource"); }); - const transformedSource = createLinkReader({ - reader: source, - pathMapping: { - linkPath: "/wow/this/is/a/beautiful/path/just/wow/", - targetPath: "/resources/" - } + + test(adapter + ": Flatten resources", async (t) => { + const source = await getAdapter({ + fsBasePath: "./test/fixtures/application.a/webapp", + virBasePath: "/resources/app/" + }, adapter); + const transformedSource = createFlatReader({ + reader: source, + namespace: "app" + }); + + const resources = await transformedSource.byGlob("**/*.js"); + t.is(resources.length, 1, "Found one resource via transformer"); + t.is(resources[0].getPath(), "/test.js", "Found correct resource"); }); - const resources = await transformedSource.byGlob("**/*.js"); - t.is(resources.length, 1, "Found one resource via transformer"); - t.is(resources[0].getPath(), "/wow/this/is/a/beautiful/path/just/wow/app/test.js", "Found correct resource"); -}); + test(adapter + ": Link resources", async (t) => { + const source = await getAdapter({ + fsBasePath: "./test/fixtures/application.a/webapp", + virBasePath: "/resources/app/" + }, adapter); + const transformedSource = createLinkReader({ + reader: source, + pathMapping: { + linkPath: "/wow/this/is/a/beautiful/path/just/wow/", + targetPath: "/resources/" + } + }); + + const resources = await transformedSource.byGlob("**/*.js"); + t.is(resources.length, 1, "Found one resource via transformer"); + t.is(resources[0].getPath(), "/wow/this/is/a/beautiful/path/just/wow/app/test.js", "Found correct resource"); + }); +}