mirror of https://github.com/svaarala/duktape.git
Sami Vaarala
9 years ago
4 changed files with 470 additions and 0 deletions
@ -0,0 +1,337 @@ |
|||
/* |
|||
* Testrunner client for simple commit jobs |
|||
* |
|||
* Node.js based default client intended for Linux, OSX, and Windows. |
|||
*/ |
|||
|
|||
var fs = require('fs'); |
|||
var path = require('path'); |
|||
var yaml = require('yamljs'); |
|||
var Promise = require('bluebird'); |
|||
var http = require('http'); |
|||
var https = require('https'); |
|||
var tmp = require('tmp'); |
|||
var child_process = require('child_process'); |
|||
var crypto = require('crypto'); |
|||
|
|||
/* |
|||
* Command line and config parsing |
|||
*/ |
|||
|
|||
var argv = require('minimist')(process.argv.slice(2)); |
|||
console.log('Command line options: ' + JSON.stringify(argv)); |
|||
var configFile = argv.config || './config.yaml'; |
|||
console.log('Load config: ' + configFile); |
|||
var clientConfig = yaml.load(configFile); |
|||
|
|||
/* |
|||
* Misc utils |
|||
*/ |
|||
|
|||
function sha1sum(x) { |
|||
return crypto.createHash('sha1').update(x).digest('hex'); |
|||
} |
|||
|
|||
function sha1sumFile(x) { |
|||
return sha1sum(fs.readFileSync(x)); |
|||
} |
|||
|
|||
function assert(x) { |
|||
if (x) { return x; } |
|||
throw new Error('assertion failed'); |
|||
} |
|||
|
|||
/* |
|||
* HTTP(S) helpers |
|||
*/ |
|||
|
|||
var serverTrustRoot = fs.readFileSync(clientConfig.serverCertificate); |
|||
|
|||
// Path -> file data
|
|||
var fileCache = {}; |
|||
|
|||
function getBasicAuthHeader() { |
|||
return 'Basic ' + new Buffer(assert(clientConfig.clientAuthUsername) + ':' + assert(clientConfig.clientAuthPassword)).toString('base64'); |
|||
} |
|||
|
|||
function getUserAgent() { |
|||
return 'testrunner-client'; |
|||
} |
|||
|
|||
function serverAuthCheck(res) { |
|||
var auth = res.headers['x-testrunner-authenticator']; // lowercase intentionally
|
|||
if (typeof auth !== 'string' || |
|||
auth !== assert(clientConfig.serverAuthPassword)) { |
|||
throw new Error('server not authorized'); |
|||
} |
|||
} |
|||
|
|||
function postJson(path, request, cb) { |
|||
return new Promise(function (resolve, reject) { |
|||
var requestData = new Buffer(JSON.stringify(request), 'utf8'); |
|||
var headers = { |
|||
'User-Agent': getUserAgent(), |
|||
'Authorization': getBasicAuthHeader(), |
|||
'Content-Type': 'application/json', |
|||
'Content-Length': requestData.length, |
|||
}; |
|||
var options = { |
|||
host: clientConfig.serverHost, |
|||
port: clientConfig.serverPort, |
|||
ca: serverTrustRoot, |
|||
rejectUnauthorized: true, |
|||
path: path, |
|||
method: 'POST', |
|||
auth: assert(clientConfig.clientAuthUsername) + ':' + assert(clientConfig.clientAuthPassword), |
|||
headers: headers |
|||
}; |
|||
|
|||
console.log('sending POST ' + path); |
|||
|
|||
var req = https.request(options, function (res) { |
|||
//res.setEncoding('utf8'); // we just want binary
|
|||
|
|||
var buffers = []; |
|||
res.on('data', function (data) { |
|||
try { |
|||
buffers.push(data); |
|||
} catch (e) { |
|||
console.log(e); |
|||
} |
|||
}); |
|||
res.on('end', function () { |
|||
try { |
|||
serverAuthCheck(res); |
|||
var data = Buffer.concat(buffers); |
|||
var rep = JSON.parse(data.toString('utf8')); |
|||
} catch (e) { |
|||
reject(e); |
|||
return; |
|||
} |
|||
resolve(rep); |
|||
}); |
|||
}); |
|||
|
|||
req.on('error', function (err) { |
|||
reject(err); |
|||
}); |
|||
|
|||
req.write(requestData); |
|||
req.end(); |
|||
}); |
|||
} |
|||
|
|||
function getFile(path, cb) { |
|||
return new Promise(function (resolve, reject) { |
|||
var headers = { |
|||
'User-Agent': getUserAgent(), |
|||
'Authorization': getBasicAuthHeader() |
|||
}; |
|||
var options = { |
|||
host: clientConfig.serverHost, |
|||
port: clientConfig.serverPort, |
|||
ca: serverTrustRoot, |
|||
rejectUnauthorized: true, |
|||
path: path, |
|||
method: 'GET', |
|||
auth: assert(clientConfig.clientAuthUsername) + ':' + assert(clientConfig.clientAuthPassword), |
|||
headers: headers |
|||
}; |
|||
|
|||
console.log('sending GET ' + path); |
|||
|
|||
var req = https.request(options, function (res) { |
|||
//res.setEncoding('utf8'); // we just want binary
|
|||
|
|||
var buffers = []; |
|||
res.on('data', function (data) { |
|||
try { |
|||
buffers.push(data); |
|||
} catch (e) { |
|||
console.log(e); |
|||
} |
|||
}); |
|||
res.on('end', function () { |
|||
try { |
|||
serverAuthCheck(res); |
|||
var data = Buffer.concat(buffers); |
|||
} catch (e) { |
|||
reject(e); |
|||
return; |
|||
} |
|||
resolve(data); |
|||
}); |
|||
}); |
|||
|
|||
req.on('error', function (err) { |
|||
reject(err); |
|||
}); |
|||
|
|||
req.end(); |
|||
}); |
|||
} |
|||
|
|||
function cachedGetFile(path) { |
|||
if (fileCache[path]) { |
|||
return new Promise(function (resolve, reject) { |
|||
resolve(fileCache[path]); |
|||
}); |
|||
} else { |
|||
return getFile(path).then(function (data) { |
|||
fileCache[path] = data; |
|||
return data; |
|||
}); |
|||
} |
|||
} |
|||
|
|||
/* |
|||
* Simple commit jobs |
|||
*/ |
|||
|
|||
function processSimpleScriptJob(rep) { |
|||
var context, repo, repoFull, repoCloneUrl, sha, script; |
|||
var tmpDir; |
|||
|
|||
var tmpDir = tmp.dirSync({ |
|||
mode: 0750, |
|||
prefix: 'testrunner-' + context + '-', |
|||
unsafeCleanup: true |
|||
}); |
|||
|
|||
return new Promise(function (resolve, reject) { |
|||
var i; |
|||
|
|||
context = assert(rep.context); |
|||
repo = assert(rep.repo); |
|||
repoFull = assert(rep.repo_full); |
|||
repoCloneUrl = assert(rep.repo_clone_url); |
|||
sha = assert(rep.sha); |
|||
|
|||
for (i = 0; i < clientConfig.supportedContexts.length; i++) { |
|||
if (clientConfig.supportedContexts[i].context === assert(rep.context)) { |
|||
script = clientConfig.supportedContexts[i].script; |
|||
} |
|||
} |
|||
if (!script) { |
|||
reject(new Error('unsupported context: ' + context)); |
|||
return; |
|||
} |
|||
|
|||
console.log('start simple commit test, repo ' + repoCloneUrl + ', sha ' + sha + ', context ' + context); |
|||
|
|||
var args = [ assert(repoFull), assert(repoCloneUrl), assert(sha), assert(context), assert(tmpDir.name) ]; |
|||
var cld = child_process.spawn(assert(script), args, { cwd: '/tmp' }); |
|||
var buffers = []; |
|||
cld.stdout.on('data', function (data) { |
|||
buffers.push(data); |
|||
}); |
|||
cld.stderr.on('data', function (data) { |
|||
buffers.push(data); // just interleave stdout/stderr for now
|
|||
}); |
|||
cld.on('close', function (code) { |
|||
resolve({ code: code, data: Buffer.concat(buffers) }); |
|||
}); |
|||
}).then(function (res) { |
|||
// Test execution finished, test case may have succeeded or failed.
|
|||
console.log('finished simple commit test, repo ' + repoCloneUrl + ', sha ' + sha + ', context ' + context + ', code ' + res.code); |
|||
//console.log(res.data.toString());
|
|||
|
|||
// Initial heuristic for providing descriptions from test script,
|
|||
// could also use an explicit output file. Match last occurrence
|
|||
// on purpose so that the script can always override a previous
|
|||
// status if necessary.
|
|||
var desc = null; |
|||
try { |
|||
desc = (function () { |
|||
var txt = res.data.toString('utf8'); // XXX: want something lenient
|
|||
var re = /^TESTRUNNER_DESCRIPTION: (.*?)$/gm; |
|||
var m; |
|||
var out = null; |
|||
while ((m = re.exec(txt)) !== null) { |
|||
out = m[1]; |
|||
} |
|||
return out; |
|||
})(); |
|||
} catch (e) { |
|||
console.log(e); |
|||
} |
|||
|
|||
postJson('/finish-commit-simple', { |
|||
repo: assert(repo), |
|||
repo_full: assert(repoFull), |
|||
repo_clone_url: assert(repoCloneUrl), |
|||
sha: assert(sha), |
|||
context: assert(context), |
|||
state: res.code === 0 ? 'success' : 'failure', |
|||
description: desc || (res.code === 0 ? 'Success' : 'Failure'), |
|||
text: res.data.toString('base64') |
|||
}).then(function (rep) { |
|||
//console.log(rep);
|
|||
}).catch(function (err) { |
|||
console.log(err); // XXX: chain error
|
|||
}); |
|||
}).catch(function (err) { |
|||
// Test execution attempt failed (which is different from a test case
|
|||
// failing).
|
|||
console.log('FAILED simple commit test, repo ' + repoCloneUrl + ', sha ' + sha + ', context ' + context); |
|||
console.log(err); |
|||
|
|||
var data = new Buffer(String(err.stack || err)); |
|||
|
|||
// XXX: indicate possibly transient nature of error
|
|||
|
|||
postJson('/finish-commit-simple', { |
|||
repo: assert(repo), |
|||
repo_full: assert(repoFull), |
|||
repo_clone_url: assert(repoCloneUrl), |
|||
sha: assert(sha), |
|||
context: assert(context), |
|||
state: 'failure', |
|||
description: 'Failed: ' + String(err), |
|||
text: data.toString('base64') |
|||
}).then(function (rep) { |
|||
//console.log(rep);
|
|||
}).catch(function (err) { |
|||
console.log(err); // XXX: chain error
|
|||
}); |
|||
}).finally(function () { |
|||
if (tmpDir) { |
|||
tmpDir.removeCallback(); |
|||
console.log('cleaned up ' + tmpDir.name); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
function requestAndExecute() { |
|||
var contexts = []; |
|||
var i; |
|||
|
|||
for (i = 0; i < clientConfig.supportedContexts.length; i++) { |
|||
contexts.push(clientConfig.supportedContexts[i].context); |
|||
} |
|||
|
|||
postJson('/get-commit-simple', { |
|||
contexts: contexts |
|||
}).then(function (rep) { |
|||
console.log(rep); |
|||
if (rep.error_code) { |
|||
throw new Error('job failed; error code ' + rep.error_code + ': ' + rep.error_description); |
|||
} |
|||
return processSimpleScriptJob(rep); |
|||
}).then(function () { |
|||
setTimeout(function () { requestAndExecute(); }, clientConfig.sleepJobSuccess); |
|||
}).catch(function (err) { |
|||
// XXX: differentiate between TIMEOUT and other errors
|
|||
console.log('job failed; sleep a bit and retry:', err); |
|||
console.log(err.stack); |
|||
setTimeout(function () { requestAndExecute(); }, clientConfig.sleepJobFailure); |
|||
}); |
|||
|
|||
setTimeout(function () {}, 10000000); // avoid exit
|
|||
} |
|||
|
|||
function main() { |
|||
requestAndExecute(); |
|||
} |
|||
|
|||
main(); |
@ -0,0 +1,26 @@ |
|||
# Testrunner client example configuration |
|||
|
|||
# Testrunner server host/port. |
|||
serverHost: 'localhost' |
|||
serverPort: 9080 |
|||
|
|||
# Testrunner server SSL trust root (Duktape CA). |
|||
serverCertificate: 'server.crt' |
|||
|
|||
# Testrunner API authentication. |
|||
serverAuthPassword: 'foobarx' |
|||
clientAuthUsername: 'foouser' |
|||
clientAuthPassword: 'foopass' |
|||
|
|||
# Sleeps between successful/failed jobs. |
|||
sleepJobSuccess: 10000 |
|||
sleepJobFailure: 60000 |
|||
|
|||
# Supported simple commit job types (contexts). Each context has a string |
|||
# name which appears directly in Github web UI, and a script which is used |
|||
# to execute test jobs of that type. |
|||
supportedContexts: |
|||
- context: 'qecmatest' |
|||
script: '/path/to/run_qecmatest.sh' # script paths must be absolute |
|||
- context: 'apitest' |
|||
script: '/path/to/run_apitest.sh' |
@ -0,0 +1,18 @@ |
|||
{ |
|||
"name": "testrunner-client", |
|||
"version": "0.1.0", |
|||
"description": "Duktape testrunner client", |
|||
"author": { |
|||
"name": "Sami Vaarala", |
|||
"email": "sami.vaarala@iki.fi" |
|||
}, |
|||
"dependencies": { |
|||
"bluebird": "^2.9.25", |
|||
"crypto": "0.0.3", |
|||
"https": "^1.0.0", |
|||
"yamljs": "^0.2.2", |
|||
"tmp": "~0.0.27", |
|||
"minimist": "~1.2.0" |
|||
}, |
|||
"main": "client.js" |
|||
} |
@ -0,0 +1,89 @@ |
|||
#!/bin/sh |
|||
|
|||
# Full name of repo, e.g. "svaarala/duktape". |
|||
REPOFULLNAME=$1 |
|||
|
|||
# Repo HTTPS clone URL, e.g. "https://github.com/svaarala/duktape.git". |
|||
REPOCLONEURL=$2 |
|||
|
|||
# Commit hash. |
|||
SHA=$3 |
|||
|
|||
# Context, e.g. "x64-qecmatest". |
|||
CONTEXT=$4 |
|||
|
|||
# Automatic temporary directory created by testclient, automatically |
|||
# deleted (recursively) by testclient. |
|||
TEMPDIR=$5 |
|||
|
|||
# Directory holding repo tar.gz clone snapshots for faster test init. |
|||
# Example: /tmp/repo-snapshots/svaarala/duktape.tar.gz. |
|||
REPO_TARGZS=/tmp/repo-snapshots |
|||
|
|||
set -e |
|||
|
|||
echo "*** Run duk-g++ test: `date`" |
|||
echo "REPOFULLNAME=$REPOFULLNAME" |
|||
echo "REPOCLONEURL=$REPOCLONEURL" |
|||
echo "SHA=$SHA" |
|||
echo "CONTEXT=$CONTEXT" |
|||
|
|||
if [ "x$TEMPDIR" = "x" ]; then |
|||
echo "Missing TEMPDIR." |
|||
exit 1 |
|||
fi |
|||
cd "$TEMPDIR" |
|||
|
|||
if echo "$REPOFULLNAME" | grep -E '[[:space:]\.]'; then |
|||
echo "Repo full name contains invalid characters" |
|||
exit 1 |
|||
fi |
|||
if [ "$REPOFULLNAME" != "svaarala/duktape" ]; then |
|||
echo "Repo is not whitelisted" |
|||
exit 1 |
|||
fi |
|||
|
|||
# This isn't great security-wise, but $REPOFULLNAME has been checked not to |
|||
# contain dots (e.g. '../'). It'd be better to resolve the absolute filename |
|||
# and ensure it begins with $REPO_TARGZS. |
|||
TARGZ=$REPO_TARGZS/$REPOFULLNAME.tar.gz |
|||
echo "Using repo tar.gz: '$TARGZ'" |
|||
if [ ! -f "$TARGZ" ]; then |
|||
echo "Repo snapshot doesn't exist: $TARGZ." |
|||
exit 1 |
|||
fi |
|||
|
|||
echo "Git preparations" |
|||
cd "$TEMPDIR" |
|||
mkdir repo |
|||
cd repo |
|||
tar -x --strip-components 1 -z -f "$TARGZ" |
|||
git clean -f |
|||
git checkout master |
|||
git pull --rebase |
|||
git clean -f |
|||
git checkout "$SHA" |
|||
|
|||
echo "" |
|||
echo "Running: make clean duk-g++" |
|||
make clean duk-g++ |
|||
|
|||
echo "" |
|||
echo "Hello world test" |
|||
./duk-g++ -e "print('hello world!');" >hello.out |
|||
echo "hello world!" >hello.require |
|||
cat hello.out |
|||
|
|||
# If stdout contains "TESTRUNNER_DESCRIPTION: xxx" it will become the Github |
|||
# status description. Last occurrence wins. If no such line is printed, the |
|||
# test client will use a generic "Success" or "Failure" text. |
|||
echo "TESTRUNNER_DESCRIPTION: DUMMY" |
|||
if cmp hello.out hello.require; then |
|||
echo "match" |
|||
echo "TESTRUNNER_DESCRIPTION: Hello world success" |
|||
exit 0 |
|||
else |
|||
echo "no match" |
|||
echo "TESTRUNNER_DESCRIPTION: Hello world failed" |
|||
exit 1 |
|||
fi |
Loading…
Reference in new issue