Many debug improvements. Close #52.

This commit is contained in:
2019-05-13 14:35:11 -06:00
parent df5651d27a
commit 87483d2d72
14 changed files with 131 additions and 47 deletions

View File

@@ -2,4 +2,4 @@
-lib utest
-debug
-main tests.ExamplesTestMain
-js js
-js index.js

15
hank/DebugMacros.hx Normal file
View File

@@ -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)';
}
}
}

View File

@@ -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<String>): Array<String> {
static function recursiveLoop(directory:String, ?files: Array<String>): Array<String> {
if (files == null) files = [];
if (sys.FileSystem.exists(directory)) {
trace("directory found: " + directory);
for (file in sys.FileSystem.readDirectory(directory)) {

View File

@@ -4,6 +4,8 @@ using StringTools;
import haxe.ds.Option;
typedef PreloadedFiles = Map<String, String>;
/**
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<String> {
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);

View File

@@ -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);
}
}
}

View File

@@ -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:

View File

@@ -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) {

View File

@@ -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<String, Array<String>> = 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);

View File

@@ -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:

View File

@@ -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
)]);
}
}