2019-01-15 17:18:56 +03:00
|
|
|
|
const path = require('path');
|
|
|
|
|
const liveServer = require('live-server');
|
|
|
|
|
|
|
|
|
|
const spinner = require('ora')();
|
|
|
|
|
|
|
|
|
|
const clientPath = path.resolve(__dirname, '../../client');
|
|
|
|
|
require('@babel/polyfill');
|
|
|
|
|
require('@babel/register')({
|
|
|
|
|
root: clientPath,
|
|
|
|
|
babelrc: false,
|
|
|
|
|
presets: ['@babel/preset-env'],
|
|
|
|
|
ignore: [/node_modules/],
|
|
|
|
|
only: [clientPath]
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const createPseudoWorker = require('./utils/pseudo-worker');
|
|
|
|
|
const {
|
|
|
|
|
default: createWorker
|
|
|
|
|
} = require('../../client/src/templates/Challenges/utils/worker-executor');
|
|
|
|
|
|
2018-11-01 18:56:15 +03:00
|
|
|
|
const { assert, AssertionError } = require('chai');
|
|
|
|
|
const Mocha = require('mocha');
|
2018-10-25 03:34:47 +03:00
|
|
|
|
const { flatten } = require('lodash');
|
2019-01-15 17:18:56 +03:00
|
|
|
|
|
|
|
|
|
const jsdom = require('jsdom');
|
|
|
|
|
|
|
|
|
|
const dom = new jsdom.JSDOM('');
|
|
|
|
|
global.document = dom.window.document;
|
|
|
|
|
|
2018-10-25 03:34:47 +03:00
|
|
|
|
const vm = require('vm');
|
|
|
|
|
|
2018-12-01 02:47:32 +03:00
|
|
|
|
const puppeteer = require('puppeteer');
|
|
|
|
|
|
2018-10-25 03:34:47 +03:00
|
|
|
|
const { getChallengesForLang } = require('../getChallenges');
|
|
|
|
|
|
|
|
|
|
const MongoIds = require('./utils/mongoIds');
|
|
|
|
|
const ChallengeTitles = require('./utils/challengeTitles');
|
2018-12-05 20:09:18 +03:00
|
|
|
|
const { challengeSchemaValidator } = require('../schema/challengeSchema');
|
2018-10-25 03:34:47 +03:00
|
|
|
|
const { challengeTypes } = require('../../client/utils/challengeTypes');
|
|
|
|
|
|
2019-04-12 16:44:14 +02:00
|
|
|
|
const { testedLangs } = require('../utils');
|
2018-12-05 22:04:30 +03:00
|
|
|
|
|
2019-01-15 17:18:56 +03:00
|
|
|
|
const {
|
|
|
|
|
buildDOMChallenge,
|
|
|
|
|
buildJSChallenge
|
|
|
|
|
} = require('../../client/src/templates/Challenges/utils/build');
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
createPoly
|
|
|
|
|
} = require('../../client/src/templates/Challenges/utils/polyvinyl');
|
|
|
|
|
|
2018-11-01 18:56:15 +03:00
|
|
|
|
const oldRunnerFail = Mocha.Runner.prototype.fail;
|
|
|
|
|
Mocha.Runner.prototype.fail = function(test, err) {
|
2018-12-05 22:04:30 +03:00
|
|
|
|
if (err instanceof AssertionError) {
|
|
|
|
|
const errMessage = String(err.message || '');
|
|
|
|
|
const assertIndex = errMessage.indexOf(': expected');
|
2018-12-01 02:47:32 +03:00
|
|
|
|
if (assertIndex !== -1) {
|
2018-12-05 22:04:30 +03:00
|
|
|
|
err.message = errMessage.slice(0, assertIndex);
|
2018-12-01 02:47:32 +03:00
|
|
|
|
}
|
|
|
|
|
// Don't show stacktrace for assertion errors.
|
2018-12-05 22:04:30 +03:00
|
|
|
|
if (err.stack) {
|
|
|
|
|
delete err.stack;
|
|
|
|
|
}
|
2018-11-01 18:56:15 +03:00
|
|
|
|
}
|
|
|
|
|
return oldRunnerFail.call(this, test, err);
|
|
|
|
|
};
|
|
|
|
|
|
2019-01-15 17:18:56 +03:00
|
|
|
|
async function newPageContext(browser) {
|
|
|
|
|
const page = await browser.newPage();
|
|
|
|
|
// it's needed for workers as context.
|
|
|
|
|
await page.goto('http://127.0.0.1:8080/index.html');
|
|
|
|
|
return page;
|
|
|
|
|
}
|
2018-10-25 03:34:47 +03:00
|
|
|
|
|
2019-01-15 17:18:56 +03:00
|
|
|
|
spinner.start();
|
|
|
|
|
spinner.text = 'Populate tests.';
|
2018-10-25 03:34:47 +03:00
|
|
|
|
|
2019-01-15 17:18:56 +03:00
|
|
|
|
let browser;
|
|
|
|
|
let page;
|
2018-10-25 03:34:47 +03:00
|
|
|
|
|
2018-12-05 22:04:30 +03:00
|
|
|
|
runTests();
|
|
|
|
|
|
|
|
|
|
async function runTests() {
|
2019-04-01 18:19:25 +02:00
|
|
|
|
process.on('unhandledRejection', err => {
|
|
|
|
|
spinner.stop();
|
|
|
|
|
throw new Error(`unhandledRejection: ${err.name}, ${err.message}`);
|
|
|
|
|
});
|
|
|
|
|
|
2019-04-12 16:44:14 +02:00
|
|
|
|
const testLangs = testedLangs();
|
2018-12-07 14:06:07 +03:00
|
|
|
|
|
2019-01-15 17:18:56 +03:00
|
|
|
|
const challenges = await Promise.all(
|
|
|
|
|
testLangs.map(lang => getChallenges(lang))
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
describe('Check challenges', function() {
|
|
|
|
|
before(async function() {
|
|
|
|
|
spinner.text = 'Testing';
|
|
|
|
|
this.timeout(50000);
|
|
|
|
|
liveServer.start({
|
|
|
|
|
host: '127.0.0.1',
|
|
|
|
|
port: '8080',
|
|
|
|
|
root: path.resolve(__dirname, 'stubs'),
|
|
|
|
|
mount: [['/js', path.join(clientPath, 'static/js')]],
|
|
|
|
|
open: false,
|
|
|
|
|
logLevel: 0
|
|
|
|
|
});
|
|
|
|
|
browser = await puppeteer.launch({
|
2019-02-24 03:10:27 +03:00
|
|
|
|
args: [
|
|
|
|
|
// Required for Docker version of Puppeteer
|
|
|
|
|
'--no-sandbox',
|
|
|
|
|
'--disable-setuid-sandbox',
|
|
|
|
|
// This will write shared memory files into /tmp instead of /dev/shm,
|
|
|
|
|
// because Docker’s default for /dev/shm is 64MB
|
|
|
|
|
'--disable-dev-shm-usage'
|
|
|
|
|
// dumpio: true
|
|
|
|
|
]
|
2019-01-15 17:18:56 +03:00
|
|
|
|
});
|
|
|
|
|
global.Worker = createPseudoWorker(await newPageContext(browser));
|
|
|
|
|
page = await newPageContext(browser);
|
|
|
|
|
await page.setViewport({ width: 300, height: 150 });
|
|
|
|
|
});
|
|
|
|
|
after(async function() {
|
|
|
|
|
this.timeout(30000);
|
|
|
|
|
if (browser) {
|
|
|
|
|
await browser.close();
|
|
|
|
|
}
|
|
|
|
|
liveServer.shutdown();
|
|
|
|
|
spinner.stop();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
challenges.forEach(populateTestsForLang);
|
|
|
|
|
});
|
2018-12-05 22:04:30 +03:00
|
|
|
|
|
|
|
|
|
run();
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-15 17:18:56 +03:00
|
|
|
|
async function getChallenges(lang) {
|
|
|
|
|
const challenges = await getChallengesForLang(lang).then(curriculum =>
|
2018-10-25 03:34:47 +03:00
|
|
|
|
Object.keys(curriculum)
|
2018-11-26 10:47:33 +03:00
|
|
|
|
.map(key => curriculum[key].blocks)
|
|
|
|
|
.reduce((challengeArray, superBlock) => {
|
|
|
|
|
const challengesForBlock = Object.keys(superBlock).map(
|
|
|
|
|
key => superBlock[key].challenges
|
|
|
|
|
);
|
|
|
|
|
return [...challengeArray, ...flatten(challengesForBlock)];
|
|
|
|
|
}, [])
|
|
|
|
|
);
|
2019-01-15 17:18:56 +03:00
|
|
|
|
return { lang, challenges };
|
|
|
|
|
}
|
2018-10-25 03:34:47 +03:00
|
|
|
|
|
2019-01-15 17:18:56 +03:00
|
|
|
|
function populateTestsForLang({ lang, challenges }) {
|
2018-12-05 22:04:30 +03:00
|
|
|
|
const mongoIds = new MongoIds();
|
|
|
|
|
const challengeTitles = new ChallengeTitles();
|
|
|
|
|
const validateChallenge = challengeSchemaValidator(lang);
|
|
|
|
|
|
2019-01-15 17:18:56 +03:00
|
|
|
|
describe(`Check challenges (${lang})`, function() {
|
2018-11-16 21:16:27 +03:00
|
|
|
|
this.timeout(5000);
|
2018-10-25 03:34:47 +03:00
|
|
|
|
|
2019-01-15 17:18:56 +03:00
|
|
|
|
challenges.forEach(challenge => {
|
|
|
|
|
describe(challenge.title || 'No title', function() {
|
2018-10-25 03:34:47 +03:00
|
|
|
|
it('Common checks', function() {
|
|
|
|
|
const result = validateChallenge(challenge);
|
|
|
|
|
if (result.error) {
|
2018-12-05 22:04:30 +03:00
|
|
|
|
throw new AssertionError(result.error);
|
2018-10-25 03:34:47 +03:00
|
|
|
|
}
|
|
|
|
|
const { id, title } = challenge;
|
|
|
|
|
mongoIds.check(id, title);
|
|
|
|
|
challengeTitles.check(title);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const { challengeType } = challenge;
|
2018-11-26 10:47:33 +03:00
|
|
|
|
if (
|
|
|
|
|
challengeType !== challengeTypes.html &&
|
|
|
|
|
challengeType !== challengeTypes.js &&
|
|
|
|
|
challengeType !== challengeTypes.bonfire &&
|
|
|
|
|
challengeType !== challengeTypes.modern &&
|
|
|
|
|
challengeType !== challengeTypes.backend
|
2018-10-25 03:34:47 +03:00
|
|
|
|
) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-25 17:23:00 +03:00
|
|
|
|
let { tests = [] } = challenge;
|
2018-10-25 03:34:47 +03:00
|
|
|
|
tests = tests.filter(test => !!test.testString);
|
|
|
|
|
if (tests.length === 0) {
|
2018-10-25 17:23:00 +03:00
|
|
|
|
it('Check tests. No tests.');
|
2018-10-25 03:34:47 +03:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe('Check tests syntax', function() {
|
|
|
|
|
tests.forEach(test => {
|
|
|
|
|
it(`Check for: ${test.text}`, function() {
|
2018-11-26 10:47:33 +03:00
|
|
|
|
assert.doesNotThrow(() => new vm.Script(test.testString));
|
2018-10-25 03:34:47 +03:00
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2019-01-15 17:18:56 +03:00
|
|
|
|
let { files = [] } = challenge;
|
2019-01-17 02:50:12 +03:00
|
|
|
|
let createTestRunner;
|
2019-01-15 17:18:56 +03:00
|
|
|
|
if (challengeType === challengeTypes.backend) {
|
|
|
|
|
it('Check tests is not implemented.');
|
|
|
|
|
return;
|
|
|
|
|
} else if (
|
|
|
|
|
challengeType === challengeTypes.js ||
|
|
|
|
|
challengeType === challengeTypes.bonfire
|
2018-11-26 10:47:33 +03:00
|
|
|
|
) {
|
2019-01-17 02:50:12 +03:00
|
|
|
|
createTestRunner = createTestRunnerForJSChallenge;
|
2019-01-15 17:18:56 +03:00
|
|
|
|
} else if (files.length === 1) {
|
2019-01-17 02:50:12 +03:00
|
|
|
|
createTestRunner = createTestRunnerForDOMChallenge;
|
2018-10-25 03:34:47 +03:00
|
|
|
|
} else {
|
2019-01-15 17:18:56 +03:00
|
|
|
|
it('Check tests.', () => {
|
|
|
|
|
throw new Error('Seed file should be only the one.');
|
|
|
|
|
});
|
2018-10-25 17:23:00 +03:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-15 17:18:56 +03:00
|
|
|
|
files = files.map(createPoly);
|
2018-10-25 17:23:00 +03:00
|
|
|
|
it('Test suite must fail on the initial contents', async function() {
|
2019-01-17 02:50:12 +03:00
|
|
|
|
this.timeout(5000 * tests.length + 1000);
|
2018-11-01 18:56:15 +03:00
|
|
|
|
// suppress errors in the console.
|
|
|
|
|
const oldConsoleError = console.error;
|
|
|
|
|
console.error = () => {};
|
2019-01-17 02:50:12 +03:00
|
|
|
|
let fails = false;
|
|
|
|
|
let testRunner;
|
|
|
|
|
try {
|
|
|
|
|
testRunner = await createTestRunner(
|
|
|
|
|
{ ...challenge, files },
|
|
|
|
|
'',
|
|
|
|
|
page
|
|
|
|
|
);
|
|
|
|
|
} catch {
|
|
|
|
|
fails = true;
|
|
|
|
|
}
|
|
|
|
|
if (!fails) {
|
|
|
|
|
for (const test of tests) {
|
2018-11-26 10:47:33 +03:00
|
|
|
|
try {
|
2019-01-17 02:50:12 +03:00
|
|
|
|
await testRunner(test);
|
2018-11-26 10:47:33 +03:00
|
|
|
|
} catch (e) {
|
2019-01-17 02:50:12 +03:00
|
|
|
|
fails = true;
|
|
|
|
|
break;
|
2018-11-26 10:47:33 +03:00
|
|
|
|
}
|
2019-01-17 02:50:12 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
2018-11-01 18:56:15 +03:00
|
|
|
|
console.error = oldConsoleError;
|
2018-10-25 17:23:00 +03:00
|
|
|
|
assert(fails, 'Test suit does not fail on the initial contents');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let { solutions = [] } = challenge;
|
|
|
|
|
const noSolution = new RegExp('// solution required');
|
2018-11-26 10:47:33 +03:00
|
|
|
|
solutions = solutions.filter(
|
|
|
|
|
solution => !!solution && !noSolution.test(solution)
|
|
|
|
|
);
|
2018-10-25 17:23:00 +03:00
|
|
|
|
|
|
|
|
|
if (solutions.length === 0) {
|
|
|
|
|
it('Check tests. No solutions');
|
2018-10-25 03:34:47 +03:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-15 17:18:56 +03:00
|
|
|
|
describe('Check tests against solutions', function() {
|
2018-10-25 03:34:47 +03:00
|
|
|
|
solutions.forEach((solution, index) => {
|
2019-10-04 12:16:30 +02:00
|
|
|
|
it(`Solution ${index + 1} must pass the tests`, async function() {
|
2019-01-17 02:50:12 +03:00
|
|
|
|
this.timeout(5000 * tests.length + 1000);
|
|
|
|
|
const testRunner = await createTestRunner(
|
|
|
|
|
{ ...challenge, files },
|
|
|
|
|
solution,
|
|
|
|
|
page
|
|
|
|
|
);
|
|
|
|
|
for (const test of tests) {
|
|
|
|
|
await testRunner(test);
|
|
|
|
|
}
|
2018-10-25 03:34:47 +03:00
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
2018-12-05 22:04:30 +03:00
|
|
|
|
}
|
2018-10-25 03:34:47 +03:00
|
|
|
|
|
2019-01-17 02:50:12 +03:00
|
|
|
|
async function createTestRunnerForDOMChallenge(
|
|
|
|
|
{ required = [], template, files },
|
|
|
|
|
solution,
|
2019-01-15 17:18:56 +03:00
|
|
|
|
context
|
|
|
|
|
) {
|
|
|
|
|
if (solution) {
|
|
|
|
|
files[0].contents = solution;
|
2018-10-25 17:23:00 +03:00
|
|
|
|
}
|
2018-10-25 03:34:47 +03:00
|
|
|
|
|
2019-02-13 10:07:14 +03:00
|
|
|
|
const { build, sources, loadEnzyme } = await buildDOMChallenge({
|
|
|
|
|
files,
|
2019-01-15 17:18:56 +03:00
|
|
|
|
required,
|
|
|
|
|
template
|
2018-12-01 02:47:32 +03:00
|
|
|
|
});
|
|
|
|
|
|
2019-01-15 17:18:56 +03:00
|
|
|
|
await context.reload();
|
|
|
|
|
await context.setContent(build);
|
2019-01-17 02:50:12 +03:00
|
|
|
|
await context.evaluate(
|
2019-02-19 01:59:12 +03:00
|
|
|
|
async (sources, loadEnzyme) => {
|
2019-02-03 19:49:27 +03:00
|
|
|
|
const code = sources && 'index' in sources ? sources['index'] : '';
|
|
|
|
|
const getUserInput = fileName => sources[fileName];
|
|
|
|
|
await document.__initTestFrame({ code, getUserInput, loadEnzyme });
|
2018-12-01 02:47:32 +03:00
|
|
|
|
},
|
2019-01-15 17:18:56 +03:00
|
|
|
|
sources,
|
|
|
|
|
loadEnzyme
|
2018-12-01 02:47:32 +03:00
|
|
|
|
);
|
2019-01-17 02:50:12 +03:00
|
|
|
|
|
2019-02-19 01:59:12 +03:00
|
|
|
|
return async ({ text, testString }) => {
|
2019-01-17 02:50:12 +03:00
|
|
|
|
try {
|
|
|
|
|
const { pass, err } = await Promise.race([
|
|
|
|
|
new Promise((_, reject) => setTimeout(() => reject('timeout'), 5000)),
|
|
|
|
|
await context.evaluate(async testString => {
|
|
|
|
|
return await document.__runTest(testString);
|
|
|
|
|
}, testString)
|
|
|
|
|
]);
|
|
|
|
|
if (!pass) {
|
2019-10-04 12:16:30 +02:00
|
|
|
|
throw new AssertionError(err.message);
|
2019-01-17 02:50:12 +03:00
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
2019-10-04 12:16:30 +02:00
|
|
|
|
reThrow(err, text);
|
2019-01-17 02:50:12 +03:00
|
|
|
|
}
|
|
|
|
|
};
|
2018-10-25 03:34:47 +03:00
|
|
|
|
}
|
|
|
|
|
|
2019-01-17 02:50:12 +03:00
|
|
|
|
async function createTestRunnerForJSChallenge({ files }, solution) {
|
2019-01-15 17:18:56 +03:00
|
|
|
|
if (solution) {
|
|
|
|
|
files[0].contents = solution;
|
2018-10-25 17:23:00 +03:00
|
|
|
|
}
|
|
|
|
|
|
2019-02-13 10:07:14 +03:00
|
|
|
|
const { build, sources } = await buildJSChallenge({ files });
|
2019-01-15 17:18:56 +03:00
|
|
|
|
const code = sources && 'index' in sources ? sources['index'] : '';
|
2018-10-25 03:34:47 +03:00
|
|
|
|
|
2019-03-14 12:08:15 +03:00
|
|
|
|
const testWorker = createWorker('test-evaluator', { terminateWorker: true });
|
2019-02-19 01:59:12 +03:00
|
|
|
|
return async ({ text, testString }) => {
|
2019-01-17 02:50:12 +03:00
|
|
|
|
try {
|
|
|
|
|
const { pass, err } = await testWorker.execute(
|
|
|
|
|
{ testString, build, code, sources },
|
|
|
|
|
5000
|
2019-03-14 12:08:15 +03:00
|
|
|
|
).done;
|
2019-01-17 02:50:12 +03:00
|
|
|
|
if (!pass) {
|
2019-10-04 12:16:30 +02:00
|
|
|
|
throw new AssertionError(err.message);
|
2019-01-17 02:50:12 +03:00
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
2019-10-04 12:16:30 +02:00
|
|
|
|
reThrow(err, text);
|
2018-10-25 03:34:47 +03:00
|
|
|
|
}
|
2019-01-17 02:50:12 +03:00
|
|
|
|
};
|
2018-10-25 03:34:47 +03:00
|
|
|
|
}
|
2019-10-04 12:16:30 +02:00
|
|
|
|
|
|
|
|
|
function reThrow(err, text) {
|
|
|
|
|
if (typeof err === 'string') {
|
|
|
|
|
throw new AssertionError(
|
|
|
|
|
`${text}
|
|
|
|
|
${err}`
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
err.message = `${text}
|
|
|
|
|
${err.message}`;
|
|
|
|
|
throw err;
|
|
|
|
|
}
|
|
|
|
|
}
|