diff --git a/.gitignore b/.gitignore index 51c36df..8b94452 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .idea/* .DS_Store node_modules/* +.env +.pem diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..6ab4755 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,15 @@ +language: node_js +node_js: 12 +env: + - NODE_ENV=test +branches: + only: + - master +jobs: + include: + - name: "windows" + os: windows + env: NODE_ENV=test DOTENV_CONFIG_PATH=.\\test\\fixtures\\.env.test + - name: "linux" + os: linux + env: NODE_ENV=test DOTENV_CONFIG_PATH=./test/fixtures/.env.test diff --git a/CHANGELOG.md b/CHANGELOG.md index 159061f..bdc3a93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,29 @@ # Changelog -## [Latest](https://github.com/cortex-lab/matlab-ci/commits/master) [1.2.0] +## [Latest](https://github.com/cortex-lab/matlab-ci/commits/master) [2.0.0] +### Added + + - there are now three major modules: lib, serve and main + - lots and lots of new tests + - support for running any tests: now CI calls a custom shell script + - new settings file and config module to validate settings + - branch pass list option in settings + - force flag for badge API + - function for converting string to bool in robust way + - function for saving test records + +### Modified + + - complete rewrite of code + - index.js renamed to main.js + - preparing the environment may be optional + - errors during test function are now saved into test record db + - new config options (see readme) + - kill child processes in job timer + + +## [1.2.0] ### Modified - test log formatted in html @@ -23,11 +45,7 @@ - suppress warnings about shadowing builtins in runAllTests - run tests in subfolders - filter out performance tests -<<<<<<< HEAD -- skip tests for commits to branches named 'documentation' -======= - skip checks for commits to branches named 'documentation' ->>>>>>> master ## [1.0.0] ### Added diff --git a/README.md b/README.md index 3e35944..45e4ee8 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,73 @@ # MATLAB-ci +[![Build Status](https://travis-ci.com/cortex-lab/matlab-ci.svg?branch=master)](https://travis-ci.com/cortex-lab/matlab-ci) 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. +Currently unsupported: +* Running tests on forked repositories +* Testing multiple repos (unless they are submodules) + ## Getting Started Run the install script to install all dependencies, then create your .env file containing your App's tokens, secrets, etc. ### Prerequisites -Requires MATLAB 2017a or later, Node.js and Git Bash. The following Node.js modules are required: +Requires Git Bash, npm v6.14 or later and Node.js v12.19.0 or later. For MATLAB tests use MATLAB 2017a or later. ``` -npm install --save express dotenv @octokit/app @octokit/request ... -github-webhook-handler xml2js localtunnel +npm install ./matlab-ci ``` ### Installing -Make sure runAllTests.m is on your MATLAB paths +Create a shell/batch script for preparing your environment, and one for running the tests (i.e. calling Python or MATLAB). +Add these to the settings.json file in config: +``` +{ + "setup_function": "./prep_env.BAT", + "test_function": "./run_tests.BAT", + "listen_port": 3000, + "timeout": 480000, + "program": "python", + "strict_coverage": false, + "events": { + "push": { + "checks": null, + "ref_ignore": ["documentation", "gh-pages"] + }, + "pull_request": { + "checks": ["continuous-integration", "coverage"], + "actions": ["opened", "synchronize", "reopened"], + "ref_ignore": ["documentation", "gh-pages"] + } + } +} +``` +Some extra optional settings: + +- `shell` - optional shell to use when calling scripts (see `child_process.execFile` options). +- `events:event:ref_include` - same as `ref_ignore`, but a pass list instead of block list. +- `kill_children` - if present and true, `tree-kill` is used to kill the child processes, required +if shell/batch script forks test process (e.g. a batch script calls python). + +Finally, ensure these scripts are executable by node: +``` +chmod u+x ./run_tests.BAT +chmod u+x ./prep_env.BAT +``` ## Running the tests -TODO +``` +mocha ./test +``` ## Deployment -To work properly you need to create install a Github app on your target repository and download the private key. Update your .env file like so: +To work properly you need to create install a +[Github app](https://docs.github.com/en/free-pro-team@latest/developers/apps/creating-a-github-app) +on your target repository and download the private key. Update your .env file like so: ``` GITHUB_PRIVATE_KEY=path\to\private-key.pem @@ -42,11 +84,17 @@ TUNNEL_SUBDOMAIN= To run at startup create a batch file with the following command: ```batch -cmd /k node -r dotenv/config dotenv_config_path=/Path/To/Env/Vars ./Path/To/index.js +cmd /k node -r dotenv/config dotenv_config_path=/Path/To/Env/Vars ./Path/To/main.js ``` Create a shortcut in your startup folder ([Windows-logo] + [R] in Windows-10 and enter the command `shell:startup`) +## Test script +Your test script must do the following: +1. Accept a commit ID as an input arg +2. Save the results into the JSON cache file without duplication +3. For code coverage the script must either save the coverage directly, or export a Cobertura formatted XML file. + ## Built With * [LocalTunnel](https://localtunnel.me) - A secure tunneling service diff --git a/config.json b/config.json deleted file mode 100644 index 004d100..0000000 --- a/config.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "timeout": 480000, - "program": "matlab" -} diff --git a/config/config.js b/config/config.js new file mode 100644 index 0000000..10d3c8b --- /dev/null +++ b/config/config.js @@ -0,0 +1,79 @@ +const userSettings = require('./settings.json') || {}; // User settings +const path = require('path'); +env = process.env.NODE_ENV || 'production'; +const appdata = process.env.APPDATA || process.env.HOME; +const dataPath = process.env.APPDATA? path.join(appdata, 'CI') : path.join(appdata, '.ci'); +const fixtureDir = path.resolve(__dirname, '..', 'test', 'fixtures'); +const dbFilename = '.db.json'; +let settings; + +// Defaults for when there's no user file; will almost certainly fail +defaults = { + setup_function: null, + test_function: null, + listen_port: 3000, + timeout: 8*60000, + program: "python", + strict_coverage: false, + events: { + push: { + checks: null, + ref_ignore: ["documentation", "gh-pages"] + }, + pull_request: { + checks: ["continuous-integration", "coverage"], + actions: ["opened", "synchronize", "reopen"], + ref_ignore: ["documentation", "gh-pages"] + } + }, + dataPath: dataPath, + dbFile: path.join(dataPath, dbFilename) +} + +// Settings for the tests +testing = { + listen_port: 3000, + timeout: 60000, + setup_function: 'prep_env.BAT', + test_function: "run_tests.BAT", + events: { + push: { + checks: "continuous-integration", + ref_ignore: "documentation" + }, + pull_request: { + checks: ["coverage", "continuous-integration"], + actions: ["opened", "synchronize"], + ref_ignore: ["documentation", "gh-pages"] + } + }, + dataPath: fixtureDir, + dbFile: path.join(fixtureDir, dbFilename) // cache of test results +} + +// Pick the settings to return +if (env.startsWith('test')) { + settings = testing; +} else if (userSettings) { + settings = userSettings; + if (!('dbFile' in settings)) { + settings.dbFile = path.join(dataPath, dbFilename) + } + if (!('dataPath' in settings)) { + settings.dataPath = dataPath; + } +} else { + settings = defaults; +} + +// Check ENV set up correctly +required = ['GITHUB_PRIVATE_KEY', 'GITHUB_APP_IDENTIFIER', 'GITHUB_WEBHOOK_SECRET', + 'WEBHOOK_PROXY_URL', 'REPO_PATH', 'REPO_NAME', 'REPO_OWNER', 'TUNNEL_HOST', + 'TUNNEL_SUBDOMAIN']; +missing = required.filter(o => { return !process.env[o] }); +if (missing.length > 0) { + errMsg = `Env not set correctly; the following variables not found: \n${missing.join(', ')}` + throw ReferenceError(errMsg) +} + +module.exports = { settings } diff --git a/config/settings.json b/config/settings.json new file mode 100644 index 0000000..d48b970 --- /dev/null +++ b/config/settings.json @@ -0,0 +1,19 @@ +{ + "setup_function": "prep_env.BAT", + "test_function": "run_tests.BAT", + "listen_port": 3000, + "timeout": 480000, + "program": "python", + "strict_coverage": false, + "events": { + "push": { + "checks": null, + "ref_ignore": ["documentation", "gh-pages"] + }, + "pull_request": { + "checks": ["continuous-integration", "coverage"], + "actions": ["opened", "synchronize", "reopened"], + "ref_ignore": ["documentation", "gh-pages"] + } + } +} diff --git a/coverage.js b/coverage.js index 2faa24c..61143be 100644 --- a/coverage.js +++ b/coverage.js @@ -18,34 +18,52 @@ * @license Apache-2.0 */ -var fs = require('fs'), +const fs = require('fs'), xml2js = require('xml2js'), crypto = require('crypto'), assert = require('assert').strict, parser = new xml2js.Parser(), - timestamp, md5, cb; -var timestamp; + path = require('path'); +var timestamp, cb; + var token = process.env.COVERALLS_TOKEN; + +/** + * Loads file containing source code, returns a hash and line count + * @param {String} path - Path to the source code file. + * @returns {Object} key `Hash` contains MD5 digest string of file; `count` contains number of lines in source file + */ +function md5(path) { + var hash = crypto.createHash('md5'); // Creating hash object + var buf = fs.readFileSync(path, 'utf-8'); // Read in file + var count = buf.split(/\r\n|\r|\n/).length; // Count the number of lines + hash.update(buf, 'utf-8'); // Update hash + return {hash: hash.digest('hex'), count: count}; +} + + /** * Formats list of classes from XML file and return object formatted for the Coveralls API. * @see {@link https://docs.coveralls.io/api-reference|Coveralls API docs} * @param {Array} classList - An array of class objects from the loaded XML file. - * @param {String} path - The root path of the code repository. + * @param {String} srcPath - The root path of the code repository. * @param {String} sha - The commit SHA for this coverage test. * @param {function} callback - The callback function to run when complete. Takes object containing array of source * code files and their code coverage * @returns {Object} * @todo Generalize path default + * @fixme Doesn't work with python's coverage */ -const formatCoverage = function(classList, path, sha) { +function formatCoverage(classList, srcPath, sha) { var job = {}; var sourceFiles = []; - path = typeof path != "undefined" ? process.env.HOMEPATH : path; // default to home dir + srcPath = typeof srcPath != "undefined" ? srcPath : process.env.HOMEPATH; // default to home dir // For each class, create file object containing array of lines covered and add to sourceFile array classList.forEach( async c => { let file = {}; // Initialize file object - var digest = md5(path + c.$.filename); // Create digest and line count for file + let fullPath = c.$.filename.startsWith(srcPath)? c.$.filename : path.join(srcPath, c.$.filename); + var digest = md5(fullPath); // Create digest and line count for file // FIXME use path lib let lines = new Array(digest.count).fill(null); // Initialize line array the size of source code file c.lines[0].line.forEach(ln => { let n = Number(ln.$.number); @@ -59,7 +77,7 @@ const formatCoverage = function(classList, path, sha) { }); job.repo_token = token; // env secret token? - job.service_name = `continuous-integration/${process.env.USERDOMAIN}`; + job.service_name = `coverage/${process.env.USERDOMAIN}`; // The associated pull request ID of the build. Used for updating the status and/or commenting. job.service_pull_request = ''; job.source_files = sourceFiles; @@ -75,28 +93,30 @@ const formatCoverage = function(classList, path, sha) { * @param {String} sha - The commit SHA for this coverage test * @param {String} repo - The repo to which the commit belongs * @param {function} callback - The callback function to run when complete - * @todo Remove assert * @todo Generalize ignoring of submodules * @see {@link https://github.com/cobertura/cobertura/wiki|Cobertura Wiki} */ -const coverage = function(path, repo, sha, callback) { +function coverage(path, repo, sha, callback) { cb = callback; // @fixme Making callback global feels hacky fs.readFile(path, function(err, data) { // Read in XML file - parser.parseString(data, function (err, result) { // Parse XML - const rigboxPath = result.coverage.sources[0].source[0]; // Extract root code path - assert(rigboxPath.endsWith('Rigbox\\'), 'Incorrect source code repository') + // @fixme deal with file not found errors + if (err) {throw err} + parser.parseString(data, function (err, result) { // Parse XML + // Extract root code path + const rootPath = (result.coverage.sources[0].source[0] || process.env.REPO_PATH).replace(/[\/|\\]+$/, '') + assert(rootPath.toLowerCase().endsWith(repo || process.env.REPO_NAME), 'Incorrect source code repository') timestamp = new Date(result.coverage.$.timestamp*1000); // Convert UNIX timestamp to Date object let classes = []; // Initialize classes array const packages = result.coverage.packages[0].package; - packages.forEach(package => { classes.push(package.classes[0].class) }); // Get all classes + packages.forEach(pkg => { classes.push(pkg.classes[0].class) }); // Get all classes classes = classes.reduce((acc, val) => acc.concat(val), []); // Flatten // The submodules - var modules = {'rigbox' : [], 'alyx-matlab' : [], 'signals' : [], 'npy-matlab' : [], 'wheelAnalysis' : []}; + var modules = {'main' : [], 'alyx-matlab' : [], 'signals' : [], 'npy-matlab' : [], 'wheelAnalysis' : []}; // Sort into piles - modules['rigbox'] = classes.filter(function (e) { - if (e.$.filename.search(/(tests\\|_.*test|docs\\)/i) != -1) {return false;} // Filter out tests and docs + modules['main'] = classes.filter(function (e) { + if (e.$.filename.search(/(tests\\|_.*test|docs\\)/i) !== -1) {return false;} // Filter out tests and docs if (!Array.isArray(e.lines[0].line)) {return false;} // Filter out files with no functional lines if (e.$.filename.startsWith('alyx-matlab\\')) {modules['alyx-matlab'].push(e); return false;} if (e.$.filename.startsWith('signals\\')) {modules.signals.push(e); return false;} @@ -104,23 +124,13 @@ const coverage = function(path, repo, sha, callback) { if (e.$.filename.startsWith('wheelAnalysis\\')) {modules.wheelAnalysis.push(e); return false;} else {return true} }); - formatCoverage(modules[repo.toLowerCase()], rigboxPath, callback); + // Select module + modules = modules[repo.toLowerCase()] || modules['main']; + formatCoverage(modules, rootPath, callback); }); }); -}; +} -/** - * Loads file containing source code, returns a hash and line count - * @param {String} path - Path to the source code file. - * @returns {Object} key `Hash` contains MD5 digest string of file; `count` contains number of lines in source file - */ -md5 = function(path) { - var hash = crypto.createHash('md5'); // Creating hash object - var buf = fs.readFileSync(path, 'utf-8'); // Read in file - var count = buf.split(/\r\n|\r|\n/).length; // Count the number of lines - hash.update(buf, 'utf-8'); // Update hash - return {hash: hash.digest('hex'), count: count}; -}; // Export Coverage module.exports = coverage; diff --git a/index.js b/index.js deleted file mode 100644 index b3f0807..0000000 --- a/index.js +++ /dev/null @@ -1,493 +0,0 @@ -/** - * @author Miles Wells - * @requires ./queue.js - * @requires ./coverage.js - * @requires module:dotenv - * @requires module:"@octokit/app" - * @requires module:"@octokit/request" - * @requires module:express - * @requires module:localtunnel - * @requires module:github-webhook-handler - * @todo save auxiliary configuration into a separate config file - * @todo add abort option for when new commits added - */ -import * as path from 'path'; - -const fs = require('fs'); -const express = require('express') -const srv = express(); -const cp = require('child_process'); -const queue = new (require('./queue.js'))() -const Coverage = require('./coverage'); -const { App } = require('@octokit/app'); -const { request } = require("@octokit/request"); -const localtunnel = require('localtunnel'); - -const id = process.env.GITHUB_APP_IDENTIFIER; -const secret = process.env.GITHUB_WEBHOOK_SECRET; -const appdata = process.env.APPDATA || process.env.HOMEPATH; -const dbFile = path.join(appdata, '.ci-db.json') // cache of test results -const config = require('./config.json'); -const timeout = config.timeout || 8*60000 - -// Configure a secure tunnel -const openTunnel = async () => { - let args = { - port: 3000, - subdomain: process.env.TUNNEL_SUBDOMAIN, - host: process.env.TUNNEL_HOST - }; - const tunnel = await localtunnel(args); - console.log(`Tunnel open on: ${tunnel.url}`); - tunnel.on('close', () => {console.log('Reconnecting'); openTunnel(); }); - tunnel.on('error', (e) => { console.error(e) }); -} - -// Create handler to verify posts signed with webhook secret. Content type must be application/json -var createHandler = require('github-webhook-handler'); -var handler = createHandler({ path: '/github', secret: process.env.GITHUB_WEBHOOK_SECRET }); -var installationAccessToken; - -const app = new App({ - id: process.env.GITHUB_APP_IDENTIFIER, - privateKey: fs.readFileSync(process.env.GITHUB_PRIVATE_KEY), - webhooks: {secret} -}); -// Authenticate app by exchanging signed JWT for access token -var token = app.getSignedJsonWebToken(); - -/** - * Callback to deal with POST requests to /github endpoint - * @param {Object} req - Request object. - * @param {Object} res - Response object. - * @param {Function} next - Handle to next callback in stack. - */ -srv.post('/github', async (req, res, next) => { - console.log('Post received') - try { - token = await app.getSignedJsonWebToken(); - //getPayloadRequest(req) GET /orgs/:org/installation - const { data } = await request("GET /repos/:owner/:repo/installation", { - owner: process.env.REPO_OWNER, - repo: process.env.REPO_NAME, - headers: { - authorization: `Bearer ${token}`, - accept: "application/vnd.github.machine-man-preview+json" - } - }); - // contains the installation id necessary to authenticate as an installation - const installationId = data.id; - installationAccessToken = await app.getInstallationAccessToken({ installationId }); - handler(req, res, () => res.end('ok')) - //next(); - } catch (error) { - next(error); - } -}); - -/** - * Load MATLAB test results from ci-db.json file. - * @param {string, array} id - Function to call with job and done callback when. - */ -function loadTestRecords(id) { - let obj = JSON.parse(fs.readFileSync(dbFile, 'utf8')); - if (!Array.isArray(obj)) obj = [obj]; // Ensure array - let records = obj.filter(o => id.includes(o.commit)); - // If single arg return as object, otherwise keep as array - return (!Array.isArray(id) && records.length === 1 ? records[0] : records) -} - -/** - * Compare coverage of two commits and post a failed status if coverage of head commit <= base commit. - * @param {object} data - job data object with coverage field holding head and base commit ids. - */ -function compareCoverage(data) { - let ids = data.coverage; - let status, description; - let records = loadTestRecords(Object.values(ids)); - // Filter duplicates just in case - records = records.filter((set => o => !set.has(o.commit) && set.add(o.commit))(new Set)); - let has_coverage = records.every(o => (typeof o.coverage !== 'undefined' && o.coverage > 0)); - // Check if any errored or failed to update coverage - if (records.filter(o => o.status === 'error').length > 0) { - status = 'failure'; - description = 'Failed to determine coverage as tests incomplete due to errors'; - } else if (records.length === 2 && has_coverage) { - // Ensure first record is for head commit - if (records[0].commit === ids.base) { records.reverse() } - // Calculate coverage change - let coverage = records[0].coverage - records[1].coverage; - status = (coverage > 0 ? 'success' : 'failure'); - description = 'Coverage ' + (coverage > 0 ? 'increased' : 'decreased') - + ' from ' + Math.round(records[1].coverage*100)/100 + '%' - + ' to ' + Math.round(records[0].coverage*100)/100 + '%'; - // TODO Maybe remove test from pile if we already have it? - } else { - for (let commit in ids) { - // Check test isn't already on the pile - let job = queue.pile.filter(o => o.data.sha === ids[commit]); - if (job.length > 0) { // Already on pile - // Add coverage key to job data structure - if (typeof job[0].data.coverage === 'undefined') { job[0].data.coverage = ids; } - } else { // Add test to queue - queue.add({ - skipPost: true, - sha: ids[commit], - owner: process.env.REPO_OWNER, - repo: data.repo, - status: '', - context: '', - coverage: ids // Note cf commit - }); - } - } - return; - } - // Post a our coverage status - request('POST /repos/:owner/:repo/statuses/:sha', { - owner: process.env.REPO_OWNER, - repo: data.repo, - headers: { - authorization: `token ${installationAccessToken}`, - accept: 'application/vnd.github.machine-man-preview+json' - }, - sha: ids.head, - state: status, - target_url: `${process.env.WEBHOOK_PROXY_URL}/events/${ids.head}`, // fail - description: description, - context: `coverage/${process.env.USERDOMAIN}` - }); -} - -// Serve the test results for requested commit id -srv.get('/github/:id', function (req, res) { - console.log('Request for test log for commit ' + req.params.id.substring(0,6)) - let log = `.\\src\\matlab_tests-${req.params.id}.log`; // TODO Generalize - fs.readFile(log, 'utf8', (err, data) => { - if (err) { - res.statusCode = 404; - res.send(`Record for commit ${req.params.id} not found`); - } else { - res.statusCode = 200; - let preText = '
';
-    	let postText = '
'; - res.send(preText + data + postText); - - } - }); - /* - const record = loadTestRecords(req.params.id); - if (typeof record == 'undefined') { - res.statusCode = 404; - res.send(`Record for commit ${req.params.id} not found`); - } else { - res.send(record['results']); - } - */ -}); - -// Serve the coverage results -srv.get('/coverage/:repo/:branch', async (req, res) => { - // Find head commit of branch - try { - const { data } = await request('GET /repos/:owner/:repo/git/refs/heads/:branch', { - owner: process.env.REPO_OWNER, - repo: req.params.repo, - branch: req.params.branch - }); - if (data.ref.endsWith('/' + req.params.branch)) { - console.log('Request for ' + req.params.branch + ' coverage') - let id = data.object.sha; - var report = {'schemaVersion': 1, 'label': 'coverage'}; - try { // Try to load coverage record - let record = await loadTestRecords(id); - if (typeof record == 'undefined' || record['coverage'] == '') {throw 404} // Test not found for commit - if (record['status'] === 'error') {throw 500} // Test found for commit but errored - report['message'] = Math.round(record['coverage']*100)/100 + '%'; - report['color'] = (record['coverage'] > 75 ? 'brightgreen' : 'red'); - } catch (err) { // No coverage value - report['message'] = (err === 404 ? 'pending' : 'unknown'); - report['color'] = 'orange'; - // Check test isn't already on the pile - let onPile = false; - for (let job of queue.pile) { if (job.id === id) { onPile = true; break; } } - if (!onPile) { // Add test to queue - queue.add({ - skipPost : true, - sha: id, - owner: process.env.REPO_OWNER, - repo: req.params.repo, - status: '', - context: ''}); - } - } finally { // Send report - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify(report));} - } else { throw 404 } // Specified repo or branch not found - } catch (error) { - let msg = (error === 404 ? `${req.params.repo}/${req.params.branch} not found` : error); // @fixme error thrown by request not 404 - console.error(msg) - res.statusCode = 401; // If not found, send 401 for security reasons - res.send(msg); - } -}); - -// Serve the build status -srv.get('/status/:repo/:branch', async (req, res) => { - // Find head commit of branch - try { - const { data } = await request('GET /repos/:owner/:repo/git/refs/heads/:branch', { - owner: process.env.REPO_OWNER, - repo: req.params.repo, - branch: req.params.branch - }); - if (data.ref.endsWith('/' + req.params.branch)) { - console.log('Request for ' + req.params.branch + ' build status') - let id = data.object.sha; - var report = {'schemaVersion': 1, 'label': 'build'}; - try { // Try to load coverage record - record = await loadTestRecords(id); - if (typeof record == 'undefined' || record['status'] == '') {throw 404} // Test not found for commit - report['message'] = (record['status'] === 'success' ? 'passing' : 'failing'); - report['color'] = (record['status'] === 'success' ? 'brightgreen' : 'red'); - } catch (err) { // No coverage value - report['message'] = (err === 404 ? 'pending' : 'unknown'); - report['color'] = 'orange'; - // Check test isn't already on the pile - let onPile = false; - for (let job of queue.pile) { if (job.id === id) { onPile = true; break; } } - if (!onPile) { // Add test to queue - queue.add({ - skipPost: true, - sha: id, - owner: process.env.REPO_OWNER, - repo: req.params.repo, - status: '', - context: ''}); - } - } finally { // Send report - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify(report));} - } else { throw 404 } // Specified repo or branch not found - } catch (error) { - let msg = (error === 404 ? `${req.params.repo}/${req.params.branch} not found` : error); // @fixme error thrown by request not 404 - console.error(msg) - res.statusCode = 401; // If not found, send 401 for security reasons - res.send(msg); - } -}); - -// Define how to process tests. Here we checkout git and call MATLAB -queue.process(async (job, done) => { - // job.data contains the custom data passed when the job was created - // job.id contains id of this job. - var sha = job.data['sha']; // Retrieve commit hash - // If the repo is a submodule, modify path - var path = process.env.REPO_PATH; - if (job.data['repo'] === 'alyx-matlab' || job.data['repo'] === 'signals') { - path = path + path.sep + job.data['repo'];} - if (job.data['repo'] === 'alyx') { sha = 'dev' } // For Alyx checkout master - // Checkout commit - checkout = cp.execFile('checkout.bat ', [sha, path], (error, stdout, stderr) => { - if (error) { // Send error status - console.error('Checkout failed: ', stderr); - job.data['status'] = 'error'; - job.data['context'] = 'Failed to checkout code: ' + stderr; - done(error); // Propagate error - return; - } - console.log(stdout) - // Go ahead with MATLAB tests - var runTests; - const timer = setTimeout(function() { - console.log('Max test time exceeded') - job.data['status'] = 'error'; - job.data['context'] = `Tests stalled after ~${(timeout / 60000).toFixed(0)} min`; - runTests.kill(); - done(new Error('Job stalled')) }, timeout); - let args = ['-r', `runAllTests (""${job.data.sha}"",""${job.data.repo}"")`, - '-wait', '-log', '-nosplash', '-logfile', `.\\src\\matlab_tests-${job.data.sha}.log`]; // TODO Generalize - let program = config.program || 'matlab' - runTests = cp.execFile(program, args, (error, stdout, stderr) => { - clearTimeout(timer); - if (error) { // Send error status - // Isolate error from log - let errStr = stderr.split(/\r?\n/).filter((str) => - {return str.startsWith('Error in \'')}).join(';'); - job.data['status'] = 'error'; - job.data['context'] = errStr; - done(error); // Propagate - } else { - const rec = loadTestRecords(job.data['sha']); // Load test result from json log - job.data['status'] = rec['status']; - job.data['context'] = rec['description']; - done(); - } - }); - }); -}); - -/** - * Callback triggered when job finishes. Called both on complete and error. - * @param {Object} job - Job object which has finished being processed. - */ -queue.on('finish', job => { // On job end post result to API - console.log(`Job ${job.id} complete`) - // If job was part of coverage test and error'd, call compare function - // (otherwise this is done by the on complete callback after writing coverage to file) - if (typeof job.data.coverage !== 'undefined' && job.data['status'] === 'error') { - compareCoverage(job.data); - } - if (job.data.skipPost === true) { return; } - request("POST /repos/:owner/:repo/statuses/:sha", { - owner: job.data['owner'], - repo: job.data['repo'], - headers: { - authorization: `token ${installationAccessToken}`, - accept: "application/vnd.github.machine-man-preview+json"}, - sha: job.data['sha'], - state: job.data['status'], - target_url: `${process.env.WEBHOOK_PROXY_URL}/github/${job.data.sha}`, // FIXME replace url - description: job.data['context'], - context: `continuous-integration/${process.env.USERDOMAIN}` - }); -}); - -/** - * Callback triggered when job completes. Called when all tests run to end. - * @param {Object} job - Job object which has finished being processed. - * @todo Save full coverage object for future inspection - */ -queue.on('complete', job => { // On job end post result to API - console.log('Updating coverage for job #' + job.id) - Coverage('./CoverageResults.xml', job.data.repo, job.data.sha, obj => { - // Digest and save percentage coverage - let misses = 0, hits = 0; - for (let file of obj.source_files) { - misses += file.coverage.filter(x => x === 0).length; - hits += file.coverage.filter(x => x > 0).length; - } - // Load data and save - let records = JSON.parse(fs.readFileSync(dbFile, 'utf8')); - if (!Array.isArray(records)) records = [records]; // Ensure array - for (let o of records) { if (o.commit === job.data.sha) {o.coverage = hits / (hits + misses) * 100; break; }} // Add percentage - // Save object - fs.writeFile(dbFile, JSON.stringify(records), function(err) { - if (err) { console.log(err); return; } - // If this test was to ascertain coverage, call comparison function - if (typeof job.data.coverage !== 'undefined') { compareCoverage(job.data); } - }); - }); -}); - -// Let fail silently: we report error via status -queue.on('error', err => {}); -// Log handler errors -handler.on('error', function (err) { - console.error('Error:', err.message) -}) - -// Handle push events -handler.on('push', async function (event) { - // Log the event - console.log('Received a push event for %s to %s', - event.payload.repository.name, - event.payload.ref) - // Ignore documentation branches - if (event.payload.ref.endsWith('documentation')) { return; } - try { // Run tests for head commit only - let head_commit = event.payload.head_commit.id; - // Post a 'pending' status while we do our tests - await request('POST /repos/:owner/:repo/statuses/:sha', { - owner: process.env.REPO_OWNER, - repo: event.payload.repository.name, - headers: { - authorization: `token ${installationAccessToken}`, - accept: 'application/vnd.github.machine-man-preview+json' - }, - sha: head_commit, - state: 'pending', - target_url: `${process.env.WEBHOOK_PROXY_URL}/events/${head_commit}`, // fail - description: 'Tests running', - context: `continuous-integration/${process.env.USERDOMAIN}` - }); - // Add a new test job to the queue - queue.add({ - sha: head_commit, - owner: process.env.REPO_OWNER, - repo: event.payload.repository.name, - status: '', - context: '' - }); - } catch (error) {console.log(error)} -}); - -// Handle pull request events -// Here we'll update coverage -handler.on('pull_request', async function (event) { - // Ignore documentation branches - if (event.payload.pull_request.head.ref === 'documentation') { return; } - // Log the event - console.log('Received a pull_request event for %s to %s', - event.payload.pull_request.head.repo.name, - event.payload.pull_request.head.ref) - if (!event.payload.action.endsWith('opened') && event.payload.action !== 'synchronize') { return; } - try { // Compare test coverage - let head_commit = event.payload.pull_request.head.sha; - let base_commit = event.payload.pull_request.base.sha; - if (false) { // TODO for alyx only - // Post a 'pending' status while we do our tests - await request('POST /repos/:owner/:repo/statuses/:sha', { - owner: process.env.REPO_OWNER, - repo: event.payload.repository.name, - headers: { - authorization: `token ${installationAccessToken}`, - accept: 'application/vnd.github.machine-man-preview+json' - }, - sha: head_commit, - state: 'pending', - target_url: `${process.env.WEBHOOK_PROXY_URL}/events/${head_commit}`, // fail - description: 'Tests running', - context: `continuous-integration/${process.env.USERDOMAIN}` - }); - } - - // Post a 'pending' status while we do our tests - request('POST /repos/:owner/:repo/statuses/:sha', { - owner: process.env.REPO_OWNER, - repo: event.payload.repository.name, - headers: { - authorization: `token ${installationAccessToken}`, - accept: 'application/vnd.github.machine-man-preview+json' - }, - sha: head_commit, - state: 'pending', - target_url: `${process.env.WEBHOOK_PROXY_URL}/events/${head_commit}`, // fail - description: 'Checking coverage', - context: `coverage/${process.env.USERDOMAIN}` - }); - // Check coverage exists - let data = { - repo: event.payload.repository.name, - coverage: {head: head_commit, base: base_commit} - }; - compareCoverage(data); - } catch (error) {console.log(error)} -}); - -// Start the server in the port 3000 -var server = srv.listen(3000, function () { - var host = server.address().address - var port = server.address().port - - console.log("Handler listening at http://%s:%s", host, port) -}); - -// Start tunnel -openTunnel(); - -// Log any unhandled errors -process.on('unhandledRejection', (reason, p) => { - console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); - console.log(reason.stack) -}); diff --git a/lib.js b/lib.js new file mode 100644 index 0000000..f634c1c --- /dev/null +++ b/lib.js @@ -0,0 +1,437 @@ +/** + * A module containing helper and callback functions for continuous integration. + */ +const fs = require('fs'); +const path = require('path'); + +const createDebug = require('debug'); +const localtunnel = require('localtunnel'); +const kill = require('tree-kill'); + +const config = require('./config/config').settings; +const Coverage = require('./coverage'); +const queue = new (require('./queue.js'))(); // The queue object for our app to use + +/** + * Return a shortened version of an int or string id + * @param {any} v - ID to shorten. + * @param {int} len - Maximum number of chars. + * @returns {String} v as a short string. + */ +function shortID(v, len=7) { + if (Array.isArray(v)) { return v.map(v => shortID(v, len)); } + if (Number.isInteger(v)) { v = v.toString(); } + if (typeof v === 'string' || v instanceof String) { v = v.substr(0, len); } + return v; // If not string, array or number, leave unchanged +} + +// Attach shortID function to logger formatter +createDebug.formatters.g = shortID +const log = createDebug('ci'); +const _log = log.extend('lib'); + + +/** + * Test commit has is valid. Assumes hash is at least 7 characters long. + * @param {String} id - String under test. + * @returns {boolean} true if id is a valid SHA + */ +function isSHA(id) { + const regex = /^[0-9a-f]{7,40}$/i; + return (typeof id === 'string' || id instanceof String) && id.match(regex) !== null +} + + +/** + * Returns a full filepath. Plays nicely with ~. + * @param {String} p - Path to resolve. + * @returns {String} A full path + */ +function fullpath(p) { + if (p[0] === '~') { + return path.join(process.env.HOME, p.slice(1)); + } else { + return path.resolve(p); + } +} + + +/** + * Util wraps input in array if not already one + * @param {Object} x - Input to ensure as array. + * @returns {Array} x as an array. + */ +function ensureArray(x) { return (Array.isArray(x))? x : [x]; } + +/** + * Will match one and only one of the string 'true','1', or 'on' regardless of capitalization and + * regardless of surrounding white-space. (Thx to shrewmouse). + * @param {string} s - String to test + * @returns {boolean} s as bool + */ +function strToBool(s) { return /^\s*(true|1|on)\s*$/i.test(s); } + + +/** + * Load test results from .db.json file. NB: Size and order of returned records not guaranteed + * @param {string, array} id - Commit SHA. + */ +function loadTestRecords(id) { + // FIXME Catch JSON parse error + _log('Loading test records from %s for id %g', config.dbFile, id); + if (!id) { throw new TypeError('invalid id'); } + if(!fs.existsSync(config.dbFile)) { + console.log('Records file not found'); + return [] + } + let obj = JSON.parse(fs.readFileSync(config.dbFile, 'utf8')); + obj = ensureArray(obj); + let records = obj.filter(o => id.includes(o.commit)); + // If single arg return as object, otherwise keep as array + return (!Array.isArray(id) && records.length === 1 ? records[0] : records) +} + + +/** + * Save test results from .db.json file. Any matching records are merged before saving. + * @param {Object, Array} r - The record(s) to save. Must contain an id field. + */ +async function saveTestRecords(r) { + var obj; // the test records + const byID = (a, b) => b.commit.localeCompare(a.commit); + r = ensureArray(r).sort(byID); + if (!r.every(x => isSHA(x.commit))) { + throw new APIError('"commit" not in record(s)'); + } + try { + let data = await fs.promises.readFile(config.dbFile, 'utf8') + obj = ensureArray(JSON.parse(data)); + let ids = r.map(x => x.commit); + let records = obj.filter(o => ids.indexOf(o.commit) >= 0); + // Update existing records + for (let old of records) { + let o = r.filter(x => x.commit === old.commit ); + if (o.length > 0) { + Object.assign(old, o.pop()); + } + } + let updated = records.map(x => x.commit); + r = r.filter(x => updated.indexOf(x.commit) === -1); + } catch (err) { + if (err && err.code === 'ENOENT') { + console.log(`Records file not found at ${config.dbFile}`); + obj = []; + } else { + throw err; + } + } + // Add new records + obj = obj.concat(r); + await fs.promises.writeFile(config.dbFile, JSON.stringify(obj)); +} + + +/** + * Updates a job's data from saved test records. + * @param {Object} job - Job object which is being processed. + * @returns {boolean} - true if record was found + */ +function updateJobFromRecord(job) { + let log = _log.extend('updateJobFromRecord'); + log('Loading test records for head commit %g', job.data['sha']); + let rec = loadTestRecords(job.data['sha']); // Load test result from json log + if (rec.length === 0) { + log('No record found, return false'); + return false; + } // No record found + rec = Array.isArray(rec) ? rec.pop() : rec; // in case of duplicates, take last + job.data['status'] = rec['status']; + job.data['description'] = rec['description']; + job.data['coverage'] = ('coverage' in rec)? rec['coverage'] : null; + if (!job.data['coverage']) { + log('Coverage missing, computing from XML'); + computeCoverage(job); // Attempt to load from XML + } else if ((job.data.context || '').startsWith('coverage')) { + log('Comparing coverage to base commit'); + compareCoverage(job); // If this test was to ascertain coverage, call comparison function + } + return true; +} + + +/** + * Curry a function for passing incomplete args. + * @param {Object} func - Function to be curried. + */ +function partial(func) { + return function curried(...args) { + if (args.length >= func.length) { + return func.apply(this, args); + } else { + return function(...args2) { + return curried.apply(this, args.concat(args2)); + } + } + }; +} + +function chain(func) { + return function curried(...args) { + if (args.length >= func.length) { + return func.apply(this, args); + } else { + return function(...args2) { + return curried.apply(this, args2.concat(args)); + } + } + }; +} + + + +/** + * Check if job already has record, if so, update from record and finish, otherwise call tests function. + * @param {Object} job - Job object which is being processed. + * @param {Function} func - The tests function to run, e.g. `runTests`. + */ +function shortCircuit(job, func=null) { + // job.data contains the custom data passed when the job was created + // job.id contains id of this job. + let log = _log.extend('shortCircuit'); + log('Checking whether to load from saved for %s @ %g', + (job.data.context || '').split('/').pop(), job.data.sha); + + // To avoid running our tests twice, set the force flag to false for any other jobs in pile that + // have the same commit ID + let sha = job.data.sha; + let others = queue.pile.filter(o => (o.data.sha === sha) && (o.id !== job.id)); + for (let other of others) { other.data.force = false } + // If lazy, load records to check whether we already have the results saved + if (job.data.force === false) { // NB: Strict equality; force by default + _log('Updating job data directly from record for job #%g', job.id); + if (updateJobFromRecord(job)) { return job.done(); } // No need to run tests; skip to complete routine + } + + // Go ahead and prepare to run tests + if (func) { return func(job); } +} + + +/** + * Configures a persistent reverse proxy to use the same port as our local server. + * @returns (Class) - A localtunnel instance + */ +const openTunnel = async () => { + let args = { + port: config.listen_port, + subdomain: process.env.TUNNEL_SUBDOMAIN, + host: process.env.TUNNEL_HOST + }; + const tunnel = await localtunnel(args); + console.log(`Tunnel open on: ${tunnel.url}`); + tunnel.on('close', () => {console.log('Reconnecting'); openTunnel(); }); + tunnel.on('error', (e) => { console.error(e) }); + return tunnel; +} + + +/** + * Starts a timer with a callback to kill the job's process. + * @param {Object} job - The Job with an associated process in the data field. + * @param {boolean} kill_children - If true all child processes are killed. + * @returns {number} - A timeout object. + */ +function startJobTimer(job, kill_children=false) { + const timeout = config.timeout || 8*60000; // How long to wait for the tests to run + return setTimeout(() => { + console.log('Max test time exceeded'); + log(kill_children? 'Killing all processes' : 'Ending test process'); + let pid = job.data.process.pid; + job.data.process.kill(); + if (kill_children) { kill(pid); } + }, timeout); +} + + +/** + * Function to update the coverage of a job by parsing the XML file. + * @param {Object} job - Job object which has finished being processed. + */ +function computeCoverage(job) { + if (typeof job.data.coverage !== 'undefined' && job.data.coverage) { + console.log('Coverage already computed for job #' + job.id) + return; + } + console.log('Updating coverage for job #' + job.id) + let xmlPath = path.join(config.dataPath, 'reports', job.data.sha, 'CoverageResults.xml') + Coverage(xmlPath, job.data.repo, job.data.sha, obj => { + // Digest and save percentage coverage + let misses = 0, hits = 0; + for (let file of obj.source_files) { + misses += file.coverage.filter(x => x === 0).length; + hits += file.coverage.filter(x => x > 0).length; + } + const coverage = hits / (hits + misses) * 100 // As percentage + job.data.coverage = coverage; // Add to job + // Load data and save TODO Move to saveTestRecord(s) function in lib + let records = JSON.parse(fs.readFileSync(config.dbFile, 'utf8')); + records = ensureArray(records); // Ensure array + for (let o of records) { if (o.commit === job.data.sha) { o.coverage = coverage; break; }} + // Save object + fs.writeFile(config.dbFile, JSON.stringify(records), function(err) { + if (err) { + job.status = 'error' + job.description = 'Failed to compute coverage from XML' + console.log(err); + return; + } + // If this test was to ascertain coverage, call comparison function + let toCompare = (job.data.context || '').startsWith('coverage') && job.data.base; + if (toCompare) { compareCoverage(job); } + }); + }); +} + + +/** + * Compare coverage of two commits and post a failed status if coverage of head commit <= base commit. + * @param {Object} job - Job object which has finished being processed. + * @todo Add support for forked PRs + */ +function compareCoverage(job) { + let log = _log.extend('compareCoverage'); + if (!(job.data.sha && job.data.base)) { + throw new ReferenceError('No sha (head) or base commit in job data'); + } + log('Comparing coverage for %g -> %g', job.data.sha, job.data.base); + var records; + if (!job.coverage) { + log('No coverage in job data; loading from records'); + records = loadTestRecords([job.data.sha, job.data.base]); + // Filter duplicates just in case + records = records.filter((set => o => !set.has(o.commit) && set.add(o.commit))(new Set)); + } else { + let curr = JSON.parse(JSON.stringify( job.data )); // Make a copy + curr.commit = curr.sha; // rename field + records = [curr, loadTestRecords(job.data.base)]; + } + log('The following records were found: %O', records); + const has_coverage = records.every(o => (typeof o.coverage !== 'undefined' && o.coverage > 0)); + + // Check if any errored or failed to update coverage + if (records.filter(o => o.status === 'error').length > 0) { + log('One or more have error status; cannot compare coverage'); + job.data.status = 'failure'; + job.data.description = 'Failed to determine coverage as tests incomplete due to errors'; + + // Both records present and they have coverage + } else if (records.length === 2 && has_coverage) { + log('Calculating coverage difference'); + // Ensure first record is for head commit + if (records[0].commit === job.data.base) { records.reverse() } + // Calculate coverage change + let delta = records[0].coverage - records[1].coverage; + let passed = config.strict_coverage? delta > 0 : delta >= 0; + job.data.status = (passed ? 'success' : 'failure'); + if (delta === 0) { + job.data.description = `Coverage remains at ${Math.round(records[1].coverage * 100) / 100}%`; + } else { + job.data.description = `Coverage ${passed ? 'increased' : 'decreased'} ` + + `from ${Math.round(records[1].coverage * 100) / 100}% ` + + `to ${Math.round(records[0].coverage * 100) / 100}%`; + } + + } else { // We need to add a new job for incomplete coverage + log('Missing record for base commit; adding new jobs'); + // TODO This could be refactored for efficiency + // Ensure we have coverage for base branch + queue.add({ + skipPost: true, // don't post, to be left for next job + force: false, // should skip if coverage already saved + sha: job.data.base, + owner: process.env.REPO_OWNER, + repo: job.data.repo, + }); + // Ensure we have coverage for head commit and post result + queue.add({ + skipPost: false, // don't post, to be left for next job + force: false, // should skip if coverage already saved + sha: job.data.sha, + base: job.data.base, + owner: process.env.REPO_OWNER, + repo: job.data.repo, + context: job.data.context // conserve context + }); + // Skip our current job as we're waiting for base coverage + job.data.skipPost = true; + } +} + + +/** + * Get the coverage results and build status data for the shields.io coverage badge API. + * If test results don't exist, a new job is added to the queue and the message is set to 'pending' + * @param {Object} data - An object with the keys 'sha', 'repo', 'owner' and 'context'. + * 'context' must be 'coverage' or 'status'. + */ +function getBadgeData(data) { + let id = data.sha; + if (!id) { + throw new ReferenceError('Invalid "sha" field in input data') + } + var report = {'schemaVersion': 1, 'label': data.context === 'status'? 'build' : 'coverage'}; + // Try to load coverage record + let record = data.force? [] : loadTestRecords(id); + // If no record found + if (record.length === 0) { + report['message'] = 'pending'; + report['color'] = 'orange'; + // Check test isn't already on the pile + let onPile = false; + for (let job of queue.pile) { if (job.id === id) { onPile = true; break; } } + if (!onPile) { // Add test to queue + data['skipPost'] = true + queue.add(data); + } + } else { + record = Array.isArray(record) ? record.pop() : record; // in case of duplicates, take last + switch (data.context) { + case 'status': + if (record['status'] === 'error' || !record['coverage']) { + report['message'] = 'unknown'; + report['color'] = 'orange'; + } else { + report['message'] = (record['status'] === 'success' ? 'passing' : 'failing'); + report['color'] = (record['status'] === 'success' ? 'brightgreen' : 'red'); + } + break; + case 'coverage': + if (record['status'] === 'error' || !record['coverage']) { + report['message'] = 'unknown'; + report['color'] = 'orange'; + } else { + report['message'] = Math.round(record['coverage'] * 100) / 100 + '%'; + report['color'] = (record['coverage'] > 75 ? 'brightgreen' : 'red'); + } + break; + default: + if (!data['context']) { + throw new ReferenceError('Context required for badge request') + } else { + throw new TypeError('Unsupported context badge request') + } + } + } + return report; +} + + +class APIError extends Error { + //... +} + +module.exports = { + ensureArray, loadTestRecords, compareCoverage, computeCoverage, getBadgeData, log, shortID, + openTunnel, APIError, queue, partial, startJobTimer, updateJobFromRecord, shortCircuit, isSHA, + fullpath, strToBool, saveTestRecords +} diff --git a/main.js b/main.js new file mode 100644 index 0000000..15fa8ba --- /dev/null +++ b/main.js @@ -0,0 +1,63 @@ +/** + * @author Miles Wells + * @requires ./queue.js, ./coverage.js, ./serve.js, ./lib.js + * @todo save auxiliary configuration into a separate config file + * @todo add abort option for when new commits added + * @todo rename context to description and use context to track posts + * @todo fix intentions + */ +const { openTunnel, queue, shortCircuit} = require('./lib'); +const { srv, handler, eventCallback, runTests, prepareEnv} = require('./serve'); +const config = require("./config/config").settings; + + +/** + * Build queue processing pipeline. The shortCircuit call checks whether the results may be loaded from file, + * bypassing the test function. + */ +const run = (job) => { prepareEnv(job, runTests); }; +queue.process((job) => { shortCircuit(job, run); }); + +// NB: Only the supported events make it this far (i.e. push and pull requests) +handler.on('*', evt => eventCallback(evt)); + + +///////////////////// ERROR HANDLING ///////////////////// + +/** + * Callback triggered when job completes. Called when uncaught error is thrown in setup or test + * functions. Do nothing as we'll process the error with the 'finish' callback. + * @param {Object} job - Job object which has finished being processed. + */ +queue.on('error', _ => {}); + + +// Log handler errors +handler.on('error', function (err) { + console.error('Error:', err.message); +}) + +// Log any unhandled errors +process.on('unhandledRejection', (reason, p) => { + console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); + console.log(reason.stack) +}); + + +///////////////////// START TUNNEL ///////////////////// + +openTunnel().then( + () => { + // Start the server on same port as tunnel + var server = srv.listen(config.listen_port, function () { + let host = server.address().address; + let port = server.address().port; + + console.log("Handler listening at http://%s:%s", host, port); + }); + }, + (e) => { + throw e; + } +) + diff --git a/package-lock.json b/package-lock.json index 8ef71ff..8de3d01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,22 @@ { "name": "matlab-ci", - "version": "1.0.0", + "version": "2.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { - "@octokit/app": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@octokit/app/-/app-4.2.1.tgz", - "integrity": "sha512-lskD8DwYAVI+7ErkDSmSlmX2YYlqhCY4ImFjEjswBHJK8HXaWeOZl1dYxYxX/+ewT76ZvamboMx/36gPq6Fyew==", + "@octokit/auth-app": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-2.10.2.tgz", + "integrity": "sha512-3PLyDISHtdCxQx0LS0Ut2E+zy2PVfDFCxDnFJ+QRvABHow/Zqj2Im7GF8zYYn8nwI1tgaoewwwd0unfXuPi48Q==", "requires": { - "@octokit/request": "^5.0.0", - "jsonwebtoken": "^8.3.0", - "lru-cache": "^6.0.0" + "@octokit/request": "^5.3.0", + "@octokit/request-error": "^2.0.0", + "@octokit/types": "^5.0.0", + "@types/lru-cache": "^5.1.0", + "deprecation": "^2.3.1", + "lru-cache": "^6.0.0", + "universal-github-app-jwt": "^1.0.1", + "universal-user-agent": "^6.0.0" } }, "@octokit/endpoint": { @@ -57,11 +62,75 @@ "@types/node": ">= 8" } }, + "@sinonjs/commons": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.1.tgz", + "integrity": "sha512-892K+kWUUi3cl+LlqEWIDrhvLgdL79tECi8JZUyq6IviKy/DNhuzCRlbHUjxK89f4ypPMMaFnFuR9Ie6DoIMsw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", + "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "@sinonjs/formatio": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-5.0.1.tgz", + "integrity": "sha512-KaiQ5pBf1MpS09MuA0kp6KBQt2JUOQycqVG1NZXvzeaXe5LGFqAKueIS0bw4w0P9r7KuBSVdUk5QjXsUdu2CxQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^5.0.2" + } + }, + "@sinonjs/samsam": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.2.0.tgz", + "integrity": "sha512-CaIcyX5cDsjcW/ab7HposFWzV1kC++4HNsfnEdFJa7cP1QIuILAKV+BgfeqRXhcnSAc76r/Rh/O5C+300BwUIw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, + "@types/jsonwebtoken": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.0.tgz", + "integrity": "sha512-9bVao7LvyorRGZCw0VmH/dr7Og+NdjYSsKAxB43OQoComFbBgsEpoR9JW6+qSq/ogwVBg8GI2MfAlk4SYI4OLg==", + "requires": { + "@types/node": "*" + } + }, + "@types/lru-cache": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-5.1.0.tgz", + "integrity": "sha512-RaE0B+14ToE4l6UqdarKPnXwVDuigfFv+5j9Dze/Nqr23yyuqdNvzcZi3xB+3Agvi5R4EOgAksfv3lXX4vBt9w==" + }, "@types/node": { "version": "14.11.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.11.tgz", "integrity": "sha512-UcaAZrL8uO5GNS+NLxkYg1RiOMgdLxCXGqs+TTupltXN8rTvUEKTOpqCV3tlcAIZJXzcBQajzmjdrvuPvnuMUw==" }, + "@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true + }, "accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", @@ -71,31 +140,102 @@ "negotiator": "0.6.2" } }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, "ansi-regex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true }, "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, "requires": { "color-convert": "^1.9.0" } }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, "axios": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz", - "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "binary-extensions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", + "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", + "dev": true + }, + "bl": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", + "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", "requires": { - "follow-redirects": "1.5.10", - "is-buffer": "^2.0.2" + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + } } }, "body-parser": { @@ -113,6 +253,49 @@ "qs": "6.7.0", "raw-body": "2.4.0", "type-is": "~1.6.17" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" } }, "buffer-equal-constant-time": { @@ -128,12 +311,92 @@ "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + } + }, + "chai-spies": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/chai-spies/-/chai-spies-1.0.0.tgz", + "integrity": "sha512-elF2ZUczBsFoP07qCfMO/zeggs8pqCf3fZGyK5+2X4AndS8jycZYID91ztD9oQ7d/0tnS963dPkd0frQEThDsg==", + "dev": true + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + } + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "chokidar": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.3.tgz", + "integrity": "sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ==", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.5.0" + } }, "cliui": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, "requires": { "string-width": "^3.1.0", "strip-ansi": "^5.2.0", @@ -144,6 +407,7 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, "requires": { "color-name": "1.1.3" } @@ -151,7 +415,28 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "content-disposition": { "version": "0.5.3", @@ -176,18 +461,47 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, + "cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", + "dev": true + }, "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "requires": { - "ms": "2.0.0" + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } } }, "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true }, "depd": { "version": "1.1.2", @@ -204,6 +518,17 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "dotenv": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" + }, "ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -220,18 +545,36 @@ "emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true }, "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -272,6 +615,37 @@ "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "fake-timers": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/fake-timers/-/fake-timers-0.1.2.tgz", + "integrity": "sha512-x9t3ZsxU3eJfBGYBf2bUFIJm7snxInwsBwm66K17Qp1GlpPJMthVFqZHd0TAckazkQoCwa0k1NuOWm3Glfe4oQ==", + "dev": true + }, + "fast-safe-stringify": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", + "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==", + "dev": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" } }, "finalhandler": { @@ -286,34 +660,46 @@ "parseurl": "~1.3.3", "statuses": "~1.5.0", "unpipe": "~1.0.0" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "requires": { - "locate-path": "^3.0.0" - } - }, - "follow-redirects": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", - "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", - "requires": { - "debug": "=3.1.0" }, "dependencies": { "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "requires": { "ms": "2.0.0" } } } }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true + }, + "follow-redirects": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz", + "integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==" + }, + "form-data": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", + "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "formidable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", + "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==", + "dev": true + }, "forwarded": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", @@ -324,11 +710,90 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "github-webhook-handler": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/github-webhook-handler/-/github-webhook-handler-1.0.0.tgz", + "integrity": "sha512-1OXGPYT2AF6gpINFoTMEn82AaeCX85bpG7hYxud5FwJR0U3PzMVRGjDP1UcV8Z++5ObcSlDDFHOEdl4p0tLkWQ==", + "requires": { + "bl": "~4.0.0" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, "http-errors": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", @@ -349,31 +814,118 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, + "interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==" + }, "ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" }, - "is-buffer": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", - "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==" + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-core-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.0.0.tgz", + "integrity": "sha512-jq1AH6C8MuteOoBPwkxHafmByhL9j5q4OaPGdbuD+ZtQJVzH+i6E3BJDQcBA09k57i2Hh2yQbEG8yObZ0jdlWw==", + "requires": { + "has": "^1.0.3" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true }, "is-plain-object": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "js-yaml": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, "jsonwebtoken": { "version": "8.5.1", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", @@ -398,6 +950,12 @@ } } }, + "just-extend": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.1.1.tgz", + "integrity": "sha512-aWgeGFW67BP3e5181Ep1Fv2v8z//iBJfrvyTnq8wG86vEESwmonn1zPBJ0VfmT9CJq2FIT0VsETtrNFm2a+SHA==", + "dev": true + }, "jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -418,39 +976,21 @@ } }, "localtunnel": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/localtunnel/-/localtunnel-2.0.0.tgz", - "integrity": "sha512-g6E0aLgYYDvQDxIjIXkgJo2+pHj3sGg4Wz/XP3h2KtZnRsWPbOQY+hw1H8Z91jep998fkcVE9l+kghO+97vllg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/localtunnel/-/localtunnel-2.0.1.tgz", + "integrity": "sha512-LiaI5wZdz0xFkIQpXbNI62ZnNn8IMsVhwxHmhA+h4vj8R9JG/07bQHWwQlyy7b95/5fVOCHJfIHv+a5XnkvaJA==", "requires": { - "axios": "0.19.0", - "debug": "4.1.1", + "axios": "0.21.1", + "debug": "4.3.1", "openurl": "1.1.1", - "yargs": "13.3.0" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } + "yargs": "16.2.0" } }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true }, "lodash.includes": { "version": "4.3.0", @@ -487,6 +1027,21 @@ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" }, + "lodash.set": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", + "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=", + "dev": true + }, + "log-symbols": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", + "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==", + "dev": true, + "requires": { + "chalk": "^4.0.0" + } + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -528,21 +1083,250 @@ "mime-db": "1.44.0" } }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "mocha": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.2.0.tgz", + "integrity": "sha512-lEWEMq2LMfNJMKeuEwb5UELi+OgFDollXaytR5ggQcHpzG3NP/R7rvixAvF+9/lLsTWhWG+4yD2M70GsM06nxw==", + "dev": true, + "requires": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.4.3", + "debug": "4.2.0", + "diff": "4.0.2", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.1.6", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "3.14.0", + "log-symbols": "4.0.0", + "minimatch": "3.0.4", + "ms": "2.1.2", + "nanoid": "3.1.12", + "serialize-javascript": "5.0.1", + "strip-json-comments": "3.1.1", + "supports-color": "7.2.0", + "which": "2.0.2", + "wide-align": "1.1.3", + "workerpool": "6.0.2", + "yargs": "13.3.2", + "yargs-parser": "13.1.2", + "yargs-unparser": "2.0.0" + }, + "dependencies": { + "debug": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", + "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "p-limit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.0.2.tgz", + "integrity": "sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + } + } + } + } + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "nanoid": { + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.12.tgz", + "integrity": "sha512-1qstj9z5+x491jfiC4Nelk+f8XBad7LN20PmyWINJEMRSf3wcAjAWysw1qaA8z6NSKe2sjq1hRSDpBH5paCb6A==", + "dev": true + }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, + "nise": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/nise/-/nise-4.0.4.tgz", + "integrity": "sha512-bTTRUNlemx6deJa+ZyoCUTRvH3liK5+N6VQZ4NIw90AgDXY6iPnsqplNFf6STcj+ePk0H/xqxnP75Lr0J0Fq3A==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0", + "@sinonjs/fake-timers": "^6.0.0", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + } + } + }, + "nock": { + "version": "13.0.4", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.0.4.tgz", + "integrity": "sha512-alqTV8Qt7TUbc74x1pKRLSENzfjp4nywovcJgi/1aXDiUxXdt7TkruSTF5MDWPP7UoPVgea4F9ghVdmX0xxnSA==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "lodash.set": "^4.3.2", + "propagate": "^2.0.0" + }, + "dependencies": { + "debug": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", + "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, "node-fetch": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -564,42 +1348,50 @@ "resolved": "https://registry.npmjs.org/openurl/-/openurl-1.1.1.tgz", "integrity": "sha1-OHW0sO96UsFW8NtB1GCduw+Us4c=" }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "requires": { - "p-limit": "^2.0.0" - } - }, "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" }, "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true + }, + "propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true + }, "proxy-addr": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", @@ -614,6 +1406,15 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -630,6 +1431,33 @@ "unpipe": "1.0.0" } }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdirp": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", + "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "requires": { + "resolve": "^1.1.6" + } + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -638,7 +1466,17 @@ "require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "resolve": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.18.1.tgz", + "integrity": "sha512-lDfCPaMKfOJXjy0dPayzPdF1phampNWr3qFCjAu+rw/qbQmr5jWH5xN2hwh9QKfw9E5v4hwV7A+jrCmL8yjjqA==", + "requires": { + "is-core-module": "^2.0.0", + "path-parse": "^1.0.6" + } }, "safe-buffer": { "version": "5.1.2", @@ -681,6 +1519,21 @@ "statuses": "~1.5.0" }, "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, "ms": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", @@ -688,6 +1541,15 @@ } } }, + "serialize-javascript": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", + "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, "serve-static": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", @@ -702,13 +1564,45 @@ "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true }, "setprototypeof": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" }, + "shelljs": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.4.tgz", + "integrity": "sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ==", + "requires": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + } + }, + "sinon": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.1.tgz", + "integrity": "sha512-naPfsamB5KEE1aiioaoqJ6MEhdUs/2vtI5w1hPAXX/UwvoPjXcwh1m5HiKx0HGgKR8lQSoFIgY5jM6KK8VrS9w==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.8.1", + "@sinonjs/fake-timers": "^6.0.1", + "@sinonjs/formatio": "^5.0.1", + "@sinonjs/samsam": "^5.2.0", + "diff": "^4.0.2", + "nise": "^4.0.4", + "supports-color": "^7.1.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, "statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -718,25 +1612,141 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, "requires": { "emoji-regex": "^7.0.1", "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^5.1.0" } }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, "strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, "requires": { "ansi-regex": "^4.1.0" } }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "superagent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-6.1.0.tgz", + "integrity": "sha512-OUDHEssirmplo3F+1HWKUrUjvnQuA+nZI6i/JJBdXb5eq9IyEQwPyPpqND+SSsxf6TygpBEkUjISVRN4/VOpeg==", + "dev": true, + "requires": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.2", + "debug": "^4.1.1", + "fast-safe-stringify": "^2.0.7", + "form-data": "^3.0.0", + "formidable": "^1.2.2", + "methods": "^1.1.2", + "mime": "^2.4.6", + "qs": "^6.9.4", + "readable-stream": "^3.6.0", + "semver": "^7.3.2" + }, + "dependencies": { + "debug": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.0.tgz", + "integrity": "sha512-jjO6JD2rKfiZQnBoRzhRTbXjHLGLfH+UtGkWLc/UXAh/rzZMyjbgn0NcfFpqT8nd1kTtFnDiJcrIFkq4UKeJVg==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "mime": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", + "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "qs": { + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", + "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==", + "dev": true + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + } + } + }, + "supertest": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.0.1.tgz", + "integrity": "sha512-8yDNdm+bbAN/jeDdXsRipbq9qMpVF7wRsbwLgsANHqdjPsCoecmlTuqEcLQMGpmojFBhxayZ0ckXmLXYq7e+0g==", + "dev": true, + "requires": { + "methods": "1.1.2", + "superagent": "6.1.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, "toidentifier": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, + "tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==" + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -746,6 +1756,15 @@ "mime-types": "~2.1.24" } }, + "universal-github-app-jwt": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-1.1.0.tgz", + "integrity": "sha512-3b+ocAjjz4JTyqaOT+NNBd5BtTuvJTxWElIoeHSVelUV9J3Jp7avmQTdLKCaoqi/5Ox2o/q+VK19TJ233rVXVQ==", + "requires": { + "@types/jsonwebtoken": "^8.3.3", + "jsonwebtoken": "^8.5.1" + } + }, "universal-user-agent": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", @@ -756,6 +1775,11 @@ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -766,15 +1790,68 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, "which-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "workerpool": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.0.2.tgz", + "integrity": "sha512-DSNyvOpFKrNusaaUwk+ej6cBj1bmhLcBfj80elGk+ZIo5JSkq+unB1dLKEOcNfJDZgjGICfhQ0Q5TbP0PvF4+Q==", + "dev": true }, "wrap-ansi": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, "requires": { "ansi-styles": "^3.2.0", "string-width": "^3.0.0", @@ -805,7 +1882,8 @@ "y18n": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true }, "yallist": { "version": "4.0.0", @@ -813,30 +1891,140 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yargs": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", - "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.1" + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz", + "integrity": "sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg==" + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==" + } } }, "yargs-parser": { "version": "13.1.2", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, "requires": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } + }, + "yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "requires": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "dependencies": { + "camelcase": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.1.0.tgz", + "integrity": "sha512-WCMml9ivU60+8rEJgELlFp1gxFcEGxwYleE3bziHEDeqsqAWGHdimB7beBFGjLzVNgPGyDsfgXLQEYMpmIFnVQ==", + "dev": true + }, + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true + } + } } } } diff --git a/package.json b/package.json index 945e616..7c05a74 100644 --- a/package.json +++ b/package.json @@ -1,30 +1,45 @@ { "name": "matlab-ci", - "version": "1.0.0", - "description": "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.", - "main": "index.js", + "version": "2.0.0", + "description": "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.", + "main": "main.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "mocha -r dotenv/config ./test", + "start": "node -r dotenv/config main.js" }, "repository": { "type": "git", "url": "git+https://github.com/cortex-lab/matlab-ci.git" }, "keywords": [], - "author": "", + "author": "Miles Wells", "license": "ISC", "bugs": { "url": "https://github.com/cortex-lab/matlab-ci/issues" }, "homepage": "https://github.com/cortex-lab/matlab-ci#readme", "dependencies": { - "@octokit/app": "^4.2.1", + "@octokit/auth-app": "^2.10.2", "@octokit/request": "^5.4.9", + "debug": "^4.3.1", + "dotenv": "^8.2.0", "express": "^4.17.1", - "localtunnel": "^2.0.0", - "dotenv": "^8.2.0" + "github-webhook-handler": "^1.0.0", + "localtunnel": "^2.0.1", + "shelljs": "^0.8.4", + "tree-kill": "^1.2.2" }, "devDependencies": { + "chai": "^4.2.0", + "chai-spies": "^1.0.0", + "fake-timers": "^0.1.2", + "mocha": "^8.2.0", + "nock": "^13.0.4", + "sinon": "^9.2.1", + "supertest": "^6.0.1", "xml2js": "^0.4.23" + }, + "engines": { + "node": ">=12.19.0" } } diff --git a/queue.js b/queue.js index 9b099d4..2bf66c4 100644 --- a/queue.js +++ b/queue.js @@ -1,4 +1,5 @@ var EventEmitter = require('events').EventEmitter +const assert = require('assert') /** * Queue module allows one to add tasks to a queue which are processed sequentially as FILO. @@ -19,22 +20,22 @@ var EventEmitter = require('events').EventEmitter /** Class representing a Queue API. */ class Queue extends EventEmitter { - + pile = []; /** * Create queue to add jobs to. * @param {string} path - Path to saved queue object (TODO). - * @param {Array} pile - Array of queued job objects. - * @param (Function) _process - Handle to job process function. + * @property {Array} pile - Array of queued job objects. + * @property (Function) _process - Handle to job process function. * @event module:Queue~finish * @event module:Queue~error * @event module:Queue~complete * @listens module:Queue~event:finish * @see {@link Job} */ + constructor(timeout, path) { super(); // Initialize properties - this.pile = []; this.path = typeof path == 'undefined' ? './queue.json' : path; //TODO Implement this.on('finish', function () { // Each time a job finishes... this.pile.shift(); // take off pile @@ -46,7 +47,10 @@ class Queue extends EventEmitter { * @param {Object} data - Data object to be stored in {@link Job}. */ add(data) { - var id = this.pile.length + 1; // generate job id + // generate 16 digit job id + let d = Date.now().toString() + let r = Math.floor(Math.random() * 1000).toString() + let id = Number((r + d).padEnd(16, '0')) this.pile.push(new Job(id, data)); // add to bottom of pile console.log('Job added (' + this.pile.length + ' on pile)') this.next(); // Start next job if idle @@ -65,7 +69,6 @@ class Queue extends EventEmitter { /** * Create callback to be triggered when process function completes. * @param {Object} job - {@link Job} object. - * @todo Change 'incomplete' => 'error' * @returns {function} 'done' callback to be called by process function */ createDoneCallback(job) { @@ -73,8 +76,8 @@ class Queue extends EventEmitter { return function( err ) { job.isRunning = false; // set false (will emit 'end') if( err ) { obj.emit('error', err, job); } - else {obj.emit('complete', job)} - obj.emit('finish', job); + else { obj.emit('complete', job) } + obj.emit('finish', err, job); } } @@ -82,12 +85,13 @@ class Queue extends EventEmitter { /** * Create callback to be triggered when process function completes. * @param {Function} func - Function to call with job and done callback when. + * @todo make done callback part of job obj? */ process(func) { this._process = async (job) => { - var done = this.createDoneCallback(job); + job.done = this.createDoneCallback(job); job.isRunning = true; - setImmediate(func, job, done); + setImmediate(func, job, job.done); console.log('Job running') }; } @@ -95,7 +99,9 @@ class Queue extends EventEmitter { /** Class representing a job in the Queue. */ class Job extends EventEmitter { - + id; + data; + running; /** * Create a job object with associated data. * @param {number} id - Job ID (unique in current Queue pile). diff --git a/runAllTests.m b/runAllTests.m index c7e82cb..2af73d1 100644 --- a/runAllTests.m +++ b/runAllTests.m @@ -1,4 +1,4 @@ -function runAllTests(id, repo) +function runAllTests(id, repo, logDir) %% Script for running all Rigbox tests % To be called for code checks and the like % TODO May add flags for levels of testing @@ -6,11 +6,12 @@ function runAllTests(id, repo) % @body Technically two different repos can have the same commit hash, in % which case the db.json file should be restructured % v1.1.2 -if nargin < 2; repo = 'rigbox'; end -if nargin < 1; id = []; end +if nargin < 2, repo = 'rigbox'; end +if nargin < 1, id = []; end +if nargin < 3, logDir = fullfile(getenv('appdata'), 'CI'); end try %% Initialize enviroment - dbPath = 'C:\Users\Experiment\db.json'; + dbPath = fullfile(logDir, '.db.json'); % TODO Load from config file fprintf('Running tests\n') fprintf('Repo = %s, sha = %s\n', repo, id) origDir = pwd; @@ -24,53 +25,62 @@ function runAllTests(id, repo) import matlab.unittest.plugins.codecoverage.CoberturaFormat % Suppress warnings about shadowed builtins in utilities folder warning('off','MATLAB:dispatcher:nameConflict') - + %% Gather Rigbox main tests main_tests = testsuite('IncludeSubfolders', true); - + %% Gather signals tests root = getOr(dat.paths,'rigbox'); signals_tests = testsuite(fullfile(root, 'signals', 'tests'), ... 'IncludeSubfolders', true); - + %% Gather alyx-matlab tests alyx_tests = testsuite(fullfile(root, 'alyx-matlab', 'tests'), ... 'IncludeSubfolders', true); - + %% Filter & run % the suite is automatically sorted based on shared fixtures. However, if % you add, remove, or reorder elements after initial suite creation, call % the sortByFixtures method to sort the suite. all_tests = [main_tests signals_tests alyx_tests]; % If the repo under test is alyx, filter out irrelevent tests - if strcmp(repo, 'alyx') + if endsWith(repo, 'alyx') all_tests = all_tests(startsWith({all_tests.Name}, 'Alyx', 'IgnoreCase', true)); - elseif strcmp(repo, 'alyx-matlab') + elseif endsWith(repo, 'alyx-matlab') all_tests = alyx_tests; - elseif strcmp(repo, 'signals') + elseif endsWith(repo, 'signals') all_tests = signals_tests; end - + % Filter out performance tests % @todo Run performance tests % @body Currently the performance tests are entirely filtered out is_perf = @(t) contains(t.Name, 'perftest', 'IgnoreCase', true); [~, all_tests] = fun.filter(is_perf, all_tests); - + runner = TestRunner.withTextOutput; - reportFile = fullfile(fileparts(dbPath), 'CoverageResults.xml'); + reportFolder = fullfile(logDir, 'reports', id); + reportFile = fullfile(reportFolder, 'CoverageResults.xml'); reportFormat = CoberturaFormat(reportFile); plugin = CodeCoveragePlugin.forFolder(root, 'Producing', reportFormat, ... 'IncludingSubfolders', true); runner.addPlugin(plugin) - + % @todo Possible to output HTML also? + if ~verLessThan('matlab', '9.6') +% import matlab.unittest.plugins.codecoverage.CoverageReport + reportFormat = CoverageReport(reportFolder); + plugin = CodeCoveragePlugin.forFolder(root, 'Producing', reportFormat, ... + 'IncludingSubfolders', true); + runner.addPlugin(plugin) + end + results = runner.run(all_tests); assert(now - file.modDate(reportFile) < 0.001, ... 'Coverage file may not have been updated') - + % If no commit id set, simply exit the function if isempty(id); return; end - + %% Diagnostics % Summarize the results of the tests and write results to the JSON file % located at dbPath @@ -82,7 +92,7 @@ function runAllTests(id, repo) 'results', results, ... 'status', status, ... 'description', context, ... - 'coverage', []); % Coverage updated by Node.js script + 'coverage', nan); % Coverage updated by Node.js script if file.exists(dbPath) data = jsondecode(fileread(dbPath)); idx = strcmp(id, {data.commit}); % Check record exists for this commit @@ -101,4 +111,4 @@ function runAllTests(id, repo) ex.stack(1).name, ex.stack(1).line, ex.identifier, ex.message) disp(getReport(ex)) % Display details for debugging if ~isempty(id), exit(1), end -end \ No newline at end of file +end diff --git a/runAllTests.py b/runAllTests.py new file mode 100644 index 0000000..3394961 --- /dev/null +++ b/runAllTests.py @@ -0,0 +1,219 @@ +"""A module for running ibllib continuous integration tests with coverage +In order for this to work ibllib and iblscripts must be installed as python package from GitHub, +as well as the coverage package. +""" +import argparse +import unittest +import re +import json +import logging +from datetime import datetime +from logging.handlers import RotatingFileHandler +from os import sep +from pathlib import Path +from typing import Iterable, List, Union + +from coverage import Coverage +from coverage.misc import CoverageException + +from ibllib.misc.flatten import flatten +from ibllib.misc.version import ibllib as ver + +logger = logging.getLogger('ibllib') + +try: # Import the test packages + import brainbox.tests, ci.tests, ibllib.tests, alf.tests, oneibl.tests +except ModuleNotFoundError as ex: + logger.warning(f'Failed to import test packages: {ex} encountered') + + +def list_tests(suite: Union[List, unittest.TestSuite, unittest.TestCase]) -> Union[List[str], str]: + """ + Returns a full list of the tests run in the format 'TestClassName/test_method' + :param suite: A TestCase or TestSuite instance, or list thereof + :return: A list of tests + """ + if isinstance(suite, list): + return flatten([list_tests(x) for x in suite]) + elif not unittest.suite._isnotsuite(suite): + return list_tests(suite._tests) + elif isinstance(suite, (unittest.TestSuite, unittest.TestCase)): + return f'{suite.__class__.__name__}/{suite._testMethodName}' + + +def generate_coverage_report(cov, save_path, strict=False, relative_to=None): + """ + Generates HTML and XML reports of test coverage and returns the total coverage + :param cov: A Coverage object + :param save_path: Where to save the coverage files + :param strict: If True, asserts that the coverage report was created + :param relative_to: The root folder for the functions coverage + :return: + """ + try: + total = cov.html_report(directory=str(save_path)) + cov.xml_report(outfile=str(save_path.joinpath('CoverageResults.xml'))) + success = save_path.joinpath('CoverageResults.xml').exists() + assert not strict or success, 'failed to generate XML coverage' + except (CoverageException, AssertionError) as ex: + if strict: + raise ex + total = None + logger.error('Failed to save coverage: %s', ex) + + if relative_to: + # Rename the HTML files for readability and to obscure the server's directory structure + pattern = re.sub(r'^[a-zA-Z]:[/\\]|[/\\]', '_', str(relative_to.parent)) + '_' # / -> _ + for file in Path(save_path).glob('*.html'): # Open each html report file + with open(file, 'r') as f: + data = f.read() + data = data.replace(pattern, '') # Remove long paths in filename links + data = data.replace(str(relative_to.parent) + sep, '') # Remove from text + with open(file, 'w') as f: + f.write(data) # Write back into file + file.rename(str(file).replace(pattern, '')) # Rename file + return total + + +def run_tests(complete: bool = True, + strict: bool = True, + dry_run: bool = False) -> (unittest.TestResult, Coverage, unittest.TestSuite): + """ + Run integration tests + :param complete: When true ibllib unit tests are run in addition to the integration tests. + :param strict: When true asserts that all gathered tests were successfully imported. This + means that a module not found error in any test module will raise an exception. + :param dry_run: When true the tests are gathered but not run. + :return Test results and coverage objects, and test suite. + """ + # Coverage recorded for all code within the source directory; otherwise just omit some + # common pyCharm files + options = {'omit': ['*pydevd_file_utils.py', 'test_*'], 'source': []} + + # Gather tests + test_dir = str(Path(ci.tests.__file__).parent) + logger.info(f'Loading integration tests from {test_dir}') + ci_tests = unittest.TestLoader().discover(test_dir, pattern='test_*') + if complete: # include ibllib and brainbox unit tests + root = Path(ibllib.__file__).parents[1] # Search relative to our imported ibllib package + test_dirs = [root.joinpath(x) for x in ('brainbox', 'oneibl', 'ibllib', 'alf')] + for tdir in test_dirs: + logger.info(f'Loading unit tests from folders: {tdir}') + assert tdir.exists(), f'Failed to find unit test folders in {tdir}' + unit_tests = unittest.TestLoader().discover(str(tdir), pattern='test_*', top_level_dir=root) + logger.info(f"Found {unit_tests.countTestCases()}, appending to the test suite") + ci_tests = unittest.TestSuite((ci_tests, *unit_tests)) + # for coverage, append the path of the test modules to the source key + options['source'].append(str(tdir)) + logger.info(f'Complete suite contains {ci_tests.countTestCases()} tests') + # Check all tests loaded successfully + not_loaded = [x[12:] for x in list_tests(ci_tests) if x.startswith('_Failed')] + if len(not_loaded) != 0: + err_msg = 'Failed to import the following tests:\n\t' + '\n\t'.join(not_loaded) + assert not strict, err_msg + logger.warning(err_msg) + + if dry_run: + return unittest.TestResult(), Coverage(**options), ci_tests + + # Run tests with coverage + cov = Coverage(**options) + cov.start() + + result = unittest.TextTestRunner(verbosity=2).run(ci_tests) + + cov.stop() + cov.save() + + return result, cov, ci_tests + + +if __name__ == "__main__": + r"""Run all the integration tests with coverage + The commit id is used to identify the test report. If none is provided no test record is saved + + python runAllTests.py --logdir --commit --repo + + Examples: + python runAllTests.py -l C:\Users\User\AppData\Roaming\CI + python runAllTests.py -l ~/.ci + """ + # Defaults + root = Path(__file__).parent.absolute() # Default root folder + repo_dir = Path(ibllib.__file__).parent # Default repository source for coverage + version = ver() + if not version or version == 'unversioned': + getattr(ibllib, '__version__', datetime.now().strftime('%Y-%m-%d_%H%M%S')) + + # Parse parameters + parser = argparse.ArgumentParser(description='Integration tests for ibllib.') + parser.add_argument('--commit', '-c', default=version, + help='commit id. If none provided record isn''t saved') + parser.add_argument('--logdir', '-l', help='the log path', default=root) + parser.add_argument('--repo', '-r', help='repo directory', default=repo_dir) + parser.add_argument('--dry-run', help='gather tests without running', action='store_true') + args = parser.parse_args() # returns data from the options specified (echo) + + # Paths + report_dir = Path(args.logdir).joinpath('reports', args.commit) + # Create the reports tree if it doesn't already exist + report_dir.mkdir(parents=True, exist_ok=True) + db_file = Path(args.logdir, '.db.json') + + # Setup backup log (NB: the system output is also saved by the ci) + logfile = report_dir / 'test_output.log' + fh = RotatingFileHandler(logfile, maxBytes=(1048576 * 5)) + logger.addHandler(fh) + logger.setLevel(logging.INFO) + + # Tests + logger.info(Path(args.repo).joinpath('*')) + result, cov, test_list = run_tests(dry_run=args.dry_run) + + # Generate report + logger.info('Saving coverage report to %s', report_dir) + + total = generate_coverage_report(cov, report_dir, relative_to=Path(ibllib.__file__).parent, + strict=not args.dry_run) + + # When running tests without a specific commit, exit without saving the result + if args.commit is parser.get_default('commit'): + exit(0) + + # Summarize the results of the tests and write results to the JSON file + logger.info('Saving outcome to %s', db_file) + status = 'success' if result.wasSuccessful() else 'failure' + n_failed = len(result.failures) + len(result.errors) + fail_str = f'{n_failed}/{result.testsRun} tests failed' + description = 'All passed' if result.wasSuccessful() else fail_str + # Save all test names if all passed, otherwise save those that failed and their error stack + if n_failed > 0: + details = [(list_tests(c), err) for c, err in result.failures + result.errors] + logger.warning(description) + else: + details = list_tests(test_list) + logger.info(description) + print(*details, sep='\n') # Print all tests for the log + + report = { + 'commit': args.commit + ('_dry-run' if args.dry_run else ''), + 'results': details, + 'status': status, + 'description': description, + 'coverage': total # coverage usually updated by Node.js script + } + + if db_file.exists(): + with open(db_file, 'r') as json_file: + records = json.load(json_file) + try: # update existing + idx = next(i for i, r in enumerate(records) if r['commit'] == args.commit) + records[idx] = report + except StopIteration: # ...or append record + records.append(report) + else: + records = [report] + + # Save record to file + with open(db_file, 'w') as json_file: + json.dump(records, json_file) diff --git a/run_tests.BAT b/run_tests.BAT new file mode 100644 index 0000000..cd3948f --- /dev/null +++ b/run_tests.BAT @@ -0,0 +1,34 @@ +::=============================================================== +:: Run Rigbox tests +:: +:: 2021-01 MW created +::=============================================================== +@ECHO OFF +:: Check parameters (id, repo_path, logdir) +IF %1.==. (GOTO Err1) +IF %2.==. (GOTO Err2) +IF %3.==. (GOTO Err3) + +:: Run tests +matlab -r runAllTests('%1','%2','%3');exit -wait -log -nosplash -logfile matlab_tests.log +:: Copy over log +SET short=%1 +SET log_file=%3\reports\%1\std_output-%short:~0,7%.log +TYPE matlab_tests.log >> %log_file% +MOVE matlab_tests.log %3\reports\%1\test_output.log +:: Kill Alyx login windows in edge +taskkill /FI "IMAGENAME eq msedge.exe" /FI "Windowtitle eq Log in | Alyx*" + +EXIT /B %ERRORLEVEL% + +:Err1 + ECHO No SHA param defined 1>&2 + EXIT /B 1 + +:Err2 + ECHO No PATH param defined 1>&2 + EXIT /B 1 + +:Err3 + ECHO No LOG param defined 1>&2 + EXIT /B 1 diff --git a/serve.js b/serve.js new file mode 100644 index 0000000..ff87d09 --- /dev/null +++ b/serve.js @@ -0,0 +1,582 @@ +/** + * A module for configuring the reverse proxy, a local server to process and make requests and + * middleware for authenticating Github requests and serving local test reports. + */ +const fs = require('fs'); +const path = require('path'); +const cp = require('child_process'); + +const express = require('express'); +const srv = express(); +const shell = require('shelljs'); +const app = require("@octokit/auth-app"); +const { request } = require('@octokit/request'); + +const config = require('./config/config').settings; +const queue = require('./lib').queue; // shared Queue from lib +const log = require('./lib').log; +const lib = require('./lib'); + +// The installation token is a temporary token required for making changes to the Github Checks. +// The token may be set each time a new Github request is received, and before an API request. +var token = {'tokenType': null}; +// A hash of the secret is send in the X-Hub-Signature; the handler checks the hash to validate +// that the request comes from GitHub. +const secret = process.env['GITHUB_WEBHOOK_SECRET']; +// Currently this app is only set up to process push and pull request events so we will have the +// handler reject any others. We will also check that only these are set up in the config. +const supportedEvents = ['push', 'pull_request']; // events the ci can handle +const maxN = 140; // The maximum n chars of the status description +const ENDPOINT = 'logs'; // The URL endpoint for fetching status check details + +// Check all config events are supported +const events = Object.keys(config.events); +if (events.some(evt => { return !supportedEvents.includes(evt); })) { + let errStr = 'One or more events in config not supported. ' + + `The following events are supported: ${supportedEvents.join(', ')}`; + throw new ReferenceError(errStr) +} + +// Create handler to verify posts signed with webhook secret. Content type must be application/json +const createHandler = require('github-webhook-handler'); +const handler = createHandler({ path: '/github', secret: secret, events: supportedEvents}); + +/** + * Fetch and assign the installation access token. Should be called each time a POST is made to + * our app's endpoint. + * @returns {Promise} - A promise resolved when the installationAccessToken var has been set. + */ +async function setAccessToken() { + let debug = log.extend('auth'); + // Return if token still valid + if (new Date(token.expiresAt) > new Date()) { return; } + // Create app instance for authenticating our GitHub app + const auth = app.createAppAuth({ + appId: process.env['GITHUB_APP_IDENTIFIER'], + privateKey: fs.readFileSync(process.env['GITHUB_PRIVATE_KEY']), + webhooks: { secret } + }); + + if (token.tokenType !== 'installation') { + debug('Fetching install ID'); + // Retrieve JSON Web Token (JWT) to authenticate as app + token = await auth({type: "app"}); + // Get installation ID + const {data: {id}} = await request("GET /repos/:owner/:repo/installation", { + owner: process.env['REPO_OWNER'], + repo: process.env['REPO_NAME'], + headers: { + authorization: `bearer ${token.token}`, + accept: "application/vnd.github.machine-man-preview+json" + } + }); + token.installationId = id; + } + debug('Fetching install token'); + // Retrieve installation token + const options = { + type: 'installation', + installationId: token.installationId + }; + token = await auth(options); + debug('Authentication complete'); +} + + +///////////////////// MAIN APP ENTRY POINT ///////////////////// + +/** + * Callback to deal with POST requests from /github endpoint, authenticates as app and passes on + * request to handler. + * @param {Object} req - Request object. + * @param {Object} res - Response object. + * @param {Function} next - Handle to next callback in stack. + * @todo split auth and handler middleware + */ +srv.post('/github', async (req, res, next) => { + console.log('Post received') + let id = req.header('x-github-hook-installation-target-id'); + if (id != process.env.GITHUB_APP_IDENTIFIER) { next(); return; } // Not for us; move on + await setAccessToken(); + log.extend('event')('X-GitHub-Event: %s', req.header('X-GitHub-Event')); + handler(req, res, () => res.end('ok')); +}); + +/** + * Register invalid Github POST requests to handler via /github endpoint. + * Failed spoof attempts may end up here but most likely it will be unsupported webhook events. + */ +handler.on('error', function (err) { + console.log('Error:', err.message); +}); + + +///////////////////// STATUS DETAILS ///////////////////// + +/** + * Serve the reports tree as a static resource; allows users to inspect the HTML coverage reports. + * We will add a link to the reports in the check details. + */ +srv.use(`/${ENDPOINT}/coverage`, express.static(path.join(config.dataPath, 'reports'))) + +/** + * Serve the test records for requested commit id. Returns JSON data for the commit. + */ +srv.get(`/${ENDPOINT}/records/:id`, function (req, res) { + let id = lib.shortID(req.params.id); + let isSHA = (req.query.branch || !lib.isSHA(req.params.id)) === false; + console.log('Request for test records for ' + (isSHA? `commit ${id}` : `branch ${req.params.id}`)); + const data = { + owner: process.env['REPO_OWNER'], + repo: req.query.module || process.env.REPO_NAME, + id: req.params.id + }; + let endpoint = `GET /repos/:owner/:repo/${isSHA ? 'commits' : 'branches'}/:id`; + request(endpoint, data) + .then(response => { + let id = isSHA ? response.data.sha : response.data.commit.sha; + log('Commit ID found: %s', id); + let record = lib.loadTestRecords(id); + if (record) { + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(record)); + } else { + res.statusCode = 404; + res.send(`${isSHA? 'Commit' : 'Branch'} ${req.params.id} not recognized.`); + } + }) + .catch(err => { + log('%s', err.message); + res.statusCode = 404; + res.send(`Record for ${isSHA? 'commit' : 'branch'} ${req.params.id} not found`); + }); +}); + +/** + * Serve the test results for requested commit id. This will be the result of a user clicking on + * the 'details' link next to the continuous integration check. The result should be an HTML + * formatted copy of the stdout for the job's process. + */ +srv.get(`/${ENDPOINT}/:id`, function (req, res) { + let id = lib.shortID(req.params.id); + let isSHA = lib.isSHA(req.params.id); + let log_only = (req.query.type || '').startsWith('log') + console.log( + `Request for test ${log_only ? 'log' : 'stdout'} for ` + + (isSHA? `commit ${id}` : `branch ${req.params.id}`) + ); + let filename = log_only? `test_output.log` : `std_output-${id}.log`; + let logFile = path.join(config.dataPath, 'reports', req.params.id, filename); + fs.readFile(logFile, 'utf8', (err, data) => { + if (err) { + log('%s', err.message); + res.statusCode = 404; + res.send(`Record for ${isSHA? 'commit' : 'branch'} ${req.params.id} not found`); + } else { + res.statusCode = 200; + // Wrap in HTML tags so that the formatting is a little nicer. + let preText = '
';
+    	   let postText = '
'; + res.send(preText + data + postText); + } + }); +}); + + +///////////////////// SHIELDS API EVENTS ///////////////////// + +/** + * Serve the coverage results and build status for the shields.io coverage badge API. Attempts to + * load the test results from file and if none exist, adds a new job to the queue. + */ +srv.get('/:badge/:repo/:branch', async (req, res) => { + const data = { + owner: process.env['REPO_OWNER'], + repo: req.params.repo, + branch: req.params.branch, + } + // Find head commit of branch + return request('GET /repos/:owner/:repo/git/refs/heads/:branch', data) + .then(response => { + data['context'] = req.params.badge; + data['sha'] = response.data.object.sha; + data['force'] = req.query.force === '' || lib.strToBool(req.query.force); + console.log(`Request for ${data.branch} ${data.context}`) + const report = lib.getBadgeData(data); // TODO If pending return 201, else 200 + // Send report + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(report));}) + .catch(err => { // Specified repo or branch not found + console.error(`${data.owner}/${data.repo}/${data.branch} not found`) + res.sendStatus((err.status === 404) ? 404 : 500) + }); +}); + + +///////////////////// QUEUE EVENTS ///////////////////// + +function runTests(job) { + const debug = log.extend('runTests'); + debug('starting job timer'); + const timer = lib.startJobTimer(job, config.kill_children === true); + + // Go ahead with tests + const sha = job.data['sha']; + const repoPath = getRepoPath(job.data.repo); + const logName = path.join(config.dataPath, 'reports', sha, `std_output-${lib.shortID(sha)}.log`); + let fcn = lib.fullpath(config.test_function); + debug('starting test child process %s', fcn); + let ops = config.shell? {'shell': config.shell} : {}; + const runTests = cp.execFile(fcn, [sha, repoPath, config.dataPath], ops, (error, stdout, stderr) => { + debug('clearing job timer'); + clearTimeout(timer); + delete job.data.process; + if (error) { // Send error status + let message; + if (error.killed || error.signal === 'SIGTERM') { + message = `Tests stalled after ~${(config.timeout / 60000).toFixed(0)} min`; + } else { + debug('error from test function: %o', error) + // Isolate error from log + // For MATLAB return the line that begins with 'Error' + let fn = (str) => { return str.startsWith('Error in \'') }; + message = stderr.split(/\r?\n/).filter(fn).join(';'); + // For Python, cat from the lost line that doesn't begin with whitespace + if (!message) { + let errArr = stderr.split(/\r?\n/); + let idx = errArr.reverse().findIndex(v => {return v.match('^\\S')}); + message = stderr.split(/\r?\n/).slice(-idx-1).join(';'); + } + if (!message) { message = error.code; } + } + // Save error into records for future reference. NB: This is currently not done for prepEnv errors + let report = { + 'commit': sha, + 'results': message, + 'status': 'error', + 'description': 'Error running ' + (config.test_function || 'test function') + }; + lib.saveTestRecords(report).then(() => { debug('updated test records'); }); + job.done(new Error(message)); // Propagate + } else { + if (!lib.updateJobFromRecord(job)) { + job.done(new Error('Failed to return test result')); + } else { + job.done(); + } + } + }); + job.data.process = runTests; + + // Write output to file + runTests.stdout.pipe(process.stdout); // Pipe to display + let logDump = fs.createWriteStream(logName, { flags: 'a' }); + runTests.stdout.pipe(logDump); + runTests.on('exit', () => { logDump.close(); }); + return runTests; +} + +function prepareEnv(job, callback) { + log('Preparing environment for job #%g', job.id) + const repoPath = getRepoPath(job.data.repo); + switch (config.setup_function) { + case undefined: + // run some basic git commands + checkout(repoPath, job.data.sha); + return callback(job); + case null: // No prep required + return callback(job); + default: + const sha = job.data['sha']; + const logDir = path.join(config.dataPath, 'reports', sha); + const logName = path.join(logDir, `std_output-${lib.shortID(sha)}.log`); + log('Calling %s with args %o', config.setup_function, [sha, repoPath, logName]); + let fcn = lib.fullpath(config.setup_function); + let ops = config.shell? {'shell': config.shell} : {}; + const prepEnv = cp.execFile(fcn, [sha, repoPath, logDir], ops, (err, stdout, stderr) => { + if (err) { + let errmsg = (err.code === 'ENOENT')? `File "${fcn}" not found` : err.code; + console.error('Checkout failed: ' + (stderr || errmsg)); + job.done(new Error(`Failed to prepare env: ${stderr || errmsg}`)); // Propagate error + return; + } + callback(job); + }); + prepEnv.stdout.pipe(process.stdout); + fs.mkdir(path.join(logDir), { recursive: true }, (err) => { + if (err) throw err; + let logDump = fs.createWriteStream(logName, { flags: 'w' }); + prepEnv.stdout.pipe(logDump); + prepEnv.on('exit', () => { logDump.close(); }); + }); + return prepEnv; + } +} + +/** + * Checkout Git repository. + * @param {String} repoPath - The path of the repository + * @param {String} ref - A commit SHA or branch name + * @todo Add error handling + */ +function checkout(repoPath, ref) { + if (!shell.which('git')) { throw new Error('Git not found on path'); } + let verify = (cmd) => { if (!cmd) { + shell.popd(); + throw new Error('Failed to checkout: ' + cmd.stderr); + } }; + if (!shell.pushd(repoPath)) { + shell.mkdir(path.resolve(repoPath + path.sep + '..')); + shell.pushd(repoPath); + verify(shell.exec(`git clone https://github.com/${env.process['REPO_OWNER']}/${env.process['REPO_NAME']}.git`)); + verify(shell.exec(`git checkout ${ref}`)); + } else { + verify(shell.exec('git fetch -a')); + verify(shell.exec('git reset --hard HEAD')); + verify(shell.exec(`git checkout ${ref}`)); + verify(shell.exec('git submodule update --init --recursive')); + verify(shell.exec('git submodule foreach git reset --hard HEAD')); + verify(shell.exec('git status')); + } + shell.popd(); +} + + +/** + * Lists the submodules within a Git repository. If none are found null is returned. + * @param {String} repoPath - The path of the repository + * @returns {Array} A list of submodule names, or null if none were found + */ +function listSubmodules(repoPath) { + if (!shell.which('git')) { throw new Error('Git not found on path'); } + shell.pushd(repoPath); + let listModules = 'git config --file .gitmodules --get-regexp path | awk \'{ print $2 }\''; + const modules = shell.exec(listModules); + shell.popd(); + return (!modules.code && modules.stdout !== '')? modules.split('\n') : null; +} + +/** + * Get the corresponding repository path for a given repo. The function first checks the settings. + * If the `repos` field doesn't exist, the path in ENV is used. If the name is not a key in the + * `repos` object then we check each repo path for submodules and return the first matching + * submodule path. Otherwise returns null. + * @param {String} name - The name of the repository + * @returns {String} The repository path if found + */ +function getRepoPath(name) { + if (!config.repos) { return process.env['REPO_PATH']; } // Legacy, to remove + if (config.repos.name) { return config.repos.name; } // Found path, return + for (let repo of config.repos) { + let modules = listSubmodules(repo); + if (modules && modules.includes(name)) { + // If the repo is a submodule, modify path + return repo + path.sep + name; + } + } +} + + +///////////////////// OTHER ///////////////////// + +/** + * Updates the status of a Github check, given an object of data from a Job. + * @param {Object} data - A dictionary of data including the commit sha, status string and context. + * @param {String} targetURL - The target URL string pointing to the check's details. + * @returns {Function} - A Github request Promise. + */ +async function updateStatus(data, targetURL = '') { + const debug = log.extend('updateStatus'); + // Validate inputs + if (!lib.isSHA(data.sha)) { throw new ReferenceError('undefined or invalid sha'); } // require sha + let supportedStates = ['pending', 'error', 'success', 'failure']; + if (supportedStates.indexOf(data.status) === -1) { + throw new lib.APIError(`status must be one of "${supportedStates.join('", "')}"`) + } + debug('Updating status to "%s" for %s @ %g', + data['status'], (data['context'] || '').split('/').pop(), data['sha']); + await setAccessToken(); + return request("POST /repos/:owner/:repo/statuses/:sha", { + owner: data['owner'] || process.env['REPO_OWNER'], + repo: data['repo'], + headers: { + authorization: `token ${token['token']}`, + accept: "application/vnd.github.machine-man-preview+json" + }, + sha: data['sha'], + state: data['status'], + target_url: targetURL, + description: data['description'].substring(0, maxN), + context: data['context'] + }); +} + + +/** + * Callback triggered when a GitHub event occurs. Here we deal with all events, adding jobs to the + * Queue as needed. If an event is not specified in the config, the callback will return ok but do + * nothing. + * Payload reference https://developer.github.com/webhooks/event-payloads/ + * @param {Object} event - The GitHub event object. + * @todo Save full coverage object for future inspection + * @todo Add support for ignore list for specific actions + * @todo Add support for regex in branch ignore list + */ +async function eventCallback (event) { + const debug = log.extend('event'); + debug('eventCallback called'); + var ref; // ref (i.e. branch name) and head commit + const eventType = event.event; // 'push' or 'pull_request' + var job_template = { // the data structure containing information about our check + sha: null, // The head commit sha to test on + base: null, // The previous commit sha (for comparing changes in code coverage) + force: false, // Whether to run tests when results already cached + owner: process.env['REPO_OWNER'], // event.payload.repository.owner.login + repo: event.payload.repository.name, // The repository name + status: 'pending', // The check state to update our context with + description: null, // A brief description of what transpired + context: null // The precise check name, keeps track of what check we're doing + } + + // Double-check the event was intended for our app. This is also done using the headers before + // this stage. None app specific webhooks could be set up and would make it this far. Could add + // some logic here to deal with generic webhook requests (i.e. skip check status update). + if (event.payload['installation']['id'] !== token['installationId']) { + throw new lib.APIError('Generic webhook events not supported (installation ID invalid)'); + } + + // Harvest data payload depending on event type + switch(eventType) { + case 'pull_request': + let pr = event.payload.pull_request; + ref = pr.head.ref; + job_template['sha'] = pr.head.sha; + job_template['base'] = pr.base.sha; + // Check for repo fork; throw error if forked // TODO Add full stack test for this behaviour + let isFork = (pr.base.repo.owner.login !== pr.head.repo.owner.login) + || (pr.base.repo.owner.login !== process.env['REPO_OWNER']) + || (pr.head.repo.name !== pr.base.repo.name); + if (isFork) { throw ReferenceError('Forked PRs not supported; check config file') } + break; + case 'push': + ref = event.payload.ref; + job_template['sha'] = event.payload.head_commit.id || event.payload.after; // Run tests for head commit only + job_template['base'] = event.payload.before; + break; + default: // Shouldn't get this far + throw new TypeError(`event "${event.event}" not supported`) + } + + // Log the event + console.log('Received a %s event for %s to %s', + eventType.replace('_', ' '), job_template['repo'], ref) + + // Determine what to do from settings + if (!(eventType in config.events)) { + // No events set; return + debug('Event "%s" not set in config', eventType); + return; + } + const todo = config.events[eventType] || {} // List of events to process + + // Check if ref in ignore list or not in include list + let incl = !todo.ref_ignore; // ignore list takes precedence + let ref_list = lib.ensureArray(todo.ref_ignore || todo.ref_include || []); + if ((ref_list.indexOf(ref.split('/').pop()) === -1) === incl) { + // Do nothing if in ignore list, or not in include list + debug(`Ref ${ref} ${incl? 'not' : 'is'} in config ref_${incl? 'include' : 'ignore'} list`); + return; + } + + // Check if action in actions list, if applicable + let actions = lib.ensureArray(todo.actions || []); + if (event.payload.action && actions && actions.indexOf(event.payload.action) === -1) { + debug('Action "%s" not set in config', event.payload.action); + return; + } + + // Validate checks to run + const checks = lib.ensureArray(todo.checks || []); + if (!todo.checks) { + // No checks to perform + debug('No checks set in config'); + return; + } + + // For each check we update it's status and add a job to the queue + let isString = x => { return (typeof x === 'string' || x instanceof String); } + for (let check of checks) { + // Invent a description for the initial status update + if (!isString(check)) { throw new TypeError('Check must be a string') } + // Copy job data and update check specific fields + let data = Object.assign({}, job_template); + data.context = `${check}/${process.env['USERDOMAIN'] || process.env['NAME']}` + switch (check) { + case 'coverage': + data.description = 'Checking coverage'; + break; + case 'continuous-integration': + data.description = 'Tests running'; + break; + default: // generic description + data.description = 'Check in progress'; + } + + // If we have two checks to perform and one already on the pile, set force to false + let qLen = queue.pile.length; + data.force = !(checks.length > 1 && qLen > 0 && queue.pile[qLen-1].data.sha === data.sha); + + /** + * Update the status and start job. + * Posts a 'pending' status while we do our tests + * We wait for the job to be added before we continue so the force flag can be set. + * NB: If the tests and env prep are too quick our outcome may be updated before the pending + * status. + */ + updateStatus(data) + .then(() => console.log(`Updated status to "pending" for ${data.context}`)) + .catch(err => { + console.log(`Failed to update status to "pending" for ${data.context}`); + console.log(err); + }); + queue.add(data); + } +} + + +///////////////////// QUEUE EVENTS ///////////////////// + +/** + * Callback triggered when job finishes. Called both on complete and error. + * @param {Object} job - Job object which has finished being processed. + */ +queue.on('finish', (err, job) => { // On job end post result to API + var target = ''; // We will only update the endpoint for coverage jobs + console.log(`Job #${lib.shortID(job.id)} finished` + (err ? ' with error' : '')); + if (job.data.skipPost === true) { return; } + + // Update target URL + if (!job.data.skipPost && job.data.context.startsWith('coverage')) { + // No URL for coverage if errored + target = err? '' : `${process.env['WEBHOOK_PROXY_URL']}/${ENDPOINT}/coverage/${job.data.sha}`; + } else { + target = `${process.env['WEBHOOK_PROXY_URL']}/${ENDPOINT}/${job.data.sha}`; + } + + // Update status if error occurred + if (err) { + job.data['status'] = 'error'; + job.data['description'] = err.message; + } + + updateStatus(job.data, target) + .then(() => console.log(`Updated status to "${job.data.status}" for ${job.data.context}`)) + .catch(err => { + console.log(`Failed to update status to "${job.data.status}" for ${job.data.context}`); + console.log(err); + }); +}); + +module.exports = { + updateStatus, srv, handler, setAccessToken, prepareEnv, runTests, eventCallback +} diff --git a/start_node.BAT b/start_node.BAT new file mode 100644 index 0000000..f35f11d --- /dev/null +++ b/start_node.BAT @@ -0,0 +1,2 @@ +set DEBUG=* +cmd /k node -r dotenv/config ./main.js \ No newline at end of file diff --git a/test/coverage.test.js b/test/coverage.test.js new file mode 100644 index 0000000..a651c31 --- /dev/null +++ b/test/coverage.test.js @@ -0,0 +1,47 @@ +const fs = require('fs'); +const assert = require('assert'); +const path = require('path'); +const sinon = require('sinon'); + +const config = require('../config/config').settings; +const Coverage = require('../coverage'); + +const dummy_id = '1c33a6e2ac7d7fc098105b21a702e104e09767cf'; + + +xdescribe('Test coverage parser:', function() { + var testable; + // Check NODE_ENV is correctly set, meaning our imported settings will be test ones + before(function () { + assert(process.env.NODE_ENV.startsWith('test'), 'Test run outside test env') + testable = function(obj, done) { + assert(obj.source_files); + done(); + }; + }) + + it('Check loading MATLAB', function (done) { + let xmlPath = path.resolve('test', 'fixtures', 'CoverageResults.mat.xml') + Coverage(xmlPath, 'rigbox', dummy_id, obj => testable(obj, done) ); + }); + + it('Check loading Python', function (done) { + let xmlPath = path.resolve('test', 'fixtures', 'CoverageResults.py.xml') + Coverage(xmlPath, '', dummy_id, obj => testable(obj, done) ); + }); +}); + + +xdescribe('Test md5 file hash:', function() { + // Check NODE_ENV is correctly set, meaning our imported settings will be test ones + before(function () { + assert(process.env.NODE_ENV.startsWith('test'), 'Test run outside test env'); + }); + + it('MD5 should return correct hash', function (done) { + let test_path = './path/to/file.mat'; + let stub = sinon.stub(fs, 'readFileSync') + .withArgs(test_path) + .returns('line1\nline2\n\rline3\n\rline4'); + }); +}); diff --git a/test/fixtures/.db.json b/test/fixtures/.db.json new file mode 100644 index 0000000..ec1fe52 --- /dev/null +++ b/test/fixtures/.db.json @@ -0,0 +1 @@ +[{"commit": "cabe27e5c8b8cb7cdc4e152f1cf013a89adc7a71", "results": [{"Duration": 0.07038330000000001, "Details": {}, "Name": "Parameters_test/test_set", "Passed": false, "Failed": true, "Incomplete": true}, {"Duration": 2.5838056999999996, "Details": {}, "Name": "Alyx_test[base_url=https___test_alyx_internationalbrainlab_org]/test_getSessions", "Passed": false, "Failed": true, "Incomplete": false}, {"Duration": 5.1105206, "Details": {}, "Name": "Block2ALF_test/test_incomplete", "Passed": true, "Failed": false, "Incomplete": false}], "status": "failure", "description": "18/320 tests failed", "coverage": 22.19690421937613}, {"commit": "1c33a6e2ac7d7fc098105b21a702e104e09767cf", "results": [{"Duration": 0.0500121, "Details": {}, "Name": "patch_test/Test3_Circle", "Passed": true, "Failed": false, "Incomplete": false}, {"Duration": 0.0482601, "Details": {}, "Name": "toStr_test/test_toStr", "Passed": true, "Failed": false, "Incomplete": false}, {"Duration": 0.0389527, "Details": {}, "Name": "Signals_test/test_output", "Passed": true, "Failed": false, "Incomplete": false}, {"Duration": 0.016370700000000002, "Details": {}, "Name": "Signals_test/test_erf", "Passed": true, "Failed": false, "Incomplete": false}, {"Duration": 0.0152839, "Details": {}, "Name": "rnd_test/test_uni", "Passed": true, "Failed": false, "Incomplete": false}], "status": "success", "description": "All passed", "coverage": 75.77018633540374}, {"commit": "7bdf62", "results": null, "status": "error", "description": "Failed to checkout code: 7bdf62", "coverage": null}] \ No newline at end of file diff --git a/test/fixtures/.env.test b/test/fixtures/.env.test new file mode 100644 index 0000000..0e02678 --- /dev/null +++ b/test/fixtures/.env.test @@ -0,0 +1,9 @@ +GITHUB_PRIVATE_KEY=./test/fixtures/priv.pem +GITHUB_APP_IDENTIFIER=123 +GITHUB_WEBHOOK_SECRET=shhhhh +WEBHOOK_PROXY_URL=https://sub.domain.ext +REPO_PATH=C:\Users\Experiment\Documents\GitHub\Hello-World +REPO_NAME=Hello-World +REPO_OWNER=Codertocat +TUNNEL_HOST=https://domain.ext +TUNNEL_SUBDOMAIN=sub diff --git a/test/fixtures/CoverageResults.mat.xml b/test/fixtures/CoverageResults.mat.xml new file mode 100644 index 0000000..d0e4d98 --- /dev/null +++ b/test/fixtures/CoverageResults.mat.xml @@ -0,0 +1,51804 @@ + + + + C:\Users\Experiment\Documents\GitHub\Rigbox\ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/fixtures/CoverageResults.py.xml b/test/fixtures/CoverageResults.py.xml new file mode 100644 index 0000000..1082311 --- /dev/null +++ b/test/fixtures/CoverageResults.py.xml @@ -0,0 +1,6617 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/fixtures/priv.pem b/test/fixtures/priv.pem new file mode 100644 index 0000000..0a63f3a --- /dev/null +++ b/test/fixtures/priv.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA1c7+9z5Pad7OejecsQ0bu3aozN3tihPmljnnudb9G3HECdnH +lWu2/a1gB9JW5TBQ+AVpum9Okx7KfqkfBKL9mcHgSL0yWMdjMfNOqNtrQqKlN4kE +p6RD++7sGbzbfZ9arwrlD/HSDAWGdGGJTSOBM6pHehyLmSC3DJoR/CTu0vTGTWXQ +rO64Z8tyXQPtVPb/YXrcUhbBp8i72b9Xky0fD6PkEebOy0Ip58XVAn2UPNlNOSPS +ye+Qjtius0Md4Nie4+X8kwVI2Qjk3dSm0sw/720KJkdVDmrayeljtKBx6AtNQsSX +gzQbeMmiqFFkwrG1+zx6E7H7jqIQ9B6bvWKXGwIDAQABAoIBAD8kBBPL6PPhAqUB +K1r1/gycfDkUCQRP4DbZHt+458JlFHm8QL6VstKzkrp8mYDRhffY0WJnYJL98tr4 +4tohsDbqFGwmw2mIaHjl24LuWXyyP4xpAGDpl9IcusjXBxLQLp2m4AKXbWpzb0OL +Ulrfc1ZooPck2uz7xlMIZOtLlOPjLz2DuejVe24JcwwHzrQWKOfA11R/9e50DVse +hnSH/w46Q763y4I0E3BIoUMsolEKzh2ydAAyzkgabGQBUuamZotNfvJoDXeCi1LD +8yNCWyTlYpJZJDDXooBU5EAsCvhN1sSRoaXWrlMSDB7r/E+aQyKua4KONqvmoJuC +21vSKeECgYEA7yW6wBkVoNhgXnk8XSZv3W+Q0xtdVpidJeNGBWnczlZrummt4xw3 +xs6zV+rGUDy59yDkKwBKjMMa42Mni7T9Fx8+EKUuhVK3PVQyajoyQqFwT1GORJNz +c/eYQ6VYOCSC8OyZmsBM2p+0D4FF2/abwSPMmy0NgyFLCUFVc3OECpkCgYEA5OAm +I3wt5s+clg18qS7BKR2DuOFWrzNVcHYXhjx8vOSWV033Oy3yvdUBAhu9A1LUqpwy +Ma+unIgxmvmUMQEdyHQMcgBsVs10dR/g2xGjMLcwj6kn+xr3JVIZnbRT50YuPhf+ +ns1ScdhP6upo9I0/sRsIuN96Gb65JJx94gQ4k9MCgYBO5V6gA2aMQvZAFLUicgzT +u/vGea+oYv7tQfaW0J8E/6PYwwaX93Y7Q3QNXCoCzJX5fsNnoFf36mIThGHGiHY6 +y5bZPPWFDI3hUMa1Hu/35XS85kYOP6sGJjf4kTLyirEcNKJUWH7CXY+00cwvTkOC +S4Iz64Aas8AilIhRZ1m3eQKBgQCUW1s9azQRxgeZGFrzC3R340LL530aCeta/6FW +CQVOJ9nv84DLYohTVqvVowdNDTb+9Epw/JDxtDJ7Y0YU0cVtdxPOHcocJgdUGHrX +ZcJjRIt8w8g/s4X6MhKasBYm9s3owALzCuJjGzUKcDHiO2DKu1xXAb0SzRcTzUCn +7daCswKBgQDOYPZ2JGmhibqKjjLFm0qzpcQ6RPvPK1/7g0NInmjPMebP0K6eSPx0 +9/49J6WTD++EajN7FhktUSYxukdWaCocAQJTDNYP0K88G4rtC2IYy5JFn9SWz5oh +x//0u+zd/R/QRUzLOw4N72/Hu+UG6MNt5iDZFCtapRaKt6OvSBwy8w== +-----END RSA PRIVATE KEY----- diff --git a/test/fixtures/pull_payload.json b/test/fixtures/pull_payload.json new file mode 100644 index 0000000..00bf89d --- /dev/null +++ b/test/fixtures/pull_payload.json @@ -0,0 +1,457 @@ +{ + "action": "opened", + "number": 2, + "pull_request": { + "url": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2", + "id": 279147437, + "node_id": "MDExOlB1bGxSZXF1ZXN0Mjc5MTQ3NDM3", + "html_url": "https://github.com/Codertocat/Hello-World/pull/2", + "diff_url": "https://github.com/Codertocat/Hello-World/pull/2.diff", + "patch_url": "https://github.com/Codertocat/Hello-World/pull/2.patch", + "issue_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/2", + "number": 2, + "state": "open", + "locked": false, + "title": "Update the README with new information.", + "user": { + "login": "Codertocat", + "id": 21031067, + "node_id": "MDQ6VXNlcjIxMDMxMDY3", + "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Codertocat", + "html_url": "https://github.com/Codertocat", + "followers_url": "https://api.github.com/users/Codertocat/followers", + "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", + "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", + "organizations_url": "https://api.github.com/users/Codertocat/orgs", + "repos_url": "https://api.github.com/users/Codertocat/repos", + "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/Codertocat/received_events", + "type": "User", + "site_admin": false + }, + "body": "This is a pretty simple change that we need to pull into master.", + "created_at": "2019-05-15T15:20:33Z", + "updated_at": "2019-05-15T15:20:33Z", + "closed_at": null, + "merged_at": null, + "merge_commit_sha": null, + "assignee": null, + "assignees": [ + + ], + "requested_reviewers": [ + + ], + "requested_teams": [ + + ], + "labels": [ + + ], + "milestone": null, + "commits_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2/commits", + "review_comments_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2/comments", + "review_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls/comments{/number}", + "comments_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/2/comments", + "statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/ec26c3e57ca3a959ca5aad62de7213c562f8c821", + "head": { + "label": "Codertocat:changes", + "ref": "changes", + "sha": "ec26c3e57ca3a959ca5aad62de7213c562f8c821", + "user": { + "login": "Codertocat", + "id": 21031067, + "node_id": "MDQ6VXNlcjIxMDMxMDY3", + "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Codertocat", + "html_url": "https://github.com/Codertocat", + "followers_url": "https://api.github.com/users/Codertocat/followers", + "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", + "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", + "organizations_url": "https://api.github.com/users/Codertocat/orgs", + "repos_url": "https://api.github.com/users/Codertocat/repos", + "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/Codertocat/received_events", + "type": "User", + "site_admin": false + }, + "repo": { + "id": 186853002, + "node_id": "MDEwOlJlcG9zaXRvcnkxODY4NTMwMDI=", + "name": "Hello-World", + "full_name": "Codertocat/Hello-World", + "private": false, + "owner": { + "login": "Codertocat", + "id": 21031067, + "node_id": "MDQ6VXNlcjIxMDMxMDY3", + "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Codertocat", + "html_url": "https://github.com/Codertocat", + "followers_url": "https://api.github.com/users/Codertocat/followers", + "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", + "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", + "organizations_url": "https://api.github.com/users/Codertocat/orgs", + "repos_url": "https://api.github.com/users/Codertocat/repos", + "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/Codertocat/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/Codertocat/Hello-World", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/Codertocat/Hello-World", + "forks_url": "https://api.github.com/repos/Codertocat/Hello-World/forks", + "keys_url": "https://api.github.com/repos/Codertocat/Hello-World/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/Codertocat/Hello-World/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/Codertocat/Hello-World/teams", + "hooks_url": "https://api.github.com/repos/Codertocat/Hello-World/hooks", + "issue_events_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/events{/number}", + "events_url": "https://api.github.com/repos/Codertocat/Hello-World/events", + "assignees_url": "https://api.github.com/repos/Codertocat/Hello-World/assignees{/user}", + "branches_url": "https://api.github.com/repos/Codertocat/Hello-World/branches{/branch}", + "tags_url": "https://api.github.com/repos/Codertocat/Hello-World/tags", + "blobs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/Codertocat/Hello-World/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/Codertocat/Hello-World/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/{sha}", + "languages_url": "https://api.github.com/repos/Codertocat/Hello-World/languages", + "stargazers_url": "https://api.github.com/repos/Codertocat/Hello-World/stargazers", + "contributors_url": "https://api.github.com/repos/Codertocat/Hello-World/contributors", + "subscribers_url": "https://api.github.com/repos/Codertocat/Hello-World/subscribers", + "subscription_url": "https://api.github.com/repos/Codertocat/Hello-World/subscription", + "commits_url": "https://api.github.com/repos/Codertocat/Hello-World/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/Codertocat/Hello-World/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/Codertocat/Hello-World/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/Codertocat/Hello-World/contents/{+path}", + "compare_url": "https://api.github.com/repos/Codertocat/Hello-World/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/Codertocat/Hello-World/merges", + "archive_url": "https://api.github.com/repos/Codertocat/Hello-World/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/Codertocat/Hello-World/downloads", + "issues_url": "https://api.github.com/repos/Codertocat/Hello-World/issues{/number}", + "pulls_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls{/number}", + "milestones_url": "https://api.github.com/repos/Codertocat/Hello-World/milestones{/number}", + "notifications_url": "https://api.github.com/repos/Codertocat/Hello-World/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/Codertocat/Hello-World/labels{/name}", + "releases_url": "https://api.github.com/repos/Codertocat/Hello-World/releases{/id}", + "deployments_url": "https://api.github.com/repos/Codertocat/Hello-World/deployments", + "created_at": "2019-05-15T15:19:25Z", + "updated_at": "2019-05-15T15:19:27Z", + "pushed_at": "2019-05-15T15:20:32Z", + "git_url": "git://github.com/Codertocat/Hello-World.git", + "ssh_url": "git@github.com:Codertocat/Hello-World.git", + "clone_url": "https://github.com/Codertocat/Hello-World.git", + "svn_url": "https://github.com/Codertocat/Hello-World", + "homepage": null, + "size": 0, + "stargazers_count": 0, + "watchers_count": 0, + "language": null, + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": true, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 2, + "license": null, + "forks": 0, + "open_issues": 2, + "watchers": 0, + "default_branch": "master" + } + }, + "base": { + "label": "Codertocat:master", + "ref": "master", + "sha": "f95f852bd8fca8fcc58a9a2d6c842781e32a215e", + "user": { + "login": "Codertocat", + "id": 21031067, + "node_id": "MDQ6VXNlcjIxMDMxMDY3", + "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Codertocat", + "html_url": "https://github.com/Codertocat", + "followers_url": "https://api.github.com/users/Codertocat/followers", + "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", + "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", + "organizations_url": "https://api.github.com/users/Codertocat/orgs", + "repos_url": "https://api.github.com/users/Codertocat/repos", + "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/Codertocat/received_events", + "type": "User", + "site_admin": false + }, + "repo": { + "id": 186853002, + "node_id": "MDEwOlJlcG9zaXRvcnkxODY4NTMwMDI=", + "name": "Hello-World", + "full_name": "Codertocat/Hello-World", + "private": false, + "owner": { + "login": "Codertocat", + "id": 21031067, + "node_id": "MDQ6VXNlcjIxMDMxMDY3", + "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Codertocat", + "html_url": "https://github.com/Codertocat", + "followers_url": "https://api.github.com/users/Codertocat/followers", + "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", + "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", + "organizations_url": "https://api.github.com/users/Codertocat/orgs", + "repos_url": "https://api.github.com/users/Codertocat/repos", + "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/Codertocat/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/Codertocat/Hello-World", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/Codertocat/Hello-World", + "forks_url": "https://api.github.com/repos/Codertocat/Hello-World/forks", + "keys_url": "https://api.github.com/repos/Codertocat/Hello-World/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/Codertocat/Hello-World/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/Codertocat/Hello-World/teams", + "hooks_url": "https://api.github.com/repos/Codertocat/Hello-World/hooks", + "issue_events_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/events{/number}", + "events_url": "https://api.github.com/repos/Codertocat/Hello-World/events", + "assignees_url": "https://api.github.com/repos/Codertocat/Hello-World/assignees{/user}", + "branches_url": "https://api.github.com/repos/Codertocat/Hello-World/branches{/branch}", + "tags_url": "https://api.github.com/repos/Codertocat/Hello-World/tags", + "blobs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/Codertocat/Hello-World/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/Codertocat/Hello-World/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/{sha}", + "languages_url": "https://api.github.com/repos/Codertocat/Hello-World/languages", + "stargazers_url": "https://api.github.com/repos/Codertocat/Hello-World/stargazers", + "contributors_url": "https://api.github.com/repos/Codertocat/Hello-World/contributors", + "subscribers_url": "https://api.github.com/repos/Codertocat/Hello-World/subscribers", + "subscription_url": "https://api.github.com/repos/Codertocat/Hello-World/subscription", + "commits_url": "https://api.github.com/repos/Codertocat/Hello-World/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/Codertocat/Hello-World/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/Codertocat/Hello-World/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/Codertocat/Hello-World/contents/{+path}", + "compare_url": "https://api.github.com/repos/Codertocat/Hello-World/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/Codertocat/Hello-World/merges", + "archive_url": "https://api.github.com/repos/Codertocat/Hello-World/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/Codertocat/Hello-World/downloads", + "issues_url": "https://api.github.com/repos/Codertocat/Hello-World/issues{/number}", + "pulls_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls{/number}", + "milestones_url": "https://api.github.com/repos/Codertocat/Hello-World/milestones{/number}", + "notifications_url": "https://api.github.com/repos/Codertocat/Hello-World/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/Codertocat/Hello-World/labels{/name}", + "releases_url": "https://api.github.com/repos/Codertocat/Hello-World/releases{/id}", + "deployments_url": "https://api.github.com/repos/Codertocat/Hello-World/deployments", + "created_at": "2019-05-15T15:19:25Z", + "updated_at": "2019-05-15T15:19:27Z", + "pushed_at": "2019-05-15T15:20:32Z", + "git_url": "git://github.com/Codertocat/Hello-World.git", + "ssh_url": "git@github.com:Codertocat/Hello-World.git", + "clone_url": "https://github.com/Codertocat/Hello-World.git", + "svn_url": "https://github.com/Codertocat/Hello-World", + "homepage": null, + "size": 0, + "stargazers_count": 0, + "watchers_count": 0, + "language": null, + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": true, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 2, + "license": null, + "forks": 0, + "open_issues": 2, + "watchers": 0, + "default_branch": "master" + } + }, + "_links": { + "self": { + "href": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2" + }, + "html": { + "href": "https://github.com/Codertocat/Hello-World/pull/2" + }, + "issue": { + "href": "https://api.github.com/repos/Codertocat/Hello-World/issues/2" + }, + "comments": { + "href": "https://api.github.com/repos/Codertocat/Hello-World/issues/2/comments" + }, + "review_comments": { + "href": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2/comments" + }, + "review_comment": { + "href": "https://api.github.com/repos/Codertocat/Hello-World/pulls/comments{/number}" + }, + "commits": { + "href": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2/commits" + }, + "statuses": { + "href": "https://api.github.com/repos/Codertocat/Hello-World/statuses/ec26c3e57ca3a959ca5aad62de7213c562f8c821" + } + }, + "author_association": "OWNER", + "draft": false, + "merged": false, + "mergeable": null, + "rebaseable": null, + "mergeable_state": "unknown", + "merged_by": null, + "comments": 0, + "review_comments": 0, + "maintainer_can_modify": false, + "commits": 1, + "additions": 1, + "deletions": 1, + "changed_files": 1 + }, + "repository": { + "id": 186853002, + "node_id": "MDEwOlJlcG9zaXRvcnkxODY4NTMwMDI=", + "name": "Hello-World", + "full_name": "Codertocat/Hello-World", + "private": false, + "owner": { + "login": "Codertocat", + "id": 21031067, + "node_id": "MDQ6VXNlcjIxMDMxMDY3", + "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Codertocat", + "html_url": "https://github.com/Codertocat", + "followers_url": "https://api.github.com/users/Codertocat/followers", + "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", + "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", + "organizations_url": "https://api.github.com/users/Codertocat/orgs", + "repos_url": "https://api.github.com/users/Codertocat/repos", + "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/Codertocat/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/Codertocat/Hello-World", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/Codertocat/Hello-World", + "forks_url": "https://api.github.com/repos/Codertocat/Hello-World/forks", + "keys_url": "https://api.github.com/repos/Codertocat/Hello-World/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/Codertocat/Hello-World/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/Codertocat/Hello-World/teams", + "hooks_url": "https://api.github.com/repos/Codertocat/Hello-World/hooks", + "issue_events_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/events{/number}", + "events_url": "https://api.github.com/repos/Codertocat/Hello-World/events", + "assignees_url": "https://api.github.com/repos/Codertocat/Hello-World/assignees{/user}", + "branches_url": "https://api.github.com/repos/Codertocat/Hello-World/branches{/branch}", + "tags_url": "https://api.github.com/repos/Codertocat/Hello-World/tags", + "blobs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/Codertocat/Hello-World/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/Codertocat/Hello-World/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/{sha}", + "languages_url": "https://api.github.com/repos/Codertocat/Hello-World/languages", + "stargazers_url": "https://api.github.com/repos/Codertocat/Hello-World/stargazers", + "contributors_url": "https://api.github.com/repos/Codertocat/Hello-World/contributors", + "subscribers_url": "https://api.github.com/repos/Codertocat/Hello-World/subscribers", + "subscription_url": "https://api.github.com/repos/Codertocat/Hello-World/subscription", + "commits_url": "https://api.github.com/repos/Codertocat/Hello-World/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/Codertocat/Hello-World/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/Codertocat/Hello-World/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/Codertocat/Hello-World/contents/{+path}", + "compare_url": "https://api.github.com/repos/Codertocat/Hello-World/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/Codertocat/Hello-World/merges", + "archive_url": "https://api.github.com/repos/Codertocat/Hello-World/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/Codertocat/Hello-World/downloads", + "issues_url": "https://api.github.com/repos/Codertocat/Hello-World/issues{/number}", + "pulls_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls{/number}", + "milestones_url": "https://api.github.com/repos/Codertocat/Hello-World/milestones{/number}", + "notifications_url": "https://api.github.com/repos/Codertocat/Hello-World/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/Codertocat/Hello-World/labels{/name}", + "releases_url": "https://api.github.com/repos/Codertocat/Hello-World/releases{/id}", + "deployments_url": "https://api.github.com/repos/Codertocat/Hello-World/deployments", + "created_at": "2019-05-15T15:19:25Z", + "updated_at": "2019-05-15T15:19:27Z", + "pushed_at": "2019-05-15T15:20:32Z", + "git_url": "git://github.com/Codertocat/Hello-World.git", + "ssh_url": "git@github.com:Codertocat/Hello-World.git", + "clone_url": "https://github.com/Codertocat/Hello-World.git", + "svn_url": "https://github.com/Codertocat/Hello-World", + "homepage": null, + "size": 0, + "stargazers_count": 0, + "watchers_count": 0, + "language": null, + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": true, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 2, + "license": null, + "forks": 0, + "open_issues": 2, + "watchers": 0, + "default_branch": "master" + }, + "sender": { + "login": "Codertocat", + "id": 21031067, + "node_id": "MDQ6VXNlcjIxMDMxMDY3", + "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Codertocat", + "html_url": "https://github.com/Codertocat", + "followers_url": "https://api.github.com/users/Codertocat/followers", + "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", + "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", + "organizations_url": "https://api.github.com/users/Codertocat/orgs", + "repos_url": "https://api.github.com/users/Codertocat/repos", + "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/Codertocat/received_events", + "type": "User", + "site_admin": false + }, + "installation": { + "id": 123 + } +} diff --git a/test/fixtures/static.js b/test/fixtures/static.js new file mode 100644 index 0000000..ef60b47 --- /dev/null +++ b/test/fixtures/static.js @@ -0,0 +1,33 @@ +const stdErr = `Traceback (most recent call last):\n + File \"runAllTests.py\", line 65, in \n + result, cov = run_tests()\n + File \"runAllTests.py\", line 37, in run_tests\n + tests = unittest.TestLoader().discover(test_dir, pattern='test_testing*')\n + File \"C:\\Users\\User\\Anaconda3\\envs\\iblenv\\lib\\unittest\\loader.py\", line 349, in discover\n + tests = list(self._find_tests(start_dir, pattern))\n + File \"C:\\Users\\User\\Anaconda3\\envs\\iblenv\\lib\\unittest\\loader.py\", line 406, in _find_tests\n + full_path, pattern, namespace)\n + File \"C:\\Users\\User\\Anaconda3\\envs\\iblenv\\lib\\unittest\\loader.py\", line 460, in _find_test_path\n + return self.loadTestsFromModule(module, pattern=pattern), False\n + File \"C:\\Users\\User\\Anaconda3\\envs\\iblenv\\lib\\unittest\\loader.py\", line 124, in loadTestsFromModule\n + tests.append(self.loadTestsFromTestCase(obj))\n + File \"C:\\Users\\User\\Anaconda3\\envs\\iblenv\\lib\\unittest\\loader.py\", line 93, in loadTestsFromTestCase\n + loaded_suite = self.suiteClass(map(testCaseClass, testCaseNames))\n + File \"C:\\Users\\User\\Anaconda3\\envs\\iblenv\\lib\\unittest\\suite.py\", line 24, in __init__\n + self.addTests(tests)\n + File \"C:\\Users\\User\\Anaconda3\\envs\\iblenv\\lib\\unittest\\suite.py\", line 57, in addTests\n + for test in tests:\n + File \"C:\\Users\\User\\Documents\\Python Scripts\\iblscripts-repo\\iblscripts\\tests\\base.py\", line 25, in __init__\n + raise FileNotFoundError(f'Invalid data root folder {self.data_path.absolute()}\\n\\t'\n +FileNotFoundError: Invalid data root folder E:\\FlatIron\\integration\n + must contain a \"Subjects_init\" folder\n`; + +// Create a constant JWT +const token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOi0zMCwiZXhwIjo1NzAsImlzcyI6MTIzfQ' + + '.Amivfieh9COk-89jINMvQh-LZtjLVT44aeulGNNZnFtHhFpNAg9gZGuf-LCjykHqQvibYPfPxD7L_d' + + 'J1t49LwhErHPRpRrs-vs3HoEVQpZMmdA1oLmCJkCC0PVP0c7nalx5wvLWHIx5hQCZ3aJfAwrH2xIaWJ' + + 'YhBKVIsR0J25O0_ouCD3JsoBu87xaTRH1yyv7COBFauBsFytkV4L0fFIVAarqPmQCWMRkEmQJn9lZZC' + + 'VLM8o9EEQibLmmeF2CF_rLeolHfLjZkYBMd9MGLPTnEbNbQiRpqqeVft0Hg2SJuKcpsKEilTVs20JdN' + + 'lY9eIUUDECsU6Mxoa-s_5ffWSHg'; + +module.exports = { stdErr, token }; diff --git a/test/lib.test.js b/test/lib.test.js new file mode 100644 index 0000000..97a3f8a --- /dev/null +++ b/test/lib.test.js @@ -0,0 +1,457 @@ +const fs = require('fs'); +const cp = require('child_process'); + +const config = require('../config/config').settings +const assert = require('assert') +const sinon = require('sinon'); +const expect = require('chai').expect +const lib = require('../lib'); +const queue = require('../lib').queue; +// TODO update package test script and add cross_env dev dependency +ids = [ + 'cabe27e5c8b8cb7cdc4e152f1cf013a89adc7a71', + '1c33a6e2ac7d7fc098105b21a702e104e09767cf', + 'hf4ac7d7fc0983748702e10738hw4382f347fu38', // Fake + '7bdf62' // errored +]; + + +/** + * A test for the function ensureArray. Should return an array but not affect array inputs. + */ +describe('Test ensureArray:', function() { + it('Check returns array', function () { + let s = 'foo' + assert(Array.isArray(lib.ensureArray(s)), 'failed to return array') + assert.deepStrictEqual(lib.ensureArray(s), [s], 'failed to return array') + let arr = ['bar'] + assert.strictEqual(lib.ensureArray(arr), arr, 'failed to return array') + }); +}); + + +/** + * This tests the shields callback which returns sheilds.io API data for coverage and build status. + */ +describe("strToBool function", () => { + it('Check valid true', () => { + strings = ['on', 'true', 'True', '1', 'ON']; + strings.forEach((x) => { expect(lib.strToBool(x)).true; }); + }); + + it('Check valid false', () => { + strings = ['', null, undefined, '0', 'false']; + strings.forEach((x) => { expect(lib.strToBool(x)).false; }); + }); +}); + + +/** + * A test for the function partial. Should curry function input. + */ +describe('Test partial:', function() { + it('expect curried function', function () { + let f = (a, b) => { return a + b; }; + let f0 = lib.partial(f); + expect(f0(2)).instanceOf(Function) + expect(f0(2, 2)).eq(4) + }); +}); + + +/** + * A test for the function updateJobFromRecord. + * @todo add test for compareCoverage call + */ +describe('Test updateJobFromRecord:', function() { + var job; + + beforeEach(function() { + queue.process(async (_job, _done) => {}); // nop + queue.pile = []; + job = { + data: { + sha: null + } + }; + }) + + it('expect no record found', function () { + job.data.sha = ids[2]; + const updated = lib.updateJobFromRecord(job); + expect(updated).false; + }); + + it('expect updated', function () { + job.data.sha = ids[0]; + const updated = lib.updateJobFromRecord(job); + expect(updated).true; + expect(job.data).deep.keys(['sha', 'status', 'description', 'coverage']); + }); +}); + + +/** + * A test for the function startJobTimer. Should kill the process when time is up and update the + * job data. + */ +describe('Test startJobTimer:', function() { + var clock; + + before(function () { + clock = sinon.useFakeTimers(); + }); + + it('expect process killed', function (done) { + const childProcess = { + kill: () => { done(); }, + pid: 10108 + }; + const job = { data: {process: childProcess} }; + lib.startJobTimer(job); + // Skip to the end... + clock.tick(config.timeout + 1); + }); + + it('expect tree-killed', function (done) { + // Test tree-kill switch. We can't stub function exports so we'll use a slow ping command + // and kill it. Should be relatively consistent across platforms. + const cmd = 'ping 127.0.0.1 -n 6 > nul'; + const childProcess = cp.exec(cmd, () => { done(); }); + childProcess.kill = () => {}; // nop + const job = { data: {process: childProcess} }; + lib.startJobTimer(job, true); + // Skip to the end... + clock.tick(config.timeout + 1); + }); + + after(() => { clock.restore(); }) +}); + + +/** + * A test for the function loadTestRecords. + */ +describe('Test loading test records:', function() { + // Check NODE_ENV is correctly set, meaning our imported settings will be test ones + before(function () { + assert(process.env.NODE_ENV.startsWith('test'), 'Test run outside test env') + }); + + it('Check loading existing record', function () { + let id = ids[0]; + const record = lib.loadTestRecords(id); + assert(record != null, 'failed to load record') + assert(!Array.isArray(record), 'failed to return single obj') + assert.strictEqual(record.commit, id, 'failed to return correct record') + }); + + it('Check loading multiple records', function () { + const records = lib.loadTestRecords(ids); + assert(records != null, 'failed to load records') + assert(Array.isArray(records), 'failed to return array') + assert.strictEqual(records.length, ids.length-1, 'failed to return both records') + }); + + it('Check loading fail', function () { + let id = ids[2] // this commit is not in db + const record = lib.loadTestRecords(id); + let isEmptyArr = x => { return Array.isArray(x) && x.length === 0; } + assert(isEmptyArr(record)) + assert(isEmptyArr(lib.loadTestRecords([id, id]))) + }); +}); + + +/** + * A test for the function saveTestRecords. + */ +describe('Test saving test records:', function() { + var backup; + + // Check NODE_ENV is correctly set, meaning our imported settings will be test ones + before(function () { + assert(process.env.NODE_ENV.startsWith('test'), 'Test run outside test env') + backup = config.dbFile + Date.now(); + fs.copyFileSync(config.dbFile, backup); + }); + + it('Check saving existing record', async function () { + const record = lib.loadTestRecords(ids[0]); + delete record['results']; // remove a field + record['status'] = 'passed'; // change a field + await lib.saveTestRecords(record); + const new_record = lib.loadTestRecords(record['commit']); + assert.strictEqual(new_record.status, record.status, 'failed to update record'); + assert(new_record.results !== undefined); + }); + + it('Check saving new records', async function () { + const records = [ + lib.loadTestRecords(ids[1]), + { + 'commit': ids[1].replace('2', '3'), // not in db + 'status': 'error', + } + ]; + records[0]['status'] = 'error'; // change a field + await lib.saveTestRecords(records); + const new_records = lib.loadTestRecords(records.map(x => x.commit)); + assert(new_records.length === 2); + for (o of new_records) { + assert.strictEqual(o.status, 'error', 'failed to update all records'); + } + }); + + it('Check validation errors', function (done) { + const record = { + commit: ids[2], + status: 'success' + }; + lib.saveTestRecords(record).catch(err => { + expect(err).instanceOf(lib.APIError); + done(); + }); + }); + + after(function () { + fs.renameSync(backup, config.dbFile); + }); +}); + + +/** + * This tests the shields callback which returns sheilds.io API data for coverage and build status. + */ +describe("getBadgeData function", () => { + var scope; // Our server mock + var sandbox; // Sandbox for spying on queue + var input; // Input data for function + + beforeEach(function () { + queue.process(async (_job, _done) => {}) // nop + sandbox = sinon.createSandbox(); + sandbox.spy(queue); + input = { + sha: null, + owner: process.env['REPO_OWNER'], + repo: '', + branch: '', + context: '' + }; + }); + + it('Check Coverage', function () { + var data, expected; + + // Low coverage + input['sha'] = ids[0]; + input['context'] = 'coverage'; + data = lib.getBadgeData(input); + expected = { + schemaVersion: 1, + label: input['context'], + message: '22.2%', + color: 'red' + }; + expect(data).to.deep.equal(expected); + sandbox.assert.notCalled(queue.add); + + // High coverage + input['sha'] = ids[1]; + expected['message'] = '75.77%'; + expected['color'] = 'brightgreen'; + data = lib.getBadgeData(input); + expect(data).to.deep.equal(expected); + sandbox.assert.notCalled(queue.add); + + // Errored + input['sha'] = ids[3]; + expected['message'] = 'unknown'; + expected['color'] = 'orange'; + data = lib.getBadgeData(input); + expect(data).to.deep.equal(expected); + sandbox.assert.notCalled(queue.add); + + // No coverage + input['sha'] = ids[2]; + expected['message'] = 'pending'; + expected['color'] = 'orange'; + data = lib.getBadgeData(input); + expect(data).to.deep.equal(expected); + sandbox.assert.calledOnce(queue.add); + }); + + it('Check build status', function () { + var data, expected; + + // Failed tests + input['sha'] = ids[0]; + input['context'] = 'status'; + data = lib.getBadgeData(input); + expected = { + schemaVersion: 1, + label: 'build', + message: 'failing', + color: 'red' + }; + expect(data).to.deep.equal(expected); + sandbox.assert.notCalled(queue.add); + + // High coverage + input['sha'] = ids[1]; + expected['message'] = 'passing'; + expected['color'] = 'brightgreen'; + data = lib.getBadgeData(input); + expect(data).to.deep.equal(expected); + sandbox.assert.notCalled(queue.add); + + // Errored + input['sha'] = ids[3]; + expected['message'] = 'unknown'; + expected['color'] = 'orange'; + data = lib.getBadgeData(input); + expect(data).to.deep.equal(expected); + sandbox.assert.notCalled(queue.add); + + // No coverage + input['sha'] = ids[2]; + expected['message'] = 'pending'; + expected['color'] = 'orange'; + data = lib.getBadgeData(input); + expect(data).to.deep.equal(expected); + sandbox.assert.calledOnce(queue.add); + }); + + it('Check force flag', function () { + input['sha'] = ids[1]; + input['context'] = 'status'; + input['force'] = true; // set force flag to true + const expected = { + schemaVersion: 1, + label: 'build', + message: 'pending', + color: 'orange' + }; + data = lib.getBadgeData(input); + expect(data).to.deep.equal(expected); + sandbox.assert.calledOnce(queue.add); + }); + + it('Check error handling', function () { + expect(() => lib.getBadgeData(input)).to.throw(ReferenceError, 'sha'); + input['sha'] = ids[0] + expect(() => lib.getBadgeData(input)).to.throw(ReferenceError, 'Context'); + input['context'] = 'updated' + expect(() => lib.getBadgeData(input)).to.throw(TypeError, 'context'); + }); + + afterEach(function () { + queue.pile = []; + sandbox.restore(); + }); + +}); + + +/** + * A test for the main queue process callback. + */ +describe('Test short circuit', function() { + + beforeEach(function () { + queue.process(async (_job, _done) => {}); // nop + queue.pile = []; + }); + + it('expect force flag set', function (done) { + // We expect that the job that's on the pile has 'force' set to false + // Add job to the pile + queue.add( { sha: ids[0] }) // Record exists + function tests(run) { + expect(run).true; + expect(queue.pile[0].data.force).false; + done(); + } + const job = { + data: { + sha: ids[0] // Record exists + }, + done: () => tests(false) + }; + + lib.shortCircuit(job, () => { tests(true); }); + }); + + it('expect short circuit', function (done) { + // We expect that the job that's on the pile has 'force' set to false + const job = { + data: { + sha: ids[0], // record exists + force: false // load from record + } + }; + function tests(run) { + expect(run).false; + expect(job.data.status).eq('failure'); + done(); + } + job.done = () => tests(false); + lib.shortCircuit(job, () => tests(true)); + }); + + it('expect forced test function called', function (done) { + // Record doesn't exist, so we expect the tests to be run anyway + function tests(run) { + expect(run).true; + done(); + } + const job = { + data: { + sha: ids[2], // record exists + force: false // load from record + }, + done: () => tests(false) + }; + lib.shortCircuit(job, () => tests(true)); + }); +}); + + +/** + * A test for shortID function. + */ +describe('Test shortID', function() { + + it('expect short str from int', function () { + const out = lib.shortID(987654321); + expect(out).eq('9876543'); + }); + + it('expect short str from str', function () { + const out = lib.shortID('98r7654321o', 3); + expect(out).eq('98r'); + }); + + it('expect works with arrays', function () { + const out = lib.shortID([987654321, '7438ht43', null], 3); + expect(out).deep.equal(['987', '743', null]); + }); + +}); + + +/** + * A test for isSHA function. + */ +describe('Test isSHA', function() { + + it('expect true on SHA', function () { + expect(lib.isSHA(ids[0])).true; + }); + + it('expect false on fake', function () { + expect(lib.isSHA(ids[2])).false; + }); +}); + diff --git a/test/main.test.js b/test/main.test.js new file mode 100644 index 0000000..f8248d0 --- /dev/null +++ b/test/main.test.js @@ -0,0 +1,133 @@ +/** + * Tests for the main module. These are full stack tests. + * @author Miles Wells + * @requires ./queue.js + * @requires module:mocha + * @requires module:chai + */ +const fs = require('fs'); +const cp = require('child_process'); +const events = require('events'); + +const nock = require('nock'); // for mocking outbound requests +const supertest = require('supertest'); // for mocking inbound requests +const sinon = require('sinon'); // for mocking local modules +const expect = require('chai').expect; +const assert = require('chai').assert; + +const lib = require('../lib'); +const queue = lib.queue; +const { handler, eventCallback, srv, prepareEnv, runTests } = require('../serve'); +const { token } = require('./fixtures/static'); +const config = require('../config/config').settings; + +// Create a constant JWT // TODO put in static +const SHA = 'cabe27e5c8b8cb7cdc4e152f1cf013a89adc7a71' + + +/** + * The hooks setup in main.js. + */ +function main() { + const run = (job) => { prepareEnv(job, runTests); }; + queue.process((job) => { lib.shortCircuit(job, run); }); + handler.on('*', evt => eventCallback(evt)); + queue.on('error', _ => {}); + handler.on('error', function (err) { + console.error('Error:', err.message); + }) + process.on('unhandledRejection', (reason, p) => { + console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); + console.log(reason.stack) + }); + lib.openTunnel() + .then(() => { + // Start the server on same port as tunnel + var server = srv.listen(config.listen_port, function () { + let host = server.address().address; + let port = server.address().port; + + console.log("Handler listening at http://%s:%s", host, port); + }); + }) + .catch(e => { throw e; }); +} + +/** + * TODO Document. + */ +xdescribe("Full stack", () => { + var scope; // Our server mock + var clock; // Our clock mock for replicable JWT + var evt; // A payload event loaded from fixtures + var sandbox; // Sandbox for spying on queue + + before(function () { + const APP_ID = process.env.GITHUB_APP_IDENTIFIER; + const evt = JSON.parse(fs.readFileSync('./test/fixtures/pull_payload.json')); + // https://runkit.com/gr2m/reproducable-jwt + clock = sinon.useFakeTimers({ + now: 0, + toFake: ['Date'] + }); + // For outgoing requests + scope = nock('https://api.github.com', { + reqheaders: { + accept: 'application/vnd.github.machine-man-preview+json', + } + }); + scope.get(`/repos/${process.env.REPO_OWNER}/${process.env.REPO_NAME}/installation`) + .matchHeader('authorization', `bearer ${token}`) + .reply(201, {id: APP_ID}); + scope.post(`/app/installations/${APP_ID}/access_tokens`) + .matchHeader('authorization', `bearer ${token}`) + .reply(201, { + token: '#t0k3N', + permissions: { + checks: "write", + metadata: "read", + contents: "read" + }, + }); + let pr = evt.pull_request; + let uri = `/repos/${pr.head.repo.owner.login}/${pr.head.repo.name}/statuses/${pr.head.sha}`; + scope.post(uri, body => { return body.state === 'pending'}) + .twice() + .reply(201, {}); + scope.post(uri, body => { return body.state === 'success'}) + .twice() + .reply(201, {}); + + sandbox = sinon.createSandbox() + const stub = sandbox.stub(cp, 'execFile'); + sandbox.stub(fs, 'createWriteStream'); + sandbox.stub(lib, 'openTunnel').resolves(null); + const execEvent = new events.EventEmitter(); + execEvent.stdout = new events.EventEmitter(); + execEvent.stdout.pipe = sandbox.spy(); + stub + .returns(execEvent) + .callsArgAsync(2, null, 'external script called', ''); + }); + + it('full stack job request', done => { + main(); + const server = supertest.agent(`http://localhost:${config.port}`); + server + .post(`/github`, evt) + .expect('Content-Type', 'application/json') + .expect(201) + .end(function (err, res) { + scope.isDone(); + if (err) return done(err); + done(); + }); + + }); + + after(function() { + clock.restore(); + queue.pile = []; + sandbox.verifyAndRestore(); + }); +}); diff --git a/test/queue.test.js b/test/queue.test.js new file mode 100644 index 0000000..fd89ea7 --- /dev/null +++ b/test/queue.test.js @@ -0,0 +1,107 @@ +/** + * Tests for the queue module + * @author Miles Wells + * @requires ./queue.js + * @requires module:mocha + * @requires module:chai + * @requires modules:chai-spies + */ +const {describe} = require('mocha'); +// const assert = require('chai').assert; +const spies = require('chai-spies'); +const chai = require('chai'); +const should = chai.should(); +const assert = require('assert'); + +chai.use(spies); + +const Queue = require('../queue.js'); + +// TODO Check only one job is running at any one time +// TODO Check console behaviour +describe('Test Queue constructor:', function() { + beforeEach(function() { + this.Q = new Queue() + this.Q.process(async (_job, _done) => {}) // nop + }); + + it('Pile should be empty on construct', function() { + assert.strictEqual(this.Q.pile.length, 0, 'pile null or not empty') + }); + + it('Pile should grow as jobs added', function() { + this.Q.add({pi: 3.14}) + assert.strictEqual(this.Q.pile.length, 1, 'failed to add job to pile') + assert.strictEqual(this.Q.pile[0].data.pi, 3.14, 'failed to fetch job data from pile') + + // Add another to the pile + this.Q.add({}) + assert.strictEqual(this.Q.pile.length, 2, 'failed to add more than one job') + let id = this.Q.pile[0].id + let isInt16 = Number.isInteger(id) && id.toString().length === 16 + assert(isInt16, `unexpected id ${id}`) + + // Check the set is unique + var ids = this.Q.pile.map((job) => { return job.id; }) + assert.strictEqual(ids.length, new Set(ids).size) + + // Add and remove jobs + this.Q.pile.pop() + this.Q.add({}) + this.Q.pile.pop() + this.Q.add({}) + this.Q.add({}) + this.Q.pile.shift() + this.Q.add({}) + this.Q.add({}) + + // Check ids still unique + ids = this.Q.pile.map((job) => { return job.id; }) + assert.strictEqual(ids.length, new Set(ids).size, 'ids not unique') + }); + + it('One job should be set to running', function() { + this.Q.add({}) + this.Q.add({}) + + let running = this.Q.pile.map((job) => { return job.running; }) + assert.deepStrictEqual(running, [true, false], 'first object must be running') + }); + +}); + +describe('Test Queue callbacks:', function() { + beforeEach(function () { + this.Q = new Queue(); + function process(job, done) { + // if outcome set for test, call done with code, otherwise do nothing + if (job.data && 'outcome' in job.data) { + job.data['processed'] = true + if (job.data['outcome'] === 1) { + done() + } else { + done({'message': 'failed'}) + } + } + } + this.spy = chai.spy(process); + this.Q.process(async (job, done) => this.spy(job, done)) + }); + + it('Check process callback', function () { + // TODO Write async check + this.Q.add({'outcome': 1}) + var spy = this.spy + this.Q.on('finish', job => { + spy.should.have.been.called.once; + }); + // this.Q.on('complete', job => { + // assert.strictEqual(job.data['outcome'], 1, 'complete called with unexpected outcome') + // }); + // this.Q.on('error', err => { + // assert.strictEqual(err.message, 'failed', 'failed to catch error') + // }); + // this.Q.add({'outcome': 1}) + // this.Q.add({'outcome': 0}) + }); +}); diff --git a/test/serve.test.js b/test/serve.test.js new file mode 100644 index 0000000..c533982 --- /dev/null +++ b/test/serve.test.js @@ -0,0 +1,779 @@ +const fs = require('fs'); +const cp = require('child_process'); +const events = require('events'); +const path = require('path'); +const nock = require('nock'); // for mocking outbound requests +const request = require('supertest'); // for mocking inbound requests +const sinon = require('sinon'); // for mocking local modules +const expect = require('chai').expect; +const assert = require('chai').assert; +const appAuth = require("@octokit/auth-app"); + +const APIError = require('../lib').APIError; +const lib = require('../lib'); +const { updateStatus, setAccessToken, eventCallback, srv, prepareEnv, runTests } = require('../serve'); +const queue = require('../lib').queue; +const config = require('../config/config').settings; +const { stdErr, token } = require('./fixtures/static'); + +const APP_ID = process.env.GITHUB_APP_IDENTIFIER; +const ENDPOINT = 'logs'; // The URL endpoint for fetching status check details +const SHA = 'cabe27e5c8b8cb7cdc4e152f1cf013a89adc7a71' + + +/** + * This tests 'setAccessToken' which handles the app authentication. + */ +describe('setAccessToken', () => { + var scope; // Our server mock + var clock; // Our clock mock for replicable JWT + var expiry = new Date(); // Date of token expiry + + /** + * This fixture injects the default null token via setAccessToken. + */ + async function resetToken() { + const token_default = {'tokenType': null}; + const now = new Date('3000-12-30'); + const sandbox = sinon.createSandbox({ + useFakeTimers: { + now: new Date(3000, 1, 1, 0, 0) + }}) + sandbox.stub(appAuth, 'createAppAuth').returns(async () => token_default); + try { await setAccessToken(); } catch (_) {} + sandbox.restore(); + } + + before(function () { + expiry.setTime(expiry.getTime() + 60e3); // 60s in the future + // https://runkit.com/gr2m/reproducable-jwt + clock = sinon.useFakeTimers({ + now: 0, + toFake: ['Date'] + }); + }); + + beforeEach(function() { + // Mock for App.installationAccessToken + scope = nock('https://api.github.com', { + reqheaders: { + accept: 'application/vnd.github.machine-man-preview+json', + } + }); + }); + + it('test setAccessToken', (done) => { + scope.get(`/repos/${process.env.REPO_OWNER}/${process.env.REPO_NAME}/installation`) + .matchHeader('authorization', `bearer ${token}`) + .reply(201, {id: APP_ID}); + scope.post(`/app/installations/${APP_ID}/access_tokens`) + .matchHeader('authorization', `bearer ${token}`) + .reply(201, { + token: '#t0k3N', + permissions: { + checks: "write", + metadata: "read", + contents: "read" + }, + }); + + setAccessToken().then(function () { + scope.isDone(); + done(); + }); + }); + + it('test install ID cached', (done) => { + // In this test we check that once the install ID is retrieved the app authentication is + // skipped (only to re-auth as installation). + scope.get(`/repos/${process.env.REPO_OWNER}/${process.env.REPO_NAME}/installation`) + .matchHeader('authorization', `bearer ${token}`) + .reply(201, {id: APP_ID}) + scope.post(`/app/installations/${APP_ID}/access_tokens`) + .twice() // Should be called twice in a row + .matchHeader('authorization', `bearer ${token}`) + .reply(201, { + token: '#t0k3N', + expires_at: expiry.toISOString(), // expires in 60s + permissions: { + checks: "write", + metadata: "read", + contents: "read" + }, + }); + + setAccessToken().then(async function () { + await setAccessToken(); + scope.isDone(); + done(); + }); + }); + + it('test token cached', (done) => { + // In this test we restore the clocks and ignore the JWT token, instead we test that a new + // token is not requested so long as the token hasn't expired + clock.restore(); + scope.get(`/repos/${process.env.REPO_OWNER}/${process.env.REPO_NAME}/installation`) + .reply(201, {id: APP_ID}) + scope.post(`/app/installations/${APP_ID}/access_tokens`) + .reply(201, { + token: '#t0k3N', + expires_at: expiry.toISOString(), + permissions: { + checks: "write", + metadata: "read", + contents: "read" + }, + }); + + setAccessToken().then(async function () { + await setAccessToken(); + scope.isDone(); + done(); + }); + }); + + after(async function() { + clock.restore(); + await resetToken(); + }) +}); + + +/** + * This tests 'updateStatus' which handles updating the GitHub statues. + */ +describe("updateStatus", () => { + var scope; // Our server mock + var spy; // A spy for authentication + var data; // Some job data to update the status with + + beforeEach(function() { + // Mock for App.installationAccessToken + scope = nock('https://api.github.com', { + reqheaders: { + accept: 'application/vnd.github.machine-man-preview+json', + } + }); + const token = {token: '#t0k3N'}; + spy = sinon.stub(appAuth, 'createAppAuth').returns(async () => token); + data = { + sha: SHA, + owner: 'okonkwe', + repo: 'borneo-function', + status: 'success', + description: '' + }; + }); + + it('updateStatus should post to given endpoint', (done) => { + scope.get(`/repos/${process.env.REPO_OWNER}/${process.env.REPO_NAME}/installation`) + .reply(201, {id: APP_ID}); + scope.post(`/repos/${data['owner']}/${data['repo']}/statuses/${data['sha']}`).reply(201); + updateStatus(data).then(() => { + expect(spy.calledOnce).true; + scope.isDone(); + done(); + }); + }); + + it('updateStatus should contain the correct data', (done) => { + data.base = 'dcb375f0'; + data.description = 'Lorem ipsum '.repeat(13); // Check max char + data.context = 'ci/test'; + const uri = `/repos/${data['owner']}/${data['repo']}/statuses/${data['sha']}`; + const url = `${process.env.WEBHOOK_PROXY_URL}/${ENDPOINT}/${data.sha}`; // target URL + const requestBodyMatcher = (body) => { + return body.state === data.status && + body.target_url === url && + body.description.length <= 140 && + body.context === data.context; + }; + scope.get(`/repos/${process.env.REPO_OWNER}/${process.env.REPO_NAME}/installation`) + .reply(201, {id: APP_ID}); + scope.post(uri, requestBodyMatcher) + .matchHeader('authorization', 'token #t0k3N') + .reply(201); + + // Run + updateStatus(data, url).then(() => { + expect(spy.calledOnce).true; + scope.isDone(); + done(); + }); + }); + + it('updateStatus should validate SHA', () => { + return updateStatus({sha: null}).catch(err => { + expect(err).to.be.instanceOf(ReferenceError); + expect(err).to.have.property('message', 'undefined or invalid sha'); + expect(spy.called).false; + }); + }); + + it('updateStatus should validate status', () => { + return updateStatus({status: 'working', sha: SHA}).catch(err => { + expect(err).to.be.instanceOf(APIError); + expect(err.message).to.contain('status'); + expect(spy.called).false; + }); + }); + + afterEach(function() { + spy.restore(); + }); +}); + + +/** + * This tests the main event callback, called when a check request comes in. We expect the + * callback to check whether the event is configured in the settings and if so, should update the + * check status to pending for each context, and add each job to the queue. + */ +describe("Github event handler callback", () => { + var scope; // Our server mock + var evt; // A payload event loaded from fixtures + var sandbox; // Sandbox for spying on queue + + /** + * This fixture ensures the `token` variable is not null. + */ + async function setToken() { + scope = nock('https://api.github.com'); + scope.get(`/repos/${process.env.REPO_OWNER}/${process.env.REPO_NAME}/installation`) + .reply(201, {id: APP_ID}); + scope.post(`/app/installations/${APP_ID}/access_tokens`) + .reply(201, { + token: '#t0k3N', + permissions: { + checks: "write", + metadata: "read", + contents: "read" + }, + }); + await setAccessToken(); + scope.done(); + } + + before(function (done) { + setToken().then(() => done()); + scope = nock('https://api.github.com', { + reqheaders: { + accept: 'application/vnd.github.machine-man-preview+json', + } + }); + }); + + beforeEach(function () { + queue.process(async (_job, _done) => {}) // nop + sandbox = sinon.createSandbox() + evt = JSON.parse(fs.readFileSync('./test/fixtures/pull_payload.json')); + }); + + it('test callback adds pending jobs', (done) => { + let pr = evt.pull_request; + let uri = `/repos/${pr.head.repo.owner.login}/${pr.head.repo.name}/statuses/${pr.head.sha}`; + scope.post(uri, body => { return body.state === 'pending'}) + .twice() + .reply(201, {}); + sandbox.spy(queue); + eventCallback({payload: evt, event: 'pull_request'}).then(function() { + expect(queue.pile.length).eq(2); // Two jobs should have been added + let data = queue.pile.pop().data; // Last added + let context = config.events.pull_request.checks; + expect(data.sha).eq(pr.head.sha); // Check head commit set + expect(data.base).eq(pr.base.sha); // Check base commit set + expect(data.force).not.true; // Check force is false (the previous job will save its results) + expect(data.owner).eq(pr.head.repo.owner.login); // Check repo owner set + expect(data.repo).eq(pr.head.repo.name); // Check repo name set + + expect(data.context.startsWith(context.pop())).true; + sandbox.assert.calledTwice(queue.add); + expect(queue.pile.pop().data.force).true; + + scope.isDone(); + done(); + }); + }); + + it('test event type error', (done) => { + sandbox.spy(queue); + eventCallback({payload: evt, event: 'issue'}).then(() => { + done(new Error('Expected method to reject.')); + }) + .catch((err) => { + sandbox.assert.notCalled(queue.add); + assert.instanceOf(err, TypeError); + done(); + }); + }); + + it('test fork', (done) => { + sandbox.spy(queue); + evt.pull_request.head.repo.owner.login = 'k1o0'; + let eventData = {payload: evt, event: 'pull_request'}; + eventCallback(eventData).then(() => { + done(new Error('Expected method to reject.')); + }) + .catch((err) => { + sandbox.assert.notCalled(queue.add); + assert.instanceOf(err, ReferenceError); + done(); + }); + }); + + afterEach(function () { + queue.pile = []; + sandbox.restore(); + }); +}); + + +/** + * This tests the shields.io badge data request callback. The badge data itself is tested by the + * lib tests. This tests the endpoint. + */ +describe('shields callback', () => { + var scope; // Our server mock + var info; // URI parameters + + before(function () { + scope = nock('https://api.github.com'); + queue.process(async (_job, _done) => {}); // nop + queue.pile = []; // ensure queue is empty + info = { + repo: 'Hello-World', + owner: 'Codertocat', + branch: 'develop' + }; + }); + + it('expect coverage response', (done) => { + // Set up response to GitHub API query + // GET /repos/:owner/:repo/git/refs/heads/:branch + scope.get(`/repos/${info.owner}/${info.repo}/git/refs/heads/${info.branch}`) + .reply(200, { + ref: `ref/heads/${info.branch}`, + object: { + sha: SHA + } + }); + + request(srv) + .get(`/coverage/${info.repo}/${info.branch}`) + .expect('Content-Type', 'application/json') + .expect(200) + .end(function (err, res) { + scope.isDone(); + if (err) return done(err); + expect(res.body).deep.keys([ + 'schemaVersion', + 'label', + 'message', + 'color' + ]); + done(); + }); + }); + + it('expect errors', (done) => { + // Set up response to GitHub API query + scope.get(`/repos/${info.owner}/${info.repo}/git/refs/heads/${info.branch}`).reply(404); + + request(srv) + .get(`/coverage/${info.repo}/${info.branch}`) + .expect(404) + .end(function (err) { + scope.isDone(); + if (err) return done(err); + done(); + }); + }); + + it('expect job forced', (done) => { + // Set up response to GitHub API query + // GET /repos/:owner/:repo/git/refs/heads/:branch + scope.get(`/repos/${info.owner}/${info.repo}/git/refs/heads/${info.branch}`) + .reply(200, { + ref: `ref/heads/${info.branch}`, + object: { + sha: SHA + } + }); + + request(srv) + .get(`/coverage/${info.repo}/${info.branch}?force=1`) + .expect('Content-Type', 'application/json') + .expect(200) + .end(function (err, res) { + scope.isDone(); + if (err) return done(err); + expect(res.body).deep.keys([ + 'schemaVersion', + 'label', + 'message', + 'color' + ]); + expect(queue.pile.length).eq(1); + done(); + }); + }); + +}); + + +/** + * This tests the logs endpoint. When provided a SHA it should read a log file and return + * it as HTML. + */ +describe('logs endpoint', () => { + var stub; // Our fs stub + var logData; // The text in our log + + before(function () { + const log_path = path.join(config.dataPath, 'reports', SHA); + logData = ['hello world', 'foobar']; + stub = sinon + .stub(fs, 'readFile') + .withArgs(path.join(log_path, `std_output-${SHA.substr(0,7)}.log`), 'utf8') + .yieldsAsync(null, logData[0]) + .withArgs(path.join(log_path, 'test_output.log'), 'utf8') + .yieldsAsync(null, logData[1]); + }); + + it('expect HTML log', (done) => { + request(srv) + .get(`/${ENDPOINT}/${SHA}`) + .expect(200) + .end(function (err, res) { + if (err) return done(err); + expect(res.text).contains(logData[0]); + expect(res.text).to.match(/^.+<\/html>$/); + done(); + }); + }); + + it('expect type param', (done) => { + request(srv) + .get(`/${ENDPOINT}/${SHA}?type=logger`) + .expect(200) + .end(function (err, res) { + if (err) return done(err); + expect(res.text).contains(logData[1]); + expect(res.text).to.match(/^.+<\/html>$/); + done(); + }); + }); + + it('expect not found', (done) => { + sinon.restore(); + request(srv) + .get(`/${ENDPOINT}/${SHA}`) + .expect(404) + .end(function (err, res) { + if (err) return done(err); + expect(res.text).contains(`${SHA} not found`) + done(); + }); + }); +}); + + +/** + * This tests the logs/records endpoint. When provided a SHA it should return the corresponding + * JSON record. + */ +describe('records endpoint', () => { + var scope; // Our server mock + + before(function () { + scope = nock('https://api.github.com'); + }); + + it('expect JSON log', (done) => { + scope.get(`/repos/${process.env.REPO_OWNER}/${process.env.REPO_NAME}/commits/${SHA}`) + .reply(200, { sha: SHA }); + // Check JSON record returned + request(srv) + .get(`/${ENDPOINT}/records/${SHA}`) + .expect(200) + .expect('Content-Type', 'application/json') + .end(function (err, res) { + if (err) return done(err); + const record = JSON.parse(res.text); + expect(record.commit).eq(SHA); + done(); + }); + }); + + it('expect works with short id', (done) => { + const id = SHA.slice(0, 7); + scope.get(`/repos/${process.env.REPO_OWNER}/${process.env.REPO_NAME}/commits/${id}`) + .reply(200, { sha: SHA } ); + // Check JSON record returned + request(srv) + .get(`/${ENDPOINT}/records/${id}`) + .expect(200) + .expect('Content-Type', 'application/json') + .end(function (err, res) { + if (err) return done(err); + const record = JSON.parse(res.text); + expect(record.commit).eq(SHA); + done(); + }); + }); + + it('expect 404 on missing', (done) => { + const id = SHA.replace('2', '3'); + scope.get(`/repos/${process.env.REPO_OWNER}/${process.env.REPO_NAME}/commits/${id}`) + .reply(404); + // Check JSON record returned + request(srv) + .get(`/${ENDPOINT}/records/${id}`) + .expect(404) + .end(function (err, res) { + if (err) return done(err); + expect(res.text).contains('not found'); + done(); + }); + }); + + it('expect works with branch and module', (done) => { + const branch = 'develop'; + const repo = 'foobar'; + scope.get(`/repos/${process.env.REPO_OWNER}/${repo}/branches/${branch}`) + .reply(200, { + commit: { + sha: SHA + } + }); + // Check JSON record returned + request(srv) + .get(`/${ENDPOINT}/records/${branch}?module=${repo}`) + .expect(200) + .expect('Content-Type', 'application/json') + .end(function (err, res) { + if (err) return done(err); + const record = JSON.parse(res.text); + expect(record.commit).eq(SHA); + done(); + }); + }); + +}); + + +/** + * This tests the coverage endpoint. Directly accessing endpoint should return 403. + */ +describe('coverage endpoint', () => { + + before(function(done) { + let reportsDir = path.join(config.dataPath, 'reports', SHA); + fs.mkdir(reportsDir, { recursive: true }, async (err) => { + if (err) throw err; + await fs.writeFile(path.join(reportsDir, 'foobar.log'), '', (err) => { if (err) throw err; }) + await fs.writeFile(path.join(reportsDir, 'index.html'), '', (err) => { if (err) throw err; }) + done() + }); + }) + + it('expect root not found', (done) => { + request(srv) + .get(`/${ENDPOINT}/coverage/`) // trailing slash essential + .expect(404) + .end(err => { + err? done(err) : done(); + }); + }); + + it('expect dir to be served', (done) => { + request(srv) + .get(`/${ENDPOINT}/coverage/${SHA}/`) // trailing slash essential + .expect(200) + .end(function (err, res) { + err? done(err) : done(); + }); + }); + + after(function() { + fs.rmdir(path.join(config.dataPath, 'reports'), {recursive: true}, err => { + if (err) throw err; + }) + + }) +}); + + +/** + * This tests the runtests and prepareEnv functions. + * @todo Check for log close on exit + */ +describe('running tests', () => { + var sandbox; // Sandbox for spying on queue + var stub; // Main fileExec stub + + beforeEach(function () { + queue.process(async (_job, _done) => {}) // nop + sandbox = sinon.createSandbox() + stub = sandbox.stub(cp, 'execFile'); + sandbox.stub(fs, 'createWriteStream'); + sandbox.stub(fs, 'mkdir').callsArg(2); + execEvent = new events.EventEmitter(); + execEvent.stdout = new events.EventEmitter(); + execEvent.stdout.pipe = sandbox.spy(); + stub.returns(execEvent); + }); + + it('test prepareEnv', async () => { + const callback = sandbox.spy(); + stub.callsArgAsync(3, null, 'preparing', ''); + const job = {data: {sha: SHA}}; + await prepareEnv(job, callback); + let log = path.join(config.dataPath, 'reports', SHA, 'std_output-cabe27e.log'); + let fn = path.resolve(path.join(__dirname, '..', 'prep_env.BAT')); + stub.calledWith(fn, [SHA, config.repo, config.dataPath]); + expect(callback.calledOnce).true; + expect(callback.calledOnceWithExactly(job)).true; + sandbox.assert.calledWith(fs.createWriteStream, log); + }); + + it('test prepareEnv with error', async (done) => { + stub.callsArgWith(3, {code: 'ENOENT'}, 'preparing', ''); + const job = { + data: {sha: SHA}, + done: (err) => { + expect(err).instanceOf(Error); + expect(err.message).to.have.string('not found'); + done(); + } + }; + prepareEnv(job); + }); + + it('test runtests', async () => { + const callback = sandbox.spy(); + stub.callsArgWith(3, null, 'running tests', ''); + const job = { + data: {sha: SHA}, + done: callback + }; + await runTests(job); + const log = path.join(config.dataPath, 'reports', SHA, 'std_output-cabe27e.log'); + sandbox.assert.calledWith(fs.createWriteStream, log, { flags: 'a' }); + const fn = path.resolve(path.join(__dirname, '..', 'run_tests.BAT')); + stub.calledWith(fn, [SHA, config.repo, config.dataPath]); + expect(callback.calledOnce).true; + expect(callback.calledOnceWithExactly()).true; + }); + + it('runtests parses MATLAB error', (done) => { + var err; + const errmsg = 'Error in MATLAB_function line 23'; + stub.callsArgWith(3, {code: 1}, 'running tests', errmsg); + sandbox.stub(fs.promises, 'writeFile').callsFake(() => { + sandbox.assert.calledWith(fs.promises.writeFile, config.dbFile); + expect(err).instanceOf(Error); + expect(err.message).to.have.string(errmsg); + done(); + }) + const job = { + data: {sha: SHA}, + done: (e) => { err = e; } + }; + runTests(job); + }); + + it('runtests parses Python error', (done) => { + var err; + stub.callsArgWith(3, {code: 1}, 'running tests', stdErr); + sandbox.stub(fs.promises, 'writeFile').callsFake(() => { + sandbox.assert.calledWith(fs.promises.writeFile, config.dbFile); + expect(err).instanceOf(Error); + let errmsg = 'FileNotFoundError: Invalid data root folder E:\\FlatIron\\integration'; + expect(err.message.startsWith(errmsg)).true; + done(); + }) + const job = { + data: {sha: SHA}, + done: (e) => { err = e; } + }; + runTests(job); + }); + + afterEach(function () { + queue.pile = []; + sandbox.verifyAndRestore(); + }); +}); + + +/** + * This tests the srv github endpoint. + * @todo Check for log close on exit + */ +describe('srv github/', () => { + var scope; // Our server mock + var clock; // Our clock mock for replicable JWT + + before(function() { + // https://runkit.com/gr2m/reproducable-jwt + clock = sinon.useFakeTimers({ + now: 0, + toFake: ['Date'] + }); + }); + + beforeEach(function() { + // Mock for App.installationAccessToken + scope = nock('https://api.github.com', { + reqheaders: { + accept: 'application/vnd.github.machine-man-preview+json', + } + }); + }); + + it('expect skipped', (done) => { + scope.get(`/repos/${process.env.REPO_OWNER}/${process.env.REPO_NAME}/installation`).reply(200); + scope.post(`/app/installations/${APP_ID}/access_tokens`).reply(200); + + request(srv) + .post(`/github`) // trailing slash essential + .set({'X-GitHub-Event': 'issues'}) + .end(function (err, res) { + expect(scope.isDone()).not.true; + err ? done(err) : done(); + }); + }); + + it('expect error caught', () => { + scope.get(`/repos/${process.env.REPO_OWNER}/${process.env.REPO_NAME}/installation`) + .reply(201, {id: APP_ID}); + scope.post(`/app/installations/${APP_ID}/access_tokens`) + .reply(201, { + token: '#t0k3N', + permissions: { + checks: "write", + metadata: "read", + contents: "read" + }, + }); + + request(srv) + .post(`/github`) // trailing slash essential + .set({ + 'X-GitHub-Event': 'issues', + 'x-github-hook-installation-target-id': process.env.GITHUB_APP_IDENTIFIER, + 'X-Hub-Signature': {'sha': null}, + 'X-GitHub-Delivery': '72d3162e-cc78-11e3-81ab-4c9367dc0958' + }) + .end(function (err) { + expect(err).is.null; // Should have caught error + }); + }); + + after(function () { + clock.restore(); + }); + +});