Skip to content

Commit 3454bc1

Browse files
authored
[BREAKING] Clone resources when writing in and reading from the Memory (#448)
BREAKING CHANGE: Resources stored in the adapters can not be modified by reference anymore. All modifications need to be persisted by using the #write method in order to be reflected in the adapter.
1 parent 62482f5 commit 3454bc1

File tree

7 files changed

+409
-141
lines changed

7 files changed

+409
-141
lines changed

lib/Resource.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class Resource {
5858
this._source = source; // Experimental, internal parameter
5959
if (this._source) {
6060
// Indicator for adapters like FileSystem to detect whether a resource has been changed
61-
this._source.modified = false;
61+
this._source.modified = this._source.modified || false;
6262
}
6363
this.__project = project; // Two underscores since "_project" was widely used in UI5 Tooling 2.0
6464

@@ -314,7 +314,7 @@ class Resource {
314314
const options = {
315315
path: this._path,
316316
statInfo: clone(this._statInfo),
317-
source: this._source
317+
source: clone(this._source)
318318
};
319319

320320
if (this._stream) {
@@ -325,6 +325,10 @@ class Resource {
325325
options.buffer = this._buffer;
326326
}
327327

328+
if (this.__project) {
329+
options.project = this.__project;
330+
}
331+
328332
return options;
329333
}
330334

lib/adapters/Memory.js

Lines changed: 64 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,24 @@ class Memory extends AbstractAdapter {
2727
this._virDirs = Object.create(null); // map full of directories
2828
}
2929

30+
/**
31+
* Matches and returns resources from a given map (either _virFiles or _virDirs).
32+
*
33+
* @private
34+
* @param {string[]} patterns
35+
* @param {object} resourceMap
36+
* @returns {Promise<module:@ui5/fs.Resource[]>}
37+
*/
38+
async _matchPatterns(patterns, resourceMap) {
39+
const resourcePaths = Object.keys(resourceMap);
40+
const matchedPaths = micromatch(resourcePaths, patterns, {
41+
dot: true
42+
});
43+
return Promise.all(matchedPaths.map((virPath) => {
44+
return resourceMap[virPath] && resourceMap[virPath].clone();
45+
}));
46+
}
47+
3048
/**
3149
* Locate resources by glob.
3250
*
@@ -55,22 +73,11 @@ class Memory extends AbstractAdapter {
5573
];
5674
}
5775

58-
const filePaths = Object.keys(this._virFiles);
59-
const matchedFilePaths = micromatch(filePaths, patterns, {
60-
dot: true
61-
});
62-
let matchedResources = matchedFilePaths.map((virPath) => {
63-
return this._virFiles[virPath];
64-
});
76+
let matchedResources = await this._matchPatterns(patterns, this._virFiles);
6577

6678
if (!options.nodir) {
67-
const dirPaths = Object.keys(this._virDirs);
68-
const matchedDirs = micromatch(dirPaths, patterns, {
69-
dot: true
70-
});
71-
matchedResources = matchedResources.concat(matchedDirs.map((virPath) => {
72-
return this._virDirs[virPath];
73-
}));
79+
const matchedDirs = await this._matchPatterns(patterns, this._virDirs);
80+
matchedResources = matchedResources.concat(matchedDirs);
7481
}
7582

7683
return matchedResources;
@@ -85,28 +92,25 @@ class Memory extends AbstractAdapter {
8592
* @param {@ui5/fs/tracing.Trace} trace Trace instance
8693
* @returns {Promise<@ui5/fs/Resource>} Promise resolving to a single resource
8794
*/
88-
_byPath(virPath, options, trace) {
95+
async _byPath(virPath, options, trace) {
8996
if (this.isPathExcluded(virPath)) {
90-
return Promise.resolve(null);
97+
return null;
98+
}
99+
if (!virPath.startsWith(this._virBasePath) && virPath !== this._virBaseDir) {
100+
// Neither starts with basePath, nor equals baseDirectory
101+
return null;
91102
}
92-
return new Promise((resolve, reject) => {
93-
if (!virPath.startsWith(this._virBasePath) && virPath !== this._virBaseDir) {
94-
// Neither starts with basePath, nor equals baseDirectory
95-
resolve(null);
96-
return;
97-
}
98103

99-
const relPath = virPath.substr(this._virBasePath.length);
100-
trace.pathCall();
104+
const relPath = virPath.substr(this._virBasePath.length);
105+
trace.pathCall();
101106

102-
const resource = this._virFiles[relPath];
107+
const resource = this._virFiles[relPath];
103108

104-
if (!resource || (options.nodir && resource.getStatInfo().isDirectory())) {
105-
resolve(null);
106-
} else {
107-
resolve(resource);
108-
}
109-
});
109+
if (!resource || (options.nodir && resource.getStatInfo().isDirectory())) {
110+
return null;
111+
} else {
112+
return await resource.clone();
113+
}
110114
}
111115

112116
/**
@@ -119,42 +123,39 @@ class Memory extends AbstractAdapter {
119123
async _write(resource) {
120124
resource = await this._migrateResource(resource);
121125
super._write(resource);
122-
return new Promise((resolve, reject) => {
123-
const relPath = resource.getPath().substr(this._virBasePath.length);
124-
log.silly("Writing to virtual path %s", resource.getPath());
125-
this._virFiles[relPath] = resource;
126-
127-
// Add virtual directories for all path segments of the written resource
128-
// TODO: Add tests for all this
129-
const pathSegments = relPath.split("/");
130-
pathSegments.pop(); // Remove last segment representing the resource itself
126+
const relPath = resource.getPath().substr(this._virBasePath.length);
127+
log.silly("Writing to virtual path %s", resource.getPath());
128+
this._virFiles[relPath] = await resource.clone();
131129

132-
pathSegments.forEach((segment, i) => {
133-
if (i >= 1) {
134-
segment = pathSegments[i - 1] + "/" + segment;
135-
}
136-
pathSegments[i] = segment;
137-
});
130+
// Add virtual directories for all path segments of the written resource
131+
// TODO: Add tests for all this
132+
const pathSegments = relPath.split("/");
133+
pathSegments.pop(); // Remove last segment representing the resource itself
138134

139-
for (let i = pathSegments.length - 1; i >= 0; i--) {
140-
const segment = pathSegments[i];
141-
if (!this._virDirs[segment]) {
142-
this._virDirs[segment] = this._createResource({
143-
project: this._project,
144-
source: {
145-
adapter: "Memory"
146-
},
147-
statInfo: { // TODO: make closer to fs stat info
148-
isDirectory: function() {
149-
return true;
150-
}
151-
},
152-
path: this._virBasePath + segment
153-
});
154-
}
135+
pathSegments.forEach((segment, i) => {
136+
if (i >= 1) {
137+
segment = pathSegments[i - 1] + "/" + segment;
155138
}
156-
resolve();
139+
pathSegments[i] = segment;
157140
});
141+
142+
for (let i = pathSegments.length - 1; i >= 0; i--) {
143+
const segment = pathSegments[i];
144+
if (!this._virDirs[segment]) {
145+
this._virDirs[segment] = this._createResource({
146+
project: this._project,
147+
source: {
148+
adapter: "Memory"
149+
},
150+
statInfo: { // TODO: make closer to fs stat info
151+
isDirectory: function() {
152+
return true;
153+
}
154+
},
155+
path: this._virBasePath + segment
156+
});
157+
}
158+
}
158159
}
159160
}
160161

test/lib/Resource.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,71 @@ test("Resource: clone resource with stream", async (t) => {
316316
t.is(clonedResourceContent, "Content", "Cloned resource has correct content string");
317317
});
318318

319+
test("Resource: clone resource with source", async (t) => {
320+
t.plan(4);
321+
322+
const resource = new Resource({
323+
path: "my/path/to/resource",
324+
source: {
325+
adapter: "FileSystem",
326+
fsPath: "/resources/my.js"
327+
}
328+
});
329+
330+
const clonedResource = await resource.clone();
331+
332+
t.not(resource.getSource(), clonedResource.getSource());
333+
t.deepEqual(clonedResource.getSource(), resource.getSource());
334+
335+
// Change existing resource and clone
336+
resource.setString("New Content");
337+
338+
const clonedResource2 = await resource.clone();
339+
340+
t.not(clonedResource.getSource(), resource.getSource());
341+
t.deepEqual(clonedResource2.getSource(), resource.getSource());
342+
});
343+
344+
test("Resource: clone resource with project", async (t) => {
345+
t.plan(2);
346+
347+
const myProject = {
348+
name: "my project"
349+
};
350+
351+
const resourceOptions = {
352+
path: "my/path/to/resource",
353+
project: myProject
354+
};
355+
356+
const resource = new Resource({
357+
path: "my/path/to/resource",
358+
project: myProject
359+
});
360+
361+
const clonedResource = await resource.clone();
362+
t.pass("Resource cloned");
363+
364+
const clonedResourceProject = await clonedResource.getProject();
365+
t.is(clonedResourceProject, resourceOptions.project, "Cloned resource should have same " +
366+
"project reference as the original resource");
367+
});
368+
369+
test("Resource: create resource with modified source", (t) => {
370+
t.plan(1);
371+
372+
const resource = new Resource({
373+
path: "my/path/to/resource",
374+
source: {
375+
adapter: "FileSystem",
376+
fsPath: "/resources/my.js",
377+
modified: true
378+
}
379+
});
380+
381+
t.true(resource.getSource().modified, "Modified flag is still true");
382+
});
383+
319384
test("getStream with createStream callback content: Subsequent content requests should throw error due " +
320385
"to drained content", async (t) => {
321386
const resource = createBasicResource();

test/lib/adapters/AbstractAdapter.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import test from "ava";
22
import AbstractAdapter from "../../../lib/adapters/AbstractAdapter.js";
3+
import {createResource} from "../../../lib/resourceFactory.js";
34

4-
class MyAbstractAdapter extends AbstractAdapter {}
5+
class MyAbstractAdapter extends AbstractAdapter { }
56

67
test("_migrateResource", async (t) => {
8+
// Any JS object which might be a kind of resource
79
const resource = {
810
_path: "/test.js"
911
};
@@ -16,3 +18,25 @@ test("_migrateResource", async (t) => {
1618

1719
t.is(migratedResource.getPath(), "/test.js");
1820
});
21+
22+
test("Write resource with another project than provided in the adapter", (t) => {
23+
const resource = createResource({
24+
path: "/test.js",
25+
project: {
26+
getName: () => "test.lib",
27+
getVersion: () => "2.0.0"
28+
}
29+
});
30+
31+
const writer = new MyAbstractAdapter({
32+
virBasePath: "/",
33+
project: {
34+
getName: () => "test.lib1",
35+
getVersion: () => "2.0.0"
36+
}
37+
});
38+
39+
const error = t.throws(() => writer._write(resource));
40+
t.is(error.message,
41+
"Unable to write resource associated with project test.lib into adapter of project test.lib1: /test.js");
42+
});

test/lib/adapters/Memory_read.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,3 +605,59 @@ test("static excludes: glob with negated directory exclude, not excluding resour
605605

606606
t.is(resources.length, 4, "Found two resources and two directories");
607607
});
608+
609+
test("byPath returns new resource", async (t) => {
610+
const originalResource = createResource({
611+
path: "/app/index.html",
612+
string: "test"
613+
});
614+
615+
const memoryAdapter = createAdapter({virBasePath: "/"});
616+
617+
await memoryAdapter.write(originalResource);
618+
619+
const returnedResource = await memoryAdapter.byPath("/app/index.html");
620+
621+
t.deepEqual(returnedResource, originalResource,
622+
"Returned resource should be deep equal to original resource");
623+
t.not(returnedResource, originalResource,
624+
"Returned resource should not have same reference as original resource");
625+
626+
const anotherReturnedResource = await memoryAdapter.byPath("/app/index.html");
627+
628+
t.deepEqual(anotherReturnedResource, originalResource,
629+
"Returned resource should be deep equal to original resource");
630+
t.not(anotherReturnedResource, originalResource,
631+
"Returned resource should not have same reference as original resource");
632+
633+
t.not(returnedResource, anotherReturnedResource,
634+
"Both returned resources should not have same reference");
635+
});
636+
637+
test("byGlob returns new resources", async (t) => {
638+
const originalResource = createResource({
639+
path: "/app/index.html",
640+
string: "test"
641+
});
642+
643+
const memoryAdapter = createAdapter({virBasePath: "/"});
644+
645+
await memoryAdapter.write(originalResource);
646+
647+
const [returnedResource] = await memoryAdapter.byGlob("/**");
648+
649+
t.deepEqual(returnedResource, originalResource,
650+
"Returned resource should be deep equal to the original resource");
651+
t.not(returnedResource, originalResource,
652+
"Returned resource should not have same reference as the original resource");
653+
654+
const [anotherReturnedResource] = await memoryAdapter.byGlob("/**");
655+
656+
t.deepEqual(anotherReturnedResource, originalResource,
657+
"Another returned resource should be deep equal to the original resource");
658+
t.not(anotherReturnedResource, originalResource,
659+
"Another returned resource should not have same reference as the original resource");
660+
661+
t.not(returnedResource, anotherReturnedResource,
662+
"Both returned resources should not have same reference");
663+
});

0 commit comments

Comments
 (0)