Skip to content

Commit 956cd85

Browse files
committed
Merge pull request #588 from apfelbox/mocha
Implement test suite runner
2 parents fd54995 + e8884a9 commit 956cd85

12 files changed

+755
-2
lines changed

.travis.yml

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
language: node_js
2+
3+
node_js:
4+
- "0.10"
5+
- "0.12"
6+
7+
before_script:
8+
- npm install -g gulp
9+
- gulp
10+
11+
script: npm test

package.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"description": "Lightweight, robust, elegant syntax highlighting. A spin-off project from Dabblet.",
55
"main": "prism.js",
66
"scripts": {
7-
"test": "echo \"Error: no test specified\" && exit 1"
7+
"test": "mocha tests/testrunner-tests.js && mocha tests/run.js"
88
},
99
"repository": {
1010
"type": "git",
@@ -18,10 +18,12 @@
1818
"license": "MIT",
1919
"readmeFilename": "README.md",
2020
"devDependencies": {
21+
"chai": "^2.3.0",
2122
"gulp": "^3.8.6",
2223
"gulp-concat": "^2.3.4",
2324
"gulp-header": "^1.0.5",
2425
"gulp-rename": "^1.2.0",
25-
"gulp-uglify": "^0.3.1"
26+
"gulp-uglify": "^0.3.1",
27+
"mocha": "^2.2.5"
2628
}
2729
}

test-suite.html

