diff --git a/.gitignore b/.gitignore index 4c2ca043b35..1b5b8ae7540 100644 --- a/.gitignore +++ b/.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 diff --git a/src/compiler/server.ml b/src/compiler/server.ml index 4240b69c353..a5816363144 100644 --- a/src/compiler/server.ml +++ b/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; diff --git a/tests/RunCi.hx b/tests/RunCi.hx index 17df2e89fac..84892e8e38c 100644 --- a/tests/RunCi.hx +++ b/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(); diff --git a/tests/server/build.hxml b/tests/server/build.hxml new file mode 100644 index 00000000000..efdbe42ba61 --- /dev/null +++ b/tests/server/build.hxml @@ -0,0 +1,5 @@ +-cp src +-main Main +-js test.js +-lib hxnodejs +-lib utest \ No newline at end of file diff --git a/tests/server/src/AsyncMacro.hx b/tests/server/src/AsyncMacro.hx new file mode 100644 index 00000000000..a255d48a06f --- /dev/null +++ b/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) { + 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); + } +} \ No newline at end of file diff --git a/tests/server/src/HaxeServer.hx b/tests/server/src/HaxeServer.hx new file mode 100644 index 00000000000..4d733298344 --- /dev/null +++ b/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; + var stdin:String; + var callback:String->Void; + var errback:String->Void; + + static var stdinSepBuf = new Buffer([1]); + + public function new(args:Array, 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; + var env:haxe.DynamicAccess; +} + +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, 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()); + } + } +} \ No newline at end of file diff --git a/tests/server/src/HaxeServerTestCase.hx b/tests/server/src/HaxeServerTestCase.hx new file mode 100644 index 00000000000..f6338729e51 --- /dev/null +++ b/tests/server/src/HaxeServerTestCase.hx @@ -0,0 +1,92 @@ +import HaxeServer; +import haxe.Json; +import utest.Assert; + +typedef Message = { + 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>; + + 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, 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(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); + } +} \ No newline at end of file diff --git a/tests/server/src/Main.hx b/tests/server/src/Main.hx new file mode 100644 index 00000000000..1600a4307de --- /dev/null +++ b/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(); + } +} \ No newline at end of file diff --git a/tests/server/src/Vfs.hx b/tests/server/src/Vfs.hx new file mode 100644 index 00000000000..95c0fa2b1b8 --- /dev/null +++ b/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); + } + } +} \ No newline at end of file diff --git a/tests/server/test/templates/Dependency.hx b/tests/server/test/templates/Dependency.hx new file mode 100644 index 00000000000..4cbc57b75d3 --- /dev/null +++ b/tests/server/test/templates/Dependency.hx @@ -0,0 +1,5 @@ +class Dependency { + static public function get() { + return "Hello World"; + } +} \ No newline at end of file diff --git a/tests/server/test/templates/HelloWorld.hx b/tests/server/test/templates/HelloWorld.hx new file mode 100644 index 00000000000..50b4dd6e0a9 --- /dev/null +++ b/tests/server/test/templates/HelloWorld.hx @@ -0,0 +1,5 @@ +class HelloWorld { + public static function main() { + trace("Hello World"); + } +} \ No newline at end of file diff --git a/tests/server/test/templates/WithDependency.hx b/tests/server/test/templates/WithDependency.hx new file mode 100644 index 00000000000..661eab717fc --- /dev/null +++ b/tests/server/test/templates/WithDependency.hx @@ -0,0 +1,5 @@ +class WithDependency { + public static function main() { + trace(Dependency.get()); + } +} \ No newline at end of file