Skip to content

Commit

Permalink
Refactor package path handling to support testing
Browse files Browse the repository at this point in the history
  • Loading branch information
Steve-Mcl committed Nov 14, 2023
1 parent 86d1ada commit 4d2f7d6
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 8 deletions.
12 changes: 6 additions & 6 deletions lib/launcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const { existsSync } = require('fs')
const fs = require('fs/promises')
const path = require('path')
const { info, debug, warn, NRlog } = require('./logging/log')
const utils = require('./utils')

const MIN_RESTART_TIME = 10000 // 10 seconds
const MAX_RESTART_COUNT = 5
Expand Down Expand Up @@ -194,13 +195,12 @@ class Launcher {
path.join(__dirname, 'plugins', 'node_modules', '@flowforge', 'flowforge-library-plugin').replace(/\\/g, '/')
]
// Check to see if we're in the dev-env - if so, we need to set a different
// path to the `@flowforge/nr-project-nodes` for development purposes
const devEnvPath = path.join(__dirname, '..', '..', '..', 'packages', 'nr-project-nodes').replace(/\\/g, '/')
if (existsSync(devEnvPath)) {
settings.nodesDir.push(devEnvPath)
} else {
settings.nodesDir.push(path.join(__dirname, '..', 'node_modules', '@flowfuse', 'nr-project-nodes').replace(/\\/g, '/'))
// path to the project-nodes for development purposes
let projectNodesPackage = '@flowfuse/nr-project-nodes' // runtime default
if (utils.isDevEnv()) {
projectNodesPackage = 'nr-project-nodes'
}
settings.nodesDir.push(utils.getPackagePath(projectNodesPackage))
const sharedLibraryConfig = {
id: 'flowfuse-team-library',
type: 'flowfuse-team-library',
Expand Down
58 changes: 57 additions & 1 deletion lib/utils.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
const path = require('path')
const existsSync = require('fs').existsSync

module.exports = {
compareNodeRedData,
compareObjects,
isObject,
hasProperty
hasProperty,
isDevEnv,
getPackagePath
}

/**
Expand Down Expand Up @@ -77,3 +82,54 @@ function isObject (object) {
function hasProperty (object, property) {
return !!(object && Object.prototype.hasOwnProperty.call(object, property))
}

const devPackages = path.join(__dirname, '..', '..', '..', 'packages')
const runtimePackages = path.join(__dirname, '..', 'node_modules')

/**
* Test if the runtime is running in a development environment.
* Development environment is defined as:
* * `NODE_ENV` is set to 'development'
* - OR
* * 'packages' directory exists AND another "known" package exists (/packages/nr-project-nodes)
* @returns {boolean} true if the runtime is running in a development environment
*/
function isDevEnv () {
// if NODE_ENV is set, use that
if (process.env.NODE_ENV) {
return process.env.NODE_ENV === 'development'
}
const devEnvTestPath = path.join(devPackages, 'nr-project-nodes').replace(/\\/g, '/')
if (existsSync(devEnvTestPath)) {
return true
}
return false
}

/**
* Get the full path to a package.
*
* When running in a development environment, the path to the package in the dev-env is returned.
*
* When running in a runtime environment, the path to the package in the device-agent node_modules is returned.
* @example
* // process.env.NODE_ENV = 'development'
* getPackagePath('nr-project-nodes')
* // returns '/path/to/dev-env/packages/nr-project-nodes'
* @example
* // process.env.NODE_ENV = '' && 'nr-project-nodes' exists in `dev-env/packages`
* getPackagePath('nr-project-nodes')
* // returns '/path/to/dev-env/packages/nr-project-nodes'
* @example
* // process.env.NODE_ENV = 'production' || 'nr-project-nodes' does not exist in `dev-env/packages`
* getPackagePath('@flowfuse/nr-project-nodes')
* // returns '/path/to/device-agent/node_modules/@flowfuse/nr-project-nodes'
* @param {...string} packageName Name of the package to get the path for
* @returns {string} The full path to the package
*/
function getPackagePath (...packageName) {
if (isDevEnv()) {
return path.join(devPackages, ...packageName).replace(/\\/g, '/')
}
return path.join(runtimePackages, ...packageName).replace(/\\/g, '/')
}
70 changes: 69 additions & 1 deletion test/unit/lib/launcher_spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const should = require('should')
const sinon = require('sinon')
const utils = require('../../../lib/utils')
const { newLauncher } = require('../../../lib/launcher')
const setup = require('../setup')
const fs = require('fs/promises')
Expand All @@ -15,14 +17,16 @@ describe('Launcher', function () {
dir: '',
verbose: true
}

const nodeEnv = process.env.NODE_ENV
beforeEach(async function () {
config.dir = await fs.mkdtemp(path.join(os.tmpdir(), 'ff-launcher-'))
await fs.mkdir(path.join(config.dir, 'project'))
})

afterEach(async function () {
await fs.rm(config.dir, { recursive: true, force: true })
process.env.NODE_ENV = nodeEnv // restore NODE_ENV
sinon.restore()
})

it('Create Snapshot Flow/Creds Files, instance bound device', async function () {
Expand Down Expand Up @@ -157,4 +161,68 @@ describe('Launcher', function () {
settings.editorTheme.should.have.property('palette')
settings.editorTheme.palette.should.not.have.a.property('catalogue')
})
it('Uses flowfuse project nodes from dev-env when detected', async function () {
// Summary: if dev-env is detected, then the launcher should use the project nodes from dev-env
const licensedConfig = {
...config,
licenseType: 'ee',
licensed: true
}
// set NODE_ENV to 'development' to simulate dev-env
process.env.NODE_ENV = 'development'
// spy utils.getPackagePath to ensure it is called with 'nr-project-nodes'
sinon.spy(utils, 'getPackagePath')
const launcher = newLauncher(licensedConfig, null, 'projectId', setup.snapshot)
await launcher.writeSettings()

// check that utils.getPackagePath was called with 'nr-project-nodes'
utils.getPackagePath.calledWith('nr-project-nodes').should.be.true()

// check that settings.nodesDir contains the dev path to the project nodes
const setFile = await fs.readFile(path.join(config.dir, 'project', 'settings.json'))
const settings = JSON.parse(setFile)
settings.should.have.property('nodesDir')

// because we are in dev-env, settings.nodesDir should contain the dev path + 'packages/nr-project-nodes'
settings.nodesDir.filter((dir) => dir.endsWith('packages/nr-project-nodes')).should.have.a.lengthOf(1)
})
it('Uses flowfuse project nodes from device node_modules is runtime', async function () {
// Summary: if dev-env is not detected, then the launcher should use the project nodes from device-agent node_modules
const licensedConfig = {
...config,
licenseType: 'ee',
licensed: true
}
// set NODE_ENV to 'production' to simulate runtime
process.env.NODE_ENV = 'production'
// spy utils.getPackagePath to ensure it is called with '@flowfuse/nr-project-nodes'
sinon.spy(utils, 'getPackagePath')
const launcher = newLauncher(licensedConfig, null, 'projectId', setup.snapshot)
await launcher.writeSettings()

// check that utils.getPackagePath was called with '@flowfuse/nr-project-nodes'
utils.getPackagePath.calledWith('@flowfuse/nr-project-nodes').should.be.true()

// check that settings.nodesDir contains the dev path to the project nodes
const setFile = await fs.readFile(path.join(config.dir, 'project', 'settings.json'))
const settings = JSON.parse(setFile)
settings.should.have.property('nodesDir')

// because we are in runtime, settings.nodesDir should contain the agent path + 'node_modules/@flowfuse/nr-project-nodes'
settings.nodesDir.filter((dir) => dir.endsWith('node_modules/@flowfuse/nr-project-nodes')).should.have.a.lengthOf(1)
})
it('Does not add path for project nodes when unlicensed', async function () {
// Summary: if NON EE, then nodesDir should either be empty OR should NOT contain 'nr-project-nodes'
const unlicensedConfig = {
...config,
licensed: false
}
const launcher = newLauncher(unlicensedConfig, null, 'projectId', setup.snapshot)
await launcher.writeSettings()
const setFile = await fs.readFile(path.join(config.dir, 'project', 'settings.json'))
const settings = JSON.parse(setFile)
if (settings.nodesDir && Array.isArray(settings.nodesDir) && settings.nodesDir.length > 0) {
settings.nodesDir.filter((dir) => dir.endsWith('nr-project-nodes')).should.have.a.lengthOf(0)
}
})
})
25 changes: 25 additions & 0 deletions test/unit/lib/util_spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const should = require('should') // eslint-disable-line
const utils = require('../../../lib/utils.js')
const path = require('path')

/*
Ensure utils used throughout agent are tested
Expand Down Expand Up @@ -103,4 +104,28 @@ describe('utils', function () {
utils.compareNodeRedData({ modules: { 'node-red': 'latest' } }, { modules: undefined }).should.be.false()
})
})
describe('getPackagePath', function () {
const devPackages = path.join(__dirname, '..', '..', '..', '..', '..', 'packages')
const runtimePackages = path.join(__dirname, '..', '..', '..', 'node_modules')
describe('developer environment', function () {
beforeEach(function () {
process.env.NODE_ENV = 'development' // simulate dev environment
})
it('should return path of the specified packageName in the dev-env path', function () {
const pkgPath = utils.getPackagePath('nr-project-nodes')
pkgPath.should.be.a.String()
pkgPath.should.eql(path.join(devPackages, 'nr-project-nodes').replace(/\\/g, '/'))
})
})
describe('runtime environment', function () {
beforeEach(function () {
process.env.NODE_ENV = 'production' // simulate runtime environment
})
it('should return path of the specified packageName in the device-agent node_modules', function () {
const pkgPath = utils.getPackagePath('@flowfuse/nr-project-nodes')
pkgPath.should.be.a.String()
pkgPath.should.eql(path.join(runtimePackages, '@flowfuse', 'nr-project-nodes').replace(/\\/g, '/'))
})
})
})
})

0 comments on commit 4d2f7d6

Please sign in to comment.