CircleCI 上的间歇性 Selenium ECONNREFUSED 错误
Intermittent Selenium ECONNREFUSED errors on CircleCI
我们的 CircleCI 测试使用 selenium,通过 selenium-webdriver 运行 UI 通过 PhantomJS 进行测试。这些测试在我们的 vagrant 环境中 100% 的时间在本地工作,但在 CircleCI 上大约有 3 次失败 1 次,出现 ECONNREFUSED 错误,例如:
Error: ECONNREFUSED connect ECONNREFUSED 10.0.4.1:59525
at ClientRequest.<anonymous> (node_modules/selenium-webdriver/http/index.js:238:15)
at Socket.socketErrorListener (_http_client.js:310:9)
at emitErrorNT (net.js:1278:8)
at _combinedTickCallback (internal/process/next_tick.js:74:11)
at process._tickCallback (internal/process/next_tick.js:98:9)
From: Task: WebDriver.navigate().to(http://127.0.0.1:8080/src/public/login.php?t=ur&ign=1&ut=ad)
at thenableWebDriverProxy.schedule (node_modules/selenium-webdriver/lib/webdriver.js:816:17)
at Navigation.to (node_modules/selenium-webdriver/lib/webdriver.js:1140:25)
at thenableWebDriverProxy.get (node_modules/selenium-webdriver/lib/webdriver.js:997:28)
at User.logIn (testJs/ui/utils/user.js:9:16)
at Host.logInAsHost (testJs/ui/utils/host.js:13:14)
at Preferences.disableRevenueGeneration (testJs/ui/utils/preferences.js:57:14)
at Context.<anonymous> (testJs/ui/tests/preferences.js:13:22)
这些错误在我们的整个测试过程中随机发生,它们不是由我们测试中的任何特定位置触发的。据我所知,这个问题发生在 selenium/selenium-webdriver 端,因为 Web 服务器保持正常运行并且没有产生任何错误。
我已经尝试了以下所有方法并且 none 有效:
- 升级到最新的 selenium-webdriver (3.4.0)
- 升级到较新版本的 nodejs (6.9.2)
- 使用了不同的网络服务器
- 升级到最新版本的 PhantomJS (1.9.7-15)
- 尝试使用
export DBUS_SESSION_BUS_ADDRESS
按照
https://github.com/SeleniumHQ/docker-selenium/issues/87
- 在 node_modules/selenium-webdriver/http/index.js 中,我修改了代码以重试 ECONNREFUSED 错误,方法是重新使用已经存在的 ECONNRESET 重试,即
if (e.code === 'ECONNRESET') {
变为 if (e.code === 'ECONNRESET' || e.code === 'ECONNREFUSED') {
。这不起作用,因为 selenium-webdriver 只是无限期地重试,由此我意识到问题似乎是一旦遇到 ECONNREFUSED 错误,selenium/selenium-webdriver 将无法恢复。
有人找到解决办法了吗?
警告:这只是一个 hack,绝不是一个合适的解决方案,但它是唯一一个似乎在 CircleCI 上可靠地为我们工作的解决方案。希望其他人会找到更好的解决方案。
我们的测试框架 mocha 似乎有一个专门为 selenium 开发的重试机制(见图):http://mochajs.org/#retry-tests。第一个问题是 after
和 before
中的代码没有重试。因此,您需要将代码移动到 beforeEach
和 afterEach
。第二个问题是您需要在 beforeEach
和 afterEach
中使用一些特殊逻辑来处理驱动程序错误。
因此,为了利用这种机制,我们必须重构我们的测试,以便每个测试都有一个 beforeEach
来设置一个新的 webdriver 实例,然后是一个 afterEach
来退出该驱动程序实例。这样,崩溃的驱动程序实例不会停止所有后续测试。我们开发了一个辅助函数,我们在所有 describe
块的开头调用它,这样我们就不必在测试中添加太多代码。
然后,我们将 this.retries(10)
添加到最上面的 describe
块。
这里是帮手class:
var webdriver = require('selenium-webdriver');
// We default to using phantomjs, but in the future, when we run tests in other browsers, e.g. via
// SauceLabs, we'll want to change the browser.
var customPhantom = webdriver.Capabilities.phantomjs();
customPhantom.set('ssl-protocol', 'any');
// For convenience in tests
global.By = webdriver.By;
global.until = webdriver.until;
var SeleniumStabilizer = function () {};
SeleniumStabilizer.prototype.MAX_RETRIES = 10;
SeleniumStabilizer.prototype.createDriver = function () {
global.driver = new webdriver.Builder()
// .forBrowser('phantomjs')
.withCapabilities(customPhantom)
.build();
// Resolves when driver is ready
return global.driver;
};
SeleniumStabilizer.prototype.init = function (opts) {
var self = this,
n = 0;
var beforeEachWithRetries = function () {
// Create a fresh selenium driver for the next test
return self.createDriver().then(function () {
if (opts.onBeforeEach) {
// Execute the test-specific defined onBeforeEach
return opts.onBeforeEach();
}
}).catch(function (err) {
// ECONNREFUSED error and we should retry?
if (err.message.indexOf('ECONNREFUSED') !== -1 && n++ < self.MAX_RETRIES) {
return beforeEachWithRetries();
} else {
throw err;
}
});
};
opts.beforeEach(function () {
n = 0;
return beforeEachWithRetries();
});
var afterEachWithRetries = function () {
return Promise.resolve().then(function () {
if (opts.onAfterEach) {
// Execute the test-specific defined onAfterEach
return opts.onAfterEach();
}
}).then(function () {
// Quit the selenium driver before starting the next test
return driver.quit();
}).catch(function (err) {
// ECONNREFUSED error and we should retry?
if (err.message.indexOf('ECONNREFUSED') !== -1 && n++ < self.MAX_RETRIES) {
// Create a new driver
return self.createDriver().then(function () {
return afterEachWithRetries();
});
} else {
throw err;
}
});
};
opts.afterEach(function () {
n = 0;
return afterEachWithRetries();
});
};
然后,你的测试看起来像:
var seleniumStabilizer = new SeleniumStabilizer();
describe('my tests', function () {
this.retries(seleniumStabilizer.MAX_RETRIES);
describe('search engines', function () {
seleniumStabilizer.init({
beforeEach: beforeEach,
afterEach: afterEach,
onBeforeEach: function () {
// e.g. return doPromiseAfterDriverSetUp();
},
onAfterEach: function () {
// e.g. return doPromiseBeforeDriverQuits();
}
});
it('should get google', function () {
return driver.get('https://www.google.com');
});
it('should get amazon', function () {
return driver.get('https://www.amazon.com');
});
});
});
我们的 CircleCI 测试使用 selenium,通过 selenium-webdriver 运行 UI 通过 PhantomJS 进行测试。这些测试在我们的 vagrant 环境中 100% 的时间在本地工作,但在 CircleCI 上大约有 3 次失败 1 次,出现 ECONNREFUSED 错误,例如:
Error: ECONNREFUSED connect ECONNREFUSED 10.0.4.1:59525
at ClientRequest.<anonymous> (node_modules/selenium-webdriver/http/index.js:238:15)
at Socket.socketErrorListener (_http_client.js:310:9)
at emitErrorNT (net.js:1278:8)
at _combinedTickCallback (internal/process/next_tick.js:74:11)
at process._tickCallback (internal/process/next_tick.js:98:9)
From: Task: WebDriver.navigate().to(http://127.0.0.1:8080/src/public/login.php?t=ur&ign=1&ut=ad)
at thenableWebDriverProxy.schedule (node_modules/selenium-webdriver/lib/webdriver.js:816:17)
at Navigation.to (node_modules/selenium-webdriver/lib/webdriver.js:1140:25)
at thenableWebDriverProxy.get (node_modules/selenium-webdriver/lib/webdriver.js:997:28)
at User.logIn (testJs/ui/utils/user.js:9:16)
at Host.logInAsHost (testJs/ui/utils/host.js:13:14)
at Preferences.disableRevenueGeneration (testJs/ui/utils/preferences.js:57:14)
at Context.<anonymous> (testJs/ui/tests/preferences.js:13:22)
这些错误在我们的整个测试过程中随机发生,它们不是由我们测试中的任何特定位置触发的。据我所知,这个问题发生在 selenium/selenium-webdriver 端,因为 Web 服务器保持正常运行并且没有产生任何错误。
我已经尝试了以下所有方法并且 none 有效:
- 升级到最新的 selenium-webdriver (3.4.0)
- 升级到较新版本的 nodejs (6.9.2)
- 使用了不同的网络服务器
- 升级到最新版本的 PhantomJS (1.9.7-15)
- 尝试使用
export DBUS_SESSION_BUS_ADDRESS
按照 https://github.com/SeleniumHQ/docker-selenium/issues/87 - 在 node_modules/selenium-webdriver/http/index.js 中,我修改了代码以重试 ECONNREFUSED 错误,方法是重新使用已经存在的 ECONNRESET 重试,即
if (e.code === 'ECONNRESET') {
变为if (e.code === 'ECONNRESET' || e.code === 'ECONNREFUSED') {
。这不起作用,因为 selenium-webdriver 只是无限期地重试,由此我意识到问题似乎是一旦遇到 ECONNREFUSED 错误,selenium/selenium-webdriver 将无法恢复。
有人找到解决办法了吗?
警告:这只是一个 hack,绝不是一个合适的解决方案,但它是唯一一个似乎在 CircleCI 上可靠地为我们工作的解决方案。希望其他人会找到更好的解决方案。
我们的测试框架 mocha 似乎有一个专门为 selenium 开发的重试机制(见图):http://mochajs.org/#retry-tests。第一个问题是 after
和 before
中的代码没有重试。因此,您需要将代码移动到 beforeEach
和 afterEach
。第二个问题是您需要在 beforeEach
和 afterEach
中使用一些特殊逻辑来处理驱动程序错误。
因此,为了利用这种机制,我们必须重构我们的测试,以便每个测试都有一个 beforeEach
来设置一个新的 webdriver 实例,然后是一个 afterEach
来退出该驱动程序实例。这样,崩溃的驱动程序实例不会停止所有后续测试。我们开发了一个辅助函数,我们在所有 describe
块的开头调用它,这样我们就不必在测试中添加太多代码。
然后,我们将 this.retries(10)
添加到最上面的 describe
块。
这里是帮手class:
var webdriver = require('selenium-webdriver');
// We default to using phantomjs, but in the future, when we run tests in other browsers, e.g. via
// SauceLabs, we'll want to change the browser.
var customPhantom = webdriver.Capabilities.phantomjs();
customPhantom.set('ssl-protocol', 'any');
// For convenience in tests
global.By = webdriver.By;
global.until = webdriver.until;
var SeleniumStabilizer = function () {};
SeleniumStabilizer.prototype.MAX_RETRIES = 10;
SeleniumStabilizer.prototype.createDriver = function () {
global.driver = new webdriver.Builder()
// .forBrowser('phantomjs')
.withCapabilities(customPhantom)
.build();
// Resolves when driver is ready
return global.driver;
};
SeleniumStabilizer.prototype.init = function (opts) {
var self = this,
n = 0;
var beforeEachWithRetries = function () {
// Create a fresh selenium driver for the next test
return self.createDriver().then(function () {
if (opts.onBeforeEach) {
// Execute the test-specific defined onBeforeEach
return opts.onBeforeEach();
}
}).catch(function (err) {
// ECONNREFUSED error and we should retry?
if (err.message.indexOf('ECONNREFUSED') !== -1 && n++ < self.MAX_RETRIES) {
return beforeEachWithRetries();
} else {
throw err;
}
});
};
opts.beforeEach(function () {
n = 0;
return beforeEachWithRetries();
});
var afterEachWithRetries = function () {
return Promise.resolve().then(function () {
if (opts.onAfterEach) {
// Execute the test-specific defined onAfterEach
return opts.onAfterEach();
}
}).then(function () {
// Quit the selenium driver before starting the next test
return driver.quit();
}).catch(function (err) {
// ECONNREFUSED error and we should retry?
if (err.message.indexOf('ECONNREFUSED') !== -1 && n++ < self.MAX_RETRIES) {
// Create a new driver
return self.createDriver().then(function () {
return afterEachWithRetries();
});
} else {
throw err;
}
});
};
opts.afterEach(function () {
n = 0;
return afterEachWithRetries();
});
};
然后,你的测试看起来像:
var seleniumStabilizer = new SeleniumStabilizer();
describe('my tests', function () {
this.retries(seleniumStabilizer.MAX_RETRIES);
describe('search engines', function () {
seleniumStabilizer.init({
beforeEach: beforeEach,
afterEach: afterEach,
onBeforeEach: function () {
// e.g. return doPromiseAfterDriverSetUp();
},
onAfterEach: function () {
// e.g. return doPromiseBeforeDriverQuits();
}
});
it('should get google', function () {
return driver.get('https://www.google.com');
});
it('should get amazon', function () {
return driver.get('https://www.amazon.com');
});
});
});