From a566ebd0e2ed1bbf0d0f871c430d3e81e6ff0240 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Thu, 17 Nov 2022 00:45:03 +0100 Subject: [PATCH] [INTERNAL] helpers/ui5Framework: Enhance test coverage --- lib/graph/helpers/ui5Framework.js | 38 +-- test/lib/graph/helpers/ui5Framework.js | 392 ++++++++++++++++++++++++- 2 files changed, 403 insertions(+), 27 deletions(-) diff --git a/lib/graph/helpers/ui5Framework.js b/lib/graph/helpers/ui5Framework.js index 31a4178bd..0039aa127 100644 --- a/lib/graph/helpers/ui5Framework.js +++ b/lib/graph/helpers/ui5Framework.js @@ -4,34 +4,35 @@ import logger from "@ui5/logger"; const log = logger.getLogger("graph:helpers:ui5Framework"); class ProjectProcessor { - constructor({libraryMetadata, workspace}) { + constructor({libraryMetadata, graph, workspace}) { this._libraryMetadata = libraryMetadata; + this._graph = graph; this._workspace = workspace; this._projectGraphPromises = Object.create(null); } - async addProjectToGraph(libName, projectGraph) { + async addProjectToGraph(libName) { if (this._projectGraphPromises[libName]) { return this._projectGraphPromises[libName]; } - return this._projectGraphPromises[libName] = this._addProjectToGraph(libName, projectGraph); + return this._projectGraphPromises[libName] = this._addProjectToGraph(libName); } - async _addProjectToGraph(libName, projectGraph) { + async _addProjectToGraph(libName) { log.verbose(`Creating project for library ${libName}...`); - if (!this._libraryMetadata[libName]) { throw new Error(`Failed to find library ${libName} in dist packages metadata.json`); } const depMetadata = this._libraryMetadata[libName]; + const graph = this._graph; - if (projectGraph.getProject(depMetadata.id)) { + if (graph.getProject(libName)) { // Already added return; } const dependencies = await Promise.all(depMetadata.dependencies.map(async (depName) => { - await this.addProjectToGraph(depName, projectGraph); + await this.addProjectToGraph(depName, graph); return depName; })); @@ -39,7 +40,7 @@ class ProjectProcessor { const resolvedOptionals = await Promise.all(depMetadata.optionalDependencies.map(async (depName) => { if (this._libraryMetadata[depName]) { log.verbose(`Resolving optional dependency ${depName} for project ${libName}...`); - await this.addProjectToGraph(depName, projectGraph); + await this.addProjectToGraph(depName, graph); return depName; } })); @@ -65,9 +66,9 @@ class ProjectProcessor { }); } const {project} = await ui5Module.getSpecifications(); - projectGraph.addProject(project); + graph.addProject(project); dependencies.forEach((dependency) => { - projectGraph.declareDependency(libName, dependency); + graph.declareDependency(libName, dependency); }); if (projectIsFromWorkspace) { // Add any dependencies that are only declared in the workspace resolved project @@ -83,13 +84,13 @@ class ProjectProcessor { throw new Error( `Unable to find dependency ${name}, required by project ${project.getName()} ` + `(resolved via ${this._workspace.getName()} workspace) in current set of libraries. ` + - `Try adding it temporarily to the root project's dependencies.`); + `Try adding it temporarily to the root project's dependencies`); } // TODO: If a cyclic dependency is declared, this will empty the event loop. // I guess this is a general issue and not limited to projects resolved via workspaces - await this.addProjectToGraph(name, projectGraph); - projectGraph.declareDependency(libName, name); + await this.addProjectToGraph(name, graph); + graph.declareDependency(libName, name); })); } } @@ -187,6 +188,7 @@ export default { } const frameworkName = rootProject.getFrameworkName(); + console.log(frameworkName); const frameworkVersion = rootProject.getFrameworkVersion(); if (!frameworkName && !frameworkVersion) { log.verbose(`Root project ${rootProject.getName()} has no framework configuration. Nothing to do here`); @@ -249,16 +251,18 @@ export default { `resolved in ${prettyHrtime(timeDiff)}`); } + const frameworkGraph = new ProjectGraph({ + rootProjectName: `fake-root-of-${rootProject.getName()}-framework-dependency-graph` + }); + const projectProcessor = new utils.ProjectProcessor({ libraryMetadata, + graph: frameworkGraph, workspace: options.workspace }); - const frameworkGraph = new ProjectGraph({ - rootProjectName: `fake-root-of-${rootProject.getName()}-framework-dependency-graph` - }); await Promise.all(referencedLibraries.map(async (libName) => { - await projectProcessor.addProjectToGraph(libName, frameworkGraph); + await projectProcessor.addProjectToGraph(libName); })); log.verbose("Joining framework graph into project graph..."); diff --git a/test/lib/graph/helpers/ui5Framework.js b/test/lib/graph/helpers/ui5Framework.js index 4d1ec0524..2374bbe07 100644 --- a/test/lib/graph/helpers/ui5Framework.js +++ b/test/lib/graph/helpers/ui5Framework.js @@ -9,6 +9,7 @@ import projectGraphBuilder from "../../../../lib/graph/projectGraphBuilder.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const applicationAPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.a"); +const libraryDPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.d"); const libraryEPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.e"); test.beforeEach(async (t) => { @@ -47,7 +48,7 @@ test.afterEach.always((t) => { esmock.purge(t.context.ui5Framework); }); -test.serial("ui5Framework translator should throw an error when framework version is not defined", async (t) => { +test.serial("enrichProjectGraph", async (t) => { const {sinon, ui5Framework, utils, Sapui5ResolverInstallStub} = t.context; const dependencyTree = { @@ -103,8 +104,14 @@ test.serial("ui5Framework translator should throw an error when framework versio ], "Sapui5Resolver#install should be called with expected args"); t.is(ProjectProcessorStub.callCount, 1, "ProjectProcessor#constructor should be called once"); - t.deepEqual(ProjectProcessorStub.getCall(0).args, [{libraryMetadata, workspace: undefined}], - "ProjectProcessor#constructor should be called with expected args"); + const projectProcessorConstructorArgs = ProjectProcessorStub.getCall(0).args[0]; + t.deepEqual(projectProcessorConstructorArgs.libraryMetadata, libraryMetadata, + "Correct libraryMetadata provided to ProjectProcessor"); + t.is(projectProcessorConstructorArgs.graph._rootProjectName, + "fake-root-of-application.a-framework-dependency-graph", + "Correct graph provided to ProjectProcessor"); + t.falsy(projectProcessorConstructorArgs.workspace, + "No workspace provided to ProjectProcessor"); t.is(addProjectToGraphStub.callCount, 3, "ProjectProcessor#getProject should be called 3 times"); t.deepEqual(addProjectToGraphStub.getCall(0).args[0], referencedLibraries[0], @@ -126,7 +133,7 @@ test.serial("ui5Framework translator should throw an error when framework versio ], "Traversed graph in correct order"); }); -test.serial("generateDependencyTree (with versionOverride)", async (t) => { +test.serial("enrichProjectGraph: With versionOverride", async (t) => { const { sinon, ui5Framework, utils, Sapui5ResolverStub, Sapui5ResolverResolveVersionStub, Sapui5ResolverInstallStub @@ -178,7 +185,7 @@ test.serial("generateDependencyTree (with versionOverride)", async (t) => { }], "Sapui5Resolver#constructor should be called with expected args"); }); -test.serial("generateDependencyTree should throw error when no framework version is provided", async (t) => { +test.serial("enrichProjectGraph should throw error when no framework version is provided", async (t) => { const {ui5Framework} = t.context; const dependencyTree = { id: "test-id", @@ -210,7 +217,7 @@ test.serial("generateDependencyTree should throw error when no framework version }, {message: "No framework version defined for root project application.a"}); }); -test.serial("generateDependencyTree should skip framework project without version", async (t) => { +test.serial("enrichProjectGraph should skip framework project without version", async (t) => { const {ui5Framework} = t.context; const dependencyTree = { id: "@sapui5/project", @@ -234,7 +241,7 @@ test.serial("generateDependencyTree should skip framework project without versio t.is(projectGraph.getAllProjects().length, 1, "Project graph should remain unchanged"); }); -test.serial("generateDependencyTree should skip framework project with version and framework config", async (t) => { +test.serial("enrichProjectGraph should skip framework project with version and framework config", async (t) => { const {ui5Framework} = t.context; const dependencyTree = { id: "@sapui5/project", @@ -266,7 +273,7 @@ test.serial("generateDependencyTree should skip framework project with version a t.is(projectGraph.getAllProjects().length, 1, "Project graph should remain unchanged"); }); -test.serial("generateDependencyTree should throw for framework project with dependency missing in graph", async (t) => { +test.serial("enrichProjectGraph should throw for framework project with dependency missing in graph", async (t) => { const {ui5Framework} = t.context; const dependencyTree = { id: "@sapui5/project", @@ -298,7 +305,41 @@ test.serial("generateDependencyTree should throw for framework project with depe "Threw with expected error message"); }); -test.serial("generateDependencyTree should ignore root project without framework configuration", async (t) => { +test.serial("enrichProjectGraph should throw for incorrect framework name", async (t) => { + const {ui5Framework, sinon} = t.context; + const dependencyTree = { + id: "project", + version: "1.2.3", + path: applicationAPath, + configuration: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + }, + framework: { + name: "SAPUI5", + version: "1.2.3", + libraries: [ + { + name: "lib1", + optional: true + } + ] + } + } + }; + + const provider = new DependencyTreeProvider({dependencyTree}); + const projectGraph = await projectGraphBuilder(provider); + + sinon.stub(projectGraph.getRoot(), "getFrameworkName").returns("Pony5"); + const err = await t.throwsAsync(ui5Framework.enrichProjectGraph(projectGraph)); + t.is(err.message, `Unknown framework.name "Pony5" for project application.a. Must be "OpenUI5" or "SAPUI5"`, + "Threw with expected error message"); +}); + +test.serial("enrichProjectGraph should ignore root project without framework configuration", async (t) => { const {ui5Framework} = t.context; const dependencyTree = { id: "@sapui5/project", @@ -528,4 +569,335 @@ test.serial("utils.getFrameworkLibrariesFromTree: Project with libraries and dep t.deepEqual(ui5Dependencies, ["lib1", "lib2", "lib6", "lib3", "lib5"]); }); -// TODO test: ProjectProcessor +test.serial("ProjectProcessor: Add project to graph", async (t) => { + const {sinon} = t.context; + const {ProjectProcessor} = t.context.utils; + const graphMock = { + getProject: sinon.stub().returns(), + addProject: sinon.stub() + }; + const projectProcessor = new ProjectProcessor({ + libraryMetadata: { + "library.e": { + id: "lib.e.id", + version: "1000.0.0", + path: libraryEPath, + dependencies: [], + optionalDependencies: [] + } + }, + graph: graphMock + }); + + await projectProcessor.addProjectToGraph("library.e"); + t.is(graphMock.getProject.callCount, 1, "graph#getProject got called once"); + t.is(graphMock.getProject.getCall(0).args[0], "library.e", "graph#getProject got called with the correct argument"); + t.is(graphMock.addProject.callCount, 1, "graph#addProject got called once"); + t.is(graphMock.addProject.getCall(0).args[0].getName(), "library.e", + "graph#addProject got with the correct project"); +}); + +test.serial("ProjectProcessor: Add same project twice", async (t) => { + const {sinon} = t.context; + const {ProjectProcessor} = t.context.utils; + const graphMock = { + getProject: sinon.stub().returns(), + addProject: sinon.stub() + }; + const projectProcessor = new ProjectProcessor({ + libraryMetadata: { + "library.e": { + id: "lib.e.id", + version: "1000.0.0", + path: libraryEPath, + dependencies: [], + optionalDependencies: [] + } + }, + graph: graphMock + }); + + await projectProcessor.addProjectToGraph("library.e"); + await projectProcessor.addProjectToGraph("library.e"); + t.is(graphMock.getProject.callCount, 1, "graph#getProject got called once"); + t.is(graphMock.getProject.getCall(0).args[0], "library.e", "graph#getProject got called with the correct argument"); + t.is(graphMock.addProject.callCount, 1, "graph#addProject got called once"); + t.is(graphMock.addProject.getCall(0).args[0].getName(), "library.e", + "graph#addProject got with the correct project"); +}); + +test.serial("ProjectProcessor: Project already in graph", async (t) => { + const {sinon} = t.context; + const {ProjectProcessor} = t.context.utils; + const graphMock = { + getProject: sinon.stub().returns("project"), + addProject: sinon.stub() + }; + const projectProcessor = new ProjectProcessor({ + libraryMetadata: { + "library.e": { + id: "lib.e.id", + version: "1000.0.0", + path: libraryEPath, + dependencies: [], + optionalDependencies: [] + } + }, + graph: graphMock + }); + + await projectProcessor.addProjectToGraph("library.e"); + t.is(graphMock.getProject.callCount, 1, "graph#getProject got called once"); + t.is(graphMock.getProject.getCall(0).args[0], "library.e", "graph#getProject got called with the correct argument"); + t.is(graphMock.addProject.callCount, 0, "graph#addProject never got called"); +}); + +test.serial("ProjectProcessor: Add project with dependencies to graph", async (t) => { + const {sinon} = t.context; + const {ProjectProcessor} = t.context.utils; + const graphMock = { + getProject: sinon.stub().returns(), + addProject: sinon.stub(), + declareDependency: sinon.stub() + }; + const projectProcessor = new ProjectProcessor({ + libraryMetadata: { + "library.e": { + id: "lib.e.id", + version: "1000.0.0", + path: libraryEPath, + dependencies: ["library.d"], + optionalDependencies: [] + }, + "library.d": { + id: "lib.d.id", + version: "120000.0.0", + path: libraryDPath, + dependencies: [], + optionalDependencies: [] + } + }, + graph: graphMock + }); + + await projectProcessor.addProjectToGraph("library.e"); + t.is(graphMock.getProject.callCount, 2, "graph#getProject got called twice"); + t.is(graphMock.getProject.getCall(0).args[0], "library.e", "graph#getProject got called with the correct argument"); + t.is(graphMock.getProject.getCall(1).args[0], "library.d", "graph#getProject got called with the correct argument"); + t.is(graphMock.addProject.callCount, 2, "graph#addProject got called twice"); + t.is(graphMock.addProject.getCall(0).args[0].getName(), "library.d", + "graph#addProject got with the correct project"); + t.is(graphMock.addProject.getCall(1).args[0].getName(), "library.e", + "graph#addProject got with the correct project"); + t.is(graphMock.declareDependency.callCount, 1, "graph#declareDependency got called once"); + t.deepEqual(graphMock.declareDependency.getCall(0).args, ["library.e", "library.d"], + "graph#declareDependency got with the correct arguments"); +}); + +test.serial("ProjectProcessor: Resolve project via workspace", async (t) => { + const {sinon} = t.context; + const {ProjectProcessor} = t.context.utils; + const graphMock = { + getProject: sinon.stub().returns(), + addProject: sinon.stub(), + declareDependency: sinon.stub() + }; + const libraryEProjectMock = { + getName: () => "library.e", + getFrameworkDependencies: sinon.stub().returns([{ + name: "library.d" + }]) + }; + const libraryDProjectMock = { + getName: () => "library.d", + getFrameworkDependencies: sinon.stub().returns([]) + }; + const moduleMock = { + getSpecifications: sinon.stub() + .onFirstCall().resolves({ + project: libraryDProjectMock + }).onSecondCall().resolves({ + project: libraryEProjectMock + }) + }; + const workspaceMock = { + getName: sinon.stub().returns("workspace name"), + getModuleByProjectName: sinon.stub().resolves(moduleMock), + }; + const projectProcessor = new ProjectProcessor({ + libraryMetadata: { + "library.e": { + id: "lib.e.id", + version: "1000.0.0", + path: libraryEPath, + dependencies: ["library.d"], + optionalDependencies: [] + }, + "library.d": { + id: "lib.d.id", + version: "120000.0.0", + path: libraryDPath, + dependencies: [], + optionalDependencies: [] + } + }, + graph: graphMock, + workspace: workspaceMock + }); + + await projectProcessor.addProjectToGraph("library.e"); + t.is(graphMock.getProject.callCount, 2, "graph#getProject got called twice"); + t.is(graphMock.getProject.getCall(0).args[0], "library.e", "graph#getProject got called with the correct argument"); + t.is(graphMock.getProject.getCall(1).args[0], "library.d", "graph#getProject got called with the correct argument"); + t.is(graphMock.addProject.callCount, 2, "graph#addProject got called once"); + t.is(graphMock.addProject.getCall(0).args[0].getName(), "library.d", + "graph#addProject got with the correct project"); + t.is(graphMock.addProject.getCall(1).args[0].getName(), "library.e", + "graph#addProject got with the correct project"); + t.is(graphMock.declareDependency.callCount, 1, "graph#declareDependency got called once"); + t.deepEqual(graphMock.declareDependency.getCall(0).args, ["library.e", "library.d"], + "graph#declareDependency got with the correct arguments"); +}); + +test.serial("ProjectProcessor: Resolve project via workspace with additional dependency", async (t) => { + const {sinon} = t.context; + const {ProjectProcessor} = t.context.utils; + const graphMock = { + getProject: sinon.stub().returns(), + addProject: sinon.stub(), + declareDependency: sinon.stub() + }; + const libraryEProjectMock = { + getName: () => "library.e", + getFrameworkDependencies: sinon.stub().returns([{ + name: "library.d" + }]) + }; + const libraryDProjectMock = { + getName: () => "library.d", + getFrameworkDependencies: sinon.stub().returns([]) + }; + const moduleMock = { + getSpecifications: sinon.stub() + .onFirstCall().resolves({ + project: libraryEProjectMock + }).onSecondCall().resolves({ + project: libraryDProjectMock + }) + }; + const workspaceMock = { + getName: sinon.stub().returns("workspace name"), + getModuleByProjectName: sinon.stub().resolves(moduleMock), + }; + const projectProcessor = new ProjectProcessor({ + libraryMetadata: { + "library.e": { + id: "lib.e.id", + version: "1000.0.0", + path: libraryEPath, + dependencies: [], // Dependency to library.d is only declared in workspace-resolved library.e + optionalDependencies: [] + }, + "library.d": { + id: "lib.d.id", + version: "120000.0.0", + path: libraryDPath, + dependencies: [], + optionalDependencies: [] + } + }, + graph: graphMock, + workspace: workspaceMock + }); + + await projectProcessor.addProjectToGraph("library.e"); + t.is(graphMock.getProject.callCount, 2, "graph#getProject got called twice"); + t.is(graphMock.getProject.getCall(0).args[0], "library.e", "graph#getProject got called with the correct argument"); + t.is(graphMock.getProject.getCall(1).args[0], "library.d", "graph#getProject got called with the correct argument"); + t.is(graphMock.addProject.callCount, 2, "graph#addProject got called once"); + t.is(graphMock.addProject.getCall(0).args[0].getName(), "library.e", + "graph#addProject got with the correct project"); + t.is(graphMock.addProject.getCall(1).args[0].getName(), "library.d", + "graph#addProject got with the correct project"); + t.is(graphMock.declareDependency.callCount, 1, "graph#declareDependency got called once"); + t.deepEqual(graphMock.declareDependency.getCall(0).args, ["library.e", "library.d"], + "graph#declareDependency got with the correct arguments"); +}); + +test.serial("ProjectProcessor: Resolve project via workspace with additional, unknown dependency", async (t) => { + const {sinon} = t.context; + const {ProjectProcessor} = t.context.utils; + const graphMock = { + getProject: sinon.stub().returns(), + addProject: sinon.stub(), + declareDependency: sinon.stub() + }; + const libraryEProjectMock = { + getName: () => "library.e", + getFrameworkDependencies: sinon.stub().returns([{ + name: "library.xyz" + }]) + }; + const libraryDProjectMock = { + getName: () => "library.d", + getFrameworkDependencies: sinon.stub().returns([]) + }; + const moduleMock = { + getSpecifications: sinon.stub() + .onFirstCall().resolves({ + project: libraryEProjectMock + }).onSecondCall().resolves({ + project: libraryDProjectMock + }) + }; + const workspaceMock = { + getName: sinon.stub().returns("workspace name"), + getModuleByProjectName: sinon.stub().resolves(moduleMock), + }; + const projectProcessor = new ProjectProcessor({ + libraryMetadata: { + "library.e": { + id: "lib.e.id", + version: "1000.0.0", + path: libraryEPath, + dependencies: ["library.d"], + optionalDependencies: [] + }, + "library.d": { + id: "lib.d.id", + version: "120000.0.0", + path: libraryDPath, + dependencies: [], + optionalDependencies: [] + } + }, + graph: graphMock, + workspace: workspaceMock + }); + + await t.throwsAsync(projectProcessor.addProjectToGraph("library.e"), { + message: + "Unable to find dependency library.xyz, required by project library.e " + + "(resolved via workspace name workspace) " + + "in current set of libraries. Try adding it temporarily to the root project's dependencies" + }, "Threw with expected error message"); +}); + +test.serial("ProjectProcessor: Project missing in metadata", async (t) => { + const {sinon} = t.context; + const {ProjectProcessor} = t.context.utils; + const graphMock = { + getProject: sinon.stub().returns(), + addProject: sinon.stub() + }; + const projectProcessor = new ProjectProcessor({ + libraryMetadata: { + "lib.x": {} + }, + graph: graphMock + }); + + await t.throwsAsync(projectProcessor.addProjectToGraph("lib.a"), { + message: "Failed to find library lib.a in dist packages metadata.json" + }, "Threw with expected error message"); +});