فهرست منبع

Add initial compilation server tests (#6248)

* add some compilation server tests

* install hxnodejs and utest

* create directory before running haxe (because --cwd)

* actually print "reusing" messages

* typemize server messages

* do it properly

* use proper positions

* check if this is the problem
Simon Krajewski 8 سال پیش
والد
کامیت
250258153d

+ 2 - 0
.gitignore

@@ -101,3 +101,5 @@ Makefile.modules
 /tests/unit/compiler_loops/All.n
 
 /tests/unit/compiler_loops/log.txt
+tests/server/test/cases/
+tests/server/test.js

+ 54 - 9
src/compiler/server.ml

@@ -5,6 +5,7 @@ open Common
 open Common.DisplayMode
 open Type
 open DisplayOutput
+open Json
 
 exception Dirty of module_def
 
@@ -24,6 +25,16 @@ type context = {
 	mutable has_error : bool;
 }
 
+type server_message =
+	| AddedDirectory of string
+	| FoundDirectories of (string * float ref) list
+	| ModulePathChanged of (module_def * float * string)
+	| NotCached of module_def
+	| Parsed of (string * string)
+	| RemovedDirectory of string
+	| Reusing of module_def
+	| SkippingDep of (module_def * module_def)
+
 let s_version =
 	Printf.sprintf "%d.%d.%d%s" version_major version_minor version_revision (match Version.version_extra with None -> "" | Some v -> " " ^ v)
 
@@ -135,6 +146,7 @@ let ssend sock str =
 let rec wait_loop process_params verbose accept =
 	Sys.catch_break false;
 	let has_parse_error = ref false in
+	let test_server_messages = DynArray.create () in
 	let cs = CompilationServer.create () in
 	let sign_string com =
 		let sign = get_signature com in
@@ -148,6 +160,34 @@ let rec wait_loop process_params verbose accept =
 		in
 		Printf.sprintf "%2s,%3s: " sign_id (short_platform_name com.platform)
 	in
+	let process_server_message com tabs =
+		if true || Common.raw_defined com "compilation-server-test" then (fun message ->
+			let module_path m = JString (s_type_path m.m_path) in
+			let kind,data = match message with
+				| AddedDirectory dir -> "addedDirectory",JString dir
+				| FoundDirectories dirs -> "foundDirectories",JInt (List.length dirs)
+				| ModulePathChanged(m,time,file) -> "modulePathChanged",module_path m
+				| NotCached m -> "notCached",module_path m
+				| Parsed(ffile,_) -> "parsed",JString ffile
+				| RemovedDirectory dir -> "removedDirectory",JString dir
+				| Reusing m -> "reusing",module_path m
+				| SkippingDep(m,m') -> "skipping",JObject ["skipped",module_path m;"dependency",module_path m']
+			in
+			let js = JObject [("kind",JString kind);("data",data)] in
+			DynArray.add test_server_messages js;
+		) else (fun message -> match message with
+			| AddedDirectory dir -> print_endline (Printf.sprintf "%sadded directory %s" (sign_string com) dir)
+			| FoundDirectories dirs -> print_endline (Printf.sprintf "%sfound %i directories" (sign_string com) (List.length dirs));
+			| ModulePathChanged(m,time,file) ->
+				print_endline (Printf.sprintf "%smodule path might have changed: %s\n\twas: %2.0f %s\n\tnow: %2.0f %s"
+					(sign_string com) (s_type_path m.m_path) m.m_extra.m_time m.m_extra.m_file time file);
+			| NotCached m -> print_endline (Printf.sprintf "%s%s not cached (%s)" (sign_string com) (s_type_path m.m_path) (if m.m_extra.m_time = -1. then "macro-in-macro" else "modified"));
+			| Parsed(ffile,info) -> print_endline (Printf.sprintf "%sparsed %s (%s)" (sign_string com) ffile info)
+			| RemovedDirectory dir -> print_endline (Printf.sprintf "%sremoved directory %s" (sign_string com) dir);
+			| Reusing m -> print_endline (Printf.sprintf "%s%sreusing %s" (sign_string com) tabs (s_type_path m.m_path));
+			| SkippingDep(m,m') -> print_endline (Printf.sprintf "%sskipping %s%s" (sign_string com) (s_type_path m.m_path) (if m == m' then "" else Printf.sprintf "(%s)" (s_type_path m'.m_path)));
+		)
+	in
 	MacroContext.macro_enable_cache := true;
 	let current_stdin = ref None in
 	Typeload.parse_hook := (fun com2 file p ->
@@ -180,7 +220,7 @@ let rec wait_loop process_params verbose accept =
 						CompilationServer.cache_file cs fkey (ftime,data);
 						"cached",false
 				end in
-				if verbose && is_unusual then print_endline (Printf.sprintf "%sparsed %s (%s)" (sign_string com2) ffile info);
+				if verbose && is_unusual then process_server_message com2 "" (Parsed(ffile,info));
 				data
 	);
 	let check_module_shadowing com paths m =
@@ -189,8 +229,7 @@ let rec wait_loop process_params verbose accept =
 			if Sys.file_exists file then begin
 				let time = file_time file in
 				if time > m.m_extra.m_time then begin
-					if verbose then print_endline (Printf.sprintf "%smodule path might have changed: %s\n\twas: %2.0f %s\n\tnow: %2.0f %s"
-						(sign_string com) (s_type_path m.m_path) m.m_extra.m_time m.m_extra.m_file time file);
+					if verbose then process_server_message com "" (ModulePathChanged(m,time,file));
 					raise Not_found
 				end
 			end
@@ -222,7 +261,7 @@ let rec wait_loop process_params verbose accept =
 							List.iter (fun dir ->
 								if not (CompilationServer.has_directory cs sign dir) then begin
 									let time = stat dir in
-									if verbose then print_endline (Printf.sprintf "%sadded directory %s" (sign_string com) dir);
+									if verbose then process_server_message com "" (AddedDirectory dir);
 									CompilationServer.add_directory cs sign (dir,ref time)
 								end;
 							) sub_dirs;
@@ -231,7 +270,7 @@ let rec wait_loop process_params verbose accept =
 							acc
 					with Unix.Unix_error _ ->
 						CompilationServer.remove_directory cs sign dir;
-						if verbose then print_endline (Printf.sprintf "%sremoved directory %s" (sign_string com) dir);
+						if verbose then process_server_message com "" (RemovedDirectory dir);
 						acc
 				) [] all_dirs
 			with Not_found ->
@@ -250,7 +289,7 @@ let rec wait_loop process_params verbose accept =
 					in
 					List.iter add_dir com.class_path;
 					List.iter add_dir (Path.find_directories (platform_name com.platform) true com.class_path);
-					if verbose then print_endline (Printf.sprintf "%sfound %i directories" (sign_string com) (List.length !dirs));
+					if verbose then process_server_message com "" (FoundDirectories !dirs);
 					CompilationServer.add_directories cs sign !dirs
 				) :: !delays;
 				(* Returning [] should be fine here because it's a new context, so we won't do any
@@ -323,7 +362,7 @@ let rec wait_loop process_params verbose accept =
 					if has_policy CheckFileContentModification && not (content_changed m m.m_extra.m_file) then begin
 						if verbose then print_endline (Printf.sprintf "%s%s changed time not but content, reusing" (sign_string com2) m.m_extra.m_file)
 					end else begin
-						if verbose then print_endline (Printf.sprintf "%s%s not cached (%s)" (sign_string com2) (s_type_path m.m_path) (if m.m_extra.m_time = -1. then "macro-in-macro" else "modified"));
+						if verbose then process_server_message com2 "" (NotCached m);
 						if m.m_extra.m_kind = MFake then Hashtbl.remove Typecore.fake_modules m.m_extra.m_file;
 						raise Not_found;
 					end
@@ -365,7 +404,7 @@ let rec wait_loop process_params verbose accept =
 					(* this was just a dependency to check : do not add to the context *)
 					PMap.iter (Hashtbl.replace com2.resources) m.m_extra.m_binded_res;
 				| _ ->
-					(*if verbose then print_endline (Printf.sprintf "%s%sreusing %s" (sign_string com2) tabs (s_type_path m.m_path));*)
+					if verbose then process_server_message com2 tabs (Reusing m);
 					m.m_extra.m_added <- !compilation_step;
 					List.iter (fun t ->
 						match t with
@@ -397,7 +436,7 @@ let rec wait_loop process_params verbose accept =
 			begin match check m with
 			| None -> ()
 			| Some m' ->
-				if verbose then print_endline (Printf.sprintf "%sskipping %s%s" (sign_string com2) (s_type_path m.m_path) (if m == m' then "" else Printf.sprintf "(%s)" (s_type_path m'.m_path)));
+				if verbose then process_server_message com2 "" (SkippingDep(m,m'));
 				tcheck();
 				raise Not_found;
 			end;
@@ -481,6 +520,7 @@ let rec wait_loop process_params verbose accept =
 			let data = parse_hxml_data hxml in
 			if verbose then print_endline ("Processing Arguments [" ^ String.concat "," data ^ "]");
 			(try
+				DynArray.clear test_server_messages;
 				Hashtbl.clear changed_directories;
 				Common.display_default := DMNone;
 				Parser.resume_display := null_pos;
@@ -506,6 +546,11 @@ let rec wait_loop process_params verbose accept =
 			| Arg.Bad msg ->
 				prerr_endline ("Error: " ^ msg);
 			);
+			if DynArray.length test_server_messages > 0 then begin
+				let b = Buffer.create 0 in
+				write_json (Buffer.add_string b) (JArray (DynArray.to_list test_server_messages));
+				write (Buffer.contents b)
+			end;
 			let fl = !delays in
 			delays := [];
 			List.iter (fun f -> f()) fl;

+ 6 - 1
tests/RunCi.hx

@@ -502,6 +502,7 @@ class RunCi {
 	static var optDir(default, never) = cwd + "optimization/";
 	static var miscDir(default, never) = cwd + "misc/";
 	static var displayDir(default, never) = cwd + "display/";
+	static var serverDir(default, never) = cwd + "server/";
 	static var gitInfo(get, null):{repo:String, branch:String, commit:String, timestamp:Float, date:String};
 	static var success(default, null) = true;
 	static function get_gitInfo() return if (gitInfo != null) gitInfo else gitInfo = {
@@ -840,6 +841,7 @@ class RunCi {
 							}
 						];
 
+						haxelibInstall("hxnodejs");
 						var env = Sys.environment();
 						if (
 							env.exists("SAUCE") &&
@@ -863,7 +865,6 @@ class RunCi {
 							// }
 
 							runCommand("npm", ["install", "wd", "q"], true);
-							haxelibInstall("hxnodejs");
 							runCommand("haxe", ["compile-saucelabs-runner.hxml"]);
 							var server = new Process("nekotools", ["server"]);
 							runCommand("node", ["bin/RunSauceLabs.js"].concat([for (js in jsOutputs) "unit-js.html?js=" + js.urlEncode()]));
@@ -875,6 +876,10 @@ class RunCi {
 						infoMsg("Test optimization:");
 						changeDirectory(optDir);
 						runCommand("haxe", ["run.hxml"]);
+						haxelibInstall("utest");
+						changeDirectory(serverDir);
+						runCommand("haxe", ["build.hxml"]);
+						runCommand("node", ["test.js"]);
 					case Java:
 						getSpodDependencies();
 						getJavaDependencies();

+ 5 - 0
tests/server/build.hxml

@@ -0,0 +1,5 @@
+-cp src
+-main Main
+-js test.js
+-lib hxnodejs
+-lib utest

+ 27 - 0
tests/server/src/AsyncMacro.hx

@@ -0,0 +1,27 @@
+import haxe.macro.Expr;
+import haxe.macro.Context;
+
+class AsyncMacro {
+	static public macro function async(e:Expr) {
+		var el = switch (e.expr) {
+			case EBlock(el): el;
+			case _: Context.error("Block expression expected", e.pos);
+		}
+		el.unshift(macro var _done = utest.Assert.createAsync(1000));
+		el.push(macro _done());
+		function loop(el:Array<Expr>) {
+			var e0 = el.shift();
+			return if (el.length == 0) {
+				e0;
+			} else switch (e0) {
+				case macro haxe($a{args}):
+					var e = loop(el);
+					args.push(macro () -> $e);
+					macro haxe($a{args});
+				case _:
+					macro { $e0; ${loop(el)}};
+			}
+		}
+		return loop(el);
+	}
+}

+ 267 - 0
tests/server/src/HaxeServer.hx

@@ -0,0 +1,267 @@
+import js.node.child_process.ChildProcess as ChildProcessObject;
+import js.node.child_process.ChildProcess.ChildProcessEvent;
+import js.node.Buffer;
+import js.node.ChildProcess;
+import js.node.stream.Readable;
+
+using StringTools;
+
+class ErrorUtils {
+    public static function errorToString(error:Dynamic, intro:String):String {
+        var result = intro + Std.string(error);
+        var stack = haxe.CallStack.exceptionStack();
+        if (stack != null && stack.length > 0)
+            result += "\n" + haxe.CallStack.toString(stack);
+        return result;
+    }
+}
+
+private class DisplayRequest {
+    // these are used for the queue
+    public var prev:DisplayRequest;
+    public var next:DisplayRequest;
+
+    var args:Array<String>;
+    var stdin:String;
+    var callback:String->Void;
+    var errback:String->Void;
+
+    static var stdinSepBuf = new Buffer([1]);
+
+    public function new(args:Array<String>, stdin:String, callback:String->Void, errback:String->Void) {
+        this.args = args;
+        this.stdin = stdin;
+        this.callback = callback;
+        this.errback = errback;
+    }
+
+    public function prepareBody():Buffer {
+        if (stdin != null) {
+            args.push("-D");
+            args.push("display-stdin");
+        }
+
+        var lenBuf = new Buffer(4);
+        var chunks = [lenBuf];
+        var length = 0;
+        for (arg in args) {
+            var buf = new Buffer(arg + "\n");
+            chunks.push(buf);
+            length += buf.length;
+        }
+
+        if (stdin != null) {
+            chunks.push(stdinSepBuf);
+            var buf = new Buffer(stdin);
+            chunks.push(buf);
+            length += buf.length + stdinSepBuf.length;
+        }
+
+        lenBuf.writeInt32LE(length, 0);
+
+        return Buffer.concat(chunks, length + 4);
+    }
+
+    public function processResult(data:String) {
+        var buf = new StringBuf();
+        var hasError = false;
+        for (line in data.split("\n")) {
+            switch (line.fastCodeAt(0)) {
+                case 0x01: // print
+                    var line = line.substring(1).replace("\x01", "\n");
+                    trace("Haxe print:\n" + line);
+                case 0x02: // error
+                    hasError = true;
+                default:
+                    buf.add(line);
+                    buf.addChar("\n".code);
+            }
+        }
+
+        var data = buf.toString().trim();
+
+        if (hasError)
+            return errback(data);
+
+        try {
+            callback(data);
+        } catch (e:Any) {
+            errback(ErrorUtils.errorToString(e, "Exception while handling Haxe completion response: "));
+        }
+    }
+}
+
+typedef DisplayServerConfigBase = {
+    var haxePath:String;
+    var arguments:Array<String>;
+    var env:haxe.DynamicAccess<String>;
+}
+
+typedef Context = {
+    function sendErrorMessage(msg:String):Void;
+    function sendLogMessage(msg:String):Void;
+    var displayServerConfig:DisplayServerConfigBase;
+}
+
+private class MessageBuffer {
+    static inline var DEFAULT_SIZE = 8192;
+
+    var index:Int;
+    var buffer:Buffer;
+
+    public function new() {
+        index = 0;
+        buffer = new Buffer(DEFAULT_SIZE);
+    }
+
+    public function append(chunk:Buffer):Void {
+        if (buffer.length - index >= chunk.length) {
+            chunk.copy(buffer, index, 0, chunk.length);
+        } else {
+            var newSize = (Math.ceil((index + chunk.length) / DEFAULT_SIZE) + 1) * DEFAULT_SIZE;
+            if (index == 0) {
+                buffer = new Buffer(newSize);
+                chunk.copy(buffer, 0, 0, chunk.length);
+            } else {
+                buffer = Buffer.concat([buffer.slice(0, index), chunk], newSize);
+            }
+        }
+        index += chunk.length;
+    }
+
+    public function tryReadLength():Int {
+        if (index < 4)
+            return -1;
+        var length = buffer.readInt32LE(0);
+        buffer = buffer.slice(4);
+        index -= 4;
+        return length;
+    }
+
+    public function tryReadContent(length:Int):String {
+        if (index < length)
+            return null;
+        var result = buffer.toString("utf-8", 0, length);
+        var nextStart = length;
+        buffer.copy(buffer, 0, nextStart);
+        index -= nextStart;
+        return result;
+    }
+
+    public function getContent():String {
+        return buffer.toString("utf-8", 0, index);
+    }
+}
+
+class HaxeServer {
+    var proc:ChildProcessObject;
+
+    var context:Context;
+    var buffer:MessageBuffer;
+    var nextMessageLength:Int;
+
+    var requestsHead:DisplayRequest;
+    var requestsTail:DisplayRequest;
+    var currentRequest:DisplayRequest;
+
+    public function new(context:Context) {
+        this.context = context;
+    }
+
+    static var reTrailingNewline = ~/\r?\n$/;
+
+    public function start() {
+        stop();
+
+        inline function error(s) context.sendErrorMessage(s);
+
+        var env = new haxe.DynamicAccess();
+        for (key in js.Node.process.env.keys())
+            env[key] = js.Node.process.env[key];
+
+        buffer = new MessageBuffer();
+        nextMessageLength = -1;
+
+        proc = ChildProcess.spawn(context.displayServerConfig.haxePath, context.displayServerConfig.arguments.concat(["--wait", "stdio"]), {env: env});
+
+        proc.stdout.on(ReadableEvent.Data, function(buf:Buffer) {
+            context.sendLogMessage(reTrailingNewline.replace(buf.toString(), ""));
+        });
+        proc.stderr.on(ReadableEvent.Data, onData);
+
+        proc.on(ChildProcessEvent.Exit, onExit);
+    }
+
+    public function stop() {
+        if (proc != null) {
+            proc.removeAllListeners();
+            proc.kill();
+            proc = null;
+        }
+
+        requestsHead = requestsTail = currentRequest = null;
+    }
+
+    public function restart(reason:String) {
+        context.sendLogMessage('Restarting Haxe completion server: $reason');
+        start();
+    }
+
+    function onExit(_, _) {
+        var haxeResponse = buffer.getContent();
+        trace("\nError message from the compiler:\n");
+        trace(haxeResponse);
+    }
+
+    function onData(data:Buffer) {
+        buffer.append(data);
+        while (true) {
+            if (nextMessageLength == -1) {
+                var length = buffer.tryReadLength();
+                if (length == -1)
+                    return;
+                nextMessageLength = length;
+            }
+            var msg = buffer.tryReadContent(nextMessageLength);
+            if (msg == null)
+                return;
+            nextMessageLength = -1;
+            if (currentRequest != null) {
+                var request = currentRequest;
+                currentRequest = null;
+                request.processResult(msg);
+                checkQueue();
+            }
+        }
+    }
+
+    public function process(args:Array<String>, stdin:String, callback:String->Void, errback:String->Void) {
+        // create a request object
+        var request = new DisplayRequest(args, stdin, callback, errback);
+
+        // add to the queue
+        if (requestsHead == null) {
+            requestsHead = requestsTail = request;
+        } else {
+            requestsTail.next = request;
+            request.prev = requestsTail;
+            requestsTail = request;
+        }
+
+        // process the queue
+        checkQueue();
+    }
+
+    function checkQueue() {
+        // there's a currently processing request, wait and don't send another one to Haxe
+        if (currentRequest != null)
+            return;
+
+        // pop the first request still in queue, set it as current and send to Haxe
+        if (requestsHead != null) {
+            currentRequest = requestsHead;
+            requestsHead = currentRequest.next;
+            proc.stdin.write(currentRequest.prepareBody());
+        }
+    }
+}

+ 92 - 0
tests/server/src/HaxeServerTestCase.hx

@@ -0,0 +1,92 @@
+import HaxeServer;
+import haxe.Json;
+import utest.Assert;
+
+typedef Message<T> = {
+	kind: String,
+	data: T
+}
+
+class TestContext {
+	public var displayServerConfig:DisplayServerConfigBase;
+
+	public function new(config:DisplayServerConfigBase) {
+		this.displayServerConfig = config;
+	}
+
+	public function sendErrorMessage(msg:String) { }
+
+	public function sendLogMessage(msg:String) { }
+}
+
+class HaxeServerTestCase {
+	var context:TestContext;
+	var server:HaxeServer;
+	var vfs:Vfs;
+	var testDir:String;
+	var messages:Array<Message<Any>>;
+
+	public function new() {
+		testDir = "test/cases/" + Type.getClassName(Type.getClass(this));
+	}
+
+	public function setup() {
+		context = new TestContext({
+			haxePath: "haxe",
+			arguments: ["-v", "-D", "compilation-server-test", "--cwd", testDir],
+			env: { }
+		});
+		vfs = new Vfs(testDir);
+		server = new HaxeServer(context);
+		server.start();
+	}
+
+	public function teardown() {
+		server.stop();
+	}
+
+	function haxe(args:Array<String>, done:Void -> Void) {
+		server.process(args, null, function(result) {
+			if (result == "") {
+				result = "{}";
+			}
+			messages = Json.parse(result);
+			done();
+		}, function(message) {
+			Assert.fail(message);
+			done();
+		});
+	}
+
+	function getTemplate(templateName:String) {
+		return sys.io.File.getContent("test/templates/" + templateName);
+	}
+
+	function hasMessage<T>(msg:{kind: String, data:T}) {
+		function compareData(data1:Dynamic, data2:Dynamic) {
+			return switch (msg.kind) {
+				case "reusing" | "notCached": data1 == data2;
+				case "skipping": data1.skipped == data2.skipped && data1.dependency == data2.dependency;
+				case _: false;
+			}
+		}
+		for (message in messages) {
+			if (message.kind == msg.kind && compareData(message.data, cast msg.data)) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	function assertReuse(module:String, ?p:haxe.PosInfos) {
+		Assert.isTrue(hasMessage({kind: "reusing", data: module}), null, p);
+	}
+
+	function assertSkipping(module:String, ?dependency:String, ?p:haxe.PosInfos) {
+		Assert.isTrue(hasMessage({kind: "skipping", data: {skipped: module, dependency: dependency == null ? module : dependency}}), null, p);
+	}
+
+	function assertNotCacheModified(module:String, ?p:haxe.PosInfos) {
+		Assert.isTrue(hasMessage({kind: "notCached", data: module}), null, p);
+	}
+}

+ 59 - 0
tests/server/src/Main.hx

@@ -0,0 +1,59 @@
+import AsyncMacro.async;
+
+class NoModification extends HaxeServerTestCase {
+	public function test() {
+		async({
+			vfs.putContent("HelloWorld.hx", getTemplate("HelloWorld.hx"));
+			var args = ["-main", "HelloWorld.hx", "--no-output", "-js", "no.js"];
+			haxe(args);
+			haxe(args);
+			assertReuse("HelloWorld");
+			haxe(args);
+			assertReuse("HelloWorld");
+		});
+	}
+}
+
+class Modification extends HaxeServerTestCase {
+	public function test() {
+		async({
+			vfs.putContent("HelloWorld.hx", getTemplate("HelloWorld.hx"));
+			var args = ["-main", "HelloWorld.hx", "--no-output", "-js", "no.js"];
+			haxe(args);
+			vfs.touchFile("HelloWorld.hx");
+			haxe(args);
+			assertSkipping("HelloWorld");
+			assertNotCacheModified("HelloWorld");
+		});
+	}
+}
+
+class Dependency extends HaxeServerTestCase {
+	public function test() {
+		async({
+			vfs.putContent("WithDependency.hx", getTemplate("WithDependency.hx"));
+			vfs.putContent("Dependency.hx", getTemplate("Dependency.hx"));
+			var args = ["-main", "WithDependency.hx", "--no-output", "-js", "no.js"];
+			haxe(args);
+			vfs.touchFile("Dependency.hx");
+			haxe(args);
+			assertSkipping("WithDependency", "Dependency");
+			assertNotCacheModified("Dependency");
+			haxe(args);
+			assertReuse("Dependency");
+			assertReuse("WithDependency");
+		});
+	}
+}
+
+class Main {
+	static public function main() {
+		Vfs.removeDir("test/cases");
+		var runner = new utest.Runner();
+		runner.addCase(new NoModification());
+		runner.addCase(new Modification());
+		runner.addCase(new Dependency());
+		utest.ui.Report.create(runner);
+		runner.run();
+	}
+}

+ 60 - 0
tests/server/src/Vfs.hx

@@ -0,0 +1,60 @@
+import js.node.Fs;
+import sys.FileSystem;
+import haxe.io.Path;
+
+class Vfs {
+	var physicalPath:String;
+
+	public function new(physicalPath:String) {
+		this.physicalPath = physicalPath;
+		if (FileSystem.exists(physicalPath)) {
+			throw 'Cannot create virtual file-system for $physicalPath: directory exists';
+		}
+		FileSystem.createDirectory(physicalPath);
+	}
+
+	public function touchFile(path:String) {
+		var path = getPhysicalPath(path);
+		FileSystem.createDirectory(path.dir);
+		var notNow = DateTools.delta(Date.now(), 1000);
+		var file = Fs.openSync(path.dir + "/" + path.file + "." + path.ext, 'a');
+		Fs.futimesSync(file, notNow, notNow);
+		Fs.closeSync(file);
+	}
+
+	public function overwriteContent(path:String, content:String) {
+		var path = getPhysicalPath(path).toString();
+		if (!FileSystem.exists(path)) {
+			throw 'Cannot overwrite content for $path: file does not exist';
+		}
+		Fs.writeFileSync(path, content);
+	}
+
+	public function putContent(path:String, content:String) {
+		var path = getPhysicalPath(path);
+		FileSystem.createDirectory(path.dir);
+		Fs.writeFileSync(path.toString(), content);
+	}
+
+	public function close() {
+		removeDir(physicalPath);
+	}
+
+	function getPhysicalPath(path:String) {
+		return new Path(Path.join([physicalPath, path]));
+	}
+
+	static public function removeDir(dir:String):Void {
+		if (FileSystem.exists(dir)) {
+			for (item in FileSystem.readDirectory(dir)) {
+				item = haxe.io.Path.join([dir, item]);
+				if (FileSystem.isDirectory(item)) {
+					removeDir(item);
+				} else {
+					FileSystem.deleteFile(item);
+				}
+			}
+			FileSystem.deleteDirectory(dir);
+		}
+	}
+}

+ 5 - 0
tests/server/test/templates/Dependency.hx

@@ -0,0 +1,5 @@
+class Dependency {
+	static public function get() {
+		return "Hello World";
+	}
+}

+ 5 - 0
tests/server/test/templates/HelloWorld.hx

@@ -0,0 +1,5 @@
+class HelloWorld {
+	public static function main() {
+		trace("Hello World");
+	}
+}

+ 5 - 0
tests/server/test/templates/WithDependency.hx

@@ -0,0 +1,5 @@
+class WithDependency {
+	public static function main() {
+		trace(Dependency.get());
+	}
+}