Ver código fonte

Running tests on MS-Windows using the Builder build system.

David Piuva 10 meses atrás
pai
commit
06b4ae7d49

+ 27 - 4
.github/workflows/ci.yml

@@ -3,16 +3,39 @@
 name: DFPSR tests
 on: [push]
 jobs:
-  test:
+  scriptedTest:
     runs-on: ${{ matrix.os }}
     strategy:
       matrix:
         os: [ubuntu-latest, macos-latest]
-        architecture: [x86_32, x86_64, arm, arm64]
     steps:
       - name: Checkout
         uses: actions/checkout@v4
-      - name: Run tests
+      - name: Run tests on Linux
+        if: matrix.os == 'ubuntu-latest'
         run: |
-          cd ./Source
+          cd ./Source/test
           ./test.sh
+      - name: Run tests on MacOS
+        if: matrix.os == 'macos-latest'
+        run: |
+          cd ./Source/test
+          ./test.sh
+  builderTest:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        os: [ubuntu-latest, macos-latest, windows-latest]
+        architecture: [x86_64, arm64]
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+      - name: Run tests on Linux
+        if: matrix.os == 'ubuntu-latest'
+        run: ./Source/test/test_linux.sh
+      - name: Run tests on MacOS
+        if: matrix.os == 'macos-latest'
+        run: ./Source/test/test_macos.sh
+      - name: Run tests on Windows
+        if: matrix.os == 'windows-latest'
+        run: .\Source\test\test_windows.bat

+ 27 - 4
.github/workflows/ci.yml.tabs

@@ -2,16 +2,39 @@
 name: DFPSR tests
 on: [push]
 jobs:
-	test:
+	scriptedTest:
 		runs-on: ${{ matrix.os }}
 		strategy:
 			matrix:
 				os: [ubuntu-latest, macos-latest]
-				architecture: [x86_32, x86_64, arm, arm64]
 		steps:
 			- name: Checkout
 				uses: actions/checkout@v4
-			- name: Run tests
+			- name: Run tests on Linux
+				if: matrix.os == 'ubuntu-latest'
 				run: |
-					cd ./Source
+					cd ./Source/test
 					./test.sh
+			- name: Run tests on MacOS
+				if: matrix.os == 'macos-latest'
+				run: |
+					cd ./Source/test
+					./test.sh
+	builderTest:
+		runs-on: ${{ matrix.os }}
+		strategy:
+			matrix:
+				os: [ubuntu-latest, macos-latest, windows-latest]
+				architecture: [x86_64, arm64]
+		steps:
+			- name: Checkout
+				uses: actions/checkout@v4
+			- name: Run tests on Linux
+				if: matrix.os == 'ubuntu-latest'
+				run: ./Source/test/test_linux.sh
+			- name: Run tests on MacOS
+				if: matrix.os == 'macos-latest'
+				run: ./Source/test/test_macos.sh
+			- name: Run tests on Windows
+				if: matrix.os == 'windows-latest'
+				run: .\Source\test\test_windows.bat

+ 1 - 12
Doc/Generator/Input/Starting.txt

@@ -44,11 +44,6 @@ On Debian based systems:
 sudo apt install libx11-dev
 sudo apt install libasound2-dev</BLOCKQUOTE></PRE>
 
-*
-Check that you have g++ installed at /usr/bin/g++ and change CPP_COMPILER_PATH in Source/tools/builder/buildProject.sh if it's located somewhere else.
-You can also change TEMPORARY_FOLDER if using another system than Linux where /tmp is called something else, or you want to keep objects in a persistent folder after your system reboots.
-The advantage of using /tmp, is that the files there can be placed in memory and you don't have to erase them manually.
-
 *
 Go to the Source\tools\wizard folder in a terminal.
 
@@ -63,15 +58,9 @@ Run the build script.
 Title2: Buliding the wizard application on Windows
 
 *
-Download a mingw edition of CodeBlocks from their website. <A href="http://www.codeblocks.org/downloads/26#windows">www.codeblocks.org/downloads</A>
+Download and install a mingw edition of CodeBlocks from their website. <A href="http://www.codeblocks.org/downloads/26#windows">www.codeblocks.org/downloads</A>
 This is the easiest way to install GNU's C++ compiler g++ on Windows, but CodeBlocks can also be used as a debugger if you create a project with the same source code, backends and compiler flags.
 
-*
-Open Source\tools\builder\buildProject.bat in a text editor.
-
-*
-Check that CPP_COMPILER_FOLDER and CPP_COMPILER_PATH refers to where your GNU C++ compiler is installed. If you installed CodeBlocks in the default path C:\Program, this should usually be the case. Otherwise just update the path.
-
 *
 Open CMD.exe, go to the Source\tools\wizard folder and execute build_windows.bat from the same folder.
 This makes sure that the build system can be found using relative paths.

+ 16 - 11
Doc/Generator/Input/StyleGuide.txt

@@ -4,26 +4,26 @@ Title: Code convention for David Forsgren Piuva's Software Renderer
 
 To keep the style consistent, the style being used in the library is explained in this document.
 ---
-1. Tabs for indentation then spaces for alignment. It's the best of both worlds by both having variable length tabs	and correct alignment that works between lines of the same indentation.
+Tabs for indentation then spaces for alignment. It's the best of both worlds by both having variable length tabs	and correct alignment that works between lines of the same indentation.
 ---
-2. No new line for opening brackets.
+No new line for opening brackets.
 Makes the code more compact and decreases the risk of copy-paste errors.
 ---
-3. No hpp extensions, use h for all headers. Could be either way, but this library uses *.h for compact naming, so keep it consistent.
+No hpp extensions, use h for all headers. Could be either way, but this library uses *.h for compact naming, so keep it consistent.
 ---
-4. C-style casting for raw data manipulation and C++-style for high-level classes when possible.
+C-style casting for raw data manipulation and C++-style for high-level classes when possible.
 When using assembly intrinsics and raw pointer manipulation to alter the state of bits,
 verbose high-level abstractions only make it harder to count CPU cycles in your head.
 Always use the tool that makes sense for the problem you're trying to solve.
 C++ style is for things that are abstracted on a higher level.
 C style is for when a byte is just a byte and you just want to manipulate it in a specific way.
 ---
-5. Follow the most relevant standard without making contemporary assumptions.
+Follow the most relevant standard without making contemporary assumptions.
 For code not intended for a specific system, follow the C++ standard.
 For code targeting a certain hardware using intrinsic functions, follow the hardware's standard.
 For code targeting a certain operating system, follow the operating system's standard.
 ---
-6. Do not assume that a type has a certain size or format unless it is specified explicitly.
+Do not assume that a type has a certain size or format unless it is specified explicitly.
 The int type is not always 32 bits, so only use when 16 bits are enough, use int32_t for a signed 32-bit integer.
 Fixed integers such as uint8_t, uint16_t, uint32_t, uint64_t, int32_t and int64_t are preferred.
 For bit manipulation, use unsigned integers to avoid depending on two's complement.
@@ -32,22 +32,27 @@ The float type does not have to be any of the IEEE standards according to the C+
 std::string is not used, because it has an undefined character encoding, so use dsr::String or dsr::ReadableString with UTF-32 instead.
 char* should only be used for constant string literals and interfacing with external libraries.
 ---
-7. The code should work for both little-endian and big-endian, because both still exist.
+The code should work for both little-endian and big-endian, because both still exist.
 You may however ignore mixed-endian.
 ---
-8. Do not call member methods with "this" set to null, because that is undefined behavior.
+Do not call member methods with "this" set to null, because that is undefined behavior.
 ---
-9. Avoid mixing side-effects with expressions for determinism across compilers.
+Leave an empty line at the end of each source document.
+Even though the C++ standard tells compilers to ignore line breaks as white space during parsing, white space is still used to separate the tokens that are used.
+A future C++ compiler might be designed to allow interactive input directly from the developer and ignore end of file for consistent behavior between command line input and source files.
+So without a line break at the end, the last token in a cpp file may be ignored on some compilers.
+---
+Avoid mixing side-effects with expressions for determinism across compilers.
 Non-deterministic expressions such as ((x++ - ++x) * x--) should never be used, so use ++ and -- in separate statements.
 Side-effects within the same depth of an expressions may be evaluated in any order because it is not specified in C++.
 Checking the return value of a function with side-effects is okay, because the side effect always come before returning the result in the called function.
 Lazy evaluation such as x != nullptr && foo(x) is okay, because lazy evaluation is well specified as only evaluating the right hand side when needed.
 Call chaining such as constructor(args).setSomeValue(value).setSomeOtherValue(value) is okay, because the execution order is explicit from differences in expression depth.
 ---
-10. Use the std library as little as possible.
+Use the std library as little as possible.
 Each compiler, operating system and standard library implementation has subtle differences in how things work, which can cause programs to break on another computer.
 The goal of this framework is to make things more consistent across platforms, so that code that works on one computer is more likely to work on another computer.
 ---
-11. Don't over-use the auto keyword.
+Don't over-use the auto keyword.
 Spelling out the type explicitly makes the code easier to read.
 ---

+ 1 - 15
Doc/Starting.html

@@ -77,12 +77,6 @@ On Debian based systems:
 sudo apt install libx11-dev
 sudo apt install libasound2-dev</BLOCKQUOTE></PRE>
 
-</P><P>
-<IMG SRC="Images/SmallDot.png">
-Check that you have g++ installed at /usr/bin/g++ and change CPP_COMPILER_PATH in Source/tools/builder/buildProject.sh if it's located somewhere else.
-You can also change TEMPORARY_FOLDER if using another system than Linux where /tmp is called something else, or you want to keep objects in a persistent folder after your system reboots.
-The advantage of using /tmp, is that the files there can be placed in memory and you don't have to erase them manually.
-
 </P><P>
 <IMG SRC="Images/SmallDot.png">
 Go to the Source\tools\wizard folder in a terminal.
@@ -100,17 +94,9 @@ Run the build script.
 </P><H2> Buliding the wizard application on Windows</H2><P>
 </P><P>
 <IMG SRC="Images/SmallDot.png">
-Download a mingw edition of CodeBlocks from their website. <A href="http://www.codeblocks.org/downloads/26#windows">www.codeblocks.org/downloads</A>
+Download and install a mingw edition of CodeBlocks from their website. <A href="http://www.codeblocks.org/downloads/26#windows">www.codeblocks.org/downloads</A>
 This is the easiest way to install GNU's C++ compiler g++ on Windows, but CodeBlocks can also be used as a debugger if you create a project with the same source code, backends and compiler flags.
 
-</P><P>
-<IMG SRC="Images/SmallDot.png">
-Open Source\tools\builder\buildProject.bat in a text editor.
-
-</P><P>
-<IMG SRC="Images/SmallDot.png">
-Check that CPP_COMPILER_FOLDER and CPP_COMPILER_PATH refers to where your GNU C++ compiler is installed. If you installed CodeBlocks in the default path C:\Program, this should usually be the case. Otherwise just update the path.
-
 </P><P>
 <IMG SRC="Images/SmallDot.png">
 Open CMD.exe, go to the Source\tools\wizard folder and execute build_windows.bat from the same folder.

+ 37 - 24
Doc/StyleGuide.html

