Initial work on in-browser test runner

Run `grunt test` and open up the `build/test/index.html` to run the
tests.
This commit is contained in:
toby 2017-02-23 13:59:58 -05:00
parent 92bd2c921e
commit 500522bdeb
9 changed files with 535 additions and 9 deletions

View File

@ -9,6 +9,10 @@ module.exports = function(grunt) {
"A persistent task which creates a development build whenever source files are modified.",
["clean:dev", "concat:css", "concat:js", "copy:htmlDev", "copy:staticDev", "chmod:build", "watch"]);
grunt.registerTask("test",
"A persistent task which creates a test build whenever source files are modified.",
["clean:dev", "concat:cssTest", "concat:jsTest", "copy:htmlTest", "copy:staticTest", "chmod:build", "watch"]);
grunt.registerTask("prod",
"Creates a production-ready build. Use the --msg flag to add a compile message.",
["eslint", "exec:stats", "clean", "jsdoc", "concat", "copy:htmlDev", "copy:htmlProd", "copy:htmlInline",
@ -50,7 +54,7 @@ module.exports = function(grunt) {
// JS includes
var jsFiles = [
var jsIncludes = [
// Third party framework libraries
"src/js/lib/jquery-2.1.1.js",
"src/js/lib/bootstrap-3.3.6.js",
@ -154,10 +158,27 @@ module.exports = function(grunt) {
"src/js/views/html/*.js",
"!src/js/views/html/main.js",
// Start the app!
"src/js/views/html/main.js",
];
var jsAppFiles = [].concat(
jsIncludes,
[
// Start the main app!
"src/js/views/html/main.js",
]
);
var jsTestFiles = [].concat(
jsIncludes,
[
"src/js/lib/vuejs/vue.min.js",
"src/js/test/*.js",
"src/js/test/tests/**/*",
// Start the test runner app!
"src/js/test/views/html/main.js",
]
);
var banner = '/**\n\
* CyberChef - The Cyber Swiss Army Knife\n\
*\n\
@ -198,6 +219,7 @@ module.exports = function(grunt) {
config: ["src/js/config/**/*.js"],
views: ["src/js/views/**/*.js"],
operations: ["src/js/operations/**/*.js"],
tests: ["src/js/test/**/*.js"],
},
jsdoc: {
options: {
@ -239,12 +261,35 @@ module.exports = function(grunt) {
],
dest: "build/dev/styles.css"
},
cssTest: {
options: {
banner: banner.replace(/\/\*\*/g, "/*!"),
process: function(content, srcpath) {
// Change special comments from /** to /*! to comply with cssmin
content = content.replace(/^\/\*\* /g, "/*! ");
return grunt.template.process(content);
}
},
src: [
"src/css/lib/**/*.css",
"src/css/structure/**/*.css",
"src/css/themes/classic.css"
],
dest: "build/test/styles.css"
},
js: {
options: {
banner: '"use strict";\n'
},
src: jsFiles,
src: jsAppFiles,
dest: "build/dev/scripts.js"
},
jsTest: {
options: {
banner: '"use strict";\n'
},
src: jsTestFiles,
dest: "build/test/scripts.js"
}
},
copy: {
@ -257,6 +302,15 @@ module.exports = function(grunt) {
src: "src/html/index.html",
dest: "build/dev/index.html"
},
htmlTest: {
options: {
process: function(content, srcpath) {
return grunt.template.process(content, templateOptions);
}
},
src: "src/html/test.html",
dest: "build/test/index.html"
},
htmlProd: {
options: {
process: function(content, srcpath) {
@ -294,6 +348,21 @@ module.exports = function(grunt) {
}
]
},
staticTest: {
files: [
{
expand: true,
cwd: "src/static/",
src: [
"**/*",
"**/.*",
"!stats.txt",
"!ga.html"
],
dest: "build/test/"
}
]
},
staticProd: {
files: [
{
@ -468,15 +537,15 @@ module.exports = function(grunt) {
},
js: {
files: "src/js/**/*.js",
tasks: ["concat:js", "chmod:build"]
tasks: ["concat:js", "concat:jsTest", "chmod:build"]
},
html: {
files: "src/html/**/*.html",
tasks: ["copy:htmlDev", "chmod:build"]
tasks: ["copy:htmlDev", "copy:htmlTest", "chmod:build"]
},
static: {
files: ["src/static/**/*", "src/static/**/.*"],
tasks: ["copy:staticDev", "chmod:build"]
tasks: ["copy:staticDev", "copy:staticTest", "chmod:build"]
},
grunt: {
files: "Gruntfile.js",

96
src/html/test.html Executable file
View File

@ -0,0 +1,96 @@
<!-- htmlmin:ignore --><!--
CyberChef - The Cyber Swiss Army Knife
@author tlwr [toby@toby.codes]
@copyright Crown Copyright 2017
@license Apache-2.0
Copyright 2017 Crown Copyright
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- htmlmin:ignore -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>CyberChef Test Runner</title>
<link rel="icon" type="image/png" href="images/favicon.ico?__inline" />
<link href="styles.css" rel="stylesheet" />
</head>
<body>
<template id="test-status-icon-template">
<span>{{ getIcon() }}</span>
</template>
<template id="test-stats-template">
<div class="text-center row">
<div class="col-md-2"
v-for="status in ['Waiting', 'Loading', 'Erroring', 'Failing']">
<test-status-icon :status="status.toLowerCase()"></test-status-icon>
<br>
{{ status }}
<br>
{{ countTestsWithStatus(status.toLowerCase()) }}
</div>
<div class="col-md-2">
<test-status-icon status="passing"></test-status-icon>
<br>
Passing
<br>
{{ countTestsWithStatus("passing") }}
/
{{ tests.length }}
</div>
<div class="col-md-2">
<test-status-icon status="passing"></test-status-icon>
<br>
% Passing
<br>
{{ ((100.0 * countTestsWithStatus("passing")) / tests.length).toFixed(1) }}%
</div>
</div>
</template>
<template id="tests-template">
<table class="table table-striped">
<tbody>
<tr v-for="test in tests">
<td class="col-md-1 col-sm-4">
<test-status-icon :status="test.status"></test-status-icon>
</td>
<td class="col-md-4 col-sm-8">
{{ test.name }}
</td>
<td class="col-md-7 col-sm-12">
<pre v-if="test.output"><code>{{ test.output }}</code></pre>
</td>
</tr>
</tbody>
</table>
</template>
<div class="container">
<main>
<h1>CyberChef Test Runner</h1>
<hr>
<test-stats :tests="tests"></test-stats>
<hr>
<tests :tests="tests"></tests>
</main>
</div>
<script type="application/javascript" src="scripts.js"></script>
</body>
</html>

View File

@ -109,6 +109,10 @@
"OutputWaiter": false,
"RecipeWaiter": false,
"SeasonalWaiter": false,
"WindowWaiter": false
"WindowWaiter": false,
/* test */
"Vue": false,
"TestRegister": false
}
}
}

8
src/js/lib/vuejs/vue.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,97 @@
/**
* TestRegister.js
*
* This is so individual files can register their tests in one place, and
* ensure that they will get run by the frontend.
*
* @author tlwr [toby@toby.codes
*
* @copyright Crown Copyright 2017
* @license Apache-2.0
*
*/
(function() {
/**
* Add a list of tests to the register.
*
* @class
*/
function TestRegister() {
this.tests = [];
}
/**
* Add a list of tests to the register.
*
* @param {Object[]} tests
*/
TestRegister.prototype.addTests = function(tests) {
this.tests = this.tests.concat(tests.map(function(test) {
test.status = "waiting";
test.output = "";
return test;
}));
};
/**
* Returns the list of tests.
*
* @returns {Object[]} tests
return this.tests;
};
/**
* Runs all the tests in the register and updates the state of each test.
*
*/
TestRegister.prototype.runTests = function() {
this.tests.forEach(function(test, i) {
var chef = new Chef();
// This resolve is to not instantly break when async operations are
// supported. Marked as TODO.
Promise.resolve(chef.bake(
test.input,
test.recipeConfig,
{},
0,
0
))
.then(function(result) {
if (result.error) {
if (test.expectedError) {
test.status = "passing";
} else {
test.status = "erroring";
test.output = [
"Erroring",
"-------",
result.error.displayStr,
].join("\n");
}
} else {
if (result.result === test.expectedOutput) {
test.status = "passing";
} else {
test.status = "failing";
test.output = [
"Failing",
"-------",
"Expected",
"-------",
test.expectedOutput,
"Actual",
"-------",
result.result,
].join("\n");
}
}
});
});
};
// Singleton TestRegister, keeping things simple and obvious.
window.TestRegister = new TestRegister();
})();

84
src/js/test/tests/core.js Normal file
View File

@ -0,0 +1,84 @@
/**
* Core tests.
*
* @copyright Crown Copyright 2017
* @license Apache-2.0
*
*/
TestRegister.addTests([
{
name: "Example error",
input: "1\n2\na\n4",
expectedOutput: "1\n2\n3\n4",
recipeConfig: [
{
op: "Fork",
args: ["\n", "\n", false],
},
{
op: "To Base",
args: [16],
},
],
},
{
name: "Example fail",
input: "1\n2\na\n4",
expectedOutput: "1\n2\n3\n4",
recipeConfig: [
{
op: "Fork",
args: ["\n", "\n", true],
},
{
op: "To Base",
args: [16],
},
],
},
{
name: "Fork: nothing",
input: "",
expectedOutput: "",
recipeConfig: [
{
op: "Fork",
args: ["\n", "\n", false],
},
],
},
{
name: "Fork, Merge: nothing",
input: "",
expectedOutput: "",
recipeConfig: [
{
op: "Fork",
args: ["\n", "\n", false],
},
{
op: "Merge",
args: [],
},
],
},
{
name: "Fork, (expect) Error, Merge",
input: "1\n2\na\n4",
expectedError: true,
recipeConfig: [
{
op: "Fork",
args: ["\n", "\n", false],
},
{
op: "To Base",
args: [16],
},
{
op: "Merge",
args: [],
},
],
},
]);

View File

@ -0,0 +1,77 @@
/**
* Base58 tests.
*
* @author tlwr [toby@toby.codes
*
* @copyright Crown Copyright 2017
* @license Apache-2.0
*
*/
TestRegister.addTests([
{
name: "To Base58 (Bitcoin): nothing",
input: "",
expectedOutput: "",
recipeConfig: [
{
op: "To Base58",
args: ["123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"],
},
],
},
{
name: "To Base58 (Ripple): nothing",
input: "",
expectedOutput: "",
recipeConfig: [
{
op: "To Base58",
args: ["rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz"],
},
],
},
{
name: "To Base58 (Bitcoin): 'hello world'",
input: "hello world",
expectedOutput: "StV1DL6CwTryKyV",
recipeConfig: [
{
op: "To Base58",
args: ["123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"],
},
],
},
{
name: "To Base58 (Ripple): 'hello world'",
input: "hello world",
expectedOutput: "StVrDLaUATiyKyV",
recipeConfig: [
{
op: "To Base58",
args: ["rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz"],
},
],
},
{
name: "From Base58 (Bitcoin): 'StV1DL6CwTryKyV'",
input: "StV1DL6CwTryKyV",
expectedOutput: "hello world",
recipeConfig: [
{
op: "From Base58",
args: ["123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"],
},
],
},
{
name: "From Base58 (Ripple): 'StVrDLaUATiyKyV'",
input: "StVrDLaUATiyKyV",
expectedOutput: "hello world",
recipeConfig: [
{
op: "From Base58",
args: ["rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz"],
},
],
},
]);

View File

@ -0,0 +1,33 @@
/**
* Base58 tests.
*
* @author tlwr [toby@toby.codes
*
* @copyright Crown Copyright 2017
* @license Apache-2.0
*
*/
TestRegister.addTests([
{
name: "To Morse Code: 'SOS'",
input: "SOS",
expectedOutput: "... --- ...",
recipeConfig: [
{
op: "To Morse Code",
args: ["-/.", "Space", "Line feed"],
},
],
},
{
name: "From Morse Code '... --- ...'",
input: "... --- ...",
expectedOutput: "SOS",
recipeConfig: [
{
op: "From Morse Code",
args: ["Space", "Line feed"],
},
],
},
]);

View File

@ -0,0 +1,58 @@
/**
* main.js
*
* Simple VueJS app for running all the tests and displaying some basic stats.
* @author tlwr [toby@toby.codes]
* @copyright Crown Copyright 2017
* @license Apache-2.0
*
*/
(function() {
Vue.component("test-status-icon", {
template: "#test-status-icon-template",
props: ["status"],
methods: {
getIcon: function() {
var icons = {
waiting: "⌚",
loading: "⚡",
passing: "✔️️",
failing: "❌",
erroring: "☠️",
};
return icons[this.status];
}
},
});
Vue.component("test-stats", {
template: "#test-stats-template",
props: ["tests"],
methods: {
countTestsWithStatus: function(status) {
return this.tests.filter(function(test) {
return test.status === status;
}).length;
},
},
});
Vue.component("tests", {
template: "#tests-template",
props: ["tests"],
});
window.TestRunner = new Vue({
el: "main",
data: {
tests: TestRegister.getTests(),
},
mounted: function() {
TestRegister.runTests();
},
});
})();