+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
5+
<meta charset="utf-8" />
6+
<link rel="shortcut icon" href="favicon.png" />
7+
<title>Running the test suite ▲ Prism</title>
8+
<link rel="stylesheet" href="style.css" />
9+
<link rel="stylesheet" href="themes/prism.css" data-noprefix />
10+
<script src="prefixfree.min.js"></script>
11+
12+
<script>var _gaq = [['_setAccount', 'UA-33746269-1'], ['_trackPageview']];</script>
13+
<script src="http://www.google-analytics.com/ga.js" async></script>
14+
</head>
15+
<body class="language-javascript">
16+
17+
<header>
18+
<div class="intro" data-src="templates/header-main.html" data-type="text/html"></div>
19+
20+
<h2>Running the test suite</h2>
21+
<p>Prism has a test suite, that ensures that the correct tokens are matched.</p>
22+
</header>
23+
24+
<section id="running-the-test-suite">
25+
<h1>Running the test suite</h1>
26+
27+
<p>Running the test suite is simple: just call <code class="language-bash">npm test</code>.</p>
28+
<p>All test files are run in isolation. A new prism instance is created for each test case. This will slow the test runner a bit down, but we can be sure that nothing leaks into the next test case.</p>
29+
</section>
30+
31+
<section id="writing-tests">
32+
<h1>Writing tests</h1>
33+
34+
<p>Thank you for writing tests! Tests are awesome! They ensure, that we can improve the codebase without breaking anything. Also, this way, we can ensure that upgrading Prism is as painless as possible for you.</p>
35+
<p>You can add new tests by creating a new test case file (with the <code>.test</code> file extension) in the tests directory which is located at <code>/tests/languages/${language}</code>.</p>
36+
37+
<section id="writing-tests-directories">
38+
<h2>Language directories</h2>
39+
<p>All tests are sorted into directories in the <code>tests/languages</code> directory. Each directory name encodes, which language you are currently testing.</p>
40+
<p><strong>All language names must match the names from the definition in <code>components.js</code>.</strong></p>
41+
42+
<h3>Example 1: testing a language in isolation (default use case)</h3>
43+
<p>Just put your test file into the directory of the language you want to test.</p>
44+
<p>So, if you want to test CSS, put your test file in <code>/tests/languages/css</code> to test CSS only. If you create a test case in this directory, the test runner will ensure that the <code>css</code> language definition including all required language definitions are correctly loaded.</p>
45+
46+
<h3>Example 2: testing language injection</h3>
47+
<p>If you want to test language injection, you typically need to load two or more languages where one language is the “main” language that is being tested, with all other languages being injected into it.</p>
48+
<p>You need to define multiple languages by separating them using a <code>+</code> sign: <code>markup+php</code>.</p>
49+
<p>The languages are loaded in order, so first markup (+ dependencies) is loaded, then php (+ dependencies). The test loader ensures that no language is loaded more than once (for example if two languages have the same dependencies).</p>
50+
<p>By default the first language is the main language: <code>markup+php</code> will have <code>markup</code> as main language. This is equal to putting your code in the following code block:</p>
51+
<pre><code class="language-markup">...
52+
&lt;pre>&lt;code class="language-markup">
53+
&lt;!-- your code here -->
54+
&lt;/code>&lt;pre>
55+
...</code></pre>
56+
57+
<p>If you need to load the languages in a given order, but you don't want to use the first language as main language, you can mark the main language with an exclamation mark: <code>markup+php!</code>. This will use <code>php</code> as main language. (You can only define one main language. The test runner will fail all tests in directories with more than one main language.)</p>
58+
59+
<p><em>Note: by loading multiple languages you can do integration tests (ensure that loading two or more languages together won't break anything).</em></p>
60+
</section>
61+
62+
<section id="writing-tests-creating-your-test-case-file">
63+
<h2>Creating your test case file</h2>
64+
<p>At first you need to create a new file in the language directory, you want to test.</p>
65+
<p><strong>Use a proper name for your test case.</strong> Please use one case of the following conventions:</p>
66+
<ul>
67+
<li><code>issue{issueid}</code>: reference a github issue id (example: <code>issue588.test</code>).</li>
68+
<li><code>{featurename}_feature</code>: group all tests to one feature in one file (example: <code>string_interpolation_feature.test</code>).</li>
69+
<li><code>{language}_inclusion</code>: test inclusion of one language into the other (example: <code>markup/php_inclusion.test</code> will test php inclusion into markup).</li>
70+
</ul>
71+
<p>You can use all conventions as a prefix, so <code>string_interpolation_feature_inline.test</code> is possible. <strong>But please take a minute or two to think of a proper name of your test case file. You are writing code not only for the computers, but also for your fellow developers.</strong></p>
72+
</section>
73+
74+
<section id="writing-tests-writing-your-test">
75+
<h2>Writing your test</h2>
76+
<p>The structure of a test case file is as follows:</p>
77+
<pre><code>
78+
... language snippet...
79+
----
80+
... the simplified token stream you expect ...</code></pre>
81+
82+
<p>Your file is built up of two or three sections, separated by three or more dashes <code>-</code>, starting at the begin of the line:</p>
83+
<ol>
84+
<li>Your language snippet. The code you want to compile using Prism. (<strong>required</strong>)</li>
85+
<li>The simplified token stream you expect. Needs to be valid JSON. (<strong>required</strong>)</li>
86+
<li>A comment explaining the test case. (<em>optional</em>)</li>
87+
</ol>
88+
<p>The easiest way would be to look at an existing test file:</p>
89+
<pre><code>var a = 5;
90+
91+
----------------------------------------------------
92+
93+
[
94+
["keyword", "var"],
95+
" a ",
96+
["operator", "="],
97+
["number", "5"],
98+
["punctuation", ";"]
99+
]
100+
101+
----------------------------------------------------
102+
103+
This is a comment explaining this test case.</code></pre>
104+
</section>
105+
106+
<section>
107+
<h2>Explaining the simplified token stream</h2>
108+
<p>While compiling, Prism transforms your source code into a token stream. This is basically a tree of nested tokens (or arrays, or strings).</p>
109+
<p>As these trees are hard to write by hand, the test runner uses a simplified version of it.</p>
110+
<p>It uses the following rules:</p>
111+
<ul>
112+
<li><code>Token</code> objects are transformed into an array: <code>[token.type, token.content]</code> (whereas <code>token.content</code> can be a nested structure).</li>
113+
<li>All strings that are either empty or only contain whitespace, are removed from the token stream.</li>
114+
<li>All empty structures are removed.</li>
115+
</ul>
116+
<p>For further information: reading the tests of the test runner (<code>tests/testrunner-tests.js</code>) will help you understand the transformation.</p>
117+
</section>
118+
</section>
119+
120+
121+
<section id="test-runner-tests">
122+
<h2>Test runner tests</h2>
123+
<p>The test runner itself is tested in a separate test case. You can find all “test core” related tests in <code>tests/testrunner-tests.js</code>.</p>
124+
<p>You shouldn't need to touch this file ever, except you modify the test runner code.</p>
125+
</section>
126+
127+
<section id="internal-structure">
128+
<h2>Internal structure</h2>
129+
<p>The global test flow is at follows:</p>
130+
<ol>
131+
<li>Run all internal tests (test the test runner).</li>
132+
<li>Find all language tests.</li>
133+
<li>Run all language tests individually.</li>
134+
<li>Report the results.</li>
135+
</ol>
136+
</section>
137+
138+
139+
<footer data-src="templates/footer.html" data-type="text/html"></footer>
140+
141+
<script src="prism.js"></script>
142+
<script src="utopia.js"></script>
143+
<script src="components.js"></script>
144+
<script src="code.js"></script>
145+
146+
</body>
147+
</html>

tests/helper/components.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"use strict";
2+
3+
var fs = require("fs");
4+
var vm = require("vm");
5+
6+
var fileContent = fs.readFileSync(__dirname + "/../../components.js", "utf8");
7+
var context = {};
8+
vm.runInNewContext(fileContent, context);
9+
10+
module.exports = context.components;

tests/helper/prism-loader.js

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"use strict";
2+
3+
var fs = require("fs");
4+
var vm = require("vm");
5+
var components = require("./components");
6+
var languagesCatalog = components.languages;
7+
8+
9+
module.exports = {
10+
11+
/**
12+
* Creates a new Prism instance with the given language loaded
13+
*
14+
* @param {string|string[]} languages
15+
* @returns {Prism}
16+
*/
17+
createInstance: function (languages) {
18+
var context = {
19+
loadedLanguages: [],
20+
Prism: this.createEmptyPrism()
21+
};
22+
languages = Array.isArray(languages) ? languages : [languages];
23+
24+
for (var i = 0, l = languages.length; i < l; i++) {
25+
context = this.loadLanguage(languages[i], context);
26+
}
27+
28+
return context.Prism;
29+
},
30+
31+
32+
/**
33+
* Loads the given language (including recursively loading the dependencies) and
34+
* appends the config to the given Prism object
35+
*
36+
* @private
37+
* @param {string} language
38+
* @param {{loadedLanguages: string[], Prism: Prism}} context
39+
* @returns {{loadedLanguages: string[], Prism: Prism}}
40+
*/
41+
loadLanguage: function (language, context) {
42+
if (!languagesCatalog[language]) {
43+
throw new Error("Language '" + language + "' not found.");
44+
}
45+
46+
// the given language was already loaded
47+
if (-1 < context.loadedLanguages.indexOf(language)) {
48+
return context;
49+
}
50+
51+
// if the language has a dependency -> load it first
52+
if (languagesCatalog[language].require) {
53+
context = this.loadLanguage(languagesCatalog[language].require, context);
54+
}
55+
56+
// load the language itself
57+
var languageSource = this.loadFileSource(language);
58+
context.Prism = this.runFileWithContext(languageSource, {Prism: context.Prism}).Prism;
59+
context.loadedLanguages.push(language);
60+
61+
return context;
62+
},
63+
64+
65+
/**
66+
* Creates a new empty prism instance
67+
*
68+
* @private
69+
* @returns {Prism}
70+
*/
71+
createEmptyPrism: function () {
72+
var coreSource = this.loadFileSource("core");
73+
var context = this.runFileWithContext(coreSource);
74+
return context.Prism;
75+
},
76+
77+
78+
/**
79+
* Cached file sources, to prevent massive HDD work
80+
*
81+
* @private
82+
* @type {Object.<string, string>}
83+
*/
84+
fileSourceCache: {},
85+
86+
87+
/**
88+
* Loads the given file source as string
89+
*
90+
* @private
91+
* @param {string} name
92+
* @returns {string}
93+
*/
94+
loadFileSource: function (name) {
95+
return this.fileSourceCache[name] = this.fileSourceCache[name] || fs.readFileSync(__dirname + "/../../components/prism-" + name + ".js", "utf8");
96+
},
97+
98+
99+
/**
100+
* Runs a VM for a given file source with the given context
101+
*
102+
* @private
103+
* @param {string} fileSource
104+
* @param {*} [context]
105+
*
106+
* @returns {*}
107+
*/
108+
runFileWithContext: function (fileSource, context) {
109+
context = context || {};
110+
vm.runInNewContext(fileSource, context);
111+
return context;
112+
}
113+
};

0 commit comments

Comments
 (0)