diff --git a/examples-debug.hxml b/examples-debug.hxml index 88e646d..1afe4a7 100644 --- a/examples-debug.hxml +++ b/examples-debug.hxml @@ -2,4 +2,4 @@ -lib utest -debug -main tests.ExamplesTestMain --js js +-js index.js diff --git a/examples/TheIntercept/main.hank b/examples/_TheIntercept/main.hank similarity index 100% rename from examples/TheIntercept/main.hank rename to examples/_TheIntercept/main.hank diff --git a/examples/TheIntercept/original.ink b/examples/_TheIntercept/original.ink similarity index 100% rename from examples/TheIntercept/original.ink rename to examples/_TheIntercept/original.ink diff --git a/examples/TheIntercept/partialdebugtest1.hlog b/examples/_TheIntercept/partialdebugtest1.hlog similarity index 100% rename from examples/TheIntercept/partialdebugtest1.hlog rename to examples/_TheIntercept/partialdebugtest1.hlog diff --git a/examples/TheIntercept/partialtest1.hlog b/examples/_TheIntercept/partialtest1.hlog similarity index 100% rename from examples/TheIntercept/partialtest1.hlog rename to examples/_TheIntercept/partialtest1.hlog diff --git a/hank/DebugMacros.hx b/hank/DebugMacros.hx new file mode 100644 index 0000000..6ab72c9 --- /dev/null +++ b/hank/DebugMacros.hx @@ -0,0 +1,15 @@ +package hank; + +import haxe.macro.Expr; +import haxe.macro.Context; + +class DebugMacros { + public static macro function watch(e: Expr): Expr { + switch (e.expr) { + case EConst(CIdent(i)): + return macro trace('$i:' + $e); + default: + throw 'Can only watch variables (for now)'; + } + } +} \ No newline at end of file diff --git a/hank/FileLoadingMacro.hx b/hank/FileLoadingMacro.hx index 430f4d3..b33cbbc 100644 --- a/hank/FileLoadingMacro.hx +++ b/hank/FileLoadingMacro.hx @@ -16,7 +16,7 @@ class FileLoadingMacro { if (file.endsWith("/")) { files.remove(file); - files = files.concat(recursiveLoop(file, [])); + files = files.concat(recursiveLoop(file)); } else { ++i; } @@ -48,7 +48,8 @@ class FileLoadingMacro { } // this function is nabbed from https://code.haxe.org/category/beginner/using-filesystem.html - static function recursiveLoop(directory:String, files: Array): Array { + static function recursiveLoop(directory:String, ?files: Array): Array { + if (files == null) files = []; if (sys.FileSystem.exists(directory)) { trace("directory found: " + directory); for (file in sys.FileSystem.readDirectory(directory)) { diff --git a/hank/HankBuffer.hx b/hank/HankBuffer.hx index 880f812..64321e6 100644 --- a/hank/HankBuffer.hx +++ b/hank/HankBuffer.hx @@ -4,6 +4,8 @@ using StringTools; import haxe.ds.Option; +typedef PreloadedFiles = Map; + /** A position in a HankBuffer, used for debugging. **/ @@ -83,14 +85,25 @@ class HankBuffer { return new HankBuffer('_', text, 1, 1); } - // TODO document why this is private -- to prevent the user from using sys at runtime - @:allow(hank.Parser, hank.FileLoadingMacro) - private static function FromFile(path: String) { + public static function FromFile(path: String, ?files: PreloadedFiles) { // Keep a raw buffer of the file for tracking accurate file positions +#if sys var rawBuffer = sys.io.File.getContent(path); +#else + if (files == null || !files.exists(path)) { + throw 'Tried to open file $path that was not pre-loaded'; + } + var rawBuffer = files[path]; +#end return new HankBuffer(path, rawBuffer); } + public function lines(): Array { + var lines = cleanBuffer.split('\n'); + drop(cleanBuffer); + return lines; + } + function stripComments(s: String, o: String, c: String, dc: Bool): String { while (s.indexOf(o) != -1) { var start = s.indexOf(o); diff --git a/hank/LogUtil.hx b/hank/LogUtil.hx index b8cf8d8..55fee52 100644 --- a/hank/LogUtil.hx +++ b/hank/LogUtil.hx @@ -8,18 +8,24 @@ class LogUtil { var linesFromFile = ''; for (item in stack) { switch (item) { - case FilePos(null, file, line): - if (file != lastFile) { - lastFile = file; - trace(linesFromFile); - linesFromFile = '$file:$line'; - } else { - linesFromFile += ':$line'; - } + case FilePos(method, file, line): + var relevantPart = Std.string(if (method != null) method else line); + if (file != lastFile) { + lastFile = file; + trace(linesFromFile); + + linesFromFile = '$file:$relevantPart'; + } else { + linesFromFile += ':$relevantPart'; + } case other: - trace('Stack contains unsupported element: $other'); - trace(stack); + trace('Stack contains unsupported element: $other'); + trace(stack); } } + if (linesFromFile.length > 0) { + trace(linesFromFile); + } } + } \ No newline at end of file diff --git a/hank/Parser.hx b/hank/Parser.hx index 3bef1d1..47b2817 100644 --- a/hank/Parser.hx +++ b/hank/Parser.hx @@ -4,6 +4,7 @@ using StringTools; using Extensions.Extensions; import hank.HankAST.ExprType; import hank.Choice.Choice; +import hank.HankBuffer; /** Parses Hank scripts into ASTs for a Story object to interpret. Additional parsing happens in Alt.hx and Output.hx @@ -55,7 +56,7 @@ class Parser { return parsedAST; } - public function parseFile(f: String, includedFile = false) : HankAST { + public function parseFile(f: String, ?files: PreloadedFiles, includedFile = false) : HankAST { var directory = ''; var lastSlashIdx = f.lastIndexOf('/'); if (lastSlashIdx != -1) { @@ -63,7 +64,7 @@ class Parser { f = f.substr(lastSlashIdx+1); } - buffers.insert(0, HankBuffer.FromFile(directory + f)); + buffers.insert(0, HankBuffer.FromFile(directory + f, files)); while (buffers.length > 0) { var position = buffers[0].position(); @@ -74,7 +75,7 @@ class Parser { var expr = parseExpr(buffers[0], position); switch(expr) { case EIncludeFile(file): - parseFile(directory + file, true); + parseFile(directory + file, files, true); case ENoOp: // Drop no-ops from the AST default: diff --git a/hank/Story.hx b/hank/Story.hx index 2e2e549..a8bb013 100644 --- a/hank/Story.hx +++ b/hank/Story.hx @@ -1,5 +1,6 @@ package hank; +import hank.HankBuffer; using StringTools; import haxe.ds.Option; @@ -69,19 +70,15 @@ class Story { return story; } - public static function FromFile(script: String, ?randomSeed: Int): Story { + public static function FromAST(script: String, ast: HankAST, ?randomSeed: Int): Story { var random = new Random(randomSeed); - - var parser = new Parser(); - var ast = parser.parseFile(script); - var storyTree = StoryNode.FromAST(ast); var nodeScopes = [storyTree]; var viewCounts = storyTree.createViewCounts(); var hInterface = new HInterface(storyTree, viewCounts); - var story = new Story(random, parser, ast, storyTree, nodeScopes, viewCounts, hInterface); + var story = new Story(random, new Parser(), ast, storyTree, nodeScopes, viewCounts, hInterface); hInterface.addVariable('story', story); story.runRootIncludedHaxe(script); @@ -89,6 +86,11 @@ class Story { return story; } + public static function FromFile(script: String, ?files: PreloadedFiles, ?randomSeed: Int): Story { + var parser = new Parser(); + var ast = parser.parseFile(script, files); + return Story.FromAST(script, ast, randomSeed); + } /* Go through each included file executing all Haxe embedded at root level */ private function runRootIncludedHaxe(rootFile: String) { diff --git a/hank/StoryTestCase.hx b/hank/StoryTestCase.hx index 281a8b7..ae5e2a4 100644 --- a/hank/StoryTestCase.hx +++ b/hank/StoryTestCase.hx @@ -3,6 +3,7 @@ package hank; using StringTools; import hank.Story.StoryFrame; import hank.LogUtil; +import hank.HankBuffer; import haxe.CallStack; import utest.Assert; @@ -10,47 +11,77 @@ import utest.Assert; class StoryTestCase extends utest.Test { var testsDirectory: String; + var files: PreloadedFiles = new Map(); - public function new(testsDirectory: String) { + public function new(testsDirectory: String, ?preloadedFiles: PreloadedFiles) { super(); this.testsDirectory = testsDirectory; + if (preloadedFiles != null) + this.files = preloadedFiles; } public function testAllExamples() { + var exampleTranscripts: Map> = new Map(); +#if sys var exampleFolders = sys.FileSystem.readDirectory(testsDirectory); - - // Iterate through every example in the examples folder for (folder in exampleFolders) { - trace('Running tests for example "${folder}"'); var files = sys.FileSystem.readDirectory('${testsDirectory}/${folder}'); + exampleTranscripts.set(folder, [for(file in files) if (file.endsWith('.hlog')) file]); + } +#else + for (file in files.keys()) { + var parts = file.split('/'); - for (file in files) { - if (file.endsWith('.hlog')) { + if (parts[0] != testsDirectory) { + continue; + } - var disabled = file.indexOf("disabled") != -1; - var debug = file.indexOf("debug") != -1; - var partial = file.indexOf("partial") != -1; - if (!disabled) { - trace(' Running ${file}'); - try { - validateAgainstTranscript('${testsDirectory}/${folder}/main.hank', '${testsDirectory}/${folder}/${file}', !partial, debug); - } catch (e: Dynamic) { - trace('Error testing $folder/$file at transcript line $lastTranscriptLine: $e'); - LogUtil.prettyPrintStack(CallStack.exceptionStack()); - Assert.fail(); - } + var folder = parts[1]; + if (!exampleTranscripts.exists(folder)) { + exampleTranscripts.set(folder, new Array()); + } + + if (parts[2].endsWith('.hlog')) { + exampleTranscripts[folder].push(parts[2]); + } + } +#end + + for (folder in exampleTranscripts.keys()) { + if (folder.startsWith('_')) { + trace('Skipping tests for example "${folder}"'); + continue; + } + trace('Running tests for example "${folder}"'); + + for (file in exampleTranscripts[folder]) { + var disabled = file.indexOf("disabled") != -1; + var debug = file.indexOf("debug") != -1; + var partial = file.indexOf("partial") != -1; + if (!disabled) { + trace(' Running ${file}'); +#if !sys + validateAgainstTranscript('${testsDirectory}/${folder}/main.hank', '${testsDirectory}/${folder}/${file}', !partial, debug); +#else + try { + validateAgainstTranscript('${testsDirectory}/${folder}/main.hank', '${testsDirectory}/${folder}/${file}', !partial, debug); + } catch (e: Dynamic) { + trace('Error testing $folder/$file at transcript line $lastTranscriptLine: $e'); + LogUtil.prettyPrintStack(CallStack.exceptionStack()); + Assert.fail(); } +#end } } } - } var lastTranscriptLine = 0; private function validateAgainstTranscript(storyFile: String, transcriptFile: String, fullTranscript: Bool = true, debug: Bool = false) { + var buffer = HankBuffer.FromFile(transcriptFile, files); + var transcriptLines = buffer.lines(); - var transcriptLines = sys.io.File.getContent(transcriptFile).split('\n'); // If the transcript starts with a random seed, make sure the story uses that seed var randomSeed = null; if (transcriptLines[0].startsWith('@')) { @@ -59,14 +90,20 @@ class StoryTestCase extends utest.Test { } var story: Story = null; + +// It's easier to debug exceptions on web if they travel up the stack +#if !sys + story = Story.FromFile(storyFile, files, randomSeed); +#else try { - story = Story.FromFile(storyFile, randomSeed); + story = Story.FromFile(storyFile, files, randomSeed); } catch (e: Dynamic) { trace('Error parsing $storyFile: $e'); LogUtil.prettyPrintStack(CallStack.exceptionStack()); Assert.fail(); return; } +#end story.hInterface.addVariable("DEBUG", debug); diff --git a/hank/StoryTree.hx b/hank/StoryTree.hx index 15ff5c5..a1361ad 100644 --- a/hank/StoryTree.hx +++ b/hank/StoryTree.hx @@ -1,6 +1,7 @@ package hank; import haxe.ds.Option; +import hank.DebugMacros.watch; @:allow(tests.StoryTreeTest) class StoryNode { @@ -70,10 +71,13 @@ class StoryNode { case EGather(Some(name), _, _) | EChoice({id: _, onceOnly: _, label: Some(name), condition: _, depth: _, output: _, divertTarget: _}): var node = new StoryNode(exprIndex); if (lastKnot == null) { + // watch(root); root.addChild(name, node); } else if (lastStitch == null) { + // watch(lastKnot); lastKnot.addChild(name, node); } else { + // watch(lastStitch); lastStitch.addChild(name, node); } default: diff --git a/tests/ExamplesTestMain.hx b/tests/ExamplesTestMain.hx index 3f9ced1..e28eb00 100644 --- a/tests/ExamplesTestMain.hx +++ b/tests/ExamplesTestMain.hx @@ -2,8 +2,13 @@ package tests; import utest.Test; import hank.StoryTestCase; +@:build(hank.FileLoadingMacro.build(["examples/"])) class ExamplesTestMain extends Test { public static function main() { - utest.UTest.run([new StoryTestCase("examples")]); + utest.UTest.run([new StoryTestCase("examples" +#if !sys + , files +#end + )]); } } \ No newline at end of file