Skip to content

Commit 3edec0a

Browse files
authored
Merge pull request #48 from cortex-lab/dev
2.1
2 parents a19038d + 28e14ea commit 3edec0a

14 files changed

+312
-207
lines changed

CHANGELOG.md

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
# Changelog
22

3-
## [Latest](https://github.com/cortex-lab/matlab-ci/commits/master) [2.0.0]
3+
## [Latest](https://github.com/cortex-lab/matlab-ci/commits/master) [2.1.0]
4+
5+
### Modified
6+
- More generic handling of submodules
7+
- Fix for computing coverage properly
8+
- Fix to issue #43
9+
- Fix for issue where jobs added when already on pile
10+
- New tests added for listSubmodules
11+
- listSubmodules and gerRepoPath now exposed in lib
12+
- Removed chai-spies dependency
13+
14+
15+
## [2.0.0]
16+
417
### Added
518

619
- there are now three major modules: lib, serve and main

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Some extra optional settings:
5050
- `events:event:ref_include` - same as `ref_ignore`, but a pass list instead of block list.
5151
- `kill_children` - if present and true, `tree-kill` is used to kill the child processes, required
5252
if shell/batch script forks test process (e.g. a batch script calls python).
53+
- `repos` - an array of submodules or map of modules to their corresponding paths.
5354

5455
Finally, ensure these scripts are executable by node:
5556
```

coverage.js

+18-14
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ var token = process.env.COVERALLS_TOKEN;
3333
* Loads file containing source code, returns a hash and line count
3434
* @param {String} path - Path to the source code file.
3535
* @returns {Object} key `Hash` contains MD5 digest string of file; `count` contains number of lines in source file
36+
* @todo Make asynchronous
3637
*/
3738
function md5(path) {
3839
var hash = crypto.createHash('md5'); // Creating hash object
@@ -58,12 +59,13 @@ function md5(path) {
5859
function formatCoverage(classList, srcPath, sha) {
5960
var job = {};
6061
var sourceFiles = [];
62+
var digest;
6163
srcPath = typeof srcPath != "undefined" ? srcPath : process.env.HOMEPATH; // default to home dir
6264
// For each class, create file object containing array of lines covered and add to sourceFile array
6365
classList.forEach( async c => {
6466
let file = {}; // Initialize file object
6567
let fullPath = c.$.filename.startsWith(srcPath)? c.$.filename : path.join(srcPath, c.$.filename);
66-
var digest = md5(fullPath); // Create digest and line count for file // FIXME use path lib
68+
digest = md5(fullPath); // Create digest and line count for file
6769
let lines = new Array(digest.count).fill(null); // Initialize line array the size of source code file
6870
c.lines[0].line.forEach(ln => {
6971
let n = Number(ln.$.number);
@@ -92,19 +94,18 @@ function formatCoverage(classList, srcPath, sha) {
9294
* @param {String} path - Path to the XML file containing coverage information.
9395
* @param {String} sha - The commit SHA for this coverage test
9496
* @param {String} repo - The repo to which the commit belongs
97+
* @param {Array} submodules - A list of submodules for separating coverage into
9598
* @param {function} callback - The callback function to run when complete
96-
* @todo Generalize ignoring of submodules
9799
* @see {@link https://github.com/cobertura/cobertura/wiki|Cobertura Wiki}
98100
*/
99-
function coverage(path, repo, sha, callback) {
101+
function coverage(path, repo, sha, submodules, callback) {
100102
cb = callback; // @fixme Making callback global feels hacky
101103
fs.readFile(path, function(err, data) { // Read in XML file
102-
// @fixme deal with file not found errors
103-
if (err) {throw err}
104+
if (err) {throw err} // @fixme deal with file not found errors
104105
parser.parseString(data, function (err, result) { // Parse XML
105106
// Extract root code path
106107
const rootPath = (result.coverage.sources[0].source[0] || process.env.REPO_PATH).replace(/[\/|\\]+$/, '')
107-
assert(rootPath.toLowerCase().endsWith(repo || process.env.REPO_NAME), 'Incorrect source code repository')
108+
assert(rootPath.endsWith(process.env.REPO_NAME), 'Incorrect source code repository')
108109
timestamp = new Date(result.coverage.$.timestamp*1000); // Convert UNIX timestamp to Date object
109110
let classes = []; // Initialize classes array
110111

@@ -113,19 +114,22 @@ function coverage(path, repo, sha, callback) {
113114
classes = classes.reduce((acc, val) => acc.concat(val), []); // Flatten
114115

115116
// The submodules
116-
var modules = {'main' : [], 'alyx-matlab' : [], 'signals' : [], 'npy-matlab' : [], 'wheelAnalysis' : []};
117+
const byModule = {'main' : []};
118+
submodules.forEach((x) => { byModule[x] = []; }); // initialize submodules
119+
117120
// Sort into piles
118-
modules['main'] = classes.filter(function (e) {
121+
byModule['main'] = classes.filter(function (e) {
119122
if (e.$.filename.search(/(tests\\|_.*test|docs\\)/i) !== -1) {return false;} // Filter out tests and docs
120123
if (!Array.isArray(e.lines[0].line)) {return false;} // Filter out files with no functional lines
121-
if (e.$.filename.startsWith('alyx-matlab\\')) {modules['alyx-matlab'].push(e); return false;}
122-
if (e.$.filename.startsWith('signals\\')) {modules.signals.push(e); return false;}
123-
if (e.$.filename.startsWith('npy-matlab\\')) {modules['npy-matlab'].push(e); return false;}
124-
if (e.$.filename.startsWith('wheelAnalysis\\')) {modules.wheelAnalysis.push(e); return false;}
125-
else {return true}
124+
for (let submodule of submodules) {
125+
if (e.$.filename.startsWith(submodule)) {
126+
byModule[submodule].push(e); return false;
127+
}
128+
}
129+
return true;
126130
});
127131
// Select module
128-
modules = modules[repo.toLowerCase()] || modules['main'];
132+
let modules = byModule[repo] || byModule['main'];
129133
formatCoverage(modules, rootPath, callback);
130134
});
131135
});

lib.js

+43-18
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const path = require('path');
77
const createDebug = require('debug');
88
const localtunnel = require('localtunnel');
99
const kill = require('tree-kill');
10+
const shell = require('shelljs');
1011

1112
const config = require('./config/config').settings;
1213
const Coverage = require('./coverage');
@@ -175,19 +176,6 @@ function partial(func) {
175176
};
176177
}
177178

178-
function chain(func) {
179-
return function curried(...args) {
180-
if (args.length >= func.length) {
181-
return func.apply(this, args);
182-
} else {
183-
return function(...args2) {
184-
return curried.apply(this, args2.concat(args));
185-
}
186-
}
187-
};
188-
}
189-
190-
191179

192180
/**
193181
* Check if job already has record, if so, update from record and finish, otherwise call tests function.
@@ -235,6 +223,42 @@ const openTunnel = async () => {
235223
}
236224

237225

226+
/**
227+
* Lists the submodules within a Git repository. If none are found null is returned.
228+
* @param {String} repoPath - The path of the repository
229+
* @returns {Array} A list of submodule names, or null if none were found
230+
*/
231+
function listSubmodules(repoPath) {
232+
if (!shell.which('git')) { throw new Error('Git not found on path'); }
233+
shell.pushd(repoPath);
234+
let listModules = 'git config --file .gitmodules --get-regexp path';
235+
const modules = shell.exec(listModules)
236+
shell.popd();
237+
return (!modules.code && modules.stdout !== '')? modules.match(/(?<=submodule.)[\w-]+/g) : [];
238+
}
239+
240+
241+
/**
242+
* Get the corresponding repository path for a given repo. The function first checks the settings.
243+
* If the `repos` field doesn't exist, the path in ENV is used. If the name is not a key in the
244+
* `repos` object then we check each repo path for submodules and return the first matching
245+
* submodule path. Otherwise returns null.
246+
* @param {String} name - The name of the repository
247+
* @returns {String} The repository path if found
248+
*/
249+
function getRepoPath(name) {
250+
if (!config.repos) { return process.env['REPO_PATH']; } // Legacy, to remove
251+
if (config.repos[name]) { return config.repos[name]; } // Found path, return
252+
const modules = listSubmodules(process.env['REPO_PATH']);
253+
let repoPath = process.env['REPO_PATH'];
254+
if (modules && modules.includes(name)) {
255+
// If the repo is a submodule, modify path
256+
repoPath += (path.sep + name);
257+
}
258+
return repoPath; // No modules matched, return default
259+
}
260+
261+
238262
/**
239263
* Starts a timer with a callback to kill the job's process.
240264
* @param {Object} job - The Job with an associated process in the data field.
@@ -263,8 +287,9 @@ function computeCoverage(job) {
263287
return;
264288
}
265289
console.log('Updating coverage for job #' + job.id)
266-
let xmlPath = path.join(config.dataPath, 'reports', job.data.sha, 'CoverageResults.xml')
267-
Coverage(xmlPath, job.data.repo, job.data.sha, obj => {
290+
const xmlPath = path.join(config.dataPath, 'reports', job.data.sha, 'CoverageResults.xml')
291+
const modules = listSubmodules(process.env.REPO_PATH);
292+
Coverage(xmlPath, job.data.repo, job.data.sha, modules, obj => {
268293
// Digest and save percentage coverage
269294
let misses = 0, hits = 0;
270295
for (let file of obj.source_files) {
@@ -388,7 +413,7 @@ function getBadgeData(data) {
388413
report['color'] = 'orange';
389414
// Check test isn't already on the pile
390415
let onPile = false;
391-
for (let job of queue.pile) { if (job.id === id) { onPile = true; break; } }
416+
for (let job of queue.pile) { if (job.data.sha === id) { onPile = true; break; } }
392417
if (!onPile) { // Add test to queue
393418
data['skipPost'] = true
394419
queue.add(data);
@@ -397,7 +422,7 @@ function getBadgeData(data) {
397422
record = Array.isArray(record) ? record.pop() : record; // in case of duplicates, take last
398423
switch (data.context) {
399424
case 'status':
400-
if (record['status'] === 'error' || !record['coverage']) {
425+
if (record['status'] === 'error') {
401426
report['message'] = 'unknown';
402427
report['color'] = 'orange';
403428
} else {
@@ -433,5 +458,5 @@ class APIError extends Error {
433458
module.exports = {
434459
ensureArray, loadTestRecords, compareCoverage, computeCoverage, getBadgeData, log, shortID,
435460
openTunnel, APIError, queue, partial, startJobTimer, updateJobFromRecord, shortCircuit, isSHA,
436-
fullpath, strToBool, saveTestRecords
461+
fullpath, strToBool, saveTestRecords, listSubmodules, getRepoPath
437462
}

package-lock.json

-6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
},
3232
"devDependencies": {
3333
"chai": "^4.2.0",
34-
"chai-spies": "^1.0.0",
3534
"fake-timers": "^0.1.2",
3635
"mocha": "^8.2.0",
3736
"nock": "^13.0.4",

runAllTests.m

+5-4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ function runAllTests(id, repo, logDir)
1212
try
1313
%% Initialize enviroment
1414
dbPath = fullfile(logDir, '.db.json'); % TODO Load from config file
15+
[~, repo] = fileparts(repo); % Ensure not full path
1516
fprintf('Running tests\n')
1617
fprintf('Repo = %s, sha = %s\n', repo, id)
1718
origDir = pwd;
@@ -30,7 +31,7 @@ function runAllTests(id, repo, logDir)
3031
main_tests = testsuite('IncludeSubfolders', true);
3132

3233
%% Gather signals tests
33-
root = getOr(dat.paths,'rigbox');
34+
root = getOr(dat.paths, 'rigbox');
3435
signals_tests = testsuite(fullfile(root, 'signals', 'tests'), ...
3536
'IncludeSubfolders', true);
3637

@@ -44,11 +45,11 @@ function runAllTests(id, repo, logDir)
4445
% the sortByFixtures method to sort the suite.
4546
all_tests = [main_tests signals_tests alyx_tests];
4647
% If the repo under test is alyx, filter out irrelevent tests
47-
if endsWith(repo, 'alyx')
48+
if strcmp(repo, 'alyx')
4849
all_tests = all_tests(startsWith({all_tests.Name}, 'Alyx', 'IgnoreCase', true));
49-
elseif endsWith(repo, 'alyx-matlab')
50+
elseif strcmp(repo, 'alyx-matlab')
5051
all_tests = alyx_tests;
51-
elseif endsWith(repo, 'signals')
52+
elseif strcmp(repo, 'signals')
5253
all_tests = signals_tests;
5354
end
5455

serve.js

+17-48
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,14 @@ async function setAccessToken() {
8585

8686
///////////////////// MAIN APP ENTRY POINT /////////////////////
8787

88+
/**
89+
* Register invalid Github POST requests to handler via /github endpoint.
90+
* Failed spoof attempts may end up here but most likely it will be unsupported webhook events.
91+
*/
92+
handler.on('error', function (err) {
93+
console.log('Error:', err.message);
94+
});
95+
8896
/**
8997
* Callback to deal with POST requests from /github endpoint, authenticates as app and passes on
9098
* request to handler.
@@ -97,17 +105,13 @@ srv.post('/github', async (req, res, next) => {
97105
console.log('Post received')
98106
let id = req.header('x-github-hook-installation-target-id');
99107
if (id != process.env.GITHUB_APP_IDENTIFIER) { next(); return; } // Not for us; move on
100-
await setAccessToken();
101-
log.extend('event')('X-GitHub-Event: %s', req.header('X-GitHub-Event'));
102-
handler(req, res, () => res.end('ok'));
103-
});
104-
105-
/**
106-
* Register invalid Github POST requests to handler via /github endpoint.
107-
* Failed spoof attempts may end up here but most likely it will be unsupported webhook events.
108-
*/
109-
handler.on('error', function (err) {
110-
console.log('Error:', err.message);
108+
if (req.header('X-GitHub-Event') in supportedEvents) {
109+
await setAccessToken();
110+
handler(req, res, () => res.end('ok'));
111+
} else {
112+
log('GitHub Event "%s" not supported', req.header('X-GitHub-Event'));
113+
res.sendStatus(400);
114+
}
111115
});
112116

113117

@@ -222,7 +226,7 @@ function runTests(job) {
222226

223227
// Go ahead with tests
224228
const sha = job.data['sha'];
225-
const repoPath = getRepoPath(job.data.repo);
229+
const repoPath = lib.getRepoPath(job.data.repo);
226230
const logName = path.join(config.dataPath, 'reports', sha, `std_output-${lib.shortID(sha)}.log`);
227231
let fcn = lib.fullpath(config.test_function);
228232
debug('starting test child process %s', fcn);
@@ -278,7 +282,7 @@ function runTests(job) {
278282

279283
function prepareEnv(job, callback) {
280284
log('Preparing environment for job #%g', job.id)
281-
const repoPath = getRepoPath(job.data.repo);
285+
const repoPath = lib.getRepoPath(job.data.repo);
282286
switch (config.setup_function) {
283287
case undefined:
284288
// run some basic git commands
@@ -342,41 +346,6 @@ function checkout(repoPath, ref) {
342346
}
343347

344348

345-
/**
346-
* Lists the submodules within a Git repository. If none are found null is returned.
347-
* @param {String} repoPath - The path of the repository
348-
* @returns {Array} A list of submodule names, or null if none were found
349-
*/
350-
function listSubmodules(repoPath) {
351-
if (!shell.which('git')) { throw new Error('Git not found on path'); }
352-
shell.pushd(repoPath);
353-
let listModules = 'git config --file .gitmodules --get-regexp path | awk \'{ print $2 }\'';
354-
const modules = shell.exec(listModules);
355-
shell.popd();
356-
return (!modules.code && modules.stdout !== '')? modules.split('\n') : null;
357-
}
358-
359-
/**
360-
* Get the corresponding repository path for a given repo. The function first checks the settings.
361-
* If the `repos` field doesn't exist, the path in ENV is used. If the name is not a key in the
362-
* `repos` object then we check each repo path for submodules and return the first matching
363-
* submodule path. Otherwise returns null.
364-
* @param {String} name - The name of the repository
365-
* @returns {String} The repository path if found
366-
*/
367-
function getRepoPath(name) {
368-
if (!config.repos) { return process.env['REPO_PATH']; } // Legacy, to remove
369-
if (config.repos.name) { return config.repos.name; } // Found path, return
370-
for (let repo of config.repos) {
371-
let modules = listSubmodules(repo);
372-
if (modules && modules.includes(name)) {
373-
// If the repo is a submodule, modify path
374-
return repo + path.sep + name;
375-
}
376-
}
377-
}
378-
379-
380349
///////////////////// OTHER /////////////////////
381350

382351
/**

0 commit comments

Comments
 (0)