Release 0.14.2
M .hgsubstate +1 -1
@@ 1,4 1,4 @@ 
-c93a13c8d24b789eb83a91e0a8350d58ef888c9f library
+6eb78205ebd8bfc3705fec4630d016a5f9d0b454 library
 213d46ad898d376bcb18ce5d3fc02f81f0e98d17 src/lithe
 5a97797856da2c6b1a18bf9fe591a64d58b04e19 src/pad
 29a29937ee1095e5ddf1655902f1ed09c810bbf3 src/paf

          
M cmake/Test.cmake +1 -2
@@ 29,7 29,6 @@ set_property(DIRECTORY APPEND PROPERTY C
 	"${CMAKE_SOURCE_DIR}/library/tests.json" )
 
 set(KRONOS_SUBMIT_TEST_AUTH "" CACHE STRING "Submit test run to database")
-message(STATUS ${KRONOS_SUBMIT_TEST_AUTH})
 
 if (KRONOS_SUBMIT_TEST_AUTH) 
 	

          
@@ 87,7 86,7 @@ foreach(var ${STATIC_TESTS})
 
 	target_link_libraries(${test_name} static_test_core)			
 
-	set_target_properties(${test_name} PROPERTIES FOLDER apps/tests COMPILE_FLAGS -D_CONSOLE)
+	set_target_properties(${test_name} PROPERTIES FOLDER "static_tests/${test_file}" COMPILE_FLAGS -D_CONSOLE)
 
 	add_test( NAME "static.${test_name}" 
 		COMMAND 

          
M configure_tests.py +22 -9
@@ 1,13 1,26 @@ 
 #!/usr/env python
 
-import json, io, sys
+import json, io, sys, os
 
-tests = json.loads(open(sys.argv[1]).read())
+basepath = os.path.dirname(sys.argv[1])
 
-for p in tests["eval"]:
-	for t in tests["eval"][p]:
-		if "Static" not in tests["eval"][p][t].get("exclude", ""):
-			if "expr" in tests["eval"][p][t]:
-				sys.stdout.write("%s\n%s\nEval({ %s } nil);" % (p, t, tests["eval"][p][t]["expr"]))
-			else:
-				sys.stdout.write("%s\n%s\nTest:%s();" % (p, t, t))
  No newline at end of file
+def process_tests(package, tests):
+	if "$include" in tests:
+		file = tests["$include"][0]
+		path = tests["$include"][1:]
+		json_file = json.loads(open(basepath + "/" + file).read())
+		for p in path:
+			json_file = json_file.get(p, {})
+		process_tests(package, json_file)
+	else:
+		for t in tests:
+			if "Static" not in tests[t].get("exclude", { }):
+				if "expr" in tests[t]:
+					sys.stdout.write("%s\n%s\nEval({ %s } nil);" % (package, t, tests[t]["expr"]))
+				else:
+					sys.stdout.write("%s\n%s\nTest:%s();" % (package, t, t))
+
+master = json.loads(open(sys.argv[1]).read()).get("eval", {})
+
+for package in master:
+	process_tests(package, master[package])
  No newline at end of file

          
M src/driver/ktests.cpp +45 -5
@@ 195,6 195,46 @@ static picojson::object BatchRunner(cons
 picojson::object GetRunInfo();
 picojson::array SplitSemVer(const std::string& a);
 
+Packages::DefaultClient bbClient;
+
+void ProcessIncludes(picojson::value& val) {
+	if (val.is<picojson::object>()) {
+		picojson::object& obj{ val.get<picojson::object>() };
+		auto inc = obj.find("$include");
+		if (inc != obj.end() && inc->second.is<picojson::array>()) {
+			auto incpath = inc->second.get<picojson::array>();
+			if (incpath.empty()) throw std::runtime_error("Bad include path");
+			auto filepath = incpath[0].to_str();
+
+			std::string includeJsonPath = bbClient.Resolve(CLOpts::package(), filepath, CLOpts::package_version());
+			std::ifstream includeJsonStream{ includeJsonPath };
+			if (includeJsonStream.is_open() == false) 
+				throw std::runtime_error("Could not open included '" + includeJsonPath + "'");
+
+			picojson::value includeJson;
+			std::string err;
+			err = picojson::parse(includeJson, includeJsonStream);
+			if (err.size()) {
+				throw std::runtime_error("Parse error while reading '" + includeJsonPath + "': " + err);
+			}
+
+			for (int i = 1; i < incpath.size(); ++i) {
+				auto pseg = incpath[i].to_str();
+				if (includeJson.contains(pseg)) {
+					// convert from reference to make a copy
+					includeJson = (picojson::value)includeJson.get(pseg);
+				} else {
+					throw std::runtime_error("Included file does not contain '" + incpath[i].to_str() + "'");
+				}
+			}
+			val = includeJson;
+		} else {
+			for (auto&& kv : obj) {
+				ProcessIncludes(kv.second);
+			}
+		}
+	}
+}
 
 int main(int argc, const char* arg[]) {
 	

          
@@ 260,8 300,6 @@ int main(int argc, const char* arg[]) {
 
 		std::clog << "\n";
 
-		Packages::DefaultClient bbClient;
-
 		auto testDataFilePath = bbClient.Resolve(CLOpts::package(), "tests.json", CLOpts::package_version());
 		std::ifstream testDataStream(testDataFilePath);
 		if (!testDataStream.is_open()) throw std::runtime_error("Could not load tests for module [" + CLOpts::package() + " " + CLOpts::package_version() + "]");

          
@@ 269,6 307,8 @@ int main(int argc, const char* arg[]) {
 		picojson::value testData;
 		auto jsonError = picojson::parse(testData, testDataStream);
         testDataStream.close();
+
+		ProcessIncludes(testData);
         
 		if (jsonError.size()) {
 			throw std::runtime_error(jsonError);

          
@@ 572,12 612,12 @@ std::string Diff(const char *scheme, con
 
 		TestSummary.Unknown(testId);
 		testData["status"] = "new";
-		return testData["result"].contains("text") ? testData["result"].get("text").to_str() : "";
+		return ( testData["result"].contains("text") ? testData["result"].get("text").to_str() + "\n" : "" );
 	} catch (std::exception & e) {
 		testData["status"] = "fail";
 		testData["diff_exception"] = e.what();
 		TestSummary.Fail(testId);
-		return "";
+		return e.what();
 	}
 }
 

          
@@ 599,7 639,7 @@ picojson::object BatchRunner(const char*
 			auto& testCases = inPackage.second.get<picojson::object>();
 			for (auto &test : testCases) {
 				if (std::regex_search(scheme + ":"s + inPackage.first + "/" + test.first, filter)) {
-					std::clog << " - " << test.first << " ... ";
+					std::clog << " - " << (test.second.contains("label") ? test.second.get("label").to_str() : test.first) << " ... ";
 					auto startTime = std::chrono::steady_clock::now();
 					picojson::object testResult;
 					auto testCase = "Test:" + test.first;

          
M src/driver/package.cpp +14 -0
@@ 456,6 456,20 @@ namespace Packages {
 		return false;
 	}
 
+	std::string uint128::digest() const {
+		uint32_t scramble = 0;
+		for (auto& w : dw) {
+			scramble ^= w;
+		}
+		const char b64[65] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-";
+		std::string d = "";
+		for (int i = 0; i < 5; i++) {
+			d.push_back(b64[scramble & 0x3f]);
+			scramble >>= 6;
+		}
+		return d;
+	}
+
 	static size_t writeHex(char * const buf, const uint128& u128) {
 		const char hexdigit[] = {
 			'0','1','2','3','4','5','6','7','8','9','0','a','b','c','d','e','f'

          
M src/driver/package.h +1 -0
@@ 79,6 79,7 @@ namespace Packages {
 		bool is_zero() const { for (auto d : dw) if (d) return false; return true; }
 		operator std::string() const;
 		std::string to_str() const { return *this; }
+		std::string digest() const;
 	};
 
 	uint128 operator*(uint128 a, uint128 b);

          
M src/website/makehtml.cpp +27 -16
@@ 760,25 760,36 @@ int link_page(const char **files, int nu
 		tmp << link;
 
 		if (link.contains("tests")) {
-			auto& t = link.get("tests");
+			auto writeFile = [](const std::string & name, const std::string& content) {
+				std::ofstream write{ name };
+				if (!write.is_open()) throw std::runtime_error("Could not write '" + name + "'");
+				write << content;
+			};
+
 			Packages::MakeMultiLevelPath("tests/");
 
-			for (auto& s : t.get("source").get<picojson::object>()) {
-				auto testFile = "tests/" + s.first + ".k";
-				std::clog << " - Tests: " << s.first << "\n";
-				std::ofstream tsource(testFile);
-				if (tsource.is_open() == false) throw std::runtime_error("Could not write test " + testFile);
-				tsource << s.second.to_str();
-			}
+			for (auto&& t : link.get("tests").get<picojson::object>()) {
+				auto&& name = t.first;
+				auto&& data = t.second.get<picojson::object>();
+				std::string source;
+				auto& src = data.at("source");
+				
+				if (src.is<picojson::array>()) {
+					for (auto& s : src.get<picojson::array>()) {
+						source += s.to_str();
+						source.push_back('\n');
+					}
+				} else {
+					source = src.get<std::string>();
+				}
 
-			std::ofstream tests{ "tests.json" };
-			if (tests.is_open() == false) throw std::runtime_error("Could not write testcase file");
-
-			tests << picojson::value{
-				picojson::object{
-					{ "audio", t.get("audio") },
-					{ "eval", t.get("eval") }
-			} };
+				writeFile("tests/" + name + ".k", source);
+				writeFile("tests/" + name + ".json", picojson::value{
+					picojson::object {
+						{ "audio", data.at("audio") },
+						{ "eval", data.at("eval") } }
+				}.serialize());
+			}
 		}
 
 		std::clog << "\n";

          
M src/website/md2json.cpp +27 -12
@@ 2,6 2,7 @@ 
 #include <fstream>
 #include <sstream>
 #include <cstdlib>
+#include <regex>
 
 #include <sys/stat.h>
 #include <time.h>

          
@@ 410,7 411,7 @@ picojson::value render_kronos_code(std::
 					auto tmpfile = "repl_"s + Packages::fnv1a(id.data(), id.size()).to_str() + ".mp3";
 
 					repl.evaluate(
-						":no:" + code + " Actions:Render(\"" + escape(tmpfile) + "\" 441000 44100 { :no:snd } )");
+						"Actions:Render(\"" + escape(tmpfile) + "\" 441000 44100 { " + code + " snd } )");
 
 					repl.audio.emplace_back(code);
 

          
@@ 526,17 527,30 @@ int compile_page(const char *in, const c
 			picojson::object audioTests;
 			picojson::object evalTests;
 
-			int c = 1;
+			auto uniqueName = [names = std::unordered_set<std::string>{}](std::string content) mutable {
+				auto hash = Packages::fnv1a(content.data(), content.size()).digest();
+				std::string nm = hash;
+				int suffix = 1;
+				while (names.count(nm)) {
+					nm = hash + "-" + std::to_string(suffix++);
+				}
+				names.emplace(nm);
+				return nm;
+			};
+
+			std::regex whitespace("\\S+");
 			for (auto& a : repl.audio) {
-				auto caseName = "Audio-" + std::to_string(c++);
+				auto caseName = "Audio-" + uniqueName(a);
 				repl.code += "\n:Test:" + caseName + "() { " + a + " snd }\n";
-				audioTests[caseName] = picojson::object{};
+				a = std::regex_replace(a, whitespace, " ", std::regex_constants::match_any);
+				audioTests[caseName] = picojson::object{ {"label", a} };
 			}
 
 			for (auto& e : repl.eval) {
-				auto caseName = "Eval-" + std::to_string(c++);
+				auto caseName = "Eval-" + uniqueName(e);
 				repl.code += "\n:Test:" + caseName + "() { Handle(" + e + " '_ ) }\n";
-				evalTests[caseName] = picojson::object{};
+				e = std::regex_replace(e, whitespace, " ", std::regex_constants::match_any).substr(6);
+				evalTests[caseName] = picojson::object{ {"label", e} };
 			}
 			
 			picojson::object json{

          
@@ 559,7 573,7 @@ int compile_page(const char *in, const c
 				auto lastSep = basename.find_last_of("\\/");
 				if (lastSep == basename.npos) lastSep = 0;
 
-				std::string name = "Web-";
+				std::string name = "";
 				for(;lastSep<basename.size();++lastSep) { 
 					auto c = basename[lastSep];
 					if (isalnum(c)) {

          
@@ 569,11 583,12 @@ int compile_page(const char *in, const c
 					}					
 				}				
 
-				json["tests"] = picojson::object {
-					{ "source", picojson::object{{name, repl.code } } },
-					{ "audio", picojson::object{{name, audioTests } } },
-					{ "eval", picojson::object{{name, evalTests } } }
-				};
+				json["tests"] = picojson::object{ { name,
+					picojson::object {
+						{ "source", repl.code },
+						{ "audio", audioTests },
+						{ "eval", evalTests },
+				} } };
 			}
 
 			write << json;

          
M version.txt +1 -1
@@ 1,1 1,1 @@ 
-0.14.1
  No newline at end of file
+0.14.2
  No newline at end of file