diff --git a/README.md b/README.md index 0ee9be12..f5aa8668 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,8 @@ JSDoc. In rare cases, users may have their Java CLASSPATH configured to override the included Rhino and point to an older version of Rhino instead. If this is the -case, simply correct the CLASSPATH to remove the older Rhino. +case, simply correct the CLASSPATH to remove the older Rhino. (On OS X, you may +need to remove the file `~/Library/Java/Extensions/js.jar`.) The version of Rhino distributed with JSDoc 3 can be found here: https://github.com/hegemonic/rhino diff --git a/lib/jsdoc/tutorial.js b/lib/jsdoc/tutorial.js index 28f794db..81ef5b5b 100644 --- a/lib/jsdoc/tutorial.js +++ b/lib/jsdoc/tutorial.js @@ -6,6 +6,27 @@ var markdown = require('jsdoc/util/markdown'); +/** Removes child tutorial from the parent. Does *not* unset child.parent though. + @param {Tutorial} parent - parent tutorial. + @param {Tutorial} child - Old child. + @private + */ +function removeChild(parent, child) { + var index = parent.children.indexOf(child); + if (index != -1) { + parent.children.splice(index, 1); + } +} + +/** Adds a child to the parent tutorial. Does *not* set child.parent though. + @param {Tutorial} parent - parent tutorial. + @param {Tutorial} child - New child. + @private + */ +function addChild(parent, child) { + parent.children.push(child); +} + /** @module jsdoc/tutorial */ @@ -28,33 +49,32 @@ exports.Tutorial = function(name, content, type) { }; /** Moves children from current parent to different one. - @param {Tutorial} parent - New parent. + @param {?Tutorial} parent - New parent. If null, the tutorial has no parent. */ exports.Tutorial.prototype.setParent = function(parent) { // removes node from old parent if (this.parent) { - this.parent.removeChild(this); + removeChild(this.parent, this); } this.parent = parent; - this.parent.addChild(this); + if (parent) { + addChild(parent, this); + } }; /** Removes children from current node. @param {Tutorial} child - Old child. */ exports.Tutorial.prototype.removeChild = function(child) { - var index = this.children.indexOf(child); - if (index != -1) { - this.children.splice(index, 1); - } + child.setParent(null); }; /** Adds new children to current node. @param {Tutorial} child - New child. */ exports.Tutorial.prototype.addChild = function(child) { - this.children.push(child); + child.setParent(this); }; /** Prepares source. diff --git a/lib/jsdoc/tutorial/resolver.js b/lib/jsdoc/tutorial/resolver.js index 1cb7884d..0a54bbb2 100644 --- a/lib/jsdoc/tutorial/resolver.js +++ b/lib/jsdoc/tutorial/resolver.js @@ -11,20 +11,80 @@ var tutorial = require('jsdoc/tutorial'), fs = require('jsdoc/fs'), + error = require('jsdoc/util/error'), path = require('path'), hasOwnProp = Object.prototype.hasOwnProperty, conf = {}, tutorials = {}, finder = /^(.*)\.(x(?:ht)?ml|html?|md|markdown|json)$/i; +/** checks if `conf` is the metadata for a single tutorial. + * A tutorial's metadata has a property 'title' and/or a property 'children'. + * @param {object} json - the object we want to test (typically from JSON.parse) + * @returns {boolean} whether `json` could be the metadata for a tutorial. + */ +function isTutorialJSON(json) { + // if conf.title exists or conf.children exists, it is metadata for a tutorial + return (json.hasOwnProperty('title') || json.hasOwnProperty('children')); +} + +/** Helper function that adds tutorial configuration to the `conf` variable. + * This helps when multiple tutorial configurations are specified in one object, + * or when a tutorial's children are specified as tutorial configurations as + * opposed to an array of tutorial names. + * + * Recurses as necessary to ensure all tutorials are added. + * + * @param {string} name - if `meta` is a configuration for a single tutorial, + * this is that tutorial's name. + * @param {object} meta - object that contains tutorial information. + * Can either be for a single tutorial, or for multiple + * (where each key in `meta` is the tutorial name and each + * value is the information for a single tutorial). + * Additionally, a tutorial's 'children' property may + * either be an array of strings (names of the child tutorials), + * OR an object giving the configuration for the child tutorials. + */ +function addTutorialConf(name, meta) { + var names, i; + if (isTutorialJSON(meta)) { + // if the children are themselves tutorial defintions as opposed to an + // array of strings, add each child. + if (meta.hasOwnProperty('children') && !Array.isArray(meta.children)) { + names = Object.keys(meta.children); + for (i = 0; i < names.length; ++i) { + addTutorialConf(names[i], meta.children[names[i]]); + } + // replace with an array of names. + meta.children = names; + } + // check if the tutorial has already been defined... + if (conf.hasOwnProperty(name)) { + error.handle(new Error("Tutorial " + name + "'s metadata is defined multiple times, only the first will be used.")); + } else { + conf[name] = meta; + } + } else { + // it's an object of tutorials, the keys are th etutorial names. + names = Object.keys(meta); + for (i = 0; i < names.length; ++i) { + addTutorialConf(names[i], meta[names[i]]); + } + } +} + /** Adds new tutorial. @param {tutorial.Tutorial} current - New tutorial. */ exports.addTutorial = function(current) { - tutorials[current.name] = current; + if (tutorials.hasOwnProperty(current.name)) { + error.handle(new Error("Tutorial with name " + current.name + " exists more than once, not adding (same name, different file extensions?)")); + } else { + tutorials[current.name] = current; - // default temporary parent - current.setParent(exports.root); + // default temporary parent + current.setParent(exports.root); + } }; /** Root tutorial. @@ -77,7 +137,8 @@ exports.load = function(_path) { // configuration file case 'json': - conf[name] = JSON.parse(content); + var meta = JSON.parse(content); + addTutorialConf(name, meta); // don't add this as a tutorial return; @@ -117,7 +178,7 @@ exports.resolve = function() { if (item.children) { item.children.forEach(function(child) { if (!(child in tutorials)) { - require('jsdoc/util/error').handle( new Error("Missing child tutorial: " + child) ); + error.handle( new Error("Missing child tutorial: " + child) ); } else { tutorials[child].setParent(current); diff --git a/test/specs/jsdoc/tutorial.js b/test/specs/jsdoc/tutorial.js index 9330fa89..1644c534 100644 --- a/test/specs/jsdoc/tutorial.js +++ b/test/specs/jsdoc/tutorial.js @@ -1,4 +1,226 @@ /*global describe: true, env: true, it: true */ describe("jsdoc/tutorial", function() { - //TODO -}); \ No newline at end of file + var tutorial = require('jsdoc/tutorial'), + name = "tuteID", + content = "Tutorial content blah blah blah & <", + tute = new tutorial.Tutorial(name, content, tutorial.TYPES.NOTAVALUE), + par = new tutorial.Tutorial('parent', "# This is the parent tutorial's content & stuff A_B X_Y", + tutorial.TYPES.MARKDOWN), + par2 = new tutorial.Tutorial('parent2', "

