diff --git a/src/common/plugin/ExecutionHelpers.js b/src/common/plugin/ExecutionHelpers.js index ebef3e7f1..84d93f5e7 100644 --- a/src/common/plugin/ExecutionHelpers.js +++ b/src/common/plugin/ExecutionHelpers.js @@ -21,7 +21,7 @@ define([ parent: dst }); - const names = this.core.getValidAttributeNames(node); + const names = this.core.getAttributeNames(node); const values = names.map(name => this.core.getAttribute(node, name)); names.forEach((name, i) => this.core.setAttribute(snapshot, name, values[i])); @@ -52,14 +52,33 @@ define([ .sort(metaTypeComparator); const [dstInput, dstOutput] = (await this.core.loadChildren(snapshot)) .sort(metaTypeComparator); - const [srcInputs, srcOutputs] = await Promise.all(srcCntrs.map(ctr => this.core.loadChildren(ctr))); - const copies = srcInputs.map(n => this.core.copyNode(n, dstInput)); + + const [srcInputs, srcOutputs] = (await Promise.all(srcCntrs.map(ctr => this.core.loadChildren(ctr)))); + + const copies = srcInputs.map(n => { + const copy = this.core.copyNode(n, dstInput); + const inheritancePath = this.getInheritedAncestors(n); + const dataMetaNode = inheritancePath.reverse() + .find(node => this.core.getAttribute(node, 'name') === 'Data'); + this.core.setPointer(copy, 'base', dataMetaNode); + this.core.setAttribute(copy, 'name', this.core.getAttribute(n, 'name')); + return copy; + }); copies.push(...srcOutputs.map(n => this.shallowCopy(n, dstOutput))); const oldNewPairs = _.zip(srcInputs.concat(srcOutputs), copies); oldNewPairs.push([node, snapshot]); return {snapshot, pairs: oldNewPairs}; } + getInheritedAncestors (node) { + const path = []; + while (node) { + path.push(node); + node = this.core.getBase(node); + } + return path; + } + shallowCopy (original, dst) { const attrNames = this.core.getAttributeNames(original); const copy = this.core.createNode({ @@ -73,6 +92,51 @@ define([ return copy; } + + async setDataContents(node, dataNode) { + const dataType = this.core.getAttribute(dataNode, 'type'); + this.core.setAttribute(node, 'type', dataType); + + const hash = this.core.getAttribute(dataNode, 'data'); + this.core.setAttribute(node, 'data', hash); + + const provOutput = this.core.getAttribute(dataNode, 'provOutput'); + if (provOutput) { + this.core.setAttribute(node, 'provOutput', provOutput); + } + + await this.clearProvenance(node); + + const provDataId = this.core.getPointerPath(dataNode, 'provenance'); + if (provDataId) { + const implOp = await this.core.loadByPath(this.rootNode, provDataId); + const provCopy = this.core.copyNode(implOp, node); + this.core.setPointer(node, 'provenance', provCopy); + } + } + + async clearProvenance(dataNode) { + const provDataId = this.core.getPointerPath(dataNode, 'provenance'); + if (provDataId) { + const provData = await this.core.loadByPath(this.rootNode, provDataId); + const {node} = this.getImplicitOperation(provData); + this.core.deleteNode(node); + } + } + + getImplicitOperation(dataNode) { + const metanodes = Object.values(this.core.getAllMetaNodes(dataNode)); + const implicitOp = metanodes + .find(node => this.core.getAttribute(node, 'name') === 'ImplicitOperation'); + let node = dataNode; + const path = []; + while (node && !this.core.isTypeOf(node, implicitOp)) { + path.push(this.core.getAttribute(node, 'name')); + node = this.core.getParent(node); + } + + return {node, path}; + } } return ExecutionHelpers; diff --git a/src/common/plugin/LocalExecutor.js b/src/common/plugin/LocalExecutor.js index 375b5e502..ba2deb514 100644 --- a/src/common/plugin/LocalExecutor.js +++ b/src/common/plugin/LocalExecutor.js @@ -57,12 +57,18 @@ define([ this.core.getAttribute(node, 'data') ]); const inputs = await this.getInputs(node); - const ids = inputs.map(i => this.core.getPath(i[2])); - const incomingData = Object.values(this.nodes) + const inputIds = inputs.map(i => this.core.getPath(i[2])); + const execution = this.core.getParent( + this.core.getParent(node) + ); + const incomingDataIds = (await this.core.loadChildren(execution)) .filter(node => this.isMetaTypeOf(node, this.META.Transporter)) - .filter(node => ids.includes(this.core.getPointerPath(node, 'dst'))) - .map(node => this.core.getPointerPath(node, 'src')) - .map(id => this.nodes[id]); + .filter(node => inputIds.includes(this.core.getPointerPath(node, 'dst'))) + .map(node => this.core.getPointerPath(node, 'src')); + + const incomingData = await Promise.all( + incomingDataIds.map(id => this.core.loadByPath(this.rootNode, id)) + ); // Remove nodes that already exist const dataNodes = incomingData.filter(dataNode => { diff --git a/src/common/viz/Buttons.js b/src/common/viz/Buttons.js index e7f1dce9c..caaea13ca 100644 --- a/src/common/viz/Buttons.js +++ b/src/common/viz/Buttons.js @@ -59,6 +59,24 @@ define([ return n && n.getBaseId(); }; + var GoToOperationDefinition = function(params) { + EasyDAGButtons.ButtonBase.call(this, params); + }; + + GoToOperationDefinition.prototype = Object.create(GoToBase.prototype); + GoToOperationDefinition.prototype._onClick = function(item) { + var node = client.getNode(item.id), + baseId = node.getBaseId(); + + const base = client.getNode(baseId); + const isSnapshot = base.getAttribute('name') === 'Operation'; + if (isSnapshot) { + WebGMEGlobal.State.registerActiveObject(item.id); + } else { + GoToBase.prototype._onClick.call(this, item); + } + }; + var CloneAndEdit = function(params) { GoToBase.call(this, params); }; @@ -131,7 +149,8 @@ define([ DeleteOne: EasyDAGButtons.DeleteOne, GoToBase: GoToBase, CloneAndEdit: CloneAndEdit, - Insert: Insert + Insert: Insert, + GoToOperationDefinition, }; }); diff --git a/src/plugins/ExecuteJob/ExecuteJob.js b/src/plugins/ExecuteJob/ExecuteJob.js index 53f6b272f..1bfcba666 100644 --- a/src/plugins/ExecuteJob/ExecuteJob.js +++ b/src/plugins/ExecuteJob/ExecuteJob.js @@ -730,9 +730,15 @@ define([ base: this.META.ExecutedJob, parent: dataNode }); - const {snapshot} = await helpers.snapshotOperation(opNode, executedJob, this.META.Operation); + const {snapshot} = await helpers.snapshotOperation( + opNode, + executedJob, + this.META.Operation + ); this.core.setPointer(executedJob, 'operation', snapshot); this.core.setPointer(dataNode, 'provenance', executedJob); + const name = this.core.getAttribute(dataNode, 'name'); + this.core.setAttribute(dataNode, 'provOutput', name); }; //////////////////////////// Special Operations //////////////////////////// diff --git a/src/plugins/ExecuteJob/metadata/Metadata.js b/src/plugins/ExecuteJob/metadata/Metadata.js index ae6cc4b88..5a46de90f 100644 --- a/src/plugins/ExecuteJob/metadata/Metadata.js +++ b/src/plugins/ExecuteJob/metadata/Metadata.js @@ -16,7 +16,7 @@ define([ async loadChildren() { const provPath = this.core.getPointerPath(this.node, 'provenance'); const children = (await this.core.loadChildren(this.node)) - .filter(node => this.core.getPath(node) !== provPath); + .filter(node => !provPath.includes(this.core.getPath(node))); return children; } diff --git a/src/plugins/ExecutePipeline/ExecutePipeline.js b/src/plugins/ExecutePipeline/ExecutePipeline.js index 4adc272c0..6019263af 100644 --- a/src/plugins/ExecutePipeline/ExecutePipeline.js +++ b/src/plugins/ExecutePipeline/ExecutePipeline.js @@ -4,6 +4,7 @@ define([ 'plugin/CreateExecution/CreateExecution/CreateExecution', 'plugin/ExecuteJob/ExecuteJob/ExecuteJob', + 'deepforge/plugin/ExecutionHelpers', 'common/storage/constants', 'common/core/constants', 'deepforge/Constants', @@ -13,6 +14,7 @@ define([ ], function ( CreateExecution, ExecuteJob, + ExecutionHelpers, STORAGE_CONSTANTS, GME_CONSTANTS, CONSTANTS, @@ -554,26 +556,12 @@ define([ const portPairs = resultPorts .map((id, i) => [this.nodes[id], this.nodes[nextPortIds[i]]]); - const forwardData = portPairs.map(async pair => { // [ resultPort, nextPort ] - const [result, next] = pair; - - let dataType = this.core.getAttribute(result, 'type'); - this.core.setAttribute(next, 'type', dataType); - - let hash = this.core.getAttribute(result, 'data'); - this.core.setAttribute(next, 'data', hash); - - const provInfoId = this.core.getPointerPath(result, 'provenance', true); - if (provInfoId) { - const provNode = await this.core.loadByPath(result, provInfoId); - const provCopy = this.core.copyNode(provNode, next); - this.core.setPointer(next, 'provenance', provCopy); - } - - this.logger.info(`forwarding data (${dataType}) from ${this.core.getPath(result)} ` + - `to ${this.core.getPath(next)}`); + const helpers = new ExecutionHelpers(this.core, this.rootNode); + const forwardData = portPairs.map(pair => { + const [resultPort, nextPort] = pair; + return helpers.setDataContents(nextPort, resultPort); }); - await forwardData; + await Promise.all(forwardData); // For all the nextPortIds, decrement the corresponding operation's incoming counts const counts = nextPortIds.map(id => this.getSiblingIdContaining(id)) diff --git a/src/plugins/ReifyArtifactProv/ReifyArtifactProv.js b/src/plugins/ReifyArtifactProv/ReifyArtifactProv.js new file mode 100644 index 000000000..641ecacd6 --- /dev/null +++ b/src/plugins/ReifyArtifactProv/ReifyArtifactProv.js @@ -0,0 +1,120 @@ +/*globals define*/ +/*eslint-env node, browser*/ + +define([ + 'plugin/PluginBase', + 'deepforge/plugin/ExecutionHelpers', + 'text!./metadata.json', +], function ( + PluginBase, + ExecutionHelpers, + pluginMetadata, +) { + 'use strict'; + + pluginMetadata = JSON.parse(pluginMetadata); + + class ReifyArtifactProv extends PluginBase { + constructor() { + super(); + this.pluginMetadata = pluginMetadata; + } + + async main(callback) { + const {artifactId} = this.getCurrentConfig(); + const artifact = await this.core.loadByPath(this.rootNode, artifactId); + if (!artifact) { + throw new Error(`Could not load artifact: ${artifactId}`); + } + + const name = this.core.getAttribute(artifact, 'name'); + const pipeline = this.core.createNode({ + base: this.META.Pipeline, + parent: this.activeNode, + }); + this.core.setAttribute(pipeline, 'name', `Provenance of ${name}`); + + const outputOp = await this.createOutputOperation(pipeline, artifact); + const [input] = await this.getOperationInputs(outputOp); + await this.addProvenanceOperation(pipeline, input); + + await this.save(`Created provenance pipeline of ${name}`); + this.result.setSuccess(true); + this.createMessage(pipeline, 'New Provenance Pipeline'); + callback(null, this.result); + } + + async addProvenanceOperation(pipeline, input) { + const operation = await this.getProvAsOperation(input); + const newOperation = this.core.copyNode(operation, pipeline); + const outputData = await this.getOutputData(newOperation, input); + if (!outputData) { + throw new Error(`Could not find output in ${this.core.getPath(operation)} referencing data ${this.core.getAttribute(input, 'data')}`); + } + this.connect(pipeline, outputData, input); + + const inputs = await this.getOperationInputs(newOperation); + await Promise.all( + inputs.map(input => this.addProvenanceOperation(pipeline, input)) + ); + // TODO: should I create a new meta type for each? + } + + async createOutputOperation(pipeline, data) { + const output = this.core.createNode({ + parent: pipeline, + base: this.META.Output, + }); + const [input] = await this.getOperationInputs(output); + const helpers = new ExecutionHelpers(this.core, this.rootNode); + await helpers.setDataContents(input, data); + const name = this.core.getAttribute(data, 'name'); + this.core.setAttribute(output, 'saveName', name); + return output; + } + + async getOperationInputs(operation) { + const inputs = (await this.core.loadChildren(operation)) + .find(node => this.core.isTypeOf(node, this.META.Inputs)); + return this.core.loadChildren(inputs); + } + + async getOperationOutputs(operation) { + const outputs = (await this.core.loadChildren(operation)) + .find(node => this.core.isTypeOf(node, this.META.Outputs)); + return this.core.loadChildren(outputs); + } + + async getProvAsOperation(artifact) { + const implOpId = this.core.getPointerPath(artifact, 'provenance'); + if (!implOpId) return; + const implicitOp = await this.core.loadByPath(this.rootNode, implOpId); + const operationId = this.core.getPointerPath(implicitOp, 'operation'); + if (!operationId) { + const name = this.core.getAttribute(implicitOp, 'name'); + throw new Error(`No operation found for ${implOpId} (${name})`); + } + return await this.core.loadByPath(this.rootNode, operationId); + } + + async getOutputData(operation, artifact) { + const outputs = await this.getOperationOutputs(operation); + const provOutput = this.core.getAttribute(artifact, 'provOutput'); + return outputs.find( + data => this.core.getAttribute(data, 'name') === provOutput + ); + } + + async connect(parent, src, dst) { + const base = this.META.Transporter; + const connection = this.core.createNode({parent, base}); + this.core.setPointer(connection, 'src', src); + this.core.setPointer(connection, 'dst', dst); + return connection; + } + } + + ReifyArtifactProv.metadata = pluginMetadata; + + return ReifyArtifactProv; +}); diff --git a/src/plugins/ReifyArtifactProv/metadata.json b/src/plugins/ReifyArtifactProv/metadata.json new file mode 100644 index 000000000..0a383cc3b --- /dev/null +++ b/src/plugins/ReifyArtifactProv/metadata.json @@ -0,0 +1,24 @@ +{ + "id": "ReifyArtifactProv", + "name": "Reify Artifact Provenance", + "version": "0.1.0", + "description": "", + "icon": { + "class": "glyphicon glyphicon-cog", + "src": "" + }, + "disableServerSideExecution": false, + "disableBrowserSideExecution": false, + "dependencies": [], + "writeAccessRequired": false, + "configStructure": [ + { + "name": "artifactId", + "displayName": "Artifact", + "description": "Create a pipeline of the provenance of the given artifact", + "value": "", + "valueType": "string", + "readOnly": false + } + ] +} diff --git a/src/seeds/devProject/devProject.webgmex b/src/seeds/devProject/devProject.webgmex index 803b0103e..16e55e728 100644 Binary files a/src/seeds/devProject/devProject.webgmex and b/src/seeds/devProject/devProject.webgmex differ diff --git a/src/seeds/pipeline/pipeline.webgmex b/src/seeds/pipeline/pipeline.webgmex index ac1339047..e2202b657 100644 Binary files a/src/seeds/pipeline/pipeline.webgmex and b/src/seeds/pipeline/pipeline.webgmex differ diff --git a/src/seeds/pipeline/releases.jsonl b/src/seeds/pipeline/releases.jsonl index ca3d11466..627da9486 100644 --- a/src/seeds/pipeline/releases.jsonl +++ b/src/seeds/pipeline/releases.jsonl @@ -3,3 +3,5 @@ {"version":"0.21.1","changelog":"Update Inheritance of Subgraph, Line, Images, ScatterPoints etc.. nodes"} {"version":"0.22.0","changelog":"Incorporate PlotlyJSON into Graph meta node"} {"version":"0.23.0","changelog":"Add TrainKeras implicit operation"} + +{"version":"0.24.0","changelog":"Add provOutput to WithProvenance mixin (required for pipeline reconstruction)"} \ No newline at end of file diff --git a/src/visualizers/panels/ArtifactIndex/ArtifactIndexControl.js b/src/visualizers/panels/ArtifactIndex/ArtifactIndexControl.js index 661c82c74..fd3fcd979 100644 --- a/src/visualizers/panels/ArtifactIndex/ArtifactIndexControl.js +++ b/src/visualizers/panels/ArtifactIndex/ArtifactIndexControl.js @@ -3,12 +3,16 @@ define([ 'deepforge/storage/index', + 'deepforge/globals', 'blob/BlobClient', - 'js/Constants' + 'js/Constants', + 'q', ], function ( Storage, + DeepForge, BlobClient, - CONSTANTS + CONSTANTS, + Q, ) { 'use strict'; @@ -70,6 +74,42 @@ define([ this._client.setAttribute(id, attr, newValue); this._client.completeTransaction(); }; + + this._widget.$el.on( + 'ReifyProvenance', + (event, artifactId) => this.reifyProvenance(artifactId), + ); + }; + + ArtifactIndexControl.prototype.reifyProvenance = async function (artifactId) { + const pluginId = 'ReifyArtifactProv'; + + const pluginContext = this._client.getCurrentPluginContext(pluginId); + pluginContext.managerConfig.activeNode = await DeepForge.places.MyPipelines(); + pluginContext.managerConfig.namespace = 'pipeline'; + pluginContext.pluginConfig = {artifactId}; + + const territory = {}; + territory[pluginContext.managerConfig.activeNode] = {children: 0}; + + const result = await this.doInTerritory( + territory, + () => Q.ninvoke(this._client, 'runBrowserPlugin', pluginId, pluginContext), + ); + + const [{activeNode: pipeline}] = result.messages; + WebGMEGlobal.State.registerActiveObject(pipeline.id); + }; + + ArtifactIndexControl.prototype.doInTerritory = async function (territory, fn) { + const deferred = Q.defer(); + const ui = this._client.addUI(this, async () => { + const result = await fn(); + this._client.removeUI(ui); + deferred.resolve(result); + }); + this._client.updateTerritory(ui, territory); + return deferred.promise; }; /* * * * * * * * Visualizer content update callbacks * * * * * * * */ @@ -121,6 +161,7 @@ define([ createdAt: node.getAttribute('createdAt'), parentId: node.getParentId(), backendName: backendName, + hasProvenance: node.getPointer('provenance').to, dataInfo, size, }; diff --git a/src/visualizers/panels/TrainKeras/TrainKerasControl.js b/src/visualizers/panels/TrainKeras/TrainKerasControl.js index 590e53ec6..687a1ce06 100644 --- a/src/visualizers/panels/TrainKeras/TrainKerasControl.js +++ b/src/visualizers/panels/TrainKeras/TrainKerasControl.js @@ -199,13 +199,14 @@ define([ ); core.setPointer(artifact, 'provenance', trainState); - const operation = await this.createOperation( + const {operation, outputNames} = await this.createOperation( core, rootNode, modelInfo, trainState ); core.setPointer(trainState, 'operation', operation); + core.setPointer(artifact, 'provOutput', outputNames[0]); const importer = new Importer(core, rootNode); const {architecture} = modelInfo; @@ -310,7 +311,10 @@ define([ core.setAttribute(outputNode, 'name', output.name); }); - return node; + return { + operation: node, + outputNames: operation.getOutputs().map(output => output.name), + }; } async getTerritory(/*nodeId*/) { diff --git a/src/visualizers/widgets/ArtifactIndex/ArtifactIndexWidget.js b/src/visualizers/widgets/ArtifactIndex/ArtifactIndexWidget.js index 1bed73dd7..68e48eaa2 100644 --- a/src/visualizers/widgets/ArtifactIndex/ArtifactIndexWidget.js +++ b/src/visualizers/widgets/ArtifactIndex/ArtifactIndexWidget.js @@ -45,6 +45,9 @@ define([ this.$el.append(this.$content); this.$list = this.$content.find('.list-content'); this.artifactModal = new ArtifactModal(); + this.artifactModal.$el.on( + 'ReifyProvenance', (event, nodeId) => this.$el.trigger('ReifyProvenance', nodeId) + ); }; ArtifactIndexWidget.prototype.onWidgetContainerResize = nop; diff --git a/src/visualizers/widgets/ArtifactIndex/ArtifactModal.html b/src/visualizers/widgets/ArtifactIndex/ArtifactModal.html index 6347821ae..94011e424 100644 --- a/src/visualizers/widgets/ArtifactIndex/ArtifactModal.html +++ b/src/visualizers/widgets/ArtifactIndex/ArtifactModal.html @@ -25,7 +25,8 @@ diff --git a/src/visualizers/widgets/ArtifactIndex/ArtifactModal.js b/src/visualizers/widgets/ArtifactIndex/ArtifactModal.js index 761f2eb50..53a33ef99 100644 --- a/src/visualizers/widgets/ArtifactIndex/ArtifactModal.js +++ b/src/visualizers/widgets/ArtifactIndex/ArtifactModal.js @@ -9,12 +9,14 @@ define([ 'use strict'; const ModalControl = function() { - this.$modal = $(MODAL_HTML); - this.$modalTitle = this.$modal.find('.artifact-name'); - this.$createdAt = this.$modal.find('.created-at'); - this.$size = this.$modal.find('.size'); - this.$backend = this.$modal.find('.backend'); - this.$dataInfo = this.$modal.find('.artifact-data-info'); + this.$el = $(MODAL_HTML); + this.$modalTitle = this.$el.find('.artifact-name'); + this.$createdAt = this.$el.find('.created-at'); + this.$size = this.$el.find('.size'); + this.$backend = this.$el.find('.backend'); + this.$dataInfo = this.$el.find('.artifact-data-info'); + this.$provBtn = this.$el.find('.reify-prov'); + this.$provBtn.on('click', () => this.onReifyClicked()); }; ModalControl.prototype.showModal = function (node) { @@ -24,7 +26,20 @@ define([ this.$backend.text(node.backendName || 'unknown'); this.$createdAt.text(createdAt); this.$dataInfo.text(`${JSON.stringify(node.dataInfo, null,2)}`); - this.$modal.modal({show: true}); + this.node = node; + if (this.node.hasProvenance) { + this.$provBtn.removeClass('disabled'); + } else { + this.$provBtn.addClass('disabled'); + } + this.$el.modal('show'); + }; + + ModalControl.prototype.onReifyClicked = function () { + if (this.node.hasProvenance) { + this.$el.trigger('ReifyProvenance', this.node.id); + this.$el.modal('hide'); + } }; return ModalControl; diff --git a/src/visualizers/widgets/PipelineEditor/SelectionManager.js b/src/visualizers/widgets/PipelineEditor/SelectionManager.js index 4d0974158..82d7f6ae9 100644 --- a/src/visualizers/widgets/PipelineEditor/SelectionManager.js +++ b/src/visualizers/widgets/PipelineEditor/SelectionManager.js @@ -31,7 +31,7 @@ define([ if (!this.selectedItem.isConnection) { // If the operation has a user-defined base type, // show a button for jumping to the base def - new Buttons.GoToBase({ + new Buttons.GoToOperationDefinition({ $pEl: this.$selection, context: this._widget, transition: transition, diff --git a/test/unit/common/plugin/ExecutionHelpers.spec.js b/test/unit/common/plugin/ExecutionHelpers.spec.js index 97ba1a0a7..81a40fdf8 100644 --- a/test/unit/common/plugin/ExecutionHelpers.spec.js +++ b/test/unit/common/plugin/ExecutionHelpers.spec.js @@ -3,6 +3,7 @@ describe('ExecutionHelpers', function() { const ExecutionHelpers = testFixture.requirejs('deepforge/plugin/ExecutionHelpers'); const gmeConfig = testFixture.getGmeConfig(); const logger = testFixture.logger.fork('ExecutionHelpers'); + const assert = require('assert'); let project, gmeAuth, storage, @@ -67,6 +68,15 @@ describe('ExecutionHelpers', function() { await helpers.snapshotOperation(helloWorldNode, activeNode); }); + + it('should snapshot attributes', async () => { + const {core, rootNode} = helpers; + const helloWorldNode = await core.loadByPath(rootNode, '/f/h/d'); + core.setAttribute(helloWorldNode, 'attr', 'value'); + + const {snapshot} = await helpers.snapshotOperation(helloWorldNode, activeNode); + assert(core.getAttributeNames(snapshot).includes('attr')); + }); }); }); diff --git a/test/unit/plugins/ReifyArtifactProv/ReifyArtifactProv.spec.js b/test/unit/plugins/ReifyArtifactProv/ReifyArtifactProv.spec.js new file mode 100644 index 000000000..ddc12e89c --- /dev/null +++ b/test/unit/plugins/ReifyArtifactProv/ReifyArtifactProv.spec.js @@ -0,0 +1,98 @@ +/*eslint-env node, mocha*/ + +describe('ReifyArtifactProv', function () { + const testFixture = require('../../../globals'); + const {promisify} = require('util'); + const gmeConfig = testFixture.getGmeConfig(); + const assert = require('assert'); + const logger = testFixture.logger.fork('ReifyArtifactProv'); + const PluginCliManager = testFixture.WebGME.PluginCliManager; + const manager = new PluginCliManager(null, logger, gmeConfig); + const projectName = 'testProject'; + const pluginName = 'ReifyArtifactProv'; + const PIPELINES = '/f'; + manager.executePlugin = promisify(manager.executePlugin); + manager.runPluginMain = promisify(manager.runPluginMain); + + let gmeAuth, + storage, + context, + pluginConfig; + + before(async function () { + gmeAuth = await testFixture.clearDBAndGetGMEAuth(gmeConfig, projectName); + storage = testFixture.getMemoryStorage(logger, gmeConfig, gmeAuth); + await storage.openDatabase(); + const importParam = { + projectSeed: testFixture.path.join(testFixture.DF_SEED_DIR, 'devProject', 'devProject.webgmex'), + projectName: projectName, + branchName: 'master', + logger: logger, + gmeConfig: gmeConfig + }; + + const importResult = await testFixture.importProject(storage, importParam); + const {project, commitHash} = importResult; + await project.createBranch('test', commitHash); + pluginConfig = { + artifactId: '/G/Z' + }; + context = { + project: project, + commitHash: commitHash, + branchName: 'test', + activeNode: PIPELINES, + namespace: 'pipeline', + }; + + }); + + after(async function () { + await storage.closeDatabase(); + await gmeAuth.unload(); + }); + + describe('project edits', function() { + it('should create new pipeline', async function () { + const plugin = await manager.initializePlugin(pluginName); + await manager.configurePlugin(plugin, pluginConfig, context); + const {core, rootNode} = plugin; + const pipelineDir = await core.loadByPath(rootNode, PIPELINES); + const initialPipelineCount = core.getChildrenPaths(pipelineDir).length; + + const result = await manager.runPluginMain(plugin); + const pipelineCount = core.getChildrenPaths(pipelineDir).length; + assert(result.success); + assert.equal(pipelineCount, initialPipelineCount + 1); + }); + + it('should preserve operation attributes', async function () { + const plugin = await manager.initializePlugin(pluginName); + await manager.configurePlugin(plugin, pluginConfig, context); + const {core, rootNode} = plugin; + const result = await manager.runPluginMain(plugin); + + const [{activeNode: newNode}] = result.messages; + const pipeline = await core.loadByPath(rootNode, newNode.id); + const numberOp = (await core.loadChildren(pipeline)) + .find(node => core.getAttribute(node, 'name') === 'Number'); + assert( + core.getAttributeNames(numberOp).includes('number'), + 'Operation is missing "number" attribute' + ); + }); + }); + + describe('messages', function() { + it('should create message for new node', async function () { + const result = await manager.executePlugin(pluginName, pluginConfig, context); + assert(result.success); + assert(result.messages.length, 'No messages created'); + const [{activeNode: pipeline}] = result.messages; + assert( + pipeline.id.startsWith(PIPELINES), + 'Pipeline is not in pipelines directory' + ); + }); + }); +});