@@ -28,44 +28,57 @@ A:active { color: #FFFFFF; background: #444444; }
 </P><P>
 To keep the style consistent, the style being used in the library is explained in this document.
 </P><IMG SRC="Images/Border.png"><P>
-1. Use common sense! If it looks wrong to human readers then it's wrong. Don't defeat the purpose of any rule by taking it too far.
+Tabs for indentation then spaces for alignment. It's the best of both worlds by both having variable length tabs	and correct alignment that works between lines of the same indentation.
 </P><IMG SRC="Images/Border.png"><P>
-2. Don't use iterators when there is any other way to accomplish the task. You can't write efficient algorithms without knowing the data structures.
-</P><IMG SRC="Images/Border.png"><P>
-3. Tabs for indentation then spaces for alignment. It's the best of both worlds by both having variable length tabs	and correct alignment that works between lines of the same indentation.
-</P><IMG SRC="Images/Border.png"><P>
-4. No dangling else, use explicit {} for safety. Otherwise someone might add an extra statement and get random crashes.
+No new line for opening brackets.
+Makes the code more compact and decreases the risk of copy-paste errors.
 </P><IMG SRC="Images/Border.png"><P>
-5. No hpp extensions, use h for all headers. Could be either way, but this library uses *.h for compact naming, so keep it consistent.
+No hpp extensions, use h for all headers. Could be either way, but this library uses *.h for compact naming, so keep it consistent.
 </P><IMG SRC="Images/Border.png"><P>
-6. C-style casting for raw data manipulation and C++-style for high-level classes.
+C-style casting for raw data manipulation and C++-style for high-level classes when possible.
 When using assembly intrinsics and raw pointer manipulation to alter the state of bits,
 verbose high-level abstractions only make it harder to count CPU cycles in your head.
 Always use the tool that makes sense for the problem you're trying to solve.
 C++ style is for things that are abstracted on a higher level.
 C style is for when a byte is just a byte and you just want to manipulate it in a specific way.
 </P><IMG SRC="Images/Border.png"><P>
-7. Don't call member methods with "this" set to nullptr.
-This would be undefined behavior and may randomly crash.
-Use global functions instead. They allow checking pointers for null
-because they are explicit arguments declared by the programmer.
+Follow the most relevant standard without making contemporary assumptions.
+For code not intended for a specific system, follow the C++ standard.
+For code targeting a certain hardware using intrinsic functions, follow the hardware's standard.
+For code targeting a certain operating system, follow the operating system's standard.
 </P><IMG SRC="Images/Border.png"><P>
-8. Avoid using STD/STL directly in SDK examples.
-Exposing types from the standard library should be done using an alias or wrapper in the dsr namespace.
-This allow replacing the standard library without breaking backward compatibility.
-The C++ standard libraries have broken backward compatibility before and it can happen again.
+Do not assume that a type has a certain size or format unless it is specified explicitly.
+The int type is not always 32 bits, so only use when 16 bits are enough, use int32_t for a signed 32-bit integer.
+Fixed integers such as uint8_t, uint16_t, uint32_t, uint64_t, int32_t and int64_t are preferred.
+For bit manipulation, use unsigned integers to avoid depending on two's complement.
+The char type is usually 8 bits large, but it is not specified by the C++ standard, so use uint8_t instead for buffers and DsrChar for 32-bit Unicode characters.
+The float type does not have to be any of the IEEE standards according to the C++ standard, but you can assume properties that are specified in a relevant standard.
+std::string is not used, because it has an undefined character encoding, so use dsr::String or dsr::ReadableString with UTF-32 instead.
+char* should only be used for constant string literals and interfacing with external libraries.
 </P><IMG SRC="Images/Border.png"><P>
-9. Don't abuse the auto keyword everywhere just to make it look more "modern".
-Explicit type safety is what makes compiled languages safer than scripting.
+The code should work for both little-endian and big-endian, because both still exist.
+You may however ignore mixed-endian.
 </P><IMG SRC="Images/Border.png"><P>
-10. No new line for opening brackets.
-Makes the code more compact and decreases the risk of copy-paste errors.
+Do not call member methods with "this" set to null, because that is undefined behavior.
+</P><IMG SRC="Images/Border.png"><P>
+Leave an empty line at the end of each source document.
+Even though the C++ standard tells compilers to ignore line breaks as white space during parsing, white space is still used to separate the tokens that are used.
+A future C++ compiler might be designed to allow interactive input directly from the developer and ignore end of file for consistent behavior between command line input and source files.
+So without a line break at the end, the last token in a cpp file may be ignored on some compilers.
+</P><IMG SRC="Images/Border.png"><P>
+Avoid mixing side-effects with expressions for determinism across compilers.
+Non-deterministic expressions such as ((x++ - ++x) * x--) should never be used, so use ++ and -- in separate statements.
+Side-effects within the same depth of an expressions may be evaluated in any order because it is not specified in C++.
+Checking the return value of a function with side-effects is okay, because the side effect always come before returning the result in the called function.
+Lazy evaluation such as x != nullptr && foo(x) is okay, because lazy evaluation is well specified as only evaluating the right hand side when needed.
+Call chaining such as constructor(args).setSomeValue(value).setSomeOtherValue(value) is okay, because the execution order is explicit from differences in expression depth.
 </P><IMG SRC="Images/Border.png"><P>
-11. Don't fix the style of someone else's code if you can easily read it.
-Especially if there's no style rule explicitly supporting the change.
-Otherwise style changes will defeat the purpose by introducing more version conflicts.
+Use the std library as little as possible.
+Each compiler, operating system and standard library implementation has subtle differences in how things work, which can cause programs to break on another computer.
+The goal of this framework is to make things more consistent across platforms, so that code that works on one computer is more likely to work on another computer.
 </P><IMG SRC="Images/Border.png"><P>
-12. Don't change things that you don't know how to test.
+Don't over-use the auto keyword.
+Spelling out the type explicitly makes the code easier to read.
 </P><IMG SRC="Images/Border.png"><P>
 </P>
 </BODY> </HTML>

+ 3 - 0
Source/DFPSR/DFPSR.DsrHead

@@ -15,6 +15,9 @@
 if Linux
 	Message "Building for Linux\n"
 end if
+if MacOS
+	Message "Building for MacOS\n"
+end if
 if Windows
 	Message "Building for Windows\n"
 end if

+ 9 - 2
Source/DFPSR/api/fileAPI.cpp

@@ -744,7 +744,7 @@ DsrProcessStatus process_getStatus(const DsrProcess &process) {
 	}
 }
 
-DsrProcess process_execute(const ReadableString& programPath, List<String> arguments) {
+DsrProcess process_execute(const ReadableString& programPath, List<String> arguments, bool mustWork) {
 	// Convert the program path into the native format.
 	String optimizedPath = file_optimizePath(programPath, LOCAL_PATH_SYNTAX);
 	// Convert
@@ -771,6 +771,9 @@ DsrProcess process_execute(const ReadableString& programPath, List<String> argum
 		if (CreateProcessW(nullptr, (LPWSTR)nativeArgs, nullptr, nullptr, true, 0, nullptr, nullptr, &startInfo, &processInfo)) {
 			return handle_create<DsrProcessImpl>(processInfo).setName("DSR Process"); // Success
 		} else {
+			if (mustWork) {
+				throwError(U"Failed to call ", programPath, U"! False returned from CreateProcessW.\n");
+			}
 			return DsrProcess(); // Failure
 		}
 	#else
@@ -793,9 +796,13 @@ DsrProcess process_execute(const ReadableString& programPath, List<String> argum
 		}
 		argv[currentArg] = nullptr;
 		pid_t pid = 0;
-		if (posix_spawn(&pid, nativePath, nullptr, nullptr, (char**)argv.getUnsafe(), environ) == 0) {
+		int error = posix_spawn(&pid, nativePath, nullptr, nullptr, (char**)argv.getUnsafe(), environ);
+		if (error == 0) {
 			return handle_create<DsrProcessImpl>(pid).setName("DSR Process"); // Success
 		} else {
+			if (mustWork) {
+				throwError(U"Failed to call ", programPath, U"! Got error code ", error, " from posix_spawn.\n");
+			}
 			return DsrProcess(); // Failure
 		}
 	#endif

+ 2 - 1
Source/DFPSR/api/fileAPI.h

@@ -309,9 +309,10 @@ namespace dsr {
 	// Path-syntax: According to the local computer.
 	// Pre-condition: The executable at path must exist and have execution rights.
 	// Side-effects: Starts the program at programPath, with programPath given again as argv[0] and arguments to argv[1...] to the program's main function.
+	//               If mustWork is true, then failure will throw an error.
 	// Post-condition: Returns a DsrProcess handle to the started process.
 	//                 On failure to start, the handle is empty and process_getStatus will then return DsrProcessStatus::NotStarted from it.
-	DsrProcess process_execute(const ReadableString& programPath, List<String> arguments);
+	DsrProcess process_execute(const ReadableString& programPath, List<String> arguments, bool mustWork = true);
 }
 
 #endif

+ 3 - 3
Source/DFPSR/api/stringAPI.h

@@ -289,11 +289,11 @@ inline String& string_toStreamIndented(String& target, const T &value, const Rea
 		static_assert(sizeof(long long) == 8, U"You need to implement integer printing for integers larger than 64 bits, or printing long long will be truncated!");
 		impl_toStreamIndented_int64(target, (int64_t)unsafeCast<long long>(value), indentation);
 	} else if (DSR_SAME_TYPE(T, unsigned short)) {
-		impl_toStreamIndented_int64(target, (int64_t)unsafeCast<short>(value), indentation);
+		impl_toStreamIndented_int64(target, (int64_t)unsafeCast<unsigned short>(value), indentation);
 	} else if (DSR_SAME_TYPE(T, unsigned int)) {
-		impl_toStreamIndented_int64(target, (int64_t)unsafeCast<int>(value), indentation);
+		impl_toStreamIndented_int64(target, (int64_t)unsafeCast<unsigned int>(value), indentation);
 	} else if (DSR_SAME_TYPE(T, unsigned long)) {
-		impl_toStreamIndented_int64(target, (int64_t)unsafeCast<long>(value), indentation);
+		impl_toStreamIndented_int64(target, (int64_t)unsafeCast<unsigned long>(value), indentation);
 	} else if (DSR_SAME_TYPE(T, unsigned long long)) {
 		static_assert(sizeof(unsigned long long) == 8, U"You need to implement integer printing for integers larger than 64 bits, or printing unsigned long long will be truncated!");
 		impl_toStreamIndented_int64(target, (int64_t)unsafeCast<unsigned long long>(value), indentation);

+ 15 - 14
Source/DFPSR/base/heap.cpp

@@ -24,21 +24,9 @@
 // TODO: Apply thread safety to more memory operations.
 //       heap_getUsedSize and heap_setUsedSize are often used together, forming a transaction without any mutex.
 
-#include "heap.h"
-#include "../api/stringAPI.h"
-#include "../api/timeAPI.h"
-#include <stdio.h>
-#include <new>
-#include "simd.h"
-
-#ifndef DISABLE_MULTI_THREADING
-	// Requires -pthread for linking
-	#include <thread>
-	#include <mutex>
-#endif
-
+#include "../settings.h"
 #if defined(USE_MICROSOFT_WINDOWS)	
-	#include <Windows.h> uint32_t getCacheLineSize() { return (uint32_t) (sizeof(void*) * 8); } // Approximation for ARM
+	#include <Windows.h>
 #elif defined(USE_MACOS)
 	#include <sys/sysctl.h>
 #elif defined(USE_LINUX)
@@ -47,6 +35,19 @@
 	#include <stdlib.h>
 #endif
 
+#ifndef DISABLE_MULTI_THREADING
+	// Requires -pthread for linking
+	#include <thread>
+	#include <mutex>
+#endif
+
+#include "heap.h"
+#include "../api/stringAPI.h"
+#include "../api/timeAPI.h"
+#include <stdio.h>
+#include <new>
+#include "simd.h"
+
 #ifdef SAFE_POINTER_CHECKS
 	#define DSR_PRINT_CACHE_LINE_SIZE
 	#define DSR_PRINT_NO_MEMORY_LEAK

+ 0 - 1
Source/check.sh

@@ -1,2 +1 @@
 cppcheck . --enable=all
-

+ 28 - 0
Source/test/TestCaller.DsrProj

@@ -0,0 +1,28 @@
+# Use C++ 2014.
+CompilerFlag "-std=c++14"
+
+# Use all locally available SIMD extensions.
+CompilerFlag "-march=native"
+
+Debug = 1
+Supressed = 1
+Graphics = 0
+Sound = 0
+Import "../DFPSR/DFPSR.DsrHead"
+
+# Compile and run each source file ending with Test.cpp in tests as its own project.
+#   All settings are inherited from the caller when using source files as projects.
+Projects from "*Test.cpp" in "tests"
+
+# TODO:
+# * Allow creating scopes for temporary settings, so that a stack keeps track of which
+#   settings were local to the scope and should be erased when leaving the scope.
+# * Or just make a method for clearing all local settings while keeping external settings such as target platform.
+
+# TestCaller needs to be called with specific arguments, so we keep supressing automatic execution for more control.
+
+# Enable to run faster by skipping compilation of testCaller when it already exists.
+#SkipIfBinaryExists
+
+# Compile the program that will run all the tests
+Crawl "testCaller.cpp"

+ 5 - 5
Source/test.sh → Source/test/test.sh

@@ -1,6 +1,6 @@
 #!/bin/bash
 
-ROOT_PATH=.
+ROOT_PATH=..
 TEMP_ROOT=${ROOT_PATH}/../../temporary
 CPP_VERSION=-std=c++14
 MODE="-DDEBUG"
@@ -8,7 +8,7 @@ DEBUGGER="-g"
 SIMD="-march=native"
 O_LEVEL=-O2
 
-chmod +x ${ROOT_PATH}/tools/build.sh;
+chmod +x ${ROOT_PATH}/tools/buildScripts/build.sh;
 ${ROOT_PATH}/tools/buildScripts/build.sh "NONE" "NONE" "${ROOT_PATH}" "${TEMP_ROOT}" "NONE" "${MODE} ${DEBUGGER} ${SIMD} ${CPP_VERSION} ${O_LEVEL}";
 if [ $? -ne 0 ]
 then
@@ -21,7 +21,7 @@ TEMP_SUB=$(echo $TEMP_SUB | tr "+" "p")
 TEMP_SUB=$(echo $TEMP_SUB | tr -d " =-")
 TEMP_DIR=${TEMP_ROOT}/${TEMP_SUB}
 
-for file in ./test/tests/*.cpp; do
+for file in ./tests/*.cpp; do
 	[ -e $file ] || continue
 	# Get name without path
 	name=${file##*/};
@@ -46,14 +46,14 @@ for file in ./test/tests/*.cpp; do
 	fi
 	# Run the test case
 	echo "Executing ${name}";
-	./${TEMP_DIR}/application;
+	./${TEMP_DIR}/application --path ./tests;
 	if [ $? -eq 0 ]
 	then
 		echo "Passed ${name}!";
 	else
 		echo "Failed ${name}!";
 		# Re-run with a memory debugger.
-		gdb -ex "run" -ex "bt" -ex "quit" --args ./${TEMP_DIR}/application;
+		gdb -ex "run" -ex "bt" -ex "quit" --args ./${TEMP_DIR}/application --path ./tests;
 		exit 1
 	fi
 done

+ 168 - 0
Source/test/testCaller.cpp

@@ -0,0 +1,168 @@
+
+// A program for calling the compiled tests.
+
+// TODO:
+// * Catch signals from ending the test with Ctrl-C to print a summary even if some tests were skipped.
+// * Create a flag to TestCaller for aborting instantly when a test fails instead of continuing with other tests.
+// * Allow creating a window and selecting which tests to run using a flag from the command line.
+
+#include "../DFPSR/includeEssentials.h"
+// TODO: Should timeAPI move to essentials when used by heap.cpp to wait for threads anyway?
+#include "../DFPSR/api/timeAPI.h"
+#include "../DFPSR/base/threading.h"
+#include "../DFPSR/base/noSimd.h"
+
+using namespace dsr;
+
+enum class TestResult {
+	None,   // Skipped or not yet executed.
+	Passed, // Passed the test.
+	Failed  // Crashed or just failed the test.
+};
+
+struct CompiledTest {
+	String name;
+	String programPath;
+	List<String> arguments;
+	DsrProcess process;
+	TestResult result;
+	CompiledTest(const ReadableString &programPath, const List<String> &arguments)
+	: name(file_getExtensionless(file_getPathlessName(programPath))), programPath(programPath), arguments(arguments), result(TestResult::None) {}
+};
+
+bool findCompiledTests(List<CompiledTest> &target, const ReadableString &folderPath) {
+	bool result = false;
+	printText(U"Finding tests in ", folderPath, U"\n");
+	file_getFolderContent(folderPath, [&target, &result, &folderPath](const ReadableString& entryPath, const ReadableString& entryName, EntryType entryType) {
+		if (entryType == EntryType::Folder) {
+			if (findCompiledTests(target, entryPath)) {
+				result = true;
+			}
+		} else if (entryType == EntryType::File) {
+			ReadableString extension = file_getExtension(entryName);
+			if (string_caseInsensitiveMatch(extension, U"C") || string_caseInsensitiveMatch(extension, U"CPP")) {
+				String programPath = file_getExtensionless(entryPath);
+				#if defined(USE_MICROSOFT_WINDOWS)
+					programPath = programPath + ".exe";
+				#endif
+				if (file_getEntryType(programPath) == EntryType::File) {
+					// Give each test the path to the source code's folder, so that it does not matter which build system is used or which folder the test is executed from.
+					target.pushConstruct(programPath, List<String>(U"--path", folderPath));
+					result = true;
+				}
+			}
+		}
+	});
+	return result;
+}
+
+static void startTest(CompiledTest &test) {
+	// Print each external call in the terminal for easy debugging when something goes wrong.
+	if (test.arguments.length() > 0) {
+		printText(U"Running test ", test.programPath, U" with");
+		for (int64_t a = 0; a < test.arguments.length(); a++) {
+			printText(U" ", test.arguments[a]);
+		}
+		printText(U"\n");
+	} else {
+		printText(U"Running test ", test.programPath, U"\n");
+	}
+	if (file_getEntryType(test.programPath) != EntryType::File) {
+		throwError(U"Failed to execute ", test.programPath, U", because the executable file was not found!\n");
+	} else {
+		test.process = process_execute(test.programPath, test.arguments);
+	}
+}
+
+DSR_MAIN_CALLER(dsrMain)
+void dsrMain(List<String> args) {
+	printText(U"Starting test runner:\n");
+	List<CompiledTest> tests;
+
+	// Printing any input arguments after the program.
+	for (int i = 1; i < args.length(); i++) {
+		printText(U"args[", i, U"] = ", args[i], U"\n");
+	}
+	for (int i = 1; i < args.length(); i++) {
+		String key = string_upperCase(args[i]);
+		String value;
+		if (i + 1 < args.length()) {
+			value = args[i + 1];
+		}
+		if (string_match(key, U"-T") || string_match(key, U"--TEST")) {
+			if (!findCompiledTests(tests, value)) {
+				throwError(U"Failed to find any tests at ", file_getAbsolutePath(value), U"!\n");
+			}
+		}
+	}
+	if (tests.length() == 0) {
+		throwError(U"TestCaller needs at least one folder path to run tests from! Use -t or --test followed by one folder path. To test multiple folders, use the flag again with another path.\n");
+	}
+	printText(tests.length(), U" tests to run:\n");
+	for (int64_t t = 0; t < tests.length(); t++) {
+		printText(U"* ", tests[t].name, U"\n");
+	}
+	int32_t workerCount = max(1, getThreadCount() - 1);
+	int32_t finishedTestCount = 0;
+	int32_t startedTestCount = 0;
+
+	int32_t passedCount = 0;
+	int32_t failedCount = 0;
+
+	while (finishedTestCount < tests.length()) {
+		// Start new tests.
+		if (startedTestCount - finishedTestCount < workerCount && startedTestCount < tests.length()) {
+			startTest(tests[startedTestCount]);
+			startedTestCount++;
+		}
+		// Wait for tests to finish.
+		if (startedTestCount > finishedTestCount) {
+			DsrProcessStatus status = process_getStatus(tests[finishedTestCount].process);
+			if (status == DsrProcessStatus::Completed) {
+				printText(U"Passed ", tests[finishedTestCount].name, U".\n");
+				tests[finishedTestCount].result = TestResult::Passed;
+				passedCount++;
+				finishedTestCount++;
+			} else if (status == DsrProcessStatus::Crashed) {
+				printText(U"Failed ", tests[finishedTestCount].name, U"!\n");
+				tests[finishedTestCount].result = TestResult::Failed;
+				failedCount++;
+				finishedTestCount++;
+			}
+			// Wait for a while to let the main thread respond to system interrupts while other cores are running tests.
+			time_sleepSeconds(0.1);
+		}
+	}
+	int skippedCount = tests.length() - (passedCount + failedCount);
+	if (passedCount > 0) {
+		printText(U"Passed ", passedCount, U" tests:\n");
+		for (int64_t t = 0; t < tests.length(); t++) {
+			if (tests[t].result == TestResult::Passed) {
+				printText(U"* ", tests[t].name, U"\n");
+			}
+		}
+	}
+	if (failedCount > 0) {
+		printText(U"Failed ", failedCount, U" tests:\n");
+		for (int64_t t = 0; t < tests.length(); t++) {
+			if (tests[t].result == TestResult::Failed) {
+				printText(U"(Failed!) ", tests[t].name, U"\n");
+			}
+		}
+	}
+	if (skippedCount > 0) {
+		printText(U"Skipped ", skippedCount, U" tests:\n");
+		for (int64_t t = 0; t < tests.length(); t++) {
+			if (tests[t].result == TestResult::None) {
+				printText(U"(Skipped!) ", tests[t].name, U"\n");
+			}
+		}
+	}
+	if (passedCount == tests.length() && failedCount == 0) {
+		printText(U"All tests passed!\n");
+	} else if (passedCount + failedCount == tests.length()) {
+		throwError(U"Failed tests!\n");
+	} else {
+		throwError(U"Aborted tests!\n");
+	}
+}

+ 68 - 45
Source/test/testTools.h

@@ -1,6 +1,7 @@
 #ifndef TEST_TOOLS
 #define TEST_TOOLS
 
+// TODO: Make it faster to crawl source by only including what is needed by the test.
 #include "../DFPSR/includeFramework.h"
 #include <csignal>
 
@@ -18,10 +19,6 @@ static bool beginsWith(const ReadableString &message, const ReadableString &pref
 }
 
 static thread_local String ExpectedErrorPrefix;
-static thread_local bool failed = false;
-
-static const int PASSED = 0;
-static const int FAILED = 1;
 
 inline bool nearValue(float a, float b) {
 	return fabs(a - b) < 0.0001f;
@@ -41,12 +38,10 @@ static void messageHandler(const ReadableString &message, MessageType type) {
 		if (string_length(ExpectedErrorPrefix) == 0) {
 			// Unexpected error!
 			string_sendMessage_default(message, MessageType::Error);
-			failed = true;
 		} else {
 			// Expected error.
 			if (!beginsWith(message, ExpectedErrorPrefix)) {
 				string_sendMessage_default(string_combine(U"Unexpected message in error!\n\nMessage:\n", message, U"\n\nExpected prefix:\n", ExpectedErrorPrefix, U"\n\n"), MessageType::Error);
-				failed = true;
 			} else {
 				string_sendMessage_default(U"*", MessageType::StandardPrinting);
 			}
@@ -57,18 +52,38 @@ static void messageHandler(const ReadableString &message, MessageType type) {
 	}
 }
 
+static void handleArguments(const List<String> &args) {
+	for (int i = 1; i < args.length(); i++) {
+		String key = string_upperCase(args[i]);
+		String value;
+		if (i + 1 < args.length()) {
+			value = args[i + 1];
+		}
+		if (string_match(key, U"-P") || string_match(key, U"--PATH")) {
+			file_setCurrentPath(value);
+		}
+	}
+}
+
+static thread_local String testName = U"Uninitialized test\n";
+static thread_local String stateName = U"New thread\n";
+
 #define START_TEST(NAME) \
-	int main() { \
-		std::signal(SIGSEGV, [](int signal) { throwError(U"Segmentation fault!"); }); \
-		string_assignMessageHandler(&messageHandler); \
-		heap_startingApplication(); \
-		printText(U"Running test \"", #NAME, "\": ");
+DSR_MAIN_CALLER(dsrMain) \
+void dsrMain(List<String> args) { \
+	testName = #NAME; \
+	stateName = U"While Assigning message handler"; \
+	std::signal(SIGSEGV, [](int signal) { throwError(U"Segmentation fault from ", testName, U"! ", stateName); }); \
+	string_assignMessageHandler(&messageHandler); \
+	stateName = U"While handling arguments\n"; \
+	handleArguments(args); \
+	stateName = U"Test start\n"; \
+	printText(U"Running test \"", #NAME, "\": ");
 
 #define END_TEST \
-		printText(U" (done)\n"); \
-		heap_terminatingApplication(); \
-		return PASSED; \
-	}
+	printText(U" (done)\n"); \
+	stateName = U"After test end\n"; \
+}
 
 #define OP_EQUALS(A, B) ((A) == (B))
 #define OP_NOT_EQUALS(A, B) ((A) != (B))
@@ -79,44 +94,51 @@ static void messageHandler(const ReadableString &message, MessageType type) {
 
 // These can be used instead of ASSERT_CRASH to handle multiple template arguments that are not enclosed within ().
 #define BEGIN_CRASH(PREFIX) \
-	ExpectedErrorPrefix = PREFIX;
+	ExpectedErrorPrefix = PREFIX; \
+	stateName = string_combine(U"During expected crash starting with ", PREFIX, U"\n");
 #define END_CRASH \
-	ExpectedErrorPrefix = ""; \
-	if (failed) return FAILED;
+	ExpectedErrorPrefix = "";
 
 // Prefix is the expected start of the error message.
 //   Just enough to know that we triggered the right error message.
 #define ASSERT_CRASH(A, PREFIX) \
 	BEGIN_CRASH(PREFIX); \
 	(void)(A); \
-	END_CRASH
+	END_CRASH \
+	stateName = string_combine(U"After expected crash starting with ", PREFIX, U"\n");
 
 #define ASSERT(CONDITION) \
+	stateName = string_combine(U"While evaluating condition ", #CONDITION, U"\n"); \
 	if (CONDITION) { \
 		printText(U"*"); \
 	} else { \
-		printText(U"\n\n"); \
-		printText(U"_______________________________ FAIL _______________________________\n"); \
-		printText(U"\n"); \
-		printText(U"Failed assertion!\nCondition: ", #CONDITION, U"\n"); \
-		printText(U"____________________________________________________________________\n"); \
-		return FAILED; \
+		stateName = string_combine(U"While reporting failure for condition ", #CONDITION, U"\n"); \
+		throwError( \
+			U"\n\n", \
+			U"_______________________________ FAIL _______________________________\n", \
+			U"\n", \
+			U"Failed assertion!\nCondition: ", #CONDITION, U"\n", \
+			U"____________________________________________________________________\n" \
+		); \
 	} \
-	if (failed) return FAILED;
+	stateName = string_combine(U"After evaluating condition ", #CONDITION, U"\n");
+
 #define ASSERT_COMP(A, B, OP, OP_NAME) \
+	stateName = string_combine(U"While evaluating comparison ", #A, " ", OP_NAME, U" ", #B, U"\n"); \
 	if (OP(A, B)) { \
 		printText(U"*"); \
 	} else { \
-		printText(U"\n\n"); \
-		printText(U"_______________________________ FAIL _______________________________\n"); \
-		printText(U"\n"); \
-		printText(U"Condition: ", #A, " ", OP_NAME, U" ", #B, U"\n"); \
-		printText((A), " ", OP_NAME, " ", (B), U" is false.\n"); \
-		printText(U"____________________________________________________________________\n"); \
-		printText(U"\n\n"); \
-		return FAILED; \
+	stateName = string_combine(U"While reporting failure for comparison ", #A, " ", OP_NAME, U" ", #B, U"\n"); \
+		throwError( \
+			U"\n\n", \
+			U"_______________________________ FAIL _______________________________\n", \
+			U"\n", \
+			U"Condition: ", #A, " ", OP_NAME, U" ", #B, U"\n", \
+			(A), " ", OP_NAME, " ", (B), U" is false.\n", \
+			U"____________________________________________________________________\n" \
+		); \
 	} \
-	if (failed) return FAILED;
+	stateName = string_combine(U"After evaluating comparison ", #A, " ", OP_NAME, U" ", #B, U"\n");
 #define ASSERT_EQUAL(A, B) ASSERT_COMP(A, B, OP_EQUALS, "==")
 #define ASSERT_NOT_EQUAL(A, B) ASSERT_COMP(A, B, OP_NOT_EQUALS, "!=")
 #define ASSERT_LESSER(A, B) ASSERT_COMP(A, B, OP_LESSER, "<")
@@ -124,22 +146,23 @@ static void messageHandler(const ReadableString &message, MessageType type) {
 #define ASSERT_GREATER(A, B) ASSERT_COMP(A, B, OP_GREATER, ">")
 #define ASSERT_GREATER_OR_EQUAL(A, B) ASSERT_COMP(A, B, OP_GREATER_OR_EQUAL, ">=")
 #define ASSERT_NEAR(A, B) \
+	stateName = string_combine(U"While evaluating approximate comparison between ", #A, " and ", #B, U"\n"); \
 	if (nearValue(A, B)) { \
 		printText(U"*"); \
 	} else { \
-		printText(U"\n\n"); \
-		printText(U"_______________________________ FAIL _______________________________\n"); \
-		printText(U"\n"); \
-		printText(U"Condition: ", #A, U" = ", #B, U"\n"); \
-		printText((A), " is not close enough to ", (B), U"\n"); \
-		printText(U"____________________________________________________________________\n"); \
-		printText(U"\n\n"); \
-		return FAILED; \
+		stateName = string_combine(U"While reporting failure for approximate comparison between ", #A, " and ", #B, U"\n"); \
+		throwError( \
+			U"\n\n", \
+			U"_______________________________ FAIL _______________________________\n", \
+			U"\n", \
+			U"Condition: ", #A, U" = ", #B, U"\n", \
+			(A), " is not close enough to ", (B), U"\n", \
+			U"____________________________________________________________________\n" \
+		); \
 	} \
-	if (failed) return FAILED;
+	stateName = string_combine(U"After evaluating approximate comparison between ", #A, " and ", #B, U"\n");
 
 const dsr::String inputPath = dsr::string_combine(U"test", file_separator(), U"input", file_separator());
 const dsr::String expectedPath = dsr::string_combine(U"test", file_separator(), U"expected", file_separator());
 
 #endif
-

+ 25 - 0
Source/test/test_linux.sh

@@ -0,0 +1,25 @@
+#!/bin/bash
+
+# This script can be called from any path, as long as the first argument sais where this script is located.
+
+TEST_FOLDER=`dirname "$(realpath $0)"`
+echo TEST_FOLDER = "${TEST_FOLDER}"
+
+PROJECT_BUILD_SCRIPT="${TEST_FOLDER}/../tools/builder/buildProject.sh"
+echo PROJECT_BUILD_SCRIPT = "${PROJECT_BUILD_SCRIPT}"
+
+PROJECT_FILE="${TEST_FOLDER}/TestCaller.DsrProj"
+echo PROJECT_FILE = "${PROJECT_FILE}"
+
+# Give execution rights.
+chmod +x "${PROJECT_BUILD_SCRIPT}";
+
+# Build TestCaller and all its tests. 
+"${PROJECT_BUILD_SCRIPT}" "${PROJECT_FILE}" Linux $@;
+if [ $? -ne 0 ]
+then
+	exit 1
+fi
+
+# Call TestCaller with a path to the folder containing tests.
+"${TEST_FOLDER}/TestCaller" "--test" "${TEST_FOLDER}/tests"

+ 25 - 0
Source/test/test_macos.sh

@@ -0,0 +1,25 @@
+#!/bin/bash
+
+# This script can be called from any path, as long as the first argument sais where this script is located.
+
+TEST_FOLDER=`dirname "$(realpath $0)"`
+echo TEST_FOLDER = "${TEST_FOLDER}"
+
+PROJECT_BUILD_SCRIPT="${TEST_FOLDER}/../tools/builder/buildProject.sh"
+echo PROJECT_BUILD_SCRIPT = "${PROJECT_BUILD_SCRIPT}"
+
+PROJECT_FILE="${TEST_FOLDER}/TestCaller.DsrProj"
+echo PROJECT_FILE = "${PROJECT_FILE}"
+
+# Give execution rights.
+chmod +x "${PROJECT_BUILD_SCRIPT}";
+
+# Build TestCaller and all its tests. 
+"${PROJECT_BUILD_SCRIPT}" "${PROJECT_FILE}" MacOS $@;
+if [ $? -ne 0 ]
+then
+	exit 1
+fi
+
+# Call TestCaller with a path to the folder containing tests.
+"${TEST_FOLDER}/TestCaller" "--test" "${TEST_FOLDER}/tests"

+ 34 - 0
Source/test/test_windows.bat

@@ -0,0 +1,34 @@
+@echo off
+
+rem This script can be called from any path, as long as the first argument sais where this script is located.
+
+echo Starting test on MS-Windows.
+
+rem Get the test folder's path from the called path.
+set TEST_FOLDER=%~dp0
+echo TEST_FOLDER = %TEST_FOLDER%
+
+set PROJECT_BUILD_SCRIPT=%TEST_FOLDER%..\tools\builder\buildProject.bat
+echo PROJECT_BUILD_SCRIPT = %PROJECT_BUILD_SCRIPT%
+
+set PROJECT_FILE=%TEST_FOLDER%TestCaller.DsrProj
+echo PROJECT_FILE = %PROJECT_FILE%
+
+rem Build TestCaller and all its tests. 
+call "%PROJECT_BUILD_SCRIPT%" "%PROJECT_FILE%" Windows %@%
+if errorlevel 0 (
+	echo Done building TestCaller.
+) else (
+	echo Failed building TestCaller.
+	exit /b 1
+)
+
+rem Call TestCaller with a path to the folder containing tests.
+echo Starting tests.
+call "%TEST_FOLDER%\TestCaller.exe" --test "%TEST_FOLDER%\tests"
+if errorlevel 0 (
+	echo Done running tests.
+) else (
+	echo Failed running tests.
+	exit /b 1
+)

+ 1 - 2
Source/test/tests/SafePointerTest.cpp

@@ -47,7 +47,6 @@ START_TEST(SafePointer)
 		#endif
 	}
 	#ifndef SAFE_POINTER_CHECKS
-		printf("WARNING! SafePointer test ran without bound checks enabled.\n");
+		#error "ERROR! SafePointer test ran without bound checks enabled!\n"
 	#endif
 END_TEST
-

+ 7 - 47
Source/test/tests/TextEncodingTest.cpp

@@ -69,42 +69,10 @@ void printBuffer(Buffer buffer) {
 	}
 }
 
-// Method for printing the character codes of a string for debugging
-void compareCharacterCodes(String textA, String textB) {
-	int lengthA = string_length(textA);
-	int lengthB = string_length(textB);
-	int minLength = lengthA < lengthB ? lengthA : lengthB;
-	printText(U"Character codes for strings of length ", lengthA, U" and ", lengthB, U":\n");
-	for (int i = 0; i < minLength; i++) {
-		uint32_t codeA = (uint32_t)textA[i];
-		uint32_t codeB = (uint32_t)textB[i];
-		printBinary(codeA, 32);
-		if (codeA == codeB) {
-			printText(U" == ");
-		} else {
-			printText(U" != ");
-		}
-		printBinary(codeB, 32);
-		printText(U" (", textA[i], U") (", textB[i], U")\n");
-	}
-	if (lengthA > lengthB) {
-		for (int i = minLength; i < lengthA; i++) {
-			uint32_t codeA = (uint32_t)textA[i];
-			printBinary(codeA, 32);
-			printText(U" (", textA[i], U")\n");
-		}
-	} else {
-		printText(U"                                    ");
-		for (int i = minLength; i < lengthB; i++) {
-			uint32_t codeB = (uint32_t)textB[i];
-			printBinary(codeB, 32);
-			printText(U" (", textB[i], U")\n");
-		}
-	}
-}
-
 START_TEST(TextEncoding)
-	String folderPath = string_combine(U"test", file_separator(), U"tests", file_separator(), U"resources", file_separator());
+	String folderPath = file_combinePaths(U".", U"resources");
+	// Check that we have a valid folder path to the resources.
+	ASSERT_EQUAL(file_getEntryType(folderPath), EntryType::Folder);
 	{ // Text encodings stored in memory
 		// Run these tests for all line encodings
 		for (int l = 0; l <= 1; l++) {
@@ -121,7 +89,6 @@ START_TEST(TextEncoding)
 				}
 				Buffer encoded = string_saveToMemory(originalLatin1, CharacterEncoding::Raw_Latin1, lineEncoding);
 				String decodedLatin1 = string_loadFromMemory(encoded);
-				//compareCharacterCodes(originalLatin1, decodedLatin1);
 				ASSERT_EQUAL(originalLatin1, decodedLatin1);
 			}
 			{ // UTF-8 up to U+10FFFF excluding \r and \0
@@ -185,8 +152,6 @@ START_TEST(TextEncoding)
 				string_appendChar(originalUTF16, 0x10FFFF); // Maximum range for UTF
 				Buffer encoded = string_saveToMemory(originalUTF16, characterEncoding, lineEncoding);
 				String decoded = string_loadFromMemory(encoded);
-				//printBuffer(encoded);
-				//compareCharacterCodes(originalUTF16, decoded);
 				ASSERT_EQUAL(originalUTF16, decoded);
 			}
 			// All UTF-16 characters excluding \r and \0
@@ -210,20 +175,16 @@ START_TEST(TextEncoding)
 		}
 	}
 	{ // Loading strings of different encodings
-		String fileLatin1 = string_load(folderPath + U"Latin1.txt", true);
-		//compareCharacterCodes(fileLatin1, expected_latin1);
+		String fileLatin1 = string_load(file_combinePaths(folderPath, U"Latin1.txt"), true);
 		ASSERT_EQUAL(fileLatin1, expected_latin1);
 
-		String fileUTF8 = string_load(folderPath + U"BomUtf8.txt", true);
-		//compareCharacterCodes(fileUTF8, expected_utf8);
+		String fileUTF8 = string_load(file_combinePaths(folderPath, U"BomUtf8.txt"), true);
 		ASSERT_EQUAL(fileUTF8, expected_utf8);
 
-		String fileUTF16LE = string_load(folderPath + U"BomUtf16Le.txt", true);
-		//compareCharacterCodes(fileUTF16LE, expected_utf16le);
+		String fileUTF16LE = string_load(file_combinePaths(folderPath, U"BomUtf16Le.txt"), true);
 		ASSERT_EQUAL(fileUTF16LE, expected_utf16le);
 
-		String fileUTF16BE = string_load(folderPath + U"BomUtf16Be.txt", true);
-		//compareCharacterCodes(fileUTF16BE, expected_utf16be);
+		String fileUTF16BE = string_load(file_combinePaths(folderPath, U"BomUtf16Be.txt"), true);
 		ASSERT_EQUAL(fileUTF16BE, expected_utf16be);
 	}
 	{ // Saving and loading text to files using every combination of character and line encoding
@@ -236,7 +197,6 @@ START_TEST(TextEncoding)
 			// Latin-1 should store up to 8 bits correctly, and write ? for complex characters
 			string_save(tempPath, originalContent, CharacterEncoding::Raw_Latin1, lineEncoding);
 			String latin1Loaded = string_load(tempPath, true);
-			//compareCharacterCodes(latin1Loaded, latin1Expected);
 			ASSERT_EQUAL(latin1Loaded, latin1Expected);
 
 			// UFT-8 should store up to 21 bits correctly

+ 4 - 1
Source/tools/buildScripts/build.sh

@@ -9,9 +9,12 @@ WINDOW_MANAGER=$5 # Which library to use for creating a window
 COMPILER_FLAGS=$6 # -DDEBUG/-DNDEBUG -std=c++14/-std=c++17 -O2/-O3
 LINKER_FLAGS=$7 # Additional linker flags for libraries and such
 
+# Replace space with underscore
 TEMP_SUB="${COMPILER_FLAGS// /_}"
+# Replace + with p
 TEMP_SUB=$(echo $TEMP_SUB | tr "+" "p")
-TEMP_SUB=$(echo $TEMP_SUB | tr -d " =-")
+# Remove = and -
+TEMP_SUB=$(echo $TEMP_SUB | tr -d "=-")
 TEMP_DIR=${TEMP_ROOT}/${TEMP_SUB}
 echo "Building version ${TEMP_SUB}"
 

+ 55 - 31
Source/tools/builder/buildProject.bat

@@ -1,22 +1,45 @@
+@echo off
 
-rem Local build settings that should be configured before building for the first time.
-
-set CPP_COMPILER_FOLDER=C:\Program\CodeBlocks\MinGW\bin
-set CPP_COMPILER_PATH=%CPP_COMPILER_FOLDER%\x86_64-w64-mingw32-g++.exe
+setlocal enabledelayedexpansion
 
-rem Change the temporary folder if want generated scripts and objects to go somewhere else.
+rem Local build settings.
+set GENERATE_SCRIPT=Yes
+rem set GENERATE_SCRIPT=No
 set TEMPORARY_FOLDER=%TEMP%
+set COMPILER_NAME=g++
 
 
 
 
 
-@echo off
+rem Get this script's folder.
+set BUILDER_FOLDER=%~dp0
+echo BUILDER_FOLDER = %BUILDER_FOLDER%
+
+rem Show which versions of the compiler are installed.
+echo Installed %COMPILER_NAME% compilers:
+where %COMPILER_NAME%
+
+rem Select the first instance.
+for /f "delims=" %%i in ('where %COMPILER_NAME% 2^>nul') do (
+	set CPP_COMPILER_PATH=%%i
+	goto :found_compiler
+)
+:found_compiler
+rem Abort if none could be found.
+if not exist !CPP_COMPILER_PATH! (
+	echo Could not find !COMPILER_NAME!.
+	exit /b 1
+)
+echo CPP_COMPILER_PATH = !CPP_COMPILER_PATH!
+
+rem Get the compiler's folder from the compiler's path.
+set CPP_COMPILER_FOLDER=!~dpCPP_COMPILER_PATH!
+echo CPP_COMPILER_FOLDER = !CPP_COMPILER_FOLDER!
 
 rem Using buildProject.bat
 rem   %1 must be the *.DsrProj path or a folder containing such projects. The path is relative to the caller location.
 rem   %2... are variable assignments sent as input to the given project file.
-
rem   CPP_COMPILER_PATH should be modified if it does not already refer to an installed C++ compiler.
 
 echo Running buildProject.bat %*
 
@@ -30,38 +53,39 @@ echo DFPSR_LIBRARY = %DFPSR_LIBRARY%
 set BUILDER_SOURCE=%BUILDER_FOLDER%\code\main.cpp %BUILDER_FOLDER%\code\Machine.cpp %BUILDER_FOLDER%\code\generator.cpp %BUILDER_FOLDER%\code\analyzer.cpp %BUILDER_FOLDER%\code\expression.cpp %DFPSR_LIBRARY%\collection\collections.cpp %DFPSR_LIBRARY%\api\fileAPI.cpp %DFPSR_LIBRARY%\api\bufferAPI.cpp %DFPSR_LIBRARY%\api\stringAPI.cpp %DFPSR_LIBRARY%\api\timeAPI.cpp %DFPSR_LIBRARY%\base\SafePointer.cpp %DFPSR_LIBRARY%\base\virtualStack.cpp %DFPSR_LIBRARY%\base\heap.cpp
 echo BUILDER_SOURCE = %BUILDER_SOURCE%
 
-echo Change CPP_COMPILER_FOLDER and CPP_COMPILER_PATH in %BUILDER_FOLDER%\buildProject.bat if you are not using %CPP_COMPILER_PATH% as your compiler.
-
 rem Check if the build system is compiled
-if exist %BUILDER_EXECUTABLE% (
+if exist "%BUILDER_EXECUTABLE%" (
 	echo Found the build system's binary.
 ) else (
 	echo Building the Builder build system for first time use.
 	pushd %CPP_COMPILER_FOLDER%
 		%CPP_COMPILER_PATH% -o %BUILDER_EXECUTABLE% %BUILDER_SOURCE% -static -static-libgcc -static-libstdc++ -std=c++14 -lstdc++
+		if errorlevel 0 (
+			echo Completed building the Builder build system.
+		) else (
+			echo Failed building the Builder build system, which is needed to build your project!
+			exit /b 1
+		)
 	popd
-	if errorlevel 0 (
-		echo Completed building the Builder build system.
-	) else (
-		echo Failed building the Builder build system, which is needed to build your project!
-		pause
-		exit /b 1
-	)
 )
 
-rem Call the build system with a filename for the output script, which is later called.
-set SCRIPT_PATH=%TEMPORARY_FOLDER%\dfpsr_compile.bat
-echo Generating %SCRIPT_PATH% from %1%
-if exist %SCRIPT_PATH% (
-	del %SCRIPT_PATH%
-)
-%BUILDER_EXECUTABLE% %SCRIPT_PATH% %* Compiler=%CPP_COMPILER_PATH% CompileFrom=%CPP_COMPILER_FOLDER%
-if exist %SCRIPT_PATH% (
-	echo Running %SCRIPT_PATH%
-	%SCRIPT_PATH%
+if "!GENERATE_SCRIPT!"=="Yes" (
+	rem Calling the build system with a script path will generate the script for all actions.
+	set SCRIPT_PATH=%TEMPORARY_FOLDER%\dfpsr_compile.bat
+	echo Generating !SCRIPT_PATH! from %1%
+	if exist "!SCRIPT_PATH!" (
+		del "!SCRIPT_PATH!"
+	)
+	!BUILDER_EXECUTABLE! "!SCRIPT_PATH!" %* "Compiler=!CPP_COMPILER_PATH!" "CompileFrom=!CPP_COMPILER_FOLDER!"
+	if exist "!SCRIPT_PATH!" (
+		echo Running !SCRIPT_PATH!
+		call "!SCRIPT_PATH!"
+		echo Done calling !SCRIPT_PATH!
+	)
+) else (
+	rem Calling the build system with only the temporary folder will call the compiler directly from the build system.
+	echo No script path provided. Builder will call !COMPILER_NAME! directly instead of generating a script.
+	!BUILDER_EXECUTABLE! "%TEMPORARY_FOLDER%" %* Compiler=!CPP_COMPILER_PATH! CompileFrom=!CPP_COMPILER_FOLDER!
 )
 
-rem Calling the build system with only the temporary folder will call the compiler directly from the build system.
-rem %BUILDER_EXECUTABLE% %TEMPORARY_FOLDER% %* Compiler=%CPP_COMPILER_PATH% CompileFrom=%CPP_COMPILER_FOLDER%
-
-pause
+endlocal

+ 40 - 12
Source/tools/builder/buildProject.sh

@@ -1,31 +1,54 @@
+#!/bin/bash
 
 # Local build settings that should be configured before building for the first time.
 
 # Make sure to erase all objects in the temporary folder before changing compiler.
 
-# Compile using GCC's C++ compiler
-CPP_COMPILER_PATH="/usr/bin/g++"
+# Select build method. (Not generating a script requires having the full path of the compiler.)
+#GENERATE_SCRIPT="Yes"
+GENERATE_SCRIPT="No"
 
-# Compile using CLANG (Needs a 'Link "stdc++"' command in each project)
-#CPP_COMPILER_PATH="/usr/bin/clang"
+# Change TEMPORARY_FOLDER if you do not want to recompile everything after each reboot.
+TEMPORARY_FOLDER="/tmp"
+COMPILER_NAME="g++"
 
-echo "Change CPP_COMPILER_PATH in ${BUILDER_FOLDER}/buildProject.sh if you are not using ${CPP_COMPILER_PATH} as your compiler."
 
-# Change TEMPORARY_FOLDER if you don't want to recompile everything after each reboot, or your operating system has a different path to the temporary folder.
-TEMPORARY_FOLDER="/tmp"
 
-# Select build method. (Not generating a script requires having the full path of the compiler.)
-#GENERATE_SCRIPT="Yes"
-GENERATE_SCRIPT="No"
 
 
+# Find the compiler.
+CPP_COMPILER_PATH=$(which "${COMPILER_NAME}")
+if [ -n "$CPP_COMPILER_PATH" ]; then
+	echo "Found ${COMPILER_NAME} at ${CPP_COMPILER_PATH}."
+else
+	echo "Could not find ${COMPILER_NAME}."
+	exit 1
+fi
+
+# Get the script's folder.
+BUILDER_FOLDER=$(dirname "$0")
+echo "BUILDER_FOLDER = ${BUILDER_FOLDER}"
+
+# Ask for permission to access the temporary folder.
+if ! ( [ -d "${TEMPORARY_FOLDER}" ] && [ -r "${TEMPORARY_FOLDER}" ] && [ -w "${TEMPORARY_FOLDER}" ] && [ -x "${TEMPORARY_FOLDER}" ] ); then
+	echo "Can not access the ${TEMPORARY_FOLDER} folder."
+	TEMPORARY_FOLDER="${BUILDER_FOLDER}/temporary"
+	mkdir "${TEMPORARY_FOLDER}"
+	if ! ( [ -d "${TEMPORARY_FOLDER}" ] && [ -r "${TEMPORARY_FOLDER}" ] && [ -w "${TEMPORARY_FOLDER}" ] && [ -x "${TEMPORARY_FOLDER}" ] ); then
+		echo "Failed not create a new folder at ${TEMPORARY_FOLDER}, aborting!"
+		exit 1
+	else
+		echo "Got read, write and execution rights to the ${TEMPORARY_FOLDER} folder, so we can use it to store temporary files."
+	fi	
+else
+	echo "Got read, write and execution rights to the ${TEMPORARY_FOLDER} folder, so we can use it to store temporary files."
+fi
 
 
 
 # Using buildProject.sh
 #   $1 must be the *.DsrProj path, which is relative to the caller location.
 #   $2... are variable assignments sent as input to the given project file.
-#   CPP_COMPILER_PATH should be modified if it does not already refer to an installed C++ compiler.
 
 echo "Running buildProject.sh $@"
 
@@ -72,5 +95,10 @@ else
 	#   A simpler solution that works with just a single line, once the build system itself has been compiled.
 	echo "Generating objects to ${TEMPORARY_FOLDER} from $1"
 	"${BUILDER_EXECUTABLE}" "${TEMPORARY_FOLDER}" "$@" "Compiler=${CPP_COMPILER_PATH}";
+	if [ $? -eq 0 ]; then
+		echo "Finished building."
+	else
+		echo "Failed building the project!"
+		exit 1
+	fi
 fi
-

+ 121 - 2
Source/tools/builder/code/Machine.cpp

@@ -33,6 +33,9 @@ static bool isUnique(const List<Flag> &list) {
 
 void printSettings(const Machine &settings) {
 	printText(U"    Project name: ", settings.projectName, U"\n");
+	for (int64_t i = 0; i < settings.crawlOrigins.length(); i++) {
+		printText(U"    Crawl origins ", settings.crawlOrigins[i], U"\n");
+	}
 	for (int64_t i = 0; i < settings.compilerFlags.length(); i++) {
 		printText(U"    Compiler flag ", settings.compilerFlags[i], U"\n");
 	}
@@ -114,14 +117,30 @@ static String evaluateExpression(Machine &target, const List<String> &tokens, in
 
 // Copy inherited variables from parent to child.
 void inheritMachine(Machine &child, const Machine &parent) {
+	// Only take selected variables, such as the target platform's name.
 	for (int64_t v = 0; v < parent.variables.length(); v++) {
-		String key = string_upperCase(parent.variables[v].key);
 		if (parent.variables[v].inherited) {
 			child.variables.push(parent.variables[v]);
 		}
 	}
 }
 
+void cloneMachine(Machine &child, const Machine &parent) {
+	// Inherit everything.
+	for (int64_t v = 0; v < parent.variables.length(); v++) {
+		child.variables.push(parent.variables[v]);
+	}
+	for (int64_t v = 0; v < parent.compilerFlags.length(); v++) {
+		child.compilerFlags.push(parent.compilerFlags[v]);
+	}
+	for (int64_t v = 0; v < parent.linkerFlags.length(); v++) {
+		child.linkerFlags.push(parent.linkerFlags[v]);
+	}
+	for (int64_t v = 0; v < parent.crawlOrigins.length(); v++) {
+		child.crawlOrigins.push(parent.crawlOrigins[v]);
+	}
+}
+
 static bool validIdentifier(const dsr::ReadableString &identifier) {
 	DsrChar first = identifier[0];
 	if (!((U'a' <= first && first <= U'z') || (U'A' <= first && first <= U'Z'))) {
@@ -136,6 +155,67 @@ static bool validIdentifier(const dsr::ReadableString &identifier) {
 	return true;
 }
 
+using NameFilter = std::function<bool(const ReadableString &filename)>;
+static NameFilter generateFilterFromPattern(const dsr::ReadableString &pattern) {
+	int64_t firstStar = string_findFirst(pattern, U'*');
+	int64_t lastStar = string_findLast(pattern, U'*');
+	if (firstStar == -1) {
+		return [pattern](const ReadableString &filename) -> bool {
+			return string_caseInsensitiveMatch(filename, pattern);
+		};
+	} else if (firstStar == lastStar) {
+		String prefix = string_before(pattern, firstStar);
+		String postfix = string_after(pattern, lastStar);
+		int64_t preLength = string_length(prefix);
+		int64_t postLength = string_length(postfix);
+		int64_t minimumLength = preLength + postLength;
+		return [prefix, postfix, preLength, postLength, minimumLength](const ReadableString &filename) -> bool {
+			int64_t nameLength = string_length(filename);
+			if (nameLength < minimumLength) {
+				return false;
+			} else {
+				ReadableString foundPrefix = string_before(filename, preLength);
+				ReadableString foundPostfix = string_from(filename, nameLength - postLength);
+				return string_caseInsensitiveMatch(foundPrefix, prefix) && string_caseInsensitiveMatch(foundPostfix, postfix);
+			}
+		};
+	} else {
+		throwError(U"Can not use '", pattern, "' as a name pattern, because the matching expression may not use more than one '*' character!\n");
+		return [](const ReadableString &filename) -> bool {
+			return false;
+		};
+	}
+}
+
+static void findFiles(const dsr::ReadableString &inPath, NameFilter filter, std::function<void(const ReadableString &path)> action) {
+	if (!file_getFolderContent(inPath, [&filter, &action](const ReadableString& entryPath, const ReadableString& entryName, EntryType entryType) {
+		if (entryType == EntryType::File) {
+			if (filter(entryName)) {
+				action(entryPath);
+			}
+		} else if (entryType == EntryType::Folder) {
+			findFiles(entryPath, filter, action);
+		}
+	})) {
+		printText("Failed to look for files in '", inPath, "'\n");
+	}
+}
+
+static void findFilesAsProjects(Machine &target, const dsr::ReadableString &inPath, const dsr::ReadableString &fromPattern) {
+	printText(U"findFilesAsProjects: Looking for ", fromPattern, U" in ", inPath, U".\n");
+	validateSettings(target, U"in the parent about to create projects from files");
+	findFiles(inPath, generateFilterFromPattern(fromPattern), [&target](const ReadableString &path) {
+		printText(U"Creating a temporary project for ", path, U"\n");		
+		// List the file as a project.
+		target.projectFromSourceFilenames.push(path);
+		Machine allInputFlags(file_getPathlessName(path));
+		cloneMachine(allInputFlags, target);
+		target.projectFromSourceSettings.push(allInputFlags);
+	});
+}
+
+// TODO: Improve error messages with line numbers and quoted content instead of just throwing errors.
+
 static void interpretLine(Machine &target, const List<String> &tokens, int64_t startTokenIndex, int64_t endTokenIndex, const dsr::ReadableString &fromPath) {
 	// Automatically clamp to safe bounds.
 	if (startTokenIndex < 0) startTokenIndex = 0;
@@ -176,6 +256,46 @@ static void interpretLine(Machine &target, const List<String> &tokens, int64_t s
 				// The right hand expression is evaluated into a path relative to the build script and used as the root for searching for source code.
 				target.crawlOrigins.push(PATH_EXPR(startTokenIndex + 1, endTokenIndex));
 				validateSettings(target, U"in target after listing a crawl origin\n");
+			} else if (string_caseInsensitiveMatch(first, U"projects")) {
+				// TODO: Should it be possible to give the string content of variables as patterns and paths?
+				//Projects from "*Test.cpp" in "tests"
+				int currentTokenIndex = startTokenIndex + 1;
+				String arg_from;
+				String arg_in;
+				while (currentTokenIndex < endTokenIndex) {
+					ReadableString key = expression_getToken(tokens, currentTokenIndex, U"");
+					ReadableString value = expression_getToken(tokens, currentTokenIndex + 1, U"");
+					if (string_caseInsensitiveMatch(key, U"from")) {
+						if (string_length(value) == 0) {
+							throwError(U"Missing folder path after 'from' keyword in 'projects' command!\n");
+						} else {
+							printText(U"Using ", value, U" as the 'from' argument.\n");
+							arg_from = string_unmangleQuote(value);
+							// Consume both key and value.
+							currentTokenIndex += 2;
+						}
+					} else if (string_caseInsensitiveMatch(key, U"in")) {
+						if (string_length(value) == 0) {
+							throwError(U"Missing file name pattern after 'in' keyword in 'projects' command!\n");
+						} else {
+							printText(U"Using ", value, U" as the 'in' argument.\n");
+							arg_in = string_unmangleQuote(value);
+							// Consume both key and value.
+							currentTokenIndex += 2;
+						}
+					} else {
+						throwError(U"Unexpected key '", key, "' in 'projects' command!\n");
+					}
+				}
+				if (string_length(arg_from) == 0 && string_length(arg_in) == 0) {
+					throwError(U"Need 'from' and 'in' keywords in 'projects' command!\n");
+				} else if (string_length(arg_from) == 0) {
+					throwError(U"Missing 'from' keyword in 'projects' command!\n");
+				} else if (string_length(arg_in) == 0) {
+					throwError(U"Missing 'in' keywords in 'projects' command!\n");
+				} else {
+					findFilesAsProjects(target, file_combinePaths(fromPath, arg_in), arg_from);
+				}
 			} else if (string_caseInsensitiveMatch(first, U"build")) {
 				// Build one or more other projects from a project file or folder path, as dependencies.
 				//   Having the same external project built twice during the same session is not allowed.
@@ -235,7 +355,6 @@ static void interpretLine(Machine &target, const List<String> &tokens, int64_t s
 					validateSettings(target, U"in target after explicitly assigning a value to a variable\n");
 				} else {
 					String errorMessage = U"Failed to parse statement: ";
-					printText(U"Failed to parse statement of tokens: ");
 					for (int64_t t = startTokenIndex; t <= endTokenIndex; t++) {
 						string_append(errorMessage, U" ", string_mangleQuote(tokens[t]));
 					}

+ 2 - 0
Source/tools/builder/code/Machine.h

@@ -22,6 +22,8 @@ void assignValue(Machine &target, const dsr::ReadableString &key, const dsr::Rea
 void evaluateScript(Machine &target, const ReadableString &scriptPath);
 
 void inheritMachine(Machine &child, const Machine &parent);
+void cloneMachine(Machine &child, const Machine &parent);
+
 void argumentsToSettings(Machine &settings, const List<String> &arguments, int64_t firstArgument, int64_t lastArgument);
 
 void printSettings(const Machine &settings);

+ 63 - 30
Source/tools/builder/code/analyzer.cpp

@@ -1,4 +1,5 @@
 
+#include "analyzer.h"
 #include "generator.h"
 #include "Machine.h"
 
@@ -404,17 +405,11 @@ static void crawlSource(ProjectContext &context, ReadableString absolutePath) {
 	}
 }
 
-void build(SessionContext &output, ReadableString projectPath, Machine &settings);
-
 static List<String> initializedProjects;
-// Using a project file path and input arguments.
-void buildProject(SessionContext &output, ReadableString projectFilePath, Machine &sharedsettings) {
-	Machine settings(file_getPathlessName(projectFilePath));
-	inheritMachine(settings, sharedsettings);
-	validateSettings(settings, string_combine(U"in settings after inheriting settings from caller, for ", projectFilePath, U"\n"));
-	printText(U"Building project at ", projectFilePath, U"\n");
+static void buildProjectFromSettings(SessionContext &output, const ReadableString &path, Machine &settings) {
+	printText(U"Building project at ", path, U"\n");
 	// Check if this project has begun building previously during this session.
-	String absolutePath = file_getAbsolutePath(projectFilePath);
+	String absolutePath = file_getAbsolutePath(path);
 	for (int64_t p = 0; p < initializedProjects.length(); p++) {
 		if (string_caseInsensitiveMatch(absolutePath, initializedProjects[p])) {
 			throwError(U"Found duplicate requests to build from the same initial script ", absolutePath, U" which could cause non-determinism if different arguments are given to each!\n");
@@ -423,15 +418,11 @@ void buildProject(SessionContext &output, ReadableString projectFilePath, Machin
 	}
 	// Remember that building of this project has started.
 	initializedProjects.push(absolutePath);
-	// Evaluate compiler settings while searching for source code mentioned in the project and imported headers.
-	printText(U"Executing project file from ", projectFilePath, U".\n");
 	ProjectContext context;
-	evaluateScript(settings, projectFilePath);
-	validateSettings(settings, string_combine(U"in settings after evaluateScript in buildProject, for ", projectFilePath, U"\n"));
 	// Find out where things are located.
-	String projectPath = file_getAbsoluteParentFolder(projectFilePath);
+	String projectPath = file_getAbsoluteParentFolder(path);
 	// Get the project's name.
-	String projectName = file_getPathlessName(file_getExtensionless(projectFilePath));
+	String projectName = file_getPathlessName(file_getExtensionless(path));
 	// If no application path is given, the new executable will be named after the project and placed in the same folder.
 	String fullProgramPath = getFlag(settings, U"ProgramPath", projectName);
 	if (string_length(output.executableExtension) > 0) {
@@ -439,47 +430,69 @@ void buildProject(SessionContext &output, ReadableString projectFilePath, Machin
 	}
 	// Interpret ProgramPath relative to the project path.
 	fullProgramPath = file_getTheoreticalAbsolutePath(fullProgramPath, projectPath);
-	// Build other projects.
+	
+	// Build projects from files. (used for running many tests)
+	for (int64_t b = 0; b < settings.projectFromSourceFilenames.length(); b++) {
+		buildFromFile(output, settings.projectFromSourceFilenames[b], settings.projectFromSourceSettings[b]);
+	}
+	
+	// Build other projects. (used for compiling programs that the main program should call)
 	for (int64_t b = 0; b < settings.otherProjectPaths.length(); b++) {
-		build(output, settings.otherProjectPaths[b], settings.otherProjectSettings[b]);
+		buildFromFolder(output, settings.otherProjectPaths[b], settings.otherProjectSettings[b]);
 	}
-	validateSettings(settings, string_combine(U"in settings after building other projects in buildProject, for ", projectFilePath, U"\n"));
+	validateSettings(settings, string_combine(U"in settings after building other projects in buildProject, for ", path, U"\n"));
 	// If the SkipIfBinaryExists flag is given, we will abort as soon as we have handled its external BuildProjects requests and confirmed that the application exists.
 	if (getFlagAsInteger(settings, U"SkipIfBinaryExists") && file_getEntryType(fullProgramPath) == EntryType::File) {
 		// SkipIfBinaryExists was active and the binary exists, so abort here to avoid redundant work.
-		printText(U"Skipping build of ", projectFilePath, U" because the SkipIfBinaryExists flag was given and ", fullProgramPath, U" was found.\n");
+		printText(U"Skipping build of ", path, U" because the SkipIfBinaryExists flag was given and ", fullProgramPath, U" was found.\n");
 		return;
 	}
 	// Once we know where the binary is and that it should be built, we can start searching for source code.
 	for (int64_t o = 0; o < settings.crawlOrigins.length(); o++) {
 		crawlSource(context, settings.crawlOrigins[o]);
 	}
-	validateSettings(settings, string_combine(U"in settings after crawling source in buildProject, for ", projectFilePath, U"\n"));
+	validateSettings(settings, string_combine(U"in settings after crawling source in buildProject, for ", path, U"\n"));
 	// Once we are done finding all source files, we can resolve the dependencies to create a graph connected by indices.
 	resolveDependencies(context);
 	if (getFlagAsInteger(settings, U"ListDependencies")) {
 		printDependencies(context);
 	}
 	gatherBuildInstructions(output, context, settings, fullProgramPath);
-	validateSettings(settings, string_combine(U"in settings after gathering build instructions in buildProject, for ", projectFilePath, U"\n"));
+	validateSettings(settings, string_combine(U"in settings after gathering build instructions in buildProject, for ", path, U"\n"));
+}
+
+// Using a project file path and input arguments.
+void buildProject(SessionContext &output, ReadableString projectFilePath, Machine &sharedSettings) {
+	// Inherit external settings.
+	Machine settings(file_getPathlessName(projectFilePath));
+	inheritMachine(settings, sharedSettings);
+	validateSettings(settings, string_combine(U"in settings after inheriting settings from caller, for ", projectFilePath, U"\n"));
+
+	// Evaluate the project's script.
+	printText(U"Executing project file from ", projectFilePath, U".\n");
+	evaluateScript(settings, projectFilePath);
+	validateSettings(settings, string_combine(U"in settings after evaluateScript in buildProject, for ", projectFilePath, U"\n"));
+
+	// Complete the project.
+	buildProjectFromSettings(output, projectFilePath, settings);
 }
 
 // Using a folder path and input arguments for all projects.
-void buildProjects(SessionContext &output, ReadableString projectFolderPath, Machine &sharedsettings) {
+void buildProjects(SessionContext &output, ReadableString projectFolderPath, Machine &sharedSettings) {
 	printText(U"Building all projects in ", projectFolderPath, U"\n");
-	file_getFolderContent(projectFolderPath, [&sharedsettings, &output](const ReadableString& entryPath, const ReadableString& entryName, EntryType entryType) {
+	file_getFolderContent(projectFolderPath, [&sharedSettings, &output](const ReadableString& entryPath, const ReadableString& entryName, EntryType entryType) {
 		if (entryType == EntryType::Folder) {
-			buildProjects(output, entryPath, sharedsettings);
+			buildProjects(output, entryPath, sharedSettings);
 		} else if (entryType == EntryType::File) {
-			ReadableString extension = string_upperCase(file_getExtension(entryName));
-			if (string_match(extension, U"DSRPROJ")) {
-				buildProject(output, entryPath, sharedsettings);
+			ReadableString extension = file_getExtension(entryName);
+			if (string_caseInsensitiveMatch(extension, U"DSRPROJ")) {
+				buildProject(output, entryPath, sharedSettings);
 			}
 		}
 	});
 }
 
-void build(SessionContext &output, ReadableString projectPath, Machine &sharedsettings) {
+void buildFromFolder(SessionContext &output, ReadableString projectPath, Machine &sharedSettings) {
 	EntryType entryType = file_getEntryType(projectPath);
 	printText(U"Building anything at ", projectPath, U" which is ", entryType, U"\n");
 	if (entryType == EntryType::File) {
@@ -488,9 +501,29 @@ void build(SessionContext &output, ReadableString projectPath, Machine &sharedse
 			printText(U"Can't use the Build keyword with a file that is not a project!\n");
 		} else {
 			// Build the given project
-			buildProject(output, projectPath, sharedsettings);
+			buildProject(output, projectPath, sharedSettings);
 		}
 	} else if (entryType == EntryType::Folder) {
-		buildProjects(output, projectPath, sharedsettings);
+		buildProjects(output, projectPath, sharedSettings);
+	}
+}
+
+void buildFromFile(SessionContext &output, ReadableString mainPath, Machine &sharedSettings) {
+	// Inherit settings, flags and dependencies from the parent, because they do not exist in single source files.
+	Machine settings(file_getPathlessName(mainPath));
+	cloneMachine(settings, sharedSettings);
+
+	ReadableString extension = file_getExtension(mainPath);
+	if (!(string_caseInsensitiveMatch(extension, U"c") || string_caseInsensitiveMatch(extension, U"cpp"))) {
+		throwError(U"Creating projects from source files is currently only supported for *.c and *.cpp, but the extension was '", extension, U"'.");
 	}
+
+	// Crawl from the selected file to discover direct dependencies.
+	settings.crawlOrigins.push(mainPath);
+
+	// Check that settings are okay.
+	validateSettings(settings, string_combine(U"in settings after inheriting settings from caller, for ", mainPath, U"\n"));
+
+	// Create the project to save as a script or build using direct calls to the compiler.
+	buildProjectFromSettings(output, mainPath, settings);
 }

+ 6 - 3
Source/tools/builder/code/analyzer.h

@@ -16,14 +16,17 @@ void resolveDependencies(ProjectContext &context);
 void printDependencies(ProjectContext &context);
 
 // Build anything in projectPath.
-void build(SessionContext &output, ReadableString projectPath, Machine &sharedsettings);
+void buildFromFolder(SessionContext &output, ReadableString projectPath, Machine &sharedSettings);
+
+// Create a project from crawling a single source file and build it.
+void buildFromFile(SessionContext &output, ReadableString mainPath, Machine &sharedSettings);
 
 // Build the project in projectFilePath.
 // Settings must be taken by value to prevent side-effects from spilling over between different scripts.
-void buildProject(SessionContext &output, ReadableString projectFilePath, Machine &sharedsettings);
+void buildProject(SessionContext &output, ReadableString projectFilePath, Machine &sharedSettings);
 
 // Build all projects in projectFolderPath.
-void buildProjects(SessionContext &output, ReadableString projectFolderPath, Machine &sharedsettings);
+void buildProjects(SessionContext &output, ReadableString projectFolderPath, Machine &sharedSettings);
 
 void gatherBuildInstructions(SessionContext &output, ProjectContext &context, Machine &settings, ReadableString programPath);
 

+ 12 - 2
Source/tools/builder/code/builderTypes.h

@@ -16,13 +16,23 @@ struct Flag {
 };
 
 struct Machine {
+	// Name of this project.
 	String projectName;
+	// Variables that can be assigned and used for logic.
 	List<Flag> variables;
+	// The flags to give the compiler.
 	List<String> compilerFlags;
+	// The flags to give the linker.
 	List<String> linkerFlags;
+	// A list of implementation files to start crawling from, usually main.cpp or a disconnected backend implementation.
 	List<String> crawlOrigins;
-	List<String> otherProjectPaths; // Corresponding to otherProjectSettings.
-	List<Machine> otherProjectSettings; // Corresponding to otherProjectPaths.
+	// Paths to look for other projects in.
+	List<String> otherProjectPaths;
+	List<Machine> otherProjectSettings;
+	// Filenames to create projects for automatically without needing project files for each.
+	//   Useful for running automated tests, so that memory leaks can easily be narrowed down to the test causing the leak.
+	List<String> projectFromSourceFilenames;
+	List<Machine> projectFromSourceSettings;
 	// When activeStackDepth < currentStackDepth, we are skipping false cases.
 	int64_t currentStackDepth = 0; // How many scopes we are inside of, from the root script including all the others.
 	int64_t activeStackDepth = 0;

+ 3 - 1
Source/tools/builder/code/generator.cpp

@@ -209,13 +209,15 @@ void produce(SessionContext &input, const ReadableString &scriptPath, ScriptLang
 			produce_printMessage<GENERATE>(generatedCode, language, string_combine(U"Starting ", programPath));
 			produce_callProgram<GENERATE>(generatedCode, language, programPath, List<String>());
 			produce_printMessage<GENERATE>(generatedCode, language, U"The program terminated.");
+		} else {
+			produce_printMessage<GENERATE>(generatedCode, language, string_combine(U"Supressed execution of ", programPath, U" as requested by the project settings."));
 		}
 	}
 	produce_resetCompilationFolder<GENERATE>(generatedCode, language);
 	produce_printMessage<GENERATE>(generatedCode, language, U"Done building.");
 
 	if (GENERATE) {
-		printText(U"Saving script to ", scriptPath, U"\n");
+		printText(U"Saving script to ", scriptPath, U":\n", generatedCode, U"\n");
 		if (language == ScriptLanguage::Batch) {
 			// Batch on MS-Windows can not recognize a Byte Order Mark, so just encode it as Latin 1.
 			string_save(scriptPath, generatedCode, CharacterEncoding::Raw_Latin1, LineEncoding::CrLf);

+ 27 - 11
Source/tools/builder/code/main.cpp

@@ -41,21 +41,37 @@ Project files:
 		* x is assigned a boolean value telling if the content of a matches "abc". (case sensitive comparison)
 			x = a matches "abc"
 	Commands:
-		* Build all projects in myFolder with the SkipIfBinaryExists flag in arbitrary order before continuing with compilation
-			Build "../myFolder" SkipIfBinaryExists
-		* Add file.cpp and other implementations found through includes into the list of source code to compile and link.
-			Crawl "folder/file.cpp"
-		* Add a linker flag as is for direct control
-			LinkerFlag -lLibrary
-		* Add a linker flag with automatic prefix for future proofing
-			Link Library
-		* Add a compiler flag as is
-			CompilerFlag -DMACRO
+		Finding source code for the current project:
+			* Add file.cpp and other implementations found through includes into the list of source code to compile and link.
+				Crawl "folder/file.cpp"
+		Settings for compiling:
+			* Add a compiler flag as is
+				CompilerFlag -DMACRO
+		Settings for linking:
+			* Add a linker flag as is for direct control
+				LinkerFlag -lLibrary
+			* Add a linker flag with automatic prefix for future proofing
+				Link Library
+		Building other projects at the same time:
+			* Build all projects in myFolder with the SkipIfBinaryExists flag in arbitrary order before continuing with compilation
+				Build "../myFolder" SkipIfBinaryExists
+		Building a project crawling from each file matching a pattern:
+			All variables are inherited, so no variables are given to the command.
+			* Build a project for each file ending with 'Test.cpp' in a folder named 'tests'.
+				Projects from "*Test.cpp" in "tests"
+			* Build a project for each file starting with 'main_' and ending with '.cpp' in a folder named "code/projects".
+				Projects from "main_*.cpp" in "code/projects"
+			* Build a project for each file named 'main.cpp' in a folder named "examples".
+				Projects from "main_*.cpp" in "examples"
+			* You can also swap argument order like this, because it is designed to be easily extended with more keywords if needed.
+				Projects in "tests" from "*Test.cpp"
 	Systems:
 		* Linux
 			Set to non-zero on Linux or similar operating systems.
 		* Windows
 			Set to non-zero on MS-Windows.
+		* MacOS
+			Set to non-zero on MacOS.
 	Variables:
 		* SkipIfBinaryExists, skips building if the binary already exists.
 		* Supressed, prevents a compiled program from running after building, which is usually given as an extra argument to Build to avoid launching all programs in a row.
@@ -160,7 +176,7 @@ void dsrMain(List<String> args) {
 			executableExtension = U".exe";
 		}
 		SessionContext buildContext = SessionContext(tempFolder, executableExtension);
-		build(buildContext, projectPath, settings);
+		buildFromFolder(buildContext, projectPath, settings);
 		validateSettings(settings, U"in settings after executing the root build script (in main)");
 		if (language == ScriptLanguage::Unknown) {
 			// Call the compiler directly.

+ 4 - 4
Source/tools/wizard/main.cpp

@@ -43,7 +43,6 @@ Project::Project(const ReadableString &projectFilePath)
 	String projectFolderPath = file_getRelativeParentFolder(projectFilePath);
 	String extensionlessProjectPath = file_getExtensionless(projectFilePath);
 	this->title = file_getPathlessName(extensionlessProjectPath);
-	// TODO: Get the native extension for each type of file? .exe, .dll, .so...
 	#ifdef USE_MICROSOFT_WINDOWS
 		this->executableFilePath = string_combine(extensionlessProjectPath, U".exe");
 	#else
@@ -141,8 +140,7 @@ static void selectProject(int64_t projectIndex) {
 	updateInterface(true);
 }
 
-static void populateInterface(const ReadableString& folderPath) {
-	findProjects(folderPath);
+static void populateInterface() {
 	for (int p = 0; p < projects.length(); p++) {
 		component_call(projectList, U"PushElement", projects[p].title);
 	}
@@ -180,7 +178,9 @@ void dsrMain(List<String> args) {
 	// Find projects to showcase.
 	//   On systems that don't allow getting the application's folder, the program must be started somewhere within the Source folder.
 	String sourceFolder = findParent(applicationFolder, U"Source");
-	populateInterface(sourceFolder);
+	findProjects(file_combinePaths(sourceFolder, U"SDK"));
+	findProjects(file_combinePaths(sourceFolder, U"templates"));
+	populateInterface();
 
 	// Bind methods to events.
 	window_setKeyboardEvent(window, [](const KeyboardEvent& event) {