diff --git a/test/Makefile b/test/Makefile index 539e29fde626..db407e8ee939 100644 --- a/test/Makefile +++ b/test/Makefile @@ -116,13 +116,13 @@ DEBUG_FLAGS=$(PIC_FLAG) -g export DMD_TEST_COVERAGE= -runnable_tests=$(wildcard runnable/*.d) $(wildcard runnable/*.sh) +runnable_tests=$(wildcard runnable/*.d runnable/*.har runnable/*.sh) runnable_test_results=$(addsuffix .out,$(addprefix $(RESULTS_DIR)/,$(runnable_tests))) -compilable_tests=$(wildcard compilable/*.d) $(wildcard compilable/*.sh) +compilable_tests=$(wildcard compilable/*.d compilable/*.har compilable/*.sh) compilable_test_results=$(addsuffix .out,$(addprefix $(RESULTS_DIR)/,$(compilable_tests))) -fail_compilation_tests=$(wildcard fail_compilation/*.d) $(wildcard fail_compilation/*.html) +fail_compilation_tests=$(wildcard fail_compilation/*.d fail_compilation/*.har fail_compilation/*.html) fail_compilation_test_results=$(addsuffix .out,$(addprefix $(RESULTS_DIR)/,$(fail_compilation_tests))) all: run_tests @@ -167,13 +167,13 @@ start_fail_compilation_tests: $(RESULTS_DIR)/.created $(test_tools) @echo "Running fail compilation tests" $(QUIET)$(MAKE) --no-print-directory run_fail_compilation_tests -$(RESULTS_DIR)/d_do_test$(EXE): tools/d_do_test.d $(RESULTS_DIR)/.created +$(RESULTS_DIR)/d_do_test$(EXE): tools/d_do_test.d tools/archive/har.d $(RESULTS_DIR)/.created @echo "Building d_do_test tool" @echo "OS: '$(OS)'" @echo "MODEL: '$(MODEL)'" @echo "PIC: '$(PIC_FLAG)'" - $(DMD) -conf= $(MODEL_FLAG) $(DEBUG_FLAGS) -unittest -run $< - $(DMD) -conf= $(MODEL_FLAG) $(DEBUG_FLAGS) -od$(RESULTS_DIR) -of$(RESULTS_DIR)$(DSEP)d_do_test$(EXE) $< + $(DMD) -conf= $(MODEL_FLAG) $(DEBUG_FLAGS) -unittest tools/archive/har.d -run $< + $(DMD) -conf= $(MODEL_FLAG) $(DEBUG_FLAGS) -od$(RESULTS_DIR) -of$(RESULTS_DIR)$(DSEP)d_do_test$(EXE) $< tools/archive/har.d $(RESULTS_DIR)/sanitize_json$(EXE): tools/sanitize_json.d $(RESULTS_DIR)/.created @echo "Building sanitize_json tool" diff --git a/test/compilable/a3682.d b/test/compilable/a3682.har similarity index 78% rename from test/compilable/a3682.d rename to test/compilable/a3682.har index 132a0770da11..c06f80ca1d99 100644 --- a/test/compilable/a3682.d +++ b/test/compilable/a3682.har @@ -1,3 +1,4 @@ +--- a3682.d // COMPILED_IMPORTS: imports/b3682.d // PERMUTE_ARGS: @@ -18,3 +19,9 @@ struct Tuple(Types...) static assert(is(typeof(s) == Tuple!(float))); } } + +--- imports/b3682.d +module imports.b3682; + +import a3682; +alias Tuple!(int) tint; diff --git a/test/compilable/art4769.d b/test/compilable/art4769.d deleted file mode 100644 index 34adcda927b1..000000000000 --- a/test/compilable/art4769.d +++ /dev/null @@ -1,19 +0,0 @@ -// http://www.digitalmars.com/webnews/newsgroups.php?art_group=digitalmars.D.bugs&article_id=4769 - -// COMPILED_IMPORTS: imports/art4769a.d imports/art4769b.d -// PERMUTE_ARGS: - -module art4769; - -private import imports.art4769a; - -struct Vector(T) -{ - DataStreamability!(T).footype f; - - static if (DataStreamability!(T).isStreamable) - void writeTo() - { - } -} - diff --git a/test/compilable/art4769.har b/test/compilable/art4769.har new file mode 100644 index 000000000000..2b1ca1f2486c --- /dev/null +++ b/test/compilable/art4769.har @@ -0,0 +1,46 @@ +--- art4769.d +// http://www.digitalmars.com/webnews/newsgroups.php?art_group=digitalmars.D.bugs&article_id=4769 + +// COMPILED_IMPORTS: imports/art4769a.d imports/art4769b.d +// PERMUTE_ARGS: + +module art4769; + +private import imports.art4769a; + +struct Vector(T) +{ + DataStreamability!(T).footype f; + + static if (DataStreamability!(T).isStreamable) + void writeTo() + { + } +} + +--- imports/art4769a.d +module imports.art4769a; + +import core.stdc.stdio; + +template DataStreamability(T) +{ + const int isStreamable = true; + alias T footype; + + void write() + { + printf("hallo\n"); + } +} + +--- imports/art4769b.d +private import imports.art4769a; +private import art4769; + +int main(char [][] args) +{ + Vector!(wchar) str; + return 0; +} + diff --git a/test/compilable/harexample.har b/test/compilable/harexample.har new file mode 100644 index 000000000000..0e27c240a4dd --- /dev/null +++ b/test/compilable/harexample.har @@ -0,0 +1,32 @@ +--- main.d +/* +PERMUTE_ARGS: +*/ +import ibar, ibaz; +void main() +{ + ibarfunc(); + ibazfunc(); +} + +--- ibar.d +import bar, std.stdio; +void ibarfunc() +{ + writeln(thing); +} + +--- ibaz.d +import baz, std.stdio; +void ibazfunc() +{ + writeln(thing); +} + +--- bar.d +module baz; +enum thing = "this is baz from bar.d"; + +--- baz.d +module bar; +enum thing = "this is bar from baz.d"; diff --git a/test/compilable/imports/art4769a.d b/test/compilable/imports/art4769a.d deleted file mode 100644 index 7c994e6f0eb9..000000000000 --- a/test/compilable/imports/art4769a.d +++ /dev/null @@ -1,15 +0,0 @@ -module imports.art4769a; - -import core.stdc.stdio; - -template DataStreamability(T) -{ - const int isStreamable = true; - alias T footype; - - void write() - { - printf("hallo\n"); - } -} - diff --git a/test/compilable/imports/art4769b.d b/test/compilable/imports/art4769b.d deleted file mode 100644 index e43b2f201e05..000000000000 --- a/test/compilable/imports/art4769b.d +++ /dev/null @@ -1,9 +0,0 @@ -private import imports.art4769a; -private import art4769; - -int main(char [][] args) -{ - Vector!(wchar) str; - return 0; -} - diff --git a/test/compilable/imports/b3682.d b/test/compilable/imports/b3682.d deleted file mode 100644 index f554e12ee7e3..000000000000 --- a/test/compilable/imports/b3682.d +++ /dev/null @@ -1,5 +0,0 @@ -module imports.b3682; - -import a3682; -alias Tuple!(int) tint; - diff --git a/test/tools/archive/har.d b/test/tools/archive/har.d new file mode 100644 index 000000000000..07cbcee6c3ca --- /dev/null +++ b/test/tools/archive/har.d @@ -0,0 +1,338 @@ +/** +HAR - Human Archive Format + +https://github.com/marler8997/har + +HAR is a simple format to represent multiple files in a single block of text, i.e. +--- +--- main.d +import foo; +void main() +{ + foofunc(); +} +--- foo.d +module foo; +void foofunc() +{ +} +--- +*/ +module archive.har; + +import std.typecons : Flag, Yes, No; +import std.array : Appender; +import std.format : format; +import std.string : startsWith, indexOf, stripRight; +import std.utf : decode, replacementDchar; +import std.path : dirName, buildPath; +import std.file : exists, isDir, mkdirRecurse; +import std.stdio : File; + +class HarException : Exception +{ + this(string msg, string file, size_t line) + { + super(msg, file, line); + } +} + +struct HarExtractor +{ + string filenameForErrors; + string outputDir; + + private bool verbose; + private File verboseFile; + + bool dryRun; + + private size_t lineNumber; + private void extractMkdir(string dir, Flag!"forEmptyDir" forEmptyDir) + { + if (exists(dir)) + { + if (!isDir(dir)) + { + if (forEmptyDir) + throw harFileException("cannot extract empty directory %s since it already exists as non-directory", + dir.formatDir); + throw harFileException("cannot extract files to non-directory %s", dir.formatDir); + } + } + else + { + if (verbose) + verboseFile.writefln("mkdir %s", dir.formatDir); + if (!dryRun) + mkdirRecurse(dir); + } + } + + void enableVerbose(File verboseFile) + { + this.verbose = true; + this.verboseFile = verboseFile; + } + + void extractFromFile(T)(string harFilename, T fileInfoCallback) + { + this.filenameForErrors = harFilename; + auto harFile = File(harFilename, "r"); + extract(harFile.byLine(Yes.keepTerminator), fileInfoCallback); + } + + void extract(T, U)(T lineRange, U fileInfoCallback) + { + if (outputDir is null) + outputDir = ""; + + lineNumber = 1; + if (lineRange.empty) + throw harFileException("file is empty"); + + auto line = lineRange.front; + auto firstLineSpaceIndex = line.indexOf(' '); + if (firstLineSpaceIndex <= 0) + throw harFileException("first line does not start with a delimiter ending with a space"); + + auto delimiter = line[0 .. firstLineSpaceIndex + 1].idup; + + LfileLoop: + for (;;) + { + auto fileInfo = parseFileLine(line[delimiter.length .. $], delimiter[0]); + auto fullFileName = buildPath(outputDir, fileInfo.filename); + fileInfoCallback(fullFileName, fileInfo); + + if (fullFileName[$-1] == '/') + { + if (!dryRun) + extractMkdir(fullFileName, Yes.forEmptyDir); + lineRange.popFront(); + if (lineRange.empty) + break; + lineNumber++; + line = lineRange.front; + if (!line.startsWith(delimiter)) + throw harFileException("expected delimiter after empty directory"); + continue; + } + + { + auto dir = dirName(fileInfo.filename); + if (dir.length > 0) + { + auto fullDir = buildPath(outputDir, dir); + extractMkdir(fullDir, No.forEmptyDir); + } + } + if (verbose) + verboseFile.writefln("creating %s", fullFileName.formatFile); + { + File currentOutputFile; + if (!dryRun) + currentOutputFile = File(fullFileName, "w"); + scope(exit) + { + if (!dryRun) + currentOutputFile.close(); + } + for (;;) + { + lineRange.popFront(); + if (lineRange.empty) + break LfileLoop; + lineNumber++; + line = lineRange.front; + if (line.startsWith(delimiter)) + break; + if (!dryRun) + currentOutputFile.write(line); + } + } + } + } + private HarException harFileException(T...)(string fmt, T args) if (T.length > 0) + { + return harFileException(format(fmt, args)); + } + private HarException harFileException(string msg) + { + return new HarException(msg, filenameForErrors, lineNumber); + } + + FileProperties parseFileLine(const(char)[] line, char firstDelimiterChar) + { + if (line.length == 0) + throw harFileException("missing filename"); + + const(char)[] filename; + const(char)[] rest; + if (line[0] == '"') + { + size_t afterFileIndex; + filename = parseQuotedFilename(line[1 .. $], &afterFileIndex); + rest = line[afterFileIndex .. $]; + } + else + { + filename = parseFilename(line); + rest = line[filename.length .. $]; + } + for (;;) + { + rest = skipSpaces(rest); + if (rest.length == 0 || rest == "\n" || rest == "\r" || rest == "\r\n" || rest[0] == firstDelimiterChar) + break; + throw harFileException("properties not implemented '%s'", rest); + } + return FileProperties(filename); + } + + void checkComponent(const(char)[] component) + { + if (component.length == 0) + throw harFileException("invalid filename, contains double slash '//'"); + if (component == "..") + throw harFileException("invalid filename, contains double dot '..' parent directory"); + } + + inout(char)[] parseFilename(inout(char)[] line) + { + if (line.length == 0 || isEndOfFileChar(line[0])) + throw harFileException("missing filename"); + + if (line[0] == '/') + throw harFileException("absolute filenames are invalid"); + + size_t start = 0; + size_t next = 0; + while (true) + { + auto cIndex = next; + auto c = decode!(Yes.useReplacementDchar)(line, next); + if (c == replacementDchar) + throw harFileException("invalid utf8 sequence"); + + if (c == '/') + { + checkComponent(line[start .. cIndex]); + if (next >= line.length) + return line[0 .. next]; + start = next; + } + else if (isEndOfFileChar(c)) + { + checkComponent(line[start .. cIndex]); + return line[0 .. cIndex]; + } + + if (next >= line.length) + { + checkComponent(line[start .. next]); + return line[0 ..next]; + } + } + } + + inout(char)[] parseQuotedFilename(inout(char)[] line, size_t* afterFileIndex) + { + if (line.length == 0) + throw harFileException("filename missing end-quote"); + if (line[0] == '"') + throw harFileException("empty filename"); + if (line[0] == '/') + throw harFileException("absolute filenames are invalid"); + + size_t start = 0; + size_t next = 0; + while(true) + { + auto cIndex = next; + auto c = decode!(Yes.useReplacementDchar)(line, next); + if (c == replacementDchar) + throw harFileException("invalid utf8 sequence"); + + if (c == '/') + { + checkComponent(line[start .. cIndex]); + start = next; + } + else if (c == '"') + { + checkComponent(line[start .. cIndex]); + *afterFileIndex = next + 1; + return line[0 .. cIndex]; + } + if (next >= line.length) + throw harFileException("filename missing end-quote"); + } + } +} + +private inout(char)[] skipSpaces(inout(char)[] str) +{ + size_t i = 0; + for (; i < str.length; i++) + { + if (str[i] != ' ') + break; + } + return str[i .. $]; +} + +private bool isEndOfFileChar(C)(const(C) c) +{ + return c == '\n' || c == ' ' || c == '\r'; +} + +struct FileProperties +{ + const(char)[] filename; +} + +auto formatDir(const(char)[] dir) +{ + if (dir.length == 0) + dir = "."; + + return formatQuotedIfSpaces(dir); +} +auto formatFile(const(char)[] file) + in { assert(file.length > 0); } do +{ + return formatQuotedIfSpaces(file); +} + +// returns a formatter that will print the given string. it will print +// it surrounded with quotes if the string contains any spaces. +auto formatQuotedIfSpaces(T...)(T args) +if (T.length > 0) +{ + struct Formatter + { + T args; + void toString(scope void delegate(const(char)[]) sink) const + { + import std.string : indexOf; + bool useQuotes = false; + foreach (arg; args) + { + if (arg.indexOf(' ') >= 0) + { + useQuotes = true; + break; + } + } + + if (useQuotes) + sink(`"`); + foreach (arg; args) + sink(arg); + if (useQuotes) + sink(`"`); + } + } + return Formatter(args); +} diff --git a/test/tools/d_do_test.d b/test/tools/d_do_test.d index 464dd8d7cc80..326728b048fe 100755 --- a/test/tools/d_do_test.d +++ b/test/tools/d_do_test.d @@ -536,25 +536,22 @@ int tryMain(string[] args) return 1; } - auto test_file = args[1]; - string input_dir = test_file.dirName(); + const test_file = args[1]; + const test_dir = test_file.dirName(); TestArgs testArgs; - switch (input_dir) + switch (test_dir) { case "compilable": testArgs.mode = TestMode.COMPILE; break; case "fail_compilation": testArgs.mode = TestMode.FAIL_COMPILE; break; case "runnable": testArgs.mode = TestMode.RUN; break; default: - writefln("Error: invalid test directory '%s', expected 'compilable', 'fail_compilation', or 'runnable'", input_dir); + writefln("Error: invalid test directory '%s', expected 'compilable', 'fail_compilation', or 'runnable'", test_dir); return 1; } - string test_base_name = test_file.baseName(); - string test_name = test_base_name.stripExtension(); - - if (test_base_name.extension() == ".sh") - return runBashTest(input_dir, test_name); + const test_base_name = test_file.baseName(); + const test_name = test_base_name.stripExtension(); EnvData envData; envData.all_args = environment.get("ARGS"); @@ -573,11 +570,26 @@ int tryMain(string[] args) envData.coverage_build = environment.get("DMD_TEST_COVERAGE") == "1"; envData.autoUpdate = environment.get("AUTO_UPDATE", "") == "1"; - string result_path = envData.results_dir ~ envData.sep; - string input_file = input_dir ~ envData.sep ~ test_base_name; - string output_dir = result_path ~ input_dir; - string output_file = result_path ~ input_file ~ ".out"; - string test_app_dmd_base = output_dir ~ envData.sep ~ test_name ~ "_"; + string input_dir; + string input_file; + if (test_file.extension() == ".har") + { + const input = extractHarTest(envData.results_dir, test_file, input_dir, test_base_name); + input_dir = input.dir; + input_file = input.file; + } + else + { + input_dir = test_dir; + input_file = test_file; + } + if (test_file.extension() == ".sh") + return runBashTest(input_dir, test_name); + + const result_path = envData.results_dir ~ envData.sep; + const output_dir = result_path ~ test_dir; + const output_file = result_path ~ test_dir ~ envData.sep ~ test_base_name ~ ".out"; + const test_app_dmd_base = output_dir ~ envData.sep ~ test_name ~ "_"; // running & linking costs time - for coverage builds we can save this if (envData.coverage_build && testArgs.mode == TestMode.RUN) @@ -643,7 +655,7 @@ int tryMain(string[] args) return 1; writef(" ... %-30s %s%s(%s)", - input_file, + test_file, testArgs.requiredArgs, (!testArgs.requiredArgs.empty ? " " : ""), testArgs.permuteArgs); @@ -824,27 +836,27 @@ int tryMain(string[] args) // remove the output file in test_results as its outdated output_file.remove(); - auto existingText = input_file.readText; + auto existingText = test_file.readText; auto updatedText = existingText.replace(ce.expected, ce.actual); if (existingText != updatedText) { - std.file.write(input_file, updatedText); - writefln("==> `TEST_OUTPUT` of %s has been updated", input_file); + std.file.write(test_file, updatedText); + writefln("==> `TEST_OUTPUT` of %s has been updated", test_file); } else { - writefln("WARNING: %s has multiple `TEST_OUTPUT` blocks and can't be auto-updated", input_file); + writefln("WARNING: %s has multiple `TEST_OUTPUT` blocks and can't be auto-updated", test_file); } return Result.return0; } f.writeln(); f.writeln("=============================="); - f.writef("Test %s failed: ", input_file); + f.writef("Test %s failed: ", test_file); f.writeln(e.msg); f.close(); - writefln("Test %s failed. The logged output:", input_file); + writefln("Test %s failed. The logged output:", test_file); writeln(output_file.readText); output_file.remove(); @@ -883,7 +895,7 @@ int tryMain(string[] args) // it was disabled but it passed! print an informational message if (testArgs.disabled) - writefln(" !!! %-30s DISABLED but PASSES!", input_file); + writefln(" !!! %-30s DISABLED but PASSES!", test_file); return 0; } @@ -901,3 +913,36 @@ int runBashTest(string input_dir, string test_name) } return process.wait(); } + +struct Input +{ + string dir; + string file; +} +Input extractHarTest(string results_dir, string test_file, string input_dir, string test_base_name) +{ + import archive.har; + + input_dir = buildPath(results_dir, input_dir, test_base_name ~ ".extracted"); + + auto extractor = HarExtractor(); + extractor.outputDir = input_dir; + auto extracted = appender!(string[])(); + try + { + extractor.extractFromFile(test_file, delegate(string fullFileName, FileProperties fileProps) { + extracted.put(fullFileName.idup); + }); + } + catch(HarException e) + { + writefln("Error: failed to extract %s: %s", test_file, e.msg); + throw new SilentQuit(); + } + if (extracted.data.length == 0) + { + writefln("Error: %s did not contain any files", test_file); + throw new SilentQuit(); + } + return Input(input_dir, extracted.data[0]); +}