Skip to content

Commit c28e4c5

Browse files
committed
Added coverage integration; swapped out smee for serveo
1 parent 58ace8b commit c28e4c5

File tree

4 files changed

+123
-45
lines changed

4 files changed

+123
-45
lines changed

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ Run the install script to install all dependencies, then create your .env file c
88

99
### Prerequisites
1010

11-
Requires MATLAB 2017a or later and Node.js. The following Node.js modules are required:
11+
Requires MATLAB 2017a or later, Node.js and Git Bash. The following Node.js modules are required:
1212

1313
```
1414
npm install --save express dotenv @octokit/app @octokit/request ...
15-
github-webhook-handler smee-client xml2js
15+
github-webhook-handler xml2js
1616
```
1717

1818
### Installing
@@ -63,6 +63,8 @@ Create a shortcut in your startup folder ([Windows-logo] + [R] in Windows-10 and
6363
## Built With
6464

6565
* [Coveralls](coveralls.io) - Code coverage
66+
* [Serveo](serveo.net) - SSH tunneling service
67+
* [Shields.io](shields.io) - Display shields
6668

6769
## Contributing
6870

coverage.js

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ var fs = require('fs'),
2323
crypto = require('crypto'),
2424
assert = require('assert').strict,
2525
parser = new xml2js.Parser(),
26-
timestamp, md5;
26+
timestamp, md5, cb;
2727
var timestamp;
2828
var token = process.env.COVERALLS_TOKEN;
2929

@@ -33,7 +33,9 @@ var token = process.env.COVERALLS_TOKEN;
3333
* @param {Array} classList - An array of class objects from the loaded XML file.
3434
* @param {String} path - The root path of the code repository.
3535
* @param {String} sha - The commit SHA for this coverage test.
36-
* @returns {Object} Object containing array of source code files and their code coverage
36+
* @param {function} callback - The callback function to run when complete. Takes object containing array of source
37+
* code files and their code coverage
38+
* @returns {Object}
3739
* @todo Generalize path default
3840
*/
3941
const formatCoverage = function(classList, path, sha) {
@@ -63,20 +65,22 @@ const formatCoverage = function(classList, path, sha) {
6365
job.source_files = sourceFiles;
6466
job.commit_sha = sha;
6567
job.run_at = timestamp; // "2013-02-18 00:52:48 -0800"
66-
return job;
68+
cb(job);
6769
}
6870

6971
/**
7072
* Loads a code coverage XML file in Cobertura Format and returns payload object for Coveralls API
7173
* @see {@link https://docs.coveralls.io/api-reference|Coveralls API docs}
7274
* @param {String} path - Path to the XML file containing coverage information.
7375
* @param {String} sha - The commit SHA for this coverage test
74-
* @returns {Object} Object containing array of source code files and their code coverage
76+
* @param {String} repo - The repo to which the commit belongs
77+
* @param {function} callback - The callback function to run when complete
7578
* @todo Remove assert
7679
* @todo Generalize ignoring of submodules
7780
* @see {@link https://github.com/cobertura/cobertura/wiki|Cobertura Wiki}
7881
*/
79-
const coverage = function(path, sha) {
82+
const coverage = function(path, repo, sha, callback) {
83+
cb = callback; // @fixme Making callback global feels hacky
8084
fs.readFile(path, function(err, data) { // Read in XML file
8185
parser.parseString(data, function (err, result) { // Parse XML
8286
const rigboxPath = result.coverage.sources[0].source[0]; // Extract root code path
@@ -90,22 +94,19 @@ const coverage = function(path, sha) {
9094
//console.log(classes.length);
9195

9296
// The submodules
93-
var alyx_matlab = [];
94-
var signals = [];
95-
var npy_matlab = [];
96-
var wheelAnalysis = [];
97+
var modules = {'rigbox' : [], 'alyx-matlab' : [], 'signals' : [], 'npy-matlab' : [], 'wheelAnalysis' : []};
9798
// Sort into piles
98-
classes = classes.filter(function (e) {
99+
modules['rigbox'] = classes.filter(function (e) {
99100
if (e.$.filename.search(/(tests\\|_.*test|docs\\)/i) != -1) {return false;} // Filter out tests and docs
100101
if (!Array.isArray(e.lines[0].line)) {return false;} // Filter out files with no functional lines
101-
if (e.$.filename.startsWith('alyx-matlab\\')) {alyx_matlab.push(e); return false;}
102-
if (e.$.filename.startsWith('signals\\')) {signals.push(e); return false;}
103-
if (e.$.filename.startsWith('npy-matlab\\')) {npy_matlab.push(e); return false;}
104-
if (e.$.filename.startsWith('wheelAnalysis\\')) {wheelAnalysis.push(e); return false;}
102+
if (e.$.filename.startsWith('alyx-matlab\\')) {modules['alyx-matlab'].push(e); return false;}
103+
if (e.$.filename.startsWith('signals\\')) {modules.signals.push(e); return false;}
104+
if (e.$.filename.startsWith('npy-matlab\\')) {modules['npy-matlab'].push(e); return false;}
105+
if (e.$.filename.startsWith('wheelAnalysis\\')) {modules.wheelAnalysis.push(e); return false;}
105106
else {return true};
106107
});
107108
//console.log(obj.source_files[0].coverage.length);
108-
return obj = formatCoverage(classes, rigboxPath);
109+
formatCoverage(modules[repo], rigboxPath, callback);
109110
});
110111
});
111112
};
@@ -115,7 +116,7 @@ const coverage = function(path, sha) {
115116
* @param {String} path - Path to the source code file.
116117
* @returns {Object} key `Hash` contains MD5 digest string of file; `count` contains number of lines in source file
117118
*/
118-
const md5 = function(path) {
119+
md5 = function(path) {
119120
var hash = crypto.createHash('md5'); // Creating hash object
120121
var buf = fs.readFileSync(path, 'utf-8'); // Read in file
121122
var count = buf.split(/\r\n|\r|\n/).length; // Count the number of lines

index.js

Lines changed: 88 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,33 @@
11
/**
22
* @author Miles Wells <[email protected]>
33
* @requires ./queue.js
4+
* @requires ./coverage.js
45
* @requires module:dotenv
56
* @requires module:"@octokit/app"
67
* @requires module:"@octokit/request"
78
* @requires module:express
89
* @requires module:github-webhook-handler
9-
* @requires module:smee-client
1010
*/
1111
const fs = require('fs');
1212
const express = require('express')
1313
const srv = express();
1414
const cp = require('child_process');
1515
const queue = new (require('./queue.js'))()
16+
const Coverage = require('./coverage');
1617
const { App } = require('@octokit/app');
1718
const { request } = require("@octokit/request");
1819

1920
const id = process.env.GITHUB_APP_IDENTIFIER;
2021
const secret = process.env.GITHUB_WEBHOOK_SECRET;
2122

22-
// Create new tunnel to receive hooks
23-
const SmeeClient = require('smee-client')
24-
const smee = new SmeeClient({
25-
source: process.env.WEBHOOK_PROXY_URL,
26-
target: 'http://localhost:3000/github',
27-
logger: console
28-
})
23+
// Configure ssh tunnel
24+
const cmd = 'ssh -tt -R 80:localhost:3000 serveo.net';
25+
const sh = String(cp.execFileSync('where', ['git'])).replace('cmd\\git.exe', 'bin\\sh.exe');
26+
const tunnel = () => {
27+
let ssh = cp.spawn(sh, ['-c', cmd])
28+
ssh.stdout.on('data', (data) => { console.log(`stdout: ${data}`); });
29+
ssh.on('exit', () => { console.log('Reconnecting to Serveo'); tunnel(); });
30+
}
2931

3032
// Create handler to verify posts signed with webhook secret. Content type must be application/json
3133
var createHandler = require('github-webhook-handler');
@@ -75,22 +77,63 @@ srv.post('/github', async (req, res, next) => {
7577
*/
7678
function loadTestRecords(id) {
7779
let obj = JSON.parse(fs.readFileSync('./db.json', 'utf8'));
78-
if (!Array.isArray(obj)) obj = [obj];
79-
let record;
80-
for (record of obj) {
81-
if (record['commit'] === id) {
82-
return record;
83-
}
84-
};
80+
if (!Array.isArray(obj)) obj = [obj]; // Ensure array
81+
return obj.find(o => o.commit === id);
8582
};
8683

87-
/*
8884
// Serve the test results for requested commit id
8985
srv.get('/github/:id', function (req, res) {
9086
console.log(req.params.id)
9187
const record = loadTestRecords(req.params.id);
9288
res.send(record['results']);
93-
}); */
89+
});
90+
91+
// Serve the coverage results
92+
srv.get('/coverage/:repo/:branch', function (req, res) {
93+
// Find head commit of branch
94+
try {
95+
const result = await request('GET /repos/:owner/:repo/git/refs/heads/:branch', {
96+
owner: 'cortex-lab', // @todo Generalize repo owner
97+
repo: req.params.repo,
98+
branch: req.params.branch
99+
});
100+
if result.data.ref.endsWith('/' + req.params.branch) {
101+
console.log('Request for ' + req.params.branch + ' coverage')
102+
let id = result.data.object.sha;
103+
var report = {'schemaVersion': 1, 'label': 'coverage'};
104+
try { // Try to load coverage record
105+
record = await loadTestRecords(id);
106+
if (typeof record == 'undefined' || record['coverage'] == []) {throw 404}; // Test not found for commit
107+
if (record['status'] === 'error') {throw 500}; // Test found for commit but errored
108+
report['message'] = record['coverage'] + '%';
109+
report['color'] = (record['coverage'] > 75 ? 'green' : 'red');
110+
} catch (err) { // No coverage value
111+
report['message'] = (err === 404 ? 'pending' : 'unknown');
112+
report['color'] = 'orange';
113+
// Check test isn't already on the pile
114+
let onPile = false;
115+
for (let job of queue.pile) { if job.id === id { onPile = true; break; } };
116+
if !onPile { // Add test to queue
117+
queue.add({
118+
sha: id,
119+
owner: 'cortex-lab', // @todo Generalize repo owner
120+
repo: req.params.repo,
121+
status: '',
122+
context: ''});
123+
}
124+
} finally { // Send report
125+
res.setHeader('Content-Type', 'application/json');
126+
console.log(report)
127+
res.end(JSON.stringify(report));}
128+
}
129+
} else { throw 404 }; // Specified repo or branch not found
130+
catch (error) {
131+
let msg = (error === 404 ? `${req.params.repo}/${req.params.branch} not found` : error);
132+
console.error(msg)
133+
res.statusCode = 401; // If not found, send 401 for security reasons
134+
res.send(msg);
135+
}
136+
});
94137

95138
// Define how to process tests. Here we checkout git and call MATLAB
96139
queue.process(async (job, done) => {
@@ -114,7 +157,7 @@ queue.process(async (job, done) => {
114157
console.log(stdout)
115158
// Go ahead with MATLAB tests
116159
var runTests;
117-
const timer = setTimeout(function() {
160+
const timer = setTimeout(function() {
118161
console.log('Max test time exceeded')
119162
job.data['status'] = 'error';
120163
job.data['context'] = 'Tests stalled after ~2 min';
@@ -161,6 +204,31 @@ queue.on('finish', job => { // On job end post result to API
161204
});
162205
});
163206

207+
/**
208+
* Callback triggered when job completes. Called when all tests run to end.
209+
* @param {Object} job - Job object which has finished being processed.
210+
* @todo Save full coverage object for future inspection
211+
*/
212+
queue.on('complete', job => { // On job end post result to API
213+
console.log('Updating coverage for ' + job.id)
214+
Coverage('./CoverageResults.xml', job.data.repo, job.data.id, obj => {
215+
// Digest and save percentage coverage
216+
let misses = 0, hits = 0;
217+
for (let file of job.source_files) {
218+
misses += file.coverage.filter(x => x == 0).length;
219+
hits += file.coverage.filter(x => x > 0).length;
220+
}
221+
// Load data and save
222+
let records = JSON.parse(fs.readFileSync('./db.json', 'utf8'));
223+
if (!Array.isArray(records)) records = [records]; // Ensure array
224+
for (let o of records) { if o.commit === job.data.id {o.coverage = hits / (hits + misses)}; break; } // Add percentage
225+
// Save object
226+
fs.writeFile('./db.json', JSON.stringify(records), function(err) {
227+
if (err) { console.log(err); }
228+
});
229+
});
230+
});
231+
164232
// Let fail silently: we report error via status
165233
queue.on('error', err => {return;});
166234
// Log handler errors
@@ -193,7 +261,7 @@ handler.on('push', async function (event) {
193261
// Add a new test job to the queue
194262
queue.add({
195263
sha: commit['id'],
196-
owner: 'cortex-lab',
264+
owner: 'cortex-lab', // @todo Generalize repo owner field
197265
repo: event.payload.repository.name,
198266
status: '',
199267
context: ''
@@ -210,7 +278,7 @@ var server = srv.listen(3000, function () {
210278
console.log("Handler listening at http://%s:%s", host, port)
211279
});
212280
// Start tunnel
213-
const events = smee.start()
281+
tunnel();
214282

215283
// Log any unhandled errors
216284
process.on('unhandledRejection', (reason, p) => {

runAllTests.m

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ function runAllTests(id, repo)
22
%% Script for running all Rigbox tests
33
% To be called for code checks and the like
44
% TODO May add flags for levels of testing
5-
% TODO Method setup in dat_test may become global fixture
6-
% TODO Delete sinusoidLayer_test from this folder
5+
% TODO Possible for repo commit sha conflict
6+
% @body Technically two different repos can have the same commit hash, in
7+
% which case the db.json file should be restructured
78
if nargin == 1; repo = 'rigbox'; end
89
try
910
%% Initialize enviroment
@@ -54,20 +55,26 @@ function runAllTests(id, repo)
5455
'Coverage file may not have been updated')
5556

5657
%% Diagnostics
57-
% failed = {all_tests([results.Failed]).Name}';
58-
% [info,filePaths] = checkcode(...);
59-
% Load benchmarks and compare for performance tests?
58+
% Summarize the results of the tests and write results to the JSON file
59+
% located at dbPath
6060
status = iff(all([results.Passed]), 'success', 'failure');
6161
failStr = sprintf('%i/%i tests failed', sum([results.Failed]), length(results));
6262
context = iff(all([results.Passed]), 'All passed', failStr);
6363
report = struct(...
6464
'commit', id, ...
6565
'results', results, ...
6666
'status', status, ...
67-
'description', context);
67+
'description', context, ...
68+
'coverage', []); % Coverage updated by Node.js script
6869
if file.exists(dbPath)
6970
data = jsondecode(fileread(dbPath));
70-
report = [report; data];
71+
idx = strcmp(id, {data.commit}); % Check record exists for this commit
72+
if any(idx) % If so update record
73+
data(idx) = report;
74+
report = data;
75+
else % ...or append record
76+
report = [report; data];
77+
end
7178
end
7279
fid = fopen(dbPath, 'w+');
7380
fprintf(fid, '%s', jsonencode(report));

0 commit comments

Comments
 (0)