Skip to content

Commit 33f6d11

Browse files
committed
Add ProjectGraph tests
1 parent 83df056 commit 33f6d11

File tree

6 files changed

+743
-52
lines changed

6 files changed

+743
-52
lines changed

lib/Module.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,18 @@ class Module {
2828
* or an absolute File System path to the project configuration file.
2929
* @param {object} [parameters.configuration]
3030
* Configuration object to use. If supplied, no ui5.yaml will be read
31-
* @param {@ui5/extension.extensions.ShimCollection} [parameters.shimCollection]
31+
* @param {@ui5/project.graph.ShimCollection} [parameters.shimCollection]
3232
* Collection of shims that might be relevant for this module
3333
*/
3434
constructor({id, version, modulePath, configPath = defaultConfigPath, configuration, shimCollection}) {
3535
if (!id) {
36-
throw new Error(`Could not create Module: Missing or empty id parameter`);
36+
throw new Error(`Could not create Module: Missing or empty parameter 'id'`);
3737
}
3838
if (!version) {
39-
throw new Error(`Could not create Module: Missing or empty version parameter`);
39+
throw new Error(`Could not create Module: Missing or empty parameter 'version'`);
4040
}
4141
if (!modulePath) {
42-
throw new Error(`Could not create Module: Missing or empty modulePath parameter`);
42+
throw new Error(`Could not create Module: Missing or empty parameter 'modulePath'`);
4343
}
4444

4545
this._id = id;

lib/graph/ProjectGraph.js

Lines changed: 77 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
const log = require("@ui5/logger").getLogger("graph:ProjectGraph");
22
/**
3-
* A rooted, directed graph representing a UI5 project that should be worked with and all its dependencies
4-
*/
3+
* A rooted, directed graph representing a UI5 project and all its dependencies
4+
*
5+
* @public
6+
* @memberof module:@ui5/project.graph
7+
*/
58
class ProjectGraph {
69
/**
10+
* @public
711
* @param {object} parameters Parameters
812
* @param {string} parameters.rootProjectName Root project name
9-
* @param {Array.<module:@ui5/project.Extension>} parameters.extensions
13+
* @param {Array.<module:@ui5/project.specification.Extension>} parameters.extensions
1014
* Final list of extensions to be used in this project tree
1115
*/
1216
constructor({rootProjectName, extensions = []}) {
1317
if (!rootProjectName) {
14-
throw new Error(`Could not create ProjectGraph: One or more required parameters are missing`);
18+
throw new Error(`Could not create ProjectGraph: Missing or empty parameter 'rootProjectName'`);
1519
}
1620
this._rootProjectName = rootProjectName;
1721
this._extensions = Object.freeze(extensions);
@@ -34,22 +38,30 @@ class ProjectGraph {
3438
if (ignoreDuplicates) {
3539
return;
3640
}
37-
throw new Error(`Could not add duplicate project '${projectName}' to project tree`);
41+
throw new Error(
42+
`Failed to add project ${projectName} to the graph: A project with that name has already been added`);
43+
}
44+
if (!isNaN(projectName)) {
45+
// Reject integer-like project names. They would take precedence when traversing object keys which
46+
// could lead to unexpected behavior. We don't really expect anyone to use such names anyways
47+
throw new Error(
48+
`Failed to add project ${projectName} to graph: Project name must not be integer-like`);
3849
}
3950
this._projects[projectName] = project;
40-
this._adjList[projectName] = [];
51+
this._adjList[projectName] = {};
4152
}
4253

4354
getProject(projectName) {
4455
return this._projects[projectName];
4556
}
4657

4758
/**
48-
* Declare a dependency from one project in the graph to another
49-
*
50-
* @param {string} fromProjectName Name of the depending project
51-
* @param {string} toProjectName Name of project on which the other depends
52-
*/
59+
* Declare a dependency from one project in the graph to another
60+
*
61+
* @public
62+
* @param {string} fromProjectName Name of the depending project
63+
* @param {string} toProjectName Name of project on which the other depends
64+
*/
5365
declareDependency(fromProjectName, toProjectName/* , optional*/) {
5466
if (!this._projects[fromProjectName]) {
5567
throw new Error(
@@ -69,18 +81,49 @@ class ProjectGraph {
6981
}
7082
}
7183

84+
7285
/**
73-
* Visit every project in the graph that can be reached by the given entry project exactly once.
74-
* The entry project defaults to the root project.
75-
* In case a cycle is detected, an error is thrown
76-
*
77-
* @param {Function} callback
78-
*/
79-
async traverseBreadthFirst(callback) {
80-
// TODO: Add parameter to define point of entry, defaulting to root
86+
* Callback for graph traversal operations
87+
*
88+
* @public
89+
* @async
90+
* @callback module:@ui5/project.graph.ProjectGraph~traversalCallback
91+
* @param {object} parameters Parameters passed to the callback
92+
* @param {module:@ui5/project.specifications.Project} parameters.project The project that is currently visited
93+
* @param {module:@ui5/project.graph.ProjectGraph~getDependencies} parameters.getDependencies
94+
* Function to access the dependencies of the project that is currently visited.
95+
* @returns {Promise} Must return a promise on which the graph traversal will wait
96+
*/
97+
98+
/**
99+
* Helper function available in the
100+
* traversalCallback]{@link module:@ui5/project.graph.ProjectGraph~traversalCallback} to access the
101+
* dependencies of the corresponding project in the current graph.
102+
* <br><br>
103+
* Note that transitive dependencies can't be accessed this way. Projects should rather add a direct
104+
* dependency to projects they need access to.
105+
*
106+
* @public
107+
* @function module:@ui5/project.graph.ProjectGraph~getDependencies
108+
* @returns {Array.<module:@ui5/project.specifications.Project>} Direct dependencies of the visited project
109+
*/
110+
111+
/**
112+
* Visit every project in the graph that can be reached by the given entry project exactly once.
113+
* The entry project defaults to the root project.
114+
* In case a cycle is detected, an error is thrown
115+
*
116+
* @public
117+
* @param {module:@ui5/project.graph.ProjectGraph~traversalCallback} callback Will be called
118+
* @param {string} [startName] Name of the project to start the traversal at. Defaults to the graph's root project
119+
*/
120+
async traverseBreadthFirst(callback, startName = this._rootProjectName) {
121+
if (!this.getProject(startName)) {
122+
throw new Error(`Failed to start graph traversal: Could not find project ${startName} in graph`);
123+
}
81124

82125
const queue = [{
83-
projectNames: [this._rootProjectName],
126+
projectNames: [startName],
84127
predecessors: []
85128
}];
86129

@@ -114,7 +157,7 @@ class ProjectGraph {
114157
await callback({
115158
project: this.getProject(projectName),
116159
getDependencies: () => {
117-
return dependencies.map(($) => this.getProject($.projectName));
160+
return dependencies.map(($) => this.getProject($));
118161
}
119162
});
120163
})();
@@ -123,15 +166,18 @@ class ProjectGraph {
123166
}
124167

125168
/**
126-
* Visit every project in the graph that can be reached by the given entry project exactly once.
127-
* The entry project defaults to the root project.
128-
* In case a cycle is detected, an error is thrown
129-
*
130-
* @param {Function} callback
131-
*/
132-
async traverseDepthFirst(callback) {
133-
// TODO: Add parameter to define point of entry, defaulting to root
134-
return this._traverseDepthFirst(this._rootProjectName, {}, [], callback);
169+
* Visit every project in the graph that can be reached by the given entry project exactly once.
170+
* The entry project defaults to the root project.
171+
* In case a cycle is detected, an error is thrown
172+
*
173+
* @param {module:@ui5/project.graph.ProjectGraph~traversalCallback} callback Will be called
174+
* @param {string} [startName] Name of the project to start the traversal at. Defaults to the graph's root project
175+
*/
176+
async traverseDepthFirst(callback, startName = this._rootProjectName) {
177+
if (!this.getProject(startName)) {
178+
throw new Error(`Failed to start graph traversal: Could not find project ${startName} in graph`);
179+
}
180+
return this._traverseDepthFirst(startName, {}, [], callback);
135181
}
136182

137183
async _traverseDepthFirst(projectName, visited, predecessors, callback) {
@@ -156,7 +202,7 @@ class ProjectGraph {
156202
await callback({
157203
project: this.getProject(projectName),
158204
getDependencies: () => {
159-
return dependencies.map(($) => this.getProject($.projectName));
205+
return dependencies.map(($) => this.getProject($));
160206
}
161207
});
162208
})();

lib/specifications/AbstractSpecification.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,20 @@ class AbstractSpecification {
1414
if (new.target === AbstractSpecification) {
1515
throw new TypeError("Class 'AbstractSpecification' is abstract");
1616
}
17-
if (!id || !version || !modulePath || !configuration) {
18-
throw new Error(`Could not create Project: One or more required parameters are missing`);
17+
if (!id) {
18+
throw new Error(`Could not create specification: Missing or empty parameter 'id'`);
19+
}
20+
if (!version) {
21+
throw new Error(`Could not create specification: Missing or empty parameter 'version'`);
22+
}
23+
if (!modulePath) {
24+
throw new Error(`Could not create specification: Missing or empty parameter 'modulePath'`);
25+
}
26+
if (!configuration) {
27+
throw new Error(`Could not create specification: Missing or empty parameter 'configuration'`);
1928
}
20-
2129
if (!(configuration instanceof Configuration)) {
22-
throw new Error(`Could not create project: 'configuration' must be an instance of ` +
30+
throw new Error(`Could not create specification: 'configuration' must be an instance of ` +
2331
`@ui5/project.specifications.Configuration`);
2432
}
2533

0 commit comments

Comments
 (0)