diff --git a/bin/xenia-test b/bin/xenia-test new file mode 100755 index 000000000..2d3c4b484 --- /dev/null +++ b/bin/xenia-test @@ -0,0 +1,16 @@ +#!/bin/sh + +DIR="$( cd "$( dirname "$0" )" && pwd )" + +CONFIG=release +case "$*" in +(*--debug*) CONFIG=debug;; +esac + +EXEC=$DIR/../build/xenia/$CONFIG/xenia-test + +if [ ! -f "$EXEC" ]; then + python $DIR/../xenia-build.py build --$CONFIG +fi + +$EXEC "$@" diff --git a/test/codegen/Makefile b/test/codegen/Makefile new file mode 100644 index 000000000..262c972a7 --- /dev/null +++ b/test/codegen/Makefile @@ -0,0 +1,37 @@ +BINUTILS=../../build/binutils/ +PPC_AS=$(BINUTILS)/gas/as-new +PPC_LD=$(BINUTILS)/ld/ld-new +PPC_OBJDUMP=$(BINUTILS)/binutils/objdump + +SRCS=$(wildcard *.s) +BINS=$(SRCS:.s=.bin) +DISASMS=$(SRCS:.s=.dis) + +%.o: %.s + $(PPC_AS) \ + -a64 \ + -be \ + -mregnames \ + -mpower7 \ + -maltivec \ + -mvsx \ + -R \ + -o $@ \ + $< + +%.dis: %.o + $(PPC_OBJDUMP) --adjust-vma=0x82010000 -Mpower7 -D -EB $< > $@ + +%.bin: %.o + $(PPC_LD) \ + -A powerpc:common64 \ + -melf64ppc \ + -EB \ + -nostdlib \ + --oformat binary \ + -Ttext 0x82010000 \ + -e 0x82010000 \ + -o $@ \ + $< + +all: $(BINS) $(DISASMS) diff --git a/test/codegen/README.md b/test/codegen/README.md new file mode 100644 index 000000000..e35adf40c --- /dev/null +++ b/test/codegen/README.md @@ -0,0 +1,61 @@ +# Codegen Tests + +This directory contains the test assets used by the automated codegen test +runner. + +Each test is structured as a source `[name].s` PPC assembly file and the +generated outputs. The outputs are made using the custom build of binutils +setup when `xenia-build setup` is called and are checked in to make it easier +to run the tests on Windows. + +Tests are run using the `xenia-test` app or via `xenia-build test`. + +## Execution + +The test binary is placed into memory at `0x82010000` and all other memory is +zeroed. + +All registers are reset to zero. In order to provide useful inputs tests can +specify `# REGISTER_IN` values. + +The code is jumped into at the starting address and executed until the last +instruction in the input file is reached. + +After all instructions complete any `# REGISTER_OUT` values are checked and if +they do not match the test is failed. + +## Annotations + +Annotations can appear at any line in a file. If a number is required it can +be in either hex or decimal form, or IEEE if floating-point. + +### REGISTER_IN + +``` +# REGISTER_IN [register name] [register value] +``` + +Sets the value of a register prior to executing the instructions. + +Examples: +``` +# REGISTER_IN r4 0x1234 +# REGISTER_IN r4 5678 +``` + +### REGISTER_OUT + +``` +# REGISTER_OUT [register name] [register value] +``` + +Defines the expected register value when the instructions have executed. +If after all instructions have completed the register value does not match +the value given here the test will fail. + +Examples: +``` +# REGISTER_OUT r3 123 +``` + +TODO: memory setup/assertions diff --git a/test/codegen/ori.bin b/test/codegen/ori.bin new file mode 100755 index 000000000..fd7833805 --- /dev/null +++ b/test/codegen/ori.bin @@ -0,0 +1 @@ +`ƒÿÿ \ No newline at end of file diff --git a/test/codegen/ori.dis b/test/codegen/ori.dis new file mode 100644 index 000000000..db3cdd1fd --- /dev/null +++ b/test/codegen/ori.dis @@ -0,0 +1,8 @@ + +ori.o: file format elf64-powerpc + + +Disassembly of section .text: + +0000000082010000 <.text>: + 82010000: 60 83 ff ff ori r3,r4,65535 diff --git a/test/codegen/ori.s b/test/codegen/ori.s new file mode 100644 index 000000000..c5e6810c4 --- /dev/null +++ b/test/codegen/ori.s @@ -0,0 +1,5 @@ +# REGISTER_IN r4 0xDEADBEEFCAFEBABE + +ori r3, r4, 0xFFFF + +# REGISTER_OUT r3 0xBABE diff --git a/tools/tools.gypi b/tools/tools.gypi index b38c747db..9b1934551 100644 --- a/tools/tools.gypi +++ b/tools/tools.gypi @@ -3,5 +3,6 @@ 'includes': [ 'xenia-info/xenia-info.gypi', 'xenia-run/xenia-run.gypi', + 'xenia-test/xenia-test.gypi', ], } diff --git a/tools/xenia-test/xenia-test.cc b/tools/xenia-test/xenia-test.cc new file mode 100644 index 000000000..1f0e6c7cd --- /dev/null +++ b/tools/xenia-test/xenia-test.cc @@ -0,0 +1,217 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2013 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include + +#include +#include + + +using namespace std; +using namespace xe; +using namespace xe::cpu; +using namespace xe::kernel; + + +DEFINE_string(test_path, "test/codegen/", + "Directory scanned for test files."); + + +typedef vector > annotations_list_t; + + +int read_annotations(xe_pal_ref pal, string& src_file_path, + annotations_list_t& annotations) { + // TODO(benvanik): use PAL instead of this + FILE* f = fopen(src_file_path.c_str(), "r"); + char line_buffer[BUFSIZ]; + while (fgets(line_buffer, sizeof(line_buffer), f)) { + if (strlen(line_buffer) > 3 && + line_buffer[0] == '#' && + line_buffer[1] == ' ') { + // Comment - check if formed like an annotation. + // We don't actually verify anything here. + char* next_space = strchr(line_buffer + 3, ' '); + if (next_space) { + // Looks legit. + string key = string(line_buffer + 2, next_space); + string value = string(next_space + 1); + while (value.find_last_of(" \t\n") == value.size() - 1) { + value.erase(value.end() - 1); + } + annotations.push_back(pair(key, value)); + } + } + } + fclose(f); + + return 0; +} + +int setup_test_state(xe_memory_ref memory, Processor* processor, + annotations_list_t& annotations) { + for (annotations_list_t::iterator it = annotations.begin(); + it != annotations.end(); ++it) { + if (it->first == "REGISTER_IN") { + printf("REGISTER_IN : %s\n", it->second.c_str()); + } + } + return 0; +} + +int check_test_results(xe_memory_ref memory, Processor* processor, + annotations_list_t& annotations) { + for (annotations_list_t::iterator it = annotations.begin(); + it != annotations.end(); ++it) { + if (it->first == "REGISTER_OUT") { + printf("REGISTER_OUT : %s\n", it->second.c_str()); + } + } + return 0; +} + +int run_test(xe_pal_ref pal, string& src_file_path) { + int result_code = 1; + + // test.s -> test.bin + string bin_file_path; + size_t dot = src_file_path.find_last_of(".s"); + bin_file_path = src_file_path; + bin_file_path.replace(dot - 1, 2, ".bin"); + + xe_memory_ref memory = NULL; + shared_ptr processor; + shared_ptr runtime; + annotations_list_t annotations; + + XEEXPECTZERO(read_annotations(pal, src_file_path, annotations)); + + xe_memory_options_t memory_options; + xe_zero_struct(&memory_options, sizeof(memory_options)); + memory = xe_memory_create(pal, memory_options); + XEEXPECTNOTNULL(memory); + + processor = shared_ptr(new Processor(pal, memory)); + XEEXPECTZERO(processor->Setup()); + + runtime = shared_ptr(new Runtime(pal, processor, XT(""))); + + // TODO(benvanik): load test binary file into memory + // bin_file_path + // XEEXPECTZERO(runtime->LoadModule(path)); + + // Setup test state from annotations. + XEEXPECTZERO(setup_test_state(memory, processor.get(), annotations)); + + // Execute test. + // TODO(benvanik): execute test + + // Assert test state expectations. + XEEXPECTZERO(check_test_results(memory, processor.get(), annotations)); + + result_code = 0; +XECLEANUP: + runtime.reset(); + processor.reset(); + xe_memory_release(memory); + return result_code; +} + +int discover_tests(string& test_path, + vector& test_files) { + // TODO(benvanik): use PAL instead of this + DIR* d = opendir(test_path.c_str()); + struct dirent* dir; + while ((dir = readdir(d))) { + if (dir->d_type == DT_REG) { + // Only return .s files. + string file_name = string(dir->d_name); + if (file_name.rfind(".s") != string::npos) { + string file_path = test_path; + if (*(test_path.end() - 1) != '/') { + file_path += "/"; + } + file_path += file_name; + test_files.push_back(file_path); + } + } + } + closedir(d); + return 0; +} + +int run_tests(const xechar_t* test_name) { + int result_code = 1; + int failed_count = 0; + int passed_count = 0; + + xe_pal_ref pal = NULL; + vector test_files; + + xe_pal_options_t pal_options; + xe_zero_struct(&pal_options, sizeof(pal_options)); + pal = xe_pal_create(pal_options); + XEEXPECTNOTNULL(pal); + + XEEXPECTZERO(discover_tests(FLAGS_test_path, test_files)); + if (!test_files.size()) { + printf("No tests discovered - invalid path?\n"); + XEFAIL(); + } + printf("%d tests discovered.\n", (int)test_files.size()); + printf("\n"); + + for (vector::iterator it = test_files.begin(); + it != test_files.end(); ++it) { + if (test_name && *it != test_name) { + continue; + } + + printf("Running %s... ", (*it).c_str()); + if (run_test(pal, *it)) { + printf("FAILED\n"); + failed_count++; + } else { + printf("PASSED\n"); + passed_count++; + } + } + + printf("\n"); + printf("Total tests: %d\n", failed_count + passed_count); + printf("Passed: %d\n", passed_count); + printf("Failed: %d\n", failed_count); + + result_code = failed_count ? 1 : 0; +XECLEANUP: + xe_pal_release(pal); + return result_code; +} + +int xenia_test(int argc, xechar_t **argv) { + string usage = "usage: "; + usage = usage + argv[0] + " some.xex"; + google::SetUsageMessage(usage); + google::SetVersionString("1.0"); + google::ParseCommandLineFlags(&argc, &argv, true); + + int result_code = 1; + + // Grab test name, if present. + const xechar_t* test_name = NULL; + if (argc >= 2) { + test_name = argv[1]; + } + + result_code = run_tests(test_name); + + google::ShutDownCommandLineFlags(); + return result_code; +} +XE_MAIN_THUNK(xenia_test); diff --git a/tools/xenia-test/xenia-test.gypi b/tools/xenia-test/xenia-test.gypi new file mode 100644 index 000000000..1114272da --- /dev/null +++ b/tools/xenia-test/xenia-test.gypi @@ -0,0 +1,24 @@ +# Copyright 2013 Ben Vanik. All Rights Reserved. +{ + 'targets': [ + { + 'target_name': 'xenia-test', + 'type': 'executable', + + 'dependencies': [ + 'xeniacore', + 'xeniacpu', + 'xeniagpu', + 'xeniakernel', + ], + + 'include_dirs': [ + '.', + ], + + 'sources': [ + 'xenia-test.cc', + ], + }, + ], +} diff --git a/xenia-build.py b/xenia-build.py index e62e76911..272973cac 100755 --- a/xenia-build.py +++ b/xenia-build.py @@ -61,6 +61,7 @@ def discover_commands(): 'pull': PullCommand(), 'gyp': GypCommand(), 'build': BuildCommand(), + 'test': TestCommand(), 'xethunk': XethunkCommand(), 'clean': CleanCommand(), 'nuke': NukeCommand(), @@ -224,6 +225,7 @@ class SetupCommand(Command): print '' # Binutils. + # TODO(benvanik): disable on Windows print '- binutils...' if not os.path.exists('build/binutils'): os.makedirs('build/binutils') @@ -400,6 +402,35 @@ class BuildCommand(Command): return 0 +class TestCommand(Command): + """'test' command.""" + + def __init__(self, *args, **kwargs): + super(TestCommand, self).__init__( + name='test', + help_short='Runs all tests.', + *args, **kwargs) + + def execute(self, args, cwd): + print 'Testing...' + print '' + + # First run make and update all of the test files. + # TOOD(benvanik): disable on Windows + print 'Updating test files...' + result = shell_call('make -C test/codegen/') + print '' + if result != 0: + return result + + # Start the test runner. + print 'Launching test runner...' + result = shell_call('bin/xenia-test') + print '' + + return result + + class XethunkCommand(Command): """'xethunk' command.""" diff --git a/xeniarc b/xeniarc index 8d1575a1c..e9dd78104 100644 --- a/xeniarc +++ b/xeniarc @@ -4,4 +4,5 @@ alias xb='python xenia-build.py' alias xbb='python xenia-build.py build' +alias xbt='python xenia-build.py test' alias xbc='python xenia-build.py clean'