Skip to content

Commit

Permalink
Merge pull request #690 from codeforequity-at/develop
Browse files Browse the repository at this point in the history
Botium Core 1.13.1
  • Loading branch information
Botium authored Jul 11, 2022
2 parents 9ac7f7e + 6020d43 commit df40ef8
Show file tree
Hide file tree
Showing 7 changed files with 1,598 additions and 953 deletions.
2,343 changes: 1,414 additions & 929 deletions package-lock.json

Large diffs are not rendered by default.

30 changes: 15 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "botium-core",
"version": "1.13.0",
"version": "1.13.1",
"description": "The Selenium for Chatbots",
"main": "index.js",
"module": "dist/botium-es.js",
Expand Down Expand Up @@ -32,25 +32,25 @@
},
"homepage": "https://www.botium.ai",
"dependencies": {
"@babel/runtime": "^7.18.3",
"@babel/runtime": "^7.18.6",
"async": "^3.2.4",
"body-parser": "^1.20.0",
"boolean": "^3.2.0",
"bottleneck": "^2.19.5",
"csv-parse": "^5.2.0",
"csv-parse": "^5.3.0",
"debug": "^4.3.4",
"esprima": "^4.0.1",
"express": "^4.18.1",
"globby": "11.0.4",
"ioredis": "^5.0.6",
"ioredis": "^5.1.0",
"is-class": "^0.0.9",
"is-json": "^2.0.1",
"jsonpath": "^1.1.1",
"lodash": "^4.17.21",
"markdown-it": "^13.0.1",
"mime-types": "^2.1.35",
"mkdirp": "^1.0.4",
"moment": "^2.29.3",
"moment": "^2.29.4",
"mustache": "^4.2.0",
"promise-retry": "^2.0.1",
"promise.allsettled": "^1.0.5",
Expand All @@ -65,33 +65,33 @@
"swagger-jsdoc": "^6.2.1",
"swagger-ui-express": "^4.4.0",
"uuid": "^8.3.2",
"vm2": "^3.9.9",
"vm2": "^3.9.10",
"write-yaml": "^1.0.0",
"xlsx": "^0.18.5",
"xregexp": "^5.1.1",
"yaml": "^2.1.1"
},
"devDependencies": {
"@babel/core": "^7.18.5",
"@babel/node": "^7.18.5",
"@babel/plugin-transform-runtime": "^7.18.5",
"@babel/preset-env": "^7.18.2",
"@babel/core": "^7.18.6",
"@babel/node": "^7.18.6",
"@babel/plugin-transform-runtime": "^7.18.6",
"@babel/preset-env": "^7.18.6",
"chai": "^4.3.6",
"chai-as-promised": "^7.1.1",
"cross-env": "^7.0.3",
"eslint": "^8.18.0",
"eslint": "^8.19.0",
"eslint-config-standard": "^17.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.2.3",
"eslint-plugin-n": "^15.2.4",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-standard": "^4.1.0",
"license-checker": "^25.0.1",
"license-compatibility-checker": "^0.3.5",
"mocha": "^10.0.0",
"nock": "^13.2.7",
"npm-check-updates": "^14.0.1",
"nock": "^13.2.8",
"npm-check-updates": "^15.2.6",
"nyc": "^15.1.0",
"rollup": "^2.75.6",
"rollup": "^2.76.0",
"rollup-plugin-babel": "^4.4.0",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-json": "^4.0.0",
Expand Down
20 changes: 13 additions & 7 deletions src/helpers/RetryHelper.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
const util = require('util')
const _ = require('lodash')
const debug = require('debug')('botium-core-RetryHelper')

module.exports = class RetryHelper {
constructor (caps, section) {
this.retrySettings = {
retries: caps[`RETRY_${section.toUpperCase()}_NUMRETRIES`] || 1,
factor: caps[`RETRY_${section.toUpperCase()}_FACTOR`] || 1,
minTimeout: caps[`RETRY_${section.toUpperCase()}_MINTIMEOUT`] || 1000
}
constructor (caps, section, options = {}) {
this.retryErrorPatterns = []
const onErrorRegexp = caps[`RETRY_${section.toUpperCase()}_ONERROR_REGEXP`] || []
if (onErrorRegexp) {
Expand All @@ -22,10 +18,20 @@ module.exports = class RetryHelper {
this.retryErrorPatterns.push(onErrorRegexp)
}
}

// to turn on retries, NUMRETRIES or ONERROR_REGEXP has to be set
this.retrySettings = {
retries: caps[`RETRY_${section.toUpperCase()}_NUMRETRIES`] || (!_.isNil(options.numRetries) ? options.numRetries : (this.retryErrorPatterns.length === 0) ? 0 : 1),
factor: caps[`RETRY_${section.toUpperCase()}_FACTOR`] || (_.isNil(options.factor) ? 1 : options.factor),
minTimeout: caps[`RETRY_${section.toUpperCase()}_MINTIMEOUT`] || (_.isNil(options.minTimeout) ? 1000 : options.minTimeout)
}

debug(`Retry for ${section} is ${this.retrySettings.retries > 0 ? 'enabled' : 'disabled'}. Settings: ${JSON.stringify(this.retrySettings)} Patterns: ${JSON.stringify(this.retryErrorPatterns.map(r => r.toString()))}`)
}

shouldRetry (err) {
if (!err || this.retryErrorPatterns.length === 0) return false
if (!err) return false
if (this.retryErrorPatterns.length === 0) return true
const errString = util.inspect(err)
for (const re of this.retryErrorPatterns) {
if (errString.match(re)) return true
Expand Down
20 changes: 20 additions & 0 deletions src/scripting/Convo.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
const util = require('util')
const _ = require('lodash')
const debug = require('debug')('botium-core-Convo')
const promiseRetry = require('promise-retry')

const BotiumMockMessage = require('../mocks/BotiumMockMessage')
const Capabilities = require('../Capabilities')
const Events = require('../Events')
const ScriptingMemory = require('./ScriptingMemory')
const { BotiumError, botiumErrorFromErr, botiumErrorFromList } = require('./BotiumError')
const { normalizeText, toString, removeBuffers, splitStringInNonEmptyLines } = require('./helper')
const RetryHelper = require('../helpers/RetryHelper')

const { LOGIC_HOOK_INCLUDE } = require('./logichook/LogicHookConsts')

Expand Down Expand Up @@ -210,6 +212,24 @@ class Convo {
}

async Run (container) {
const retryHelper = new RetryHelper(container.caps, 'CONVO')
return promiseRetry(async (retry, number) => {
return this.RunImpl(container).catch(err => {
const retryRemaining = retryHelper.retrySettings.retries - number + 1
if (retryHelper.shouldRetry(err)) {
debug(`Convo failed with error "${err.message || JSON.stringify(err)}". Retry ${retryRemaining > 0 ? 'enabled' : 'disabled'} (remaining #${retryRemaining}/${retryHelper.retrySettings.retries}, criterion matches)`)
retry(err)
} else {
if (retryHelper.retryErrorPatterns.length > 0) {
debug(`Convo failed with error "${err.message || JSON.stringify(err)}". Retry 'disabled' (remaining (#${retryRemaining}/${retryHelper.retrySettings.retries}), criterion does not match)`)
}
throw err
}
})
}, retryHelper.retrySettings)
}

async RunImpl (container) {
const transcript = new Transcript({
steps: [],
attachments: [],
Expand Down
2 changes: 1 addition & 1 deletion src/scripting/logichook/LogicHookUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ module.exports = class LogicHookUtils {
})
return vm.run(script)
} catch (err) {
throw new Error(`${err.message || err}`)
throw new Error(`Script ${key} is not valid - ${err.message || err}`)
}
} else {
throw new Error(`Script "${key}" is not valid - only functions and javascript code accepted`)
Expand Down
134 changes: 134 additions & 0 deletions test/convo/retryconvo.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
const path = require('path')
const assert = require('chai').assert
const BotDriver = require('../../').BotDriver
const Capabilities = require('../../').Capabilities

const echoConnector = (numErrors, errorText) => ({ queueBotSays }) => {
let errors = 0
return {
UserSays (msg) {
if (errors >= numErrors) {
const botMsg = { sender: 'bot', sourceData: msg.sourceData, messageText: msg.messageText }
queueBotSays(botMsg)
} else {
errors++
return Promise.reject(errorText)
}
}
}
}

describe('convo.retries', function () {
beforeEach(async function () {
this.init = async (numErrors, errorText, errorPatterns, numRetries) => {
const myCaps = {
[Capabilities.PROJECTNAME]: 'convo.retry',
[Capabilities.CONTAINERMODE]: echoConnector(numErrors, errorText),
RETRY_CONVO_MINTIMEOUT: 10,
RETRY_CONVO_ONERROR_REGEXP: errorPatterns
}
if (!isNaN(numRetries)) {
myCaps.RETRY_CONVO_NUMRETRIES = numRetries
}
this.driver = new BotDriver(myCaps)
this.compiler = this.driver.BuildCompiler()
this.container = await this.driver.Build()
await this.container.Start()
}
})
afterEach(async function () {
await this.container.Stop()
await this.container.Clean()
})

it('should fail without retry', async function () {
await this.init(1, 'myerror')

this.compiler.ReadScript(path.resolve(__dirname, 'convos'), '1step.convo.txt')
try {
await this.compiler.convos[0].Run(this.container)
} catch (err) {
assert.isTrue(err.message.indexOf('myerror') >= 0)
return
}
assert.fail('should have failed without retry')
})
it('should succeed after one retry with default numRetries', async function () {
await this.init(1, 'myerror', 'myerror')

this.compiler.ReadScript(path.resolve(__dirname, 'convos'), '1step.convo.txt')
await this.compiler.convos[0].Run(this.container)
})
it('should succeed after one retry with joker regex', async function () {
await this.init(1, 'myerror', null, 1)

this.compiler.ReadScript(path.resolve(__dirname, 'convos'), '1step.convo.txt')
await this.compiler.convos[0].Run(this.container)
})
it('should fail after one retry with default settings', async function () {
await this.init(2, 'myerror', 'myerror')

this.compiler.ReadScript(path.resolve(__dirname, 'convos'), '1step.convo.txt')
try {
await this.compiler.convos[0].Run(this.container)
} catch (err) {
assert.isTrue(err.message.indexOf('myerror') >= 0)
return
}
assert.fail('should have failed after first retry')
})
it('should succeed after many retries', async function () {
await this.init(5, 'myerror', 'myerror', 5)

this.compiler.ReadScript(path.resolve(__dirname, 'convos'), '1step.convo.txt')
await this.compiler.convos[0].Run(this.container)
})
it('should succeed after too less retries', async function () {
await this.init(5, 'myerror', 'myerror', 4)

this.compiler.ReadScript(path.resolve(__dirname, 'convos'), '1step.convo.txt')
try {
await this.compiler.convos[0].Run(this.container)
} catch (err) {
assert.isTrue(err.message.indexOf('myerror') >= 0)
return
}
assert.fail('should have failed after four retries')
})
it('should succeed after one retry with regexp pattern', async function () {
await this.init(1, 'myerror', /myeRRor/i)

this.compiler.ReadScript(path.resolve(__dirname, 'convos'), '1step.convo.txt')
await this.compiler.convos[0].Run(this.container)
})
it('should fail after one retry with unmatched regexp pattern', async function () {
await this.init(1, 'myerror', /myeRRor1/i)

this.compiler.ReadScript(path.resolve(__dirname, 'convos'), '1step.convo.txt')
try {
await this.compiler.convos[0].Run(this.container)
} catch (err) {
assert.isTrue(err.message.indexOf('myerror') >= 0)
return
}
assert.fail('should have failed with unmatched retry pattern')
})
it('should succeed after one retry with regexp pattern array', async function () {
await this.init(1, 'myerror', [/myeRRor/i, /myeRRor1/i])

this.compiler.ReadScript(path.resolve(__dirname, 'convos'), '1step.convo.txt')
await this.compiler.convos[0].Run(this.container)
})
it('should fail after one retry with unmatched regexp pattern array', async function () {
await this.init(1, 'myerror', [/myeRRor1/i, /myeRRor2/i])

this.compiler.ReadScript(path.resolve(__dirname, 'convos'), '1step.convo.txt')
try {
await this.compiler.convos[0].Run(this.container)
} catch (err) {
assert.isTrue(err.message.indexOf('myerror') >= 0)
return
}
assert.fail('should have failed with unmatched retry pattern')
})
})
2 changes: 1 addition & 1 deletion test/logichooks/hookfromsrc.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ describe('logichooks.hookfromsrc', function () {
await compiler.convos[0].Run(container)
assert.fail('it should have failed')
} catch (err) {
assert.isTrue(err.message.includes('Line 6: assertion error - Unexpected end of input'))
assert.isTrue(err.message.includes('Line 6: assertion error - Script assertConvoStep is not valid'))
}
})
})
Expand Down

0 comments on commit df40ef8

Please sign in to comment.