Add the tester
This commit is contained in:
		| @@ -4,15 +4,14 @@ require 'net/http' | ||||
| require 'rake/clean' | ||||
| require 'open3' | ||||
| require 'etc' | ||||
| require_relative 'shared' | ||||
|  | ||||
| GCC_VERSION = "14.2.0" | ||||
| BINUTILS_VERSION = '2.43.1' | ||||
| GLIBC_VERSION = '2.40' | ||||
| KERNEL_VERSION = '5.15.166' | ||||
|  | ||||
| TMP = Pathname.new('./build') | ||||
|  | ||||
| CLEAN.include TMP | ||||
| CLOBBER.include TMP | ||||
|  | ||||
| class BuildTarget | ||||
|   attr_accessor(:build, :gcc, :target, :tmp) | ||||
| @@ -87,11 +86,16 @@ def download_and_unarchive(url, target) | ||||
|         download_and_unarchive URI.parse(response['location']) | ||||
|       when Net::HTTPSuccess | ||||
|         Open3.popen2 'tar', '-C', target.to_path, archive_type, '-xv' do |stdin, stdout, wait_thread| | ||||
|           stdout.close | ||||
|           Thread.new do | ||||
|             stdout.each { |line| puts line } | ||||
|           end | ||||
|  | ||||
|           response.read_body do |chunk| | ||||
|             stdin.write chunk | ||||
|           end | ||||
|           stdin.close | ||||
|  | ||||
|           wait_thread.value | ||||
|         end | ||||
|       else | ||||
|         response.error! | ||||
| @@ -301,7 +305,27 @@ namespace :cross do | ||||
|     sh env, 'make', '-j', Etc.nprocessors.to_s, chdir: cwd.to_path | ||||
|     sh env, 'make', 'install', chdir: cwd.to_path | ||||
|   end | ||||
|  | ||||
|   task :init, [:target]  do |_, args| | ||||
|     options = find_build_target GCC_VERSION, args | ||||
|     env = { | ||||
|       'PATH' => "#{options.rootfs.realpath + 'bin'}:#{ENV['PATH']}" | ||||
|     } | ||||
|     sh env, 'riscv32-unknown-linux-gnu-gcc', | ||||
|       '-ffreestanding', '-static', | ||||
|       '-o', (options.tools + 'init').to_path, | ||||
|       'tools/init.c' | ||||
|   end | ||||
| end | ||||
|  | ||||
| task cross: ['cross:binutils', 'cross:gcc1', 'cross:headers', 'cross:kernel', 'cross:glibc', 'cross:gcc2'] do | ||||
| desc 'Build cross toolchain' | ||||
| task cross: [ | ||||
|   'cross:binutils', | ||||
|   'cross:gcc1', | ||||
|   'cross:headers', | ||||
|   'cross:kernel', | ||||
|   'cross:glibc', | ||||
|   'cross:gcc2', | ||||
|   'cross:init' | ||||
| ] do | ||||
| end | ||||
|   | ||||
							
								
								
									
										1
									
								
								rakelib/shared.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								rakelib/shared.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| TMP = Pathname.new('./build') | ||||
							
								
								
									
										80
									
								
								rakelib/tester.rake
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								rakelib/tester.rake
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | ||||
| require 'open3' | ||||
| require 'rake/clean' | ||||
| require_relative 'shared' | ||||
|  | ||||
| CLEAN.include(TMP + 'riscv') | ||||
|  | ||||
| LINKER = 'build/rootfs/riscv32-unknown-linux-gnu/bin/ld' | ||||
|  | ||||
| namespace :test do | ||||
|   test_sources = FileList['tests/vm/*.elna'] | ||||
|   compiler = `cabal list-bin elna`.strip | ||||
|   object_directory = TMP + 'riscv/tests' | ||||
|   root_directory = TMP + 'riscv/root' | ||||
|   executable_directory = root_directory + 'tests' | ||||
|   expectation_directory = root_directory + 'expectations' | ||||
|   init = TMP + 'riscv/root/init' | ||||
|  | ||||
|   directory root_directory | ||||
|   directory object_directory | ||||
|   directory executable_directory | ||||
|   directory expectation_directory | ||||
|  | ||||
|   test_files = test_sources.flat_map do |test_source| | ||||
|     test_basename = File.basename(test_source, '.elna') | ||||
|     test_object = object_directory + test_basename.ext('.o') | ||||
|  | ||||
|     file test_object => [test_source, object_directory] do | ||||
|       sh compiler, '--output', test_object.to_path, test_source | ||||
|     end | ||||
|     test_executable = executable_directory + test_basename | ||||
|  | ||||
|     file test_executable => [test_object, executable_directory] do | ||||
|       sh LINKER, '-o', test_executable.to_path, test_object.to_path | ||||
|     end | ||||
|     expectation_name = test_basename.ext '.txt' | ||||
|     source_expectation = "tests/expectations/#{expectation_name}" | ||||
|     target_expectation = expectation_directory + expectation_name | ||||
|  | ||||
|     file target_expectation => [source_expectation, expectation_directory] do | ||||
|       cp source_expectation, target_expectation | ||||
|     end | ||||
|  | ||||
|     [test_executable, target_expectation] | ||||
|   end | ||||
|  | ||||
|   file init => [root_directory] do |task| | ||||
|     cp (TMP + 'tools/init'), task.name | ||||
|   end | ||||
|   test_files << init << executable_directory << expectation_directory | ||||
|  | ||||
|   file (TMP + 'riscv/root.cpio') => test_files do |task| | ||||
|     root_files = task.prerequisites | ||||
|       .map { |prerequisite| Pathname.new(prerequisite).relative_path_from(root_directory).to_path } | ||||
|  | ||||
|     File.open task.name, 'wb' do |cpio_file| | ||||
|       cpio_options = { | ||||
|         chdir: root_directory.to_path | ||||
|       } | ||||
|       cpio_stream = Open3.popen2 'cpio', '-o', '--format=newc', cpio_options do |stdin, stdout, wait_thread| | ||||
|         stdin.write root_files.join("\n") | ||||
|         stdin.close | ||||
|         stdout.each { |chunk| cpio_file.write chunk } | ||||
|         wait_thread.value | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   task :vm => (TMP + 'riscv/root.cpio') do |task| | ||||
|     kernels = FileList.glob(TMP + 'tools/linux-*/arch/riscv/boot/Image') | ||||
|  | ||||
|     sh 'qemu-system-riscv32', | ||||
|       '-nographic', | ||||
|       '-M', 'virt', | ||||
|       '-bios', 'default', | ||||
|       '-kernel', kernels.first, | ||||
|       '-append', 'quiet panic=1', | ||||
|       '-initrd', task.prerequisites.first, | ||||
|       '-no-reboot' | ||||
|   end | ||||
| end | ||||
							
								
								
									
										0
									
								
								tests/expectations/empty.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tests/expectations/empty.txt
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								tests/vm/empty.elna
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tests/vm/empty.elna
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										205
									
								
								tools/init.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										205
									
								
								tools/init.c
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,205 @@ | ||||
| #include <stdio.h> | ||||
| #include <dirent.h> | ||||
| #include <fcntl.h> | ||||
| #include <unistd.h> | ||||
| #include <sys/stat.h> | ||||
| #include <sys/wait.h> | ||||
| #include <sys/reboot.h> | ||||
|  | ||||
| #define FILENAME_BUFFER_SIZE 256 | ||||
|  | ||||
| size_t read_command(int descriptor, char *command_buffer) | ||||
| { | ||||
|     ssize_t bytes_read = 0; | ||||
|     size_t read_so_far = 0; | ||||
|  | ||||
|     while ((bytes_read = read(descriptor, command_buffer + read_so_far, FILENAME_BUFFER_SIZE - read_so_far - 1)) > 0) | ||||
|     { | ||||
|         read_so_far += bytes_read; | ||||
|         if (read_so_far >= FILENAME_BUFFER_SIZE - 1) | ||||
|         { | ||||
|             break; | ||||
|         } | ||||
|     } | ||||
|     command_buffer[read_so_far] = 0; | ||||
|     return read_so_far; | ||||
| } | ||||
|  | ||||
| enum status | ||||
| { | ||||
|     status_success, | ||||
|     status_failure, | ||||
|     status_warning, | ||||
|     status_fatal | ||||
| }; | ||||
|  | ||||
| unsigned int make_path(char *destination, const char *directory, const char *filename, const char *extension) | ||||
| { | ||||
|     unsigned int i = 0; | ||||
|  | ||||
|     for (; i < FILENAME_BUFFER_SIZE; i++) | ||||
|     { | ||||
|         if (directory[i] == 0) | ||||
|         { | ||||
|             break; | ||||
|         } | ||||
|         destination[i] = directory[i]; | ||||
|     } | ||||
|     for (int j = 0; i < FILENAME_BUFFER_SIZE; i++, j++) | ||||
|     { | ||||
|         if (filename[j] == 0) | ||||
|         { | ||||
|             break; | ||||
|         } | ||||
|         destination[i] = filename[j]; | ||||
|     } | ||||
|     if (extension == NULL) | ||||
|     { | ||||
|         goto done; | ||||
|     } | ||||
|     for (int j = 0; i < FILENAME_BUFFER_SIZE; i++, j++) | ||||
|     { | ||||
|         if (extension[j] == 0) | ||||
|         { | ||||
|             break; | ||||
|         } | ||||
|         destination[i] = extension[j]; | ||||
|     } | ||||
| done: | ||||
|     destination[i] = 0; | ||||
|  | ||||
|     return i; | ||||
| } | ||||
|  | ||||
| enum status run_test(const char *file_entry_name) | ||||
| { | ||||
|     printf("Running %s. ", file_entry_name); | ||||
|  | ||||
|     char filename[FILENAME_BUFFER_SIZE]; | ||||
|     char command_buffer[FILENAME_BUFFER_SIZE]; | ||||
|     char file_buffer[256]; | ||||
|     int pipe_ends[2]; | ||||
|  | ||||
|     if (pipe(pipe_ends) == -1) | ||||
|     { | ||||
|         perror("pipe"); | ||||
|         return status_fatal; | ||||
|     } | ||||
|     make_path(filename, "./tests/", file_entry_name, NULL); | ||||
|  | ||||
|     int child_pid = fork(); | ||||
|     if (child_pid == -1) | ||||
|     { | ||||
|         return status_fatal; | ||||
|     } | ||||
|     else if (child_pid == 0) | ||||
|     { | ||||
|         close(STDIN_FILENO); | ||||
|         close(STDERR_FILENO); | ||||
|         close(pipe_ends[0]); // Close the read end. | ||||
|  | ||||
|         if (dup2(pipe_ends[1], STDOUT_FILENO) == -1) | ||||
|         { | ||||
|             perror("dup2"); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             execl(filename, filename); | ||||
|             perror("execl"); | ||||
|         } | ||||
|         close(STDOUT_FILENO); | ||||
|         close(pipe_ends[1]); | ||||
|         _exit(1); | ||||
|     } | ||||
|     else | ||||
|     { | ||||
|         close(pipe_ends[1]); // Close the write end. | ||||
|         read_command(pipe_ends[0], command_buffer); | ||||
|         close(pipe_ends[0]); | ||||
|  | ||||
|         int wait_status = 0; | ||||
|         wait(&wait_status); | ||||
|  | ||||
|         make_path(filename, "./expectations/", file_entry_name, ".txt"); | ||||
|  | ||||
|         FILE *expectation_descriptor = fopen(filename, "r"); | ||||
|  | ||||
|         if (expectation_descriptor == NULL) | ||||
|         { | ||||
|             return status_warning; | ||||
|         } | ||||
|         size_t read_from_file = fread(file_buffer, 1, sizeof(file_buffer) - 1, expectation_descriptor); | ||||
|         fclose(expectation_descriptor); | ||||
|  | ||||
|         file_buffer[read_from_file] = 0; | ||||
|         for (unsigned int i = 0; ; ++i) | ||||
|         { | ||||
|             if (command_buffer[i] == 0 && file_buffer[i] == 0) | ||||
|             { | ||||
|                 fwrite("\n", 1, 1, stdout); | ||||
|                 return status_success; | ||||
|             } | ||||
|             else if (command_buffer[i] != file_buffer[i]) | ||||
|             { | ||||
|                 printf("Failed. Got:\n%s", command_buffer); | ||||
|                 return status_failure; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct summary | ||||
| { | ||||
|     size_t total; | ||||
|     size_t failure; | ||||
|     size_t success; | ||||
| }; | ||||
|  | ||||
| void walk() | ||||
| { | ||||
|     DIR *directory_stream = opendir("./tests"); | ||||
|     struct dirent *file_entry; | ||||
|  | ||||
|     struct summary test_summary = { .total = 0, .failure = 0, .success = 0 }; | ||||
|  | ||||
|     while ((file_entry = readdir(directory_stream)) != NULL) | ||||
|     { | ||||
|         if (file_entry->d_name[0] == '.') | ||||
|         { | ||||
|             continue; | ||||
|         } | ||||
|         ++test_summary.total; | ||||
|         switch (run_test(file_entry->d_name)) | ||||
|         { | ||||
|             case status_failure: | ||||
|                 ++test_summary.failure; | ||||
|                 break; | ||||
|             case status_success: | ||||
|                 ++test_summary.success; | ||||
|                 break; | ||||
|             case status_warning: | ||||
|                 break; | ||||
|             case status_fatal: | ||||
|                 goto end_walk; | ||||
|         } | ||||
|     } | ||||
|     printf("Successful: %lu, Failed: %lu, Total: %lu.\n", | ||||
|             test_summary.success, test_summary.failure, test_summary.total); | ||||
| end_walk: | ||||
|     closedir(directory_stream); | ||||
| } | ||||
|  | ||||
| int main() | ||||
| { | ||||
|     int dev_console = open("/dev/console", O_WRONLY); | ||||
|     if (dev_console != -1) | ||||
|     { | ||||
|         dup2(dev_console, STDOUT_FILENO); | ||||
|         walk(); | ||||
|         close(dev_console); | ||||
|     } | ||||
|     sync(); | ||||
|     reboot(RB_POWER_OFF); | ||||
|  | ||||
|     return 1; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user