This is the second parent tutorial

", + tutorial.TYPES.HTML); + + + it("module should exist", function() { + expect(tutorial).toBeDefined(); + expect(typeof tutorial).toBe("object"); + }); + + it("should export a Tutorial function", function() { + expect(tutorial.Tutorial).toBeDefined(); + expect(typeof tutorial.Tutorial).toBe("function"); + }); + + it("should export a TYPES object", function() { + expect(tutorial.TYPES).toBeDefined(); + expect(typeof tutorial.TYPES).toBe("object"); + }); + + describe("tutorial.TYPES", function() { + it("should have a HTML property", function() { + expect(tutorial.TYPES.HTML).toBeDefined(); + }); + + it("should have a MARKDOWN property", function() { + expect(tutorial.TYPES.MARKDOWN).toBeDefined(); + }); + }); + + describe("Tutorial", function() { + it("should have 'setParent' function", function() { + expect(tutorial.Tutorial.prototype.setParent).toBeDefined(); + expect(typeof tutorial.Tutorial.prototype.setParent).toBe("function"); + }); + + it("should have 'removeChild' function", function() { + expect(tutorial.Tutorial.prototype.removeChild).toBeDefined(); + expect(typeof tutorial.Tutorial.prototype.removeChild).toBe("function"); + }); + + it("should have 'addChild' function", function() { + expect(tutorial.Tutorial.prototype.addChild).toBeDefined(); + expect(typeof tutorial.Tutorial.prototype.addChild).toBe("function"); + }); + + it("should have 'parse' function", function() { + expect(tutorial.Tutorial.prototype.parse).toBeDefined(); + expect(typeof tutorial.Tutorial.prototype.parse).toBe("function"); + }); + + it("should have a 'name' property", function() { + expect(tute.name).toBeDefined(); + expect(typeof tute.name).toBe("string"); + expect(tute.name).toBe(name); + }); + + it("should have a 'title' property, by default set to to the tute's name", function() { + expect(tute.title).toBeDefined(); + expect(typeof tute.title).toBe("string"); + expect(tute.title).toBe(name); + // Testing of overriding a tutorial's title in its JSON file is + // covered in tutorial/resolver.js tests. + }); + + it("should have a 'content' property set to the tutorial's content", function() { + expect(tute.content).toBeDefined(); + expect(typeof tute.content).toBe("string"); + expect(tute.content).toBe(content); + }); + + it("should have a 'type' property set to the tutorial's type", function() { + expect(par.type).toBeDefined(); + expect(typeof par.type).toBe(typeof tutorial.TYPES.MARKDOWN); + expect(par.type).toBe(tutorial.TYPES.MARKDOWN); + }); + + it("should have a 'parent' property, initially null", function() { + expect(tute.parent).toBeDefined(); + expect(tute.parent).toBe(null); + }); + + it("should have a 'children' property, an empty array", function() { + expect(tute.children).toBeDefined(); + expect(Array.isArray(tute.children)).toBe(true); + expect(tute.children.length).toBe(0); + }); + + describe("setParent", function() { + it("adding a parent sets the child's 'parent' property", function() { + tute.setParent(par); + expect(tute.parent).toBe(par); + }); + + it("adding a parent adds the child to the parent's 'children' property", function() { + expect(par.children).toContain(tute); + }); + + it("re-parenting removes the child from the previous parent", function() { + tute.setParent(par2); + + expect(tute.parent).toBe(par2); + expect(par2.children).toContain(tute); + expect(par.children).not.toContain(tute); + }); + + it("calling setParent with a null parent unsets the child's parent and removes the child from its previous parent", function() { + expect(par2.children).toContain(tute); + tute.setParent(null); + + expect(tute.parent).toBe(null); + expect(par2.children).not.toContain(tute); + }); + }); + + describe("addChild", function() { + it("adding a child tutorial adds the child to the parent's 'children' property", function() { + tute.setParent(null); + var n = par.children.length; + + par.addChild(tute); + + expect(par.children.length).toBe(n + 1); + expect(par.children).toContain(tute); + }); + + it("adding a child tutorial sets the child's parent to to the parent tutorial", function() { + expect(tute.parent).toBe(par); + }); + + it("adding a child tutorial removes the child from its old parent", function() { + // tue is currently owned by par; we reparent it to par2 + expect(tute.parent).toBe(par); + par2.addChild(tute); + + expect(tute.parent).toBe(par2); + expect(par.children).not.toContain(tute); + expect(par2.children).toContain(tute); + }); + }); + + describe("removeChild", function() { + function removeChild() { + par2.removeChild(par); + } + + it("removing a tutorial that is not a child silently passes", function() { + var n = par2.children.length; + expect(removeChild).not.toThrow(); + expect(par2.children.length).toBe(n); + }); + + it("removing a child removes the child from the parent's 'children' property", function() { + tute.setParent(par2); + expect(par2.children.length).toBe(1); + + par2.removeChild(tute); + + expect(par2.children).not.toContain(tute); + expect(par2.children.length).toBe(0); + }); + + it("removing a child unsets the child's 'parent' property", function() { + expect(tute.parent).toBe(null); + }); + }); + + describe("various inheritance tests with addChild, setParent and removeChild", function() { + it("parenting and unparenting via addChild, setParent and removeChild makes sure inheritance is set accordingly", function() { + // unparent everything. + tute.setParent(null); + par.setParent(null); + par2.setParent(null); + + // let tute belong to par + tute.setParent(par); + expect(tute.parent).toBe(par); + expect(par2.children.length).toBe(0); + expect(par.children.length).toBe(1); + expect(par.children[0]).toBe(tute); + + // addChild tute to par2. its parent should now be par2, and + // it can't be the child of two parents + par2.addChild(tute); + expect(tute.parent).toBe(par2); + expect(par.children.length).toBe(0); + expect(par2.children.length).toBe(1); + expect(par2.children[0]).toBe(tute); + + // removeChild tute from par2. tute should now be unparented. + par2.removeChild(tute); + expect(tute.parent).toBe(null); + expect(par.children.length).toBe(0); + expect(par2.children.length).toBe(0); + }); + }); + + describe("parse", function() { + it("Tutorials with HTML type return content as-is", function() { + expect(par2.parse()).toBe("

This is the second parent tutorial

"); + }); + + it("Tutorials with MARKDOWN type go through the markdown parser, respecting configuration options", function() { + var old = env.conf.markdown; + env.conf.markdown = {parser: 'evilstreak'}; + expect(par.parse()).toBe("

This is the parent tutorial's content & stuff AB XY

"); + + env.conf.markdown = {parser: 'gfm'}; + expect(par.parse()).toBe("

This is the parent tutorial's content & stuff A_B X_Y

"); + + env.conf.markdown = old; + }); + + it("Tutorials with unrecognised type are returned as-is", function() { + expect(tute.parse()).toBe(content); + }); + }); + }); +}); diff --git a/test/specs/jsdoc/tutorial/resolver.js b/test/specs/jsdoc/tutorial/resolver.js index 0322ad4d..5d1260b0 100644 --- a/test/specs/jsdoc/tutorial/resolver.js +++ b/test/specs/jsdoc/tutorial/resolver.js @@ -1,33 +1,214 @@ /*global afterEach: true, describe: true, env: true, expect: true, it: true */ describe("jsdoc/tutorial/resolver", function() { - /*jshint evil: true */ - - // TODO: more tests - var resolver = require('jsdoc/tutorial/resolver'), + tutorial = require('jsdoc/tutorial'), lenient = !!env.opts.lenient, log = eval(console.log); - function missingTutorial() { - resolver.load(__dirname + "/test/tutorials/incomplete"); + /*jshint evil: true */ + it("should exist", function() { + expect(resolver).toBeDefined(); + expect(typeof resolver).toEqual('object'); + }); + + it("should export a 'addTutorial' function", function() { + expect(resolver.addTutorial).toBeDefined(); + expect(typeof resolver.addTutorial).toEqual("function"); + }); + + it("should export a 'load' function", function() { + expect(resolver.load).toBeDefined(); + expect(typeof resolver.load).toEqual("function"); + }); + + it("should export a 'resolve' function", function() { + expect(resolver.resolve).toBeDefined(); + expect(typeof resolver.resolve).toEqual("function"); + }); + + it("should export a 'root' tutorial", function() { + expect(resolver.root).toBeDefined(); + expect(resolver.root instanceof tutorial.Tutorial).toEqual(true); + }); + + it("exported 'root' tutorial should export a 'getByName' function", function() { + expect(resolver.root.getByName).toBeDefined(); + expect(typeof resolver.root.getByName).toEqual("function"); + }); + + // note: every time we addTutorial or run the resolver, we are *adding* + // to the root tutorial. + + // addTutorial + var tute = new tutorial.Tutorial('myTutorial', '', tutorial.TYPES.HTML); + resolver.addTutorial(tute); + describe("addTutorial", function() { + + it("should add a default parent of the root tutorial", function() { + expect(tute.parent).toEqual(resolver.root); + }); + + it("should be added to the root tutorial as a child", function() { + expect(resolver.root.children[0]).toEqual(tute); + }); + }); + + // root.getByName + describe("root.getByName", function() { + it("can retrieve tutorials by name", function() { + expect(resolver.root.getByName('myTutorial')).toEqual(tute); + }); + }); + + // load + resolver.load(__dirname + "/test/tutorials/tutorials"); + var childNames = resolver.root.children.map(function (t) { return t.name; }), + test = resolver.root.getByName('test'), + test2 = resolver.root.getByName('test2'), + test3 = resolver.root.getByName('test3'), + test4 = resolver.root.getByName('test4'); + test6 = resolver.root.getByName('test6'); + + describe("load", function() { + + it("all tutorials are added, initially as top-level tutorials", function() { + // check they were added + expect(test).toBeDefined(); + expect(test2).toBeDefined(); + expect(test3).toBeDefined(); + expect(test4).toBeDefined(); + expect(test6).toBeDefined(); + // check they are top-level in resolver.root + expect(childNames.indexOf('test')).not.toEqual(-1); + expect(childNames.indexOf('test2')).not.toEqual(-1); + expect(childNames.indexOf('test3')).not.toEqual(-1); + expect(childNames.indexOf('test4')).not.toEqual(-1); + expect(childNames.indexOf('test6')).not.toEqual(-1); + }); + + it("non-tutorials are skipped", function() { + expect(resolver.root.getByName('multple')).toBeUndefined(); + expect(resolver.root.getByName('test5')).toBeUndefined(); + }); + + + it("tutorial types are determined correctly", function() { + // test.html, test2.markdown, test3.html, test4.md, test6.xml + expect(test.type).toEqual(tutorial.TYPES.HTML); + expect(test2.type).toEqual(tutorial.TYPES.MARKDOWN); + expect(test3.type).toEqual(tutorial.TYPES.HTML); + expect(test4.type).toEqual(tutorial.TYPES.MARKDOWN); + expect(test6.type).toEqual(tutorial.TYPES.HTML); + }); + + }); + + // resolve + // myTutorial + // test + // |- test2 + // |- test6 + // |- test3 + // |- test4 + describe("resolve", function() { resolver.resolve(); - } + it("hierarchy is resolved properly no matter how the children property is defined", function() { + // root has child 'test' + expect(resolver.root.children.length).toEqual(2); + expect(resolver.root.children.indexOf(test)).not.toEqual(-1); + expect(test.parent).toEqual(resolver.root); - afterEach(function() { - env.opts.lenient = lenient; - console.log = log; + // test has child 'test2' + expect(test.children.length).toEqual(1); + expect(test.children[0]).toEqual(test2); + expect(test2.parent).toEqual(test); + + // test2 has children test3, test6 + expect(test2.children.length).toEqual(2); + expect(test2.children.indexOf(test3)).not.toEqual(-1); + expect(test2.children.indexOf(test6)).not.toEqual(-1); + expect(test3.parent).toEqual(test2); + expect(test6.parent).toEqual(test2); + + // test3 has child test4 + expect(test3.children.length).toEqual(1); + expect(test3.children[0]).toEqual(test4); + expect(test4.parent).toEqual(test3); + }); + + it("tutorials without configuration files have titles matching filenames", function() { + // test6.xml didn't have a metadata + expect(test6.title).toEqual('test6'); + }); + + it("tutorials with configuration files have titles matching filenames", function() { + // test.json had info for just test.json + expect(test.title).toEqual("Test tutorial"); + }); + + it("multiple tutorials can appear in a configuration file", function() { + expect(test2.title).toEqual("Test 2"); + expect(test3.title).toEqual("Test 3"); + expect(test4.title).toEqual("Test 4"); + }); }); - it("throws an exception for missing tutorials if the lenient option is not enabled", function() { - env.opts.lenient = false; + // error reporting. + describe("Error reporting", function() { + // Tests for error reporting. + function missingTutorial() { + resolver.load(__dirname + "/test/tutorials/incomplete"); + resolver.resolve(); + } + function duplicateNamedTutorials() { + // can't add a tutorial if another with its name has already been added + resolver.addTutorial(tute); + } + function duplicateDefinedTutorials() { + // can't have a tutorial's metadata defined twice in .json files + resolver.load(__dirname + "/test/tutorials/duplicateDefined"); + resolver.resolve(); + } - expect(missingTutorial).toThrow(); + afterEach(function() { + env.opts.lenient = lenient; + console.log = log; + }); + + it("throws an exception for missing tutorials if the lenient option is not enabled", function() { + env.opts.lenient = false; + + expect(missingTutorial).toThrow(); + }); + + it("doesn't throw an exception for missing tutorials if the lenient option is enabled", function() { + console.log = function() {}; + env.opts.lenient = true; + + expect(missingTutorial).not.toThrow(); + }); + + it("throws an exception for duplicate-named tutorials (e.g. test.md, test.html) if the lenient option is not enabled", function() { + env.opts.lenient = false; + expect(duplicateNamedTutorials).toThrow(); + }); + + it("doesn't throw an exception for duplicate-named tutorials (e.g. test.md, test.html) if the lenient option is not enabled", function() { + console.log = function() {}; + env.opts.lenient = true; + expect(duplicateNamedTutorials).not.toThrow(); + }); + + it("throws an exception for tutorials defined twice in .jsons if the lenient option is not enabled", function() { + env.opts.lenient = false; + expect(duplicateDefinedTutorials).toThrow(); + }); + + it("doesn't throw an exception for tutorials defined twice in .jsons if the lenient option is not enabled", function() { + console.log = function() {}; + env.opts.lenient = true; + expect(duplicateDefinedTutorials).not.toThrow(); + }); }); - it("doesn't throw an exception for missing tutorials if the lenient option is enabled", function() { - console.log = function() {}; - env.opts.lenient = true; - - expect(missingTutorial).not.toThrow(); - }); -}); \ No newline at end of file +}); diff --git a/test/tutorials/duplicateDefined/asdf.html b/test/tutorials/duplicateDefined/asdf.html new file mode 100644 index 00000000..1f8a6622 --- /dev/null +++ b/test/tutorials/duplicateDefined/asdf.html @@ -0,0 +1 @@ +

tutorial ASDF

diff --git a/test/tutorials/duplicateDefined/asdf.json b/test/tutorials/duplicateDefined/asdf.json new file mode 100644 index 00000000..c769dc54 --- /dev/null +++ b/test/tutorials/duplicateDefined/asdf.json @@ -0,0 +1 @@ +{"title": "Conflicting title"} diff --git a/test/tutorials/duplicateDefined/index.json b/test/tutorials/duplicateDefined/index.json new file mode 100644 index 00000000..9ceb182f --- /dev/null +++ b/test/tutorials/duplicateDefined/index.json @@ -0,0 +1,5 @@ +{ + "asdf": { + "title": "Tutorial Asdf" + } +} diff --git a/test/tutorials/incomplete/test.html b/test/tutorials/incomplete/parent.html similarity index 100% rename from test/tutorials/incomplete/test.html rename to test/tutorials/incomplete/parent.html diff --git a/test/tutorials/incomplete/parent.json b/test/tutorials/incomplete/parent.json new file mode 100644 index 00000000..f2e4765a --- /dev/null +++ b/test/tutorials/incomplete/parent.json @@ -0,0 +1 @@ +{"title": "missing child tutorial", "children": ["child"]} diff --git a/test/tutorials/incomplete/test.json b/test/tutorials/incomplete/test.json deleted file mode 100644 index 078dc3f5..00000000 --- a/test/tutorials/incomplete/test.json +++ /dev/null @@ -1 +0,0 @@ -{"title": "missing child tutorial", "children": ["test2"]} diff --git a/test/tutorials/tutorials/multiple.json b/test/tutorials/tutorials/multiple.json new file mode 100644 index 00000000..994d8655 --- /dev/null +++ b/test/tutorials/tutorials/multiple.json @@ -0,0 +1,12 @@ +{ + "test2": { + "title": "Test 2", + "children": ["test3", "test6"] + }, + "test3": { + "title": "Test 3", + "children": { + "test4": {"title": "Test 4"} + } + } +} diff --git a/test/tutorials/tutorials/test2.json b/test/tutorials/tutorials/test2.json deleted file mode 100644 index 3c7d98c5..00000000 --- a/test/tutorials/tutorials/test2.json +++ /dev/null @@ -1 +0,0 @@ -{"title": "Test 2"} diff --git a/test/tutorials/tutorials/test3.htm b/test/tutorials/tutorials/test3.htm new file mode 100644 index 00000000..8eac0524 --- /dev/null +++ b/test/tutorials/tutorials/test3.htm @@ -0,0 +1,3 @@ +

Test3.html

+ +

{@link Test}

diff --git a/test/tutorials/tutorials/test4.md b/test/tutorials/tutorials/test4.md new file mode 100644 index 00000000..0be836ad --- /dev/null +++ b/test/tutorials/tutorials/test4.md @@ -0,0 +1 @@ +# test4.md diff --git a/test/tutorials/tutorials/test5.txt b/test/tutorials/tutorials/test5.txt new file mode 100644 index 00000000..1769f0de --- /dev/null +++ b/test/tutorials/tutorials/test5.txt @@ -0,0 +1 @@ +Should not be included as a tutorial. diff --git a/test/tutorials/tutorials/test6.xml b/test/tutorials/tutorials/test6.xml new file mode 100644 index 00000000..1c489be3 --- /dev/null +++ b/test/tutorials/tutorials/test6.xml @@ -0,0 +1 @@ +

test 6 - has no metadata