Browse Source

Setup tests for source maps (#6914)

* setup tests for sourcemaps

* cleanup

* removed FailExample
Alexander Kuzmenko 7 years ago
parent
commit
e3568bea9c

+ 4 - 1
.gitignore

@@ -112,4 +112,7 @@ tests/server/test.js
 tests/unit/pypy3-*
 tests/unit/pypy3-*
 tmp.tmp
 tmp.tmp
 
 
-dev-display.hxml
+dev-display.hxml
+
+.DS_Store
+tests/sourcemaps/bin

+ 1 - 0
tests/runci/Config.hx

@@ -17,6 +17,7 @@ class Config {
 	static public final miscDir = cwd + "misc/";
 	static public final miscDir = cwd + "misc/";
 	static public final displayDir = cwd + "display/";
 	static public final displayDir = cwd + "display/";
 	static public final serverDir = cwd + "server/";
 	static public final serverDir = cwd + "server/";
+	static public final sourcemapsDir = cwd + "sourcemaps/";
 
 
 	static public final ci:Null<Ci> =
 	static public final ci:Null<Ci> =
 		if (Sys.getEnv("TRAVIS") == "true")
 		if (Sys.getEnv("TRAVIS") == "true")

+ 3 - 0
tests/runci/targets/Macro.hx

@@ -13,6 +13,9 @@ class Macro {
 		changeDirectory(displayDir);
 		changeDirectory(displayDir);
 		runCommand("haxe", ["build.hxml"]);
 		runCommand("haxe", ["build.hxml"]);
 
 
+		changeDirectory(sourcemapsDir);
+		runCommand("haxe", ["run.hxml"]);
+
 		changeDirectory(miscDir);
 		changeDirectory(miscDir);
 		getCsDependencies();
 		getCsDependencies();
 		getPythonDependencies();
 		getPythonDependencies();

+ 4 - 0
tests/sourcemaps/run.hxml

@@ -0,0 +1,4 @@
+-cp src
+-D eval-stack
+-main Test
+--interp

+ 87 - 0
tests/sourcemaps/src/Test.hx

@@ -0,0 +1,87 @@
+import haxe.Json;
+import validation.ValidationReport;
+import haxe.io.Path;
+import sys.io.Process;
+import validation.Target;
+
+using sys.FileSystem;
+using StringTools;
+using Test;
+using Lambda;
+
+class Test {
+	static public function main() {
+		installDependencies();
+
+		var success = true;
+		for(target in [Js, Php]) {
+			Sys.println('[Testing ${target.getName()}]');
+			collectCases().iter(module -> success = runCase(module, target) && success);
+		}
+
+		Sys.exit(success ? 0 : 1);
+	}
+
+	static function runCase(module:String, target:Target):Bool {
+		var proc = new Process(
+			'haxe',
+			targetCompilerFlags(target).concat([
+				'-cp', 'src',
+				'-lib', 'sourcemap',
+				'-D', 'source-map',
+				'-D', 'analyzer-optimize',
+				'-dce', 'full',
+				'-main', module
+			])
+		);
+		var stdErr = proc.stderr.readAll().toString();
+		var stdOut = proc.stdout.readAll().toString();
+		if(stdErr.trim().length > 0) {
+			Sys.println(stdErr.trim());
+		}
+
+		var report:ValidationReport = try {
+			Json.parse(stdOut);
+		} catch(e:Dynamic) {
+			Sys.println('$module: Failed to parse compilation result:\n$stdOut');
+			return false;
+		}
+
+		Sys.println('$module: ${report.summary.assertions} tests, ${report.summary.failures} failures');
+
+		if(!report.success) {
+			for(error in report.errors) {
+				Sys.println('${error.pos.file}:${error.pos.line}:${error.pos.column}: Failed to find expected code: ${error.expected}');
+			}
+		}
+
+		return report.success;
+	}
+
+	static function targetCompilerFlags(target:Target):Array<String> {
+		return switch(target) {
+			case Js: ['-js', 'bin/test.js'];
+			case Php: ['-php', 'bin/php'];
+		}
+	}
+
+	static function collectCases():Array<String> {
+		var result = [];
+		for(fileName in 'src/cases'.readDirectory()) {
+			var path = new Path(fileName);
+			if(path.isTestCase()) {
+				result.push('cases.${path.file}');
+			}
+		}
+		return result;
+	}
+
+	static function isTestCase(path:Path):Bool {
+		var firstChar = path.file.charAt(0);
+		return path.ext == 'hx' && firstChar == firstChar.toUpperCase();
+	}
+
+	static function installDependencies() {
+		Sys.command('haxelib', ['install', 'sourcemap']);
+	}
+}

+ 163 - 0
tests/sourcemaps/src/Validator.hx

@@ -0,0 +1,163 @@
+#if macro
+import validation.Target;
+import validation.Lines;
+import validation.ValidationReport;
+import validation.ValidationError;
+import validation.Exception;
+import haxe.display.Position.Location;
+import haxe.macro.Context;
+import haxe.macro.Expr;
+import haxe.macro.Compiler;
+import haxe.io.Path;
+
+using sys.io.File;
+using haxe.macro.Tools;
+using StringTools;
+#end
+
+class Validator {
+	macro static public function expect(hxExpr:Expr, output:Expr):Expr {
+		var outMap = try {
+			parseExpectedOutput(output);
+		} catch(e:ExpectedOutputException) {
+			Context.error(e.msg, e.pos);
+			return macro {};
+		}
+
+		expected.push({pos:hxExpr.pos, out:outMap});
+
+		if(!onAfterGenerateAdded) {
+			onAfterGenerateAdded = true;
+			Context.onAfterGenerate(verifyExpectations);
+		}
+
+		return hxExpr;
+	}
+
+#if macro
+	static var onAfterGenerateAdded = false;
+	static var expected = new Array<{pos:Position, out:Map<Target,String>}>();
+	static var sourceMapsCache = new Map<String,SourceMap>();
+
+	static function verifyExpectations() {
+		var target = try {
+			getCurrentTarget();
+		} catch(e:InvalidTargetException) {
+			Context.error(e.msg, e.pos);
+			return;
+		}
+
+		var errors = [];
+
+		for(expectation in expected) {
+			var expectedCode = expectation.out.get(target);
+			var location = expectation.pos.toLocation();
+			var hxPos = location.range.start;
+			var generated = getGeneratedContent(target, location);
+			var found = false;
+			//look for the mappings generated from the `hxPos`
+			generated.map.eachMapping(genPos -> {
+				if(found) return;
+				if(genPos.originalLine == hxPos.line && genPos.originalColumn + 1 == hxPos.character) {
+					var code = generated.code[genPos.generatedLine].substr(genPos.generatedColumn);
+					//compare with expected output ignoring leading whitespaces
+					if(code.trim().startsWith(expectedCode)) {
+						found = true;
+					}
+				}
+			});
+			if(!found) {
+				errors.push({
+					expected: expectedCode,
+					pos: {
+						file: location.file,
+						line: hxPos.line,
+						column: hxPos.character
+					}
+				});
+			}
+		}
+
+		Sys.println(haxe.Json.stringify(buildValidationReport(errors), '    '));
+	}
+
+	static function buildValidationReport(errors:Array<ValidationError>):ValidationReport {
+		return {
+			success: errors.length == 0,
+			summary: {
+				assertions: expected.length,
+				failures: errors.length
+			},
+			errors: errors
+		}
+	}
+
+	static function getCurrentTarget():Target {
+		return if(Context.defined('js')) {
+			Js;
+		} else if(Context.defined('php')) {
+			Php;
+		} else {
+			throw new InvalidTargetException('Current target is not supported', haxe.macro.PositionTools.here());
+		}
+	}
+
+	static function getGeneratedContent(target:Target, location:Location):{code:Lines, map:SourceMap} {
+		var sourceFile;
+		var mapFile;
+		switch(target) {
+			case Js:
+				sourceFile = Compiler.getOutput();
+				mapFile = '${Compiler.getOutput()}.map';
+			case Php:
+				var phpFilePath = new Path(location.file.substr(location.file.indexOf('cases')));
+				phpFilePath.ext = 'php';
+				sourceFile = Path.join([Compiler.getOutput(), 'lib', phpFilePath.toString()]);
+				mapFile = '$sourceFile.map';
+		}
+
+		return {
+			code: sourceFile.getContent().split('\n'),
+			map: getSourceMap(mapFile)
+		}
+	}
+
+	static function getSourceMap(mapFile:String):SourceMap {
+		var sourceMap = sourceMapsCache.get(mapFile);
+		if(sourceMap == null) {
+			sourceMap = new SourceMap(mapFile.getContent());
+			sourceMapsCache.set(mapFile, sourceMap);
+		}
+		return sourceMap;
+	}
+
+	static function parseExpectedOutput(output:Expr):Null<Map<Target,String>> {
+		var result = new Map();
+		switch(output) {
+			case macro $a{exprs}:
+				var expectedTargets = Target.getConstructors();
+				for(expr in exprs) {
+					switch(expr) {
+						case macro $target => ${{expr:EConst(CString(outStr))}}:
+							var target = switch(target) {
+								case macro Js: Js;
+								case macro Php: Php;
+								case _: throw new ExpectedOutputException('Invalid target in expected output: ${target.toString()}', target.pos);
+							}
+							expectedTargets.remove(target.getName());
+							result.set(target, outStr);
+						case _:
+							throw new ExpectedOutputException('Invalid item in map declaration for expected output. Should be `Target => "generated_code"`', expr.pos);
+					}
+				}
+				if(expectedTargets.length > 0) {
+					throw new ExpectedOutputException('Missing expected code for targets: ${expectedTargets.join(', ')}.', output.pos);
+				}
+			case _:
+				throw new ExpectedOutputException('The second argument for "expect" should be a map declaration.', output.pos);
+		}
+		return result;
+
+	}
+#end
+}

+ 7 - 0
tests/sourcemaps/src/cases/Trace.hx

@@ -0,0 +1,7 @@
+package cases;
+
+class Trace {
+	static public function main() {
+		expect(trace(''), [Js => "console.log", Php => "(Log::$trace)"]);
+	}
+}

+ 1 - 0
tests/sourcemaps/src/cases/import.hx

@@ -0,0 +1 @@
+import Validator.expect;

+ 17 - 0
tests/sourcemaps/src/validation/Exception.hx

@@ -0,0 +1,17 @@
+package validation;
+
+import haxe.macro.Expr;
+
+class Exception {
+	public var msg(default,null):String;
+	public var pos(default,null):Null<Position>;
+
+	public function new(msg:String, ?pos:Position) {
+		this.msg = msg;
+		this.pos = pos;
+	}
+}
+
+class ExpectedOutputException extends Exception {}
+
+class InvalidTargetException extends Exception {}

+ 13 - 0
tests/sourcemaps/src/validation/Lines.hx

@@ -0,0 +1,13 @@
+package validation;
+
+/**
+ *  Represents some text content split in lines
+ */
+abstract Lines(Array<String>) from Array<String> {
+	/**
+	 *  Get content on the specified 1-based line number
+	 */
+	@:arrayAccess inline function get(lineNumber:Int):String {
+		return this[lineNumber - 1];
+	}
+}

+ 6 - 0
tests/sourcemaps/src/validation/Target.hx

@@ -0,0 +1,6 @@
+package validation;
+
+enum Target {
+	Js;
+	Php;
+}

+ 15 - 0
tests/sourcemaps/src/validation/ValidationError.hx

@@ -0,0 +1,15 @@
+package validation;
+
+typedef ValidationError = {
+	/** Code expected to be generated from the haxe code */
+	var expected:String;
+	/** Position of the haxe code, which is expected to be generated as `expected` code */
+	var pos:{
+		/** .hx File */
+		var file:String;
+		/** 1-base line number in .hx file */
+		var line:Int;
+		/** 1-base column number in .hx file */
+		var column:Int;
+	}
+}

+ 10 - 0
tests/sourcemaps/src/validation/ValidationReport.hx

@@ -0,0 +1,10 @@
+package validation;
+
+typedef ValidationReport = {
+	var success:Bool;
+	var summary:{
+		var assertions:Int;
+		var failures:Int;
+	};
+	var errors:Array<ValidationError>;
+}