Skip to content

Commit 5726447

Browse files
authored
Merge pull request #68 from cortex-lab/dev
v3.0.0
2 parents 3a7efa9 + df64c8c commit 5726447

21 files changed

+3619
-2199
lines changed

CHANGELOG.md

+28-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,33 @@
11
# Changelog
22

3-
## [Latest](https://github.com/cortex-lab/matlab-ci/commits/master) [2.2.0]
3+
## [Latest](https://github.com/cortex-lab/matlab-ci/commits/master) [3.0.0]
4+
5+
## Added
6+
7+
- any number of tasks may be added for a job, which are then executed in series
8+
- now serves a Webpage that shows the log in realtime
9+
- added a jobs endpoint to see which jobs are on the pile
10+
- stderr is piped to log file
11+
- flake8 errors are neatly captured in GitHub status description
12+
- param to skip checks when only ignored files changed
13+
- param to skip draft PR event checks
14+
15+
## Modified
16+
17+
- renamed MATLAB-CI to labCI
18+
- records endpoint can return pending jobs
19+
- tests badge endpoint returns 'error' on errored tests instead of 'unknown'
20+
- job waits for coverage calculation and updating of records before finishing
21+
- On successful completion of tests the duration is appended to the description
22+
23+
## [2.2.1]
24+
25+
## Modified
26+
27+
- fix error where github event incorrectly rejected
28+
- fix bug incorrect log name when endpoint called with branch name
29+
30+
## [2.2.0]
431

532
## Added
633
- nyc dependency for manual coverage of matlab-ci

README.md

+12-7
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
# MATLAB-ci
1+
# LabCI
22
[![Build Status](https://travis-ci.com/cortex-lab/matlab-ci.svg?branch=master)](https://travis-ci.com/cortex-lab/matlab-ci)
3-
[![Coverage](https://img.shields.io/badge/coverage-81.07-green)](https://img.shields.io/badge/coverage-72.35-yellowgreen)
3+
[![Coverage](https://img.shields.io/badge/coverage-92.13-brightgreen)](https://img.shields.io/badge/coverage-72.35-yellowgreen)
44

5-
A small set of modules written in Node.js for running automated tests of MATLAB code in response to GitHub events. Also submits code coverage to the Coveralls API.
5+
A small set of modules written in Node.js for running automated tests of MATLAB and Python code in response to GitHub events. Also submits code coverage to the Coveralls API.
66

77
Currently unsupported:
88
* Running tests on forked repositories
@@ -26,11 +26,8 @@ Create a shell/batch script for preparing your environment, and one for running
2626
Add these to the settings.json file in config:
2727
```
2828
{
29-
"setup_function": "./prep_env.BAT",
30-
"test_function": "./run_tests.BAT",
3129
"listen_port": 3000,
3230
"timeout": 480000,
33-
"program": "python",
3431
"strict_coverage": false,
3532
"events": {
3633
"push": {
@@ -40,15 +37,23 @@ Add these to the settings.json file in config:
4037
"pull_request": {
4138
"checks": ["continuous-integration", "coverage"],
4239
"actions": ["opened", "synchronize", "reopened"],
43-
"ref_ignore": ["documentation", "gh-pages"]
40+
"ref_ignore": ["documentation", "gh-pages"],
41+
"files_ignore": [".*\\.yml", ".*\\.md", "LICEN[SC]E"]
4442
}
4543
}
44+
"routines": {
45+
"*": ["prep_env.BAT", "run_tests.BAT"]
46+
}
4647
}
4748
```
4849
Some extra optional settings:
4950

5051
- `shell` - optional shell to use when calling scripts (see `child_process.execFile` options).
5152
- `events:event:ref_include` - same as `ref_ignore`, but a pass list instead of block list.
53+
- `events:event:files_ignore` - list of files whose changes can be ignored. If only ignored files
54+
are changed checks are skipped.
55+
- `events:pull_request:ignore_drafts` - if true draft pull request actions are skipped (NB: Be
56+
sure to add 'ready_for_review' to the actions list when ignoring drafts).
5257
- `kill_children` - if present and true, `tree-kill` is used to kill the child processes, required
5358
if shell/batch script forks test process (e.g. a batch script calls python).
5459
- `repos` - an array of submodules or map of modules to their corresponding paths.

config/config.js

+35-35
Original file line numberDiff line numberDiff line change
@@ -2,78 +2,78 @@ const userSettings = require('./settings.json') || {}; // User settings
22
const path = require('path');
33
env = process.env.NODE_ENV || 'production';
44
const appdata = process.env.APPDATA || process.env.HOME;
5-
const dataPath = process.env.APPDATA? path.join(appdata, 'CI') : path.join(appdata, '.ci');
5+
const dataPath = process.env.APPDATA ? path.join(appdata, 'CI') : path.join(appdata, '.ci');
66
const fixtureDir = path.resolve(__dirname, '..', 'test', 'fixtures');
77
const dbFilename = '.db.json';
88
let settings;
99

1010
// Defaults for when there's no user file; will almost certainly fail
11-
defaults = {
12-
setup_function: null,
13-
test_function: null,
11+
const defaults = {
12+
max_description_len: 140, // GitHub status API has a description char limit
1413
listen_port: 3000,
15-
timeout: 8*60000,
16-
program: "python",
14+
timeout: 8 * 60000,
1715
strict_coverage: false,
1816
events: {
1917
push: {
2018
checks: null,
21-
ref_ignore: ["documentation", "gh-pages"]
19+
ref_ignore: ['documentation', 'gh-pages']
2220
},
2321
pull_request: {
24-
checks: ["continuous-integration", "coverage"],
25-
actions: ["opened", "synchronize", "reopen"],
26-
ref_ignore: ["documentation", "gh-pages"]
22+
checks: ['continuous-integration', 'coverage'],
23+
actions: ['opened', 'synchronize', 'reopen'],
24+
ref_ignore: ['documentation', 'gh-pages']
2725
}
2826
},
2927
dataPath: dataPath,
3028
dbFile: path.join(dataPath, dbFilename)
31-
}
29+
};
3230

3331
// Settings for the tests
34-
testing = {
32+
const testing = {
3533
listen_port: 3000,
3634
timeout: 60000,
37-
setup_function: 'prep_env.BAT',
38-
test_function: "run_tests.BAT",
3935
events: {
4036
push: {
41-
checks: "continuous-integration",
42-
ref_ignore: "documentation"
37+
checks: 'continuous-integration',
38+
ref_ignore: 'documentation'
4339
},
4440
pull_request: {
45-
checks: ["coverage", "continuous-integration"],
46-
actions: ["opened", "synchronize"],
47-
ref_ignore: ["documentation", "gh-pages"]
41+
checks: ['coverage', 'continuous-integration'],
42+
actions: ['opened', 'synchronize'],
43+
ref_ignore: ['documentation', 'gh-pages']
4844
}
4945
},
46+
routines: {
47+
'*': ['prep_env.BAT', 'run_tests.BAT']
48+
},
5049
dataPath: fixtureDir,
5150
dbFile: path.join(fixtureDir, dbFilename) // cache of test results
52-
}
51+
};
5352

5453
// Pick the settings to return
5554
if (env.startsWith('test')) {
56-
settings = testing;
55+
settings = testing;
5756
} else if (userSettings) {
58-
settings = userSettings;
59-
if (!('dbFile' in settings)) {
60-
settings.dbFile = path.join(dataPath, dbFilename)
61-
}
62-
if (!('dataPath' in settings)) {
63-
settings.dataPath = dataPath;
64-
}
57+
settings = userSettings;
6558
} else {
66-
settings = defaults;
59+
settings = defaults;
60+
}
61+
62+
// Ensure defaults for absent fields
63+
for (let field in defaults) {
64+
if (!(field in settings)) settings[field] = defaults[field];
6765
}
6866

6967
// Check ENV set up correctly
7068
required = ['GITHUB_PRIVATE_KEY', 'GITHUB_APP_IDENTIFIER', 'GITHUB_WEBHOOK_SECRET',
71-
'WEBHOOK_PROXY_URL', 'REPO_PATH', 'REPO_NAME', 'REPO_OWNER', 'TUNNEL_HOST',
72-
'TUNNEL_SUBDOMAIN'];
73-
missing = required.filter(o => { return !process.env[o] });
69+
'WEBHOOK_PROXY_URL', 'REPO_PATH', 'REPO_NAME', 'REPO_OWNER', 'TUNNEL_HOST',
70+
'TUNNEL_SUBDOMAIN'];
71+
missing = required.filter(o => {
72+
return !process.env[o];
73+
});
7474
if (missing.length > 0) {
75-
errMsg = `Env not set correctly; the following variables not found: \n${missing.join(', ')}`
76-
throw ReferenceError(errMsg)
75+
errMsg = `Env not set correctly; the following variables not found: \n${missing.join(', ')}`;
76+
throw ReferenceError(errMsg);
7777
}
7878

79-
module.exports = { settings }
79+
module.exports = { settings };

config/settings.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
{
2-
"setup_function": "prep_env.BAT",
3-
"test_function": "run_tests.BAT",
42
"listen_port": 3000,
53
"timeout": 480000,
6-
"program": "python",
74
"strict_coverage": false,
85
"events": {
96
"push": {
@@ -15,5 +12,8 @@
1512
"actions": ["opened", "synchronize", "reopened"],
1613
"ref_ignore": ["documentation", "gh-pages"]
1714
}
15+
},
16+
"routines": {
17+
"*": ["prep_env.BAT", "run_tests.BAT"]
1818
}
1919
}

coverage.js

+68-72
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,9 @@
2121
const fs = require('fs'),
2222
xml2js = require('xml2js'),
2323
crypto = require('crypto'),
24-
assert = require('assert').strict,
2524
parser = new xml2js.Parser(),
2625
path = require('path');
27-
var timestamp, cb;
26+
var timestamp;
2827

2928
var token = process.env.COVERALLS_TOKEN;
3029

@@ -33,14 +32,14 @@ var token = process.env.COVERALLS_TOKEN;
3332
* Loads file containing source code, returns a hash and line count
3433
* @param {String} path - Path to the source code file.
3534
* @returns {Object} key `Hash` contains MD5 digest string of file; `count` contains number of lines in source file
36-
* @todo Make asynchronous
3735
*/
3836
function md5(path) {
39-
var hash = crypto.createHash('md5'); // Creating hash object
40-
var buf = fs.readFileSync(path, 'utf-8'); // Read in file
41-
var count = buf.split(/\r\n|\r|\n/).length; // Count the number of lines
42-
hash.update(buf, 'utf-8'); // Update hash
43-
return {hash: hash.digest('hex'), count: count};
37+
const hash = crypto.createHash('md5'); // Creating hash object
38+
const buf = fs.readFileSync(path, 'utf-8'); // Read in file
39+
const count = buf.split(/\r\n|\r|\n/).length; // Count the number of lines
40+
hash.update(buf, 'utf-8'); // Update hash
41+
42+
return {hash: hash.digest('hex'), count: count};
4443
}
4544

4645

@@ -50,42 +49,41 @@ function md5(path) {
5049
* @param {Array} classList - An array of class objects from the loaded XML file.
5150
* @param {String} srcPath - The root path of the code repository.
5251
* @param {String} sha - The commit SHA for this coverage test.
53-
* @param {function} callback - The callback function to run when complete. Takes object containing array of source
54-
* code files and their code coverage
5552
* @returns {Object}
5653
* @todo Generalize path default
57-
* @fixme Doesn't work with python's coverage
5854
*/
59-
function formatCoverage(classList, srcPath, sha) {
60-
var job = {};
61-
var sourceFiles = [];
62-
var digest;
63-
srcPath = typeof srcPath != "undefined" ? srcPath : process.env.HOMEPATH; // default to home dir
64-
// For each class, create file object containing array of lines covered and add to sourceFile array
65-
classList.forEach( async c => {
66-
let file = {}; // Initialize file object
67-
let fullPath = c.$.filename.startsWith(srcPath)? c.$.filename : path.join(srcPath, c.$.filename);
68-
digest = md5(fullPath); // Create digest and line count for file
69-
let lines = new Array(digest.count).fill(null); // Initialize line array the size of source code file
70-
c.lines[0].line.forEach(ln => {
71-
let n = Number(ln.$.number);
72-
if (n <= digest.count) {lines[n] = Number(ln.$.hits) }
73-
});
74-
// create source file object
75-
file.name = c.$.filename;
76-
file.source_digest = digest.hash;
77-
file.coverage = lines; // file.coverage[0] == line 1
78-
sourceFiles.push(file);
79-
});
55+
async function formatCoverage(classList, srcPath, sha) {
56+
var job = {};
57+
var sourceFiles = [];
58+
var digest;
59+
srcPath = typeof srcPath != 'undefined' ? srcPath : process.env.REPO_PATH; // default to home dir
60+
// For each class, create file object containing array of lines covered and add to sourceFile array
61+
await Promise.all(classList.map(async c => {
62+
let file = {}; // Initialize file object
63+
let fullPath = c.$.filename.startsWith(srcPath) ? c.$.filename : path.join(srcPath, c.$.filename);
64+
digest = md5(fullPath); // Create digest and line count for file
65+
let lines = new Array(digest.count).fill(null); // Initialize line array the size of source code file
66+
c.lines[0].line.forEach(ln => {
67+
let n = Number(ln.$.number);
68+
if (n <= digest.count) {
69+
lines[n] = Number(ln.$.hits);
70+
}
71+
});
72+
// create source file object
73+
file.name = c.$.filename;
74+
file.source_digest = digest.hash;
75+
file.coverage = lines; // file.coverage[0] == line 1
76+
sourceFiles.push(file);
77+
}));
8078

81-
job.repo_token = token; // env secret token?
82-
job.service_name = `coverage/${process.env.USERDOMAIN}`;
83-
// The associated pull request ID of the build. Used for updating the status and/or commenting.
84-
job.service_pull_request = '';
85-
job.source_files = sourceFiles;
86-
job.commit_sha = sha;
87-
job.run_at = timestamp; // "2013-02-18 00:52:48 -0800"
88-
cb(job);
79+
job.repo_token = token; // env secret token
80+
job.service_name = `coverage/${process.env.USERDOMAIN}`;
81+
// The associated pull request ID of the build. Used for updating the status and/or commenting.
82+
job.service_pull_request = '';
83+
job.source_files = sourceFiles;
84+
job.commit_sha = sha;
85+
job.run_at = timestamp; // "2013-02-18 00:52:48 -0800"
86+
return job;
8987
}
9088

9189
/**
@@ -95,44 +93,42 @@ function formatCoverage(classList, srcPath, sha) {
9593
* @param {String} sha - The commit SHA for this coverage test
9694
* @param {String} repo - The repo to which the commit belongs
9795
* @param {Array} submodules - A list of submodules for separating coverage into
98-
* @param {function} callback - The callback function to run when complete
9996
* @see {@link https://github.com/cobertura/cobertura/wiki|Cobertura Wiki}
10097
*/
101-
function coverage(path, repo, sha, submodules, callback) {
102-
cb = callback; // @fixme Making callback global feels hacky
103-
fs.readFile(path, function(err, data) { // Read in XML file
104-
if (err) {throw err} // @fixme deal with file not found errors
105-
parser.parseString(data, function (err, result) { // Parse XML
106-
// Extract root code path
107-
const rootPath = (result.coverage.sources[0].source[0] || process.env.REPO_PATH).replace(/[\/|\\]+$/, '')
108-
assert(rootPath.endsWith(process.env.REPO_NAME), 'Incorrect source code repository')
109-
timestamp = new Date(result.coverage.$.timestamp*1000); // Convert UNIX timestamp to Date object
110-
let classes = []; // Initialize classes array
98+
function coverage(path, repo, sha, submodules) {
99+
return fs.promises.readFile(path) // Read in XML file
100+
.then(parser.parseStringPromise) // Parse XML
101+
.then(result => {
102+
// Extract root code path
103+
const rootPath = (result.coverage.sources[0].source[0] || process.env.REPO_PATH)
104+
.replace(/[\/|\\]+$/, '');
105+
timestamp = new Date(result.coverage.$.timestamp * 1000); // Convert UNIX timestamp to Date object
106+
let classes = []; // Initialize classes array
111107

112-
const packages = result.coverage.packages[0].package;
113-
packages.forEach(pkg => { classes.push(pkg.classes[0].class) }); // Get all classes
114-
classes = classes.reduce((acc, val) => acc.concat(val), []); // Flatten
108+
const packages = result.coverage.packages[0].package;
109+
packages.forEach(pkg => { classes.push(pkg.classes[0].class); }); // Get all classes
110+
classes = classes.reduce((acc, val) => acc.concat(val), []); // Flatten
115111

116-
// The submodules
117-
const byModule = {'main' : []};
118-
submodules.forEach((x) => { byModule[x] = []; }); // initialize submodules
112+
// The submodules
113+
const byModule = {'main': []};
114+
submodules.forEach((x) => { byModule[x] = []; }); // initialize submodules
119115

120-
// Sort into piles
121-
byModule['main'] = classes.filter(function (e) {
122-
if (e.$.filename.search(/(tests\\|_.*test|docs\\)/i) !== -1) {return false;} // Filter out tests and docs
123-
if (!Array.isArray(e.lines[0].line)) {return false;} // Filter out files with no functional lines
124-
for (let submodule of submodules) {
125-
if (e.$.filename.startsWith(submodule)) {
126-
byModule[submodule].push(e); return false;
127-
}
128-
}
129-
return true;
116+
// Sort into piles
117+
byModule['main'] = classes.filter(function (e) {
118+
if (e.$.filename.search(/(tests\\|_.*test|docs\\)/i) !== -1) return false; // Filter out tests and docs
119+
if (!Array.isArray(e.lines[0].line)) return false; // Filter out files with no functional lines
120+
for (let submodule of submodules) {
121+
if (e.$.filename.startsWith(submodule)) {
122+
byModule[submodule].push(e);
123+
return false;
124+
}
125+
}
126+
return true;
127+
});
128+
// Select module
129+
let modules = byModule[repo] || byModule['main'];
130+
return formatCoverage(modules, rootPath, sha);
130131
});
131-
// Select module
132-
let modules = byModule[repo] || byModule['main'];
133-
formatCoverage(modules, rootPath, callback);
134-
});
135-
});
136132
}
137133

138134

0 commit comments

Comments
 (0)