diff --git a/Fuse/AbstractRegisterState.cs b/Fuse/AbstractRegisterState.cs index 4185b0a..6d79e46 100644 --- a/Fuse/AbstractRegisterState.cs +++ b/Fuse/AbstractRegisterState.cs @@ -30,7 +30,7 @@ namespace Fuse protected virtual void ParseInternalState(string line) { - var tokens = line.Split(new char[] { ' ', '\t' }); + var tokens = line.Split(new char[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); this.ParseInternalState(tokens); } diff --git a/Fuse/TestEvent.cs b/Fuse/TestEvent.cs index 611f134..b401c66 100644 --- a/Fuse/TestEvent.cs +++ b/Fuse/TestEvent.cs @@ -35,7 +35,7 @@ namespace Fuse private bool TryParseLine(string line) { - var split = line.Split(new char[] { ' ', '\t' }); + var split = line.Split(new char[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); return this.TryParseLine(split); } diff --git a/Fuse/TestEvents.cs b/Fuse/TestEvents.cs index 76548d3..2fcfa57 100644 --- a/Fuse/TestEvents.cs +++ b/Fuse/TestEvents.cs @@ -14,7 +14,7 @@ namespace Fuse public void Parse(Lines lines) { - var success = false; + bool success; do { var e = new TestEvent(); diff --git a/Z80/Z80.FuseTest/App.config b/Z80/Z80.FuseTest/App.config new file mode 100644 index 0000000..193aecc --- /dev/null +++ b/Z80/Z80.FuseTest/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Z80/Z80.FuseTest/Program.cs b/Z80/Z80.FuseTest/Program.cs new file mode 100644 index 0000000..1bc9fc1 --- /dev/null +++ b/Z80/Z80.FuseTest/Program.cs @@ -0,0 +1,17 @@ +// +// Copyright (c) Adrian Conlon. All rights reserved. +// + +namespace Fuse +{ + class Program + { + static void Main(string[] args) + { + var suite = new TestSuite("fuse-tests\\tests"); + suite.Read(); + suite.Parse(); + suite.Run(); + } + } +} diff --git a/Z80/Z80.FuseTest/Properties/AssemblyInfo.cs b/Z80/Z80.FuseTest/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..cbbccd5 --- /dev/null +++ b/Z80/Z80.FuseTest/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Z80.FuseTest")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Z80.FuseTest")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("0233ff6c-db1a-4353-8e42-d22717770226")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Z80/Z80.FuseTest/RegisterState.cs b/Z80/Z80.FuseTest/RegisterState.cs new file mode 100644 index 0000000..007b807 --- /dev/null +++ b/Z80/Z80.FuseTest/RegisterState.cs @@ -0,0 +1,32 @@ +// +// Copyright (c) Adrian Conlon. All rights reserved. +// +namespace Fuse +{ + using System; + using System.Globalization; + + public class RegisterState : AbstractRegisterState, IRegisterState + { + public int I { get; private set; } = -1; + + public int R { get; private set; } = -1; + + public bool IFF1 { get; private set; } = false; + + public bool IFF2 { get; private set; } = false; + + public int IM { get; private set; } = -1; + + protected override void ParseInternalState(string[] tokens) + { + this.I = Convert.ToInt32(tokens[0], 16); + this.R = Convert.ToInt32(tokens[1], 16); + this.IFF1 = Convert.ToInt32(tokens[2], CultureInfo.InvariantCulture) == 1; + this.IFF2 = Convert.ToInt32(tokens[3], CultureInfo.InvariantCulture) == 1; + this.IM = Convert.ToInt32(tokens[4], CultureInfo.InvariantCulture); + this.Halted = Convert.ToInt32(tokens[5], CultureInfo.InvariantCulture) == 1; + this.TStates = Convert.ToInt32(tokens[6], CultureInfo.InvariantCulture); + } + } +} diff --git a/Z80/Z80.FuseTest/TestRunner.cs b/Z80/Z80.FuseTest/TestRunner.cs new file mode 100644 index 0000000..07fddf7 --- /dev/null +++ b/Z80/Z80.FuseTest/TestRunner.cs @@ -0,0 +1,361 @@ +// +// Copyright (c) Adrian Conlon. All rights reserved. +// + +namespace Fuse +{ + public enum Register + { + AF, + BC, + DE, + HL, + AF_, + BC_, + DE_, + HL_, + IX, + IY, + SP, + PC, + MEMPTR, + } + + public class TestRunner : EightBit.Bus + { + private readonly Test test; + private readonly Result result; + private readonly EightBit.Ram ram = new EightBit.Ram(0x10000); + private readonly EightBit.InputOutput ports = new EightBit.InputOutput(); + private readonly EightBit.Z80 cpu; + + public TestRunner(Test test, Result result) + { + this.cpu = new EightBit.Z80(this, this.ports); + this.test = test; + this.result = result; + } + + public bool Failed { get; private set; } = false; + + public bool Unimplemented { get; private set; } = false; + + public override EightBit.MemoryMapping Mapping(ushort address) => new EightBit.MemoryMapping(this.ram, 0, EightBit.Mask.Mask16, EightBit.AccessLevel.ReadWrite); + + public void Run() + { + this.RaisePOWER(); + this.Initialize(); + var allowedCycles = this.test.RegisterState.TStates; + try + { + this.cpu.Run(allowedCycles); + this.Check(); + } + catch (System.InvalidOperationException error) + { + this.Unimplemented = true; + System.Console.Error.WriteLine($"**** Error: {error.Message}"); + } + } + + public override void RaisePOWER() + { + base.RaisePOWER(); + this.cpu.RaisePOWER(); + this.cpu.RaiseRESET(); + this.cpu.RaiseINT(); + this.cpu.RaiseNMI(); + this.InitialiseRegisters(); + this.InitialiseMemory(); + } + + public override void LowerPOWER() + { + this.cpu.LowerPOWER(); + base.LowerPOWER(); + } + + public override void Initialize() + { + } + + private static void DumpDifference(string description, byte expected, byte actual) + { + var output = $"**** {description}, Expected: {expected:x2}, Got {actual:x2}"; + System.Console.Error.WriteLine(output); + } + + private static void DumpDifference(string highDescription, string lowDescription, EightBit.Register16 expected, EightBit.Register16 actual) + { + var expectedHigh = expected.High; + var expectedLow = expected.Low; + + var actualHigh = actual.High; + var actualLow = actual.Low; + + if (expectedHigh != actualHigh) + { + DumpDifference(highDescription, actualHigh, expectedHigh); + } + + if (expectedLow != actualLow) + { + DumpDifference(lowDescription, actualLow, expectedLow); + } + } + + private void InitialiseRegisters() + { + var testState = this.test.RegisterState; + var inputRegisters = testState.Registers; + + this.cpu.AF.Word = inputRegisters[(int)Register.AF_].Word; + this.cpu.BC.Word = inputRegisters[(int)Register.BC_].Word; + this.cpu.DE.Word = inputRegisters[(int)Register.DE_].Word; + this.cpu.HL.Word = inputRegisters[(int)Register.HL_].Word; + this.cpu.Exx(); + this.cpu.ExxAF(); + this.cpu.AF.Word = inputRegisters[(int)Register.AF].Word; + this.cpu.BC.Word = inputRegisters[(int)Register.BC].Word; + this.cpu.DE.Word = inputRegisters[(int)Register.DE].Word; + this.cpu.HL.Word = inputRegisters[(int)Register.HL].Word; + + this.cpu.IX.Word = inputRegisters[(int)Register.IX].Word; + this.cpu.IY.Word = inputRegisters[(int)Register.IY].Word; + + this.cpu.SP.Word = inputRegisters[(int)Register.SP].Word; + this.cpu.PC.Word = inputRegisters[(int)Register.PC].Word; + + this.cpu.MEMPTR.Word = inputRegisters[(int)Register.MEMPTR].Word; + + this.cpu.IV = (byte)testState.I; + this.cpu.REFRESH = (byte)testState.R; + this.cpu.IFF1 = testState.IFF1; + this.cpu.IFF2 = testState.IFF2; + this.cpu.IM = testState.IM; + } + + private void InitialiseMemory() + { + foreach (var memoryDatum in this.test.MemoryData) + { + var address = memoryDatum.Address; + foreach (var seed in memoryDatum.Bytes) + { + this.Poke(address++, seed); + } + } + } + + private void Check() + { + this.Checkregisters(); + this.CheckMemory(); + } + + private void Checkregisters() + { + var expectedState = this.result.RegisterState; + var expectedRegisters = expectedState.Registers; + + var af = this.cpu.AF.Word == expectedRegisters[(int)Register.AF].Word; + var bc = this.cpu.BC.Word == expectedRegisters[(int)Register.BC].Word; + var de = this.cpu.DE.Word == expectedRegisters[(int)Register.DE].Word; + var hl = this.cpu.HL.Word == expectedRegisters[(int)Register.HL].Word; + + this.cpu.Exx(); + this.cpu.ExxAF(); + + var af_ = this.cpu.AF.Word == expectedRegisters[(int)Register.AF_].Word; + var bc_ = this.cpu.BC.Word == expectedRegisters[(int)Register.BC_].Word; + var de_ = this.cpu.DE.Word == expectedRegisters[(int)Register.DE_].Word; + var hl_ = this.cpu.HL.Word == expectedRegisters[(int)Register.HL_].Word; + + var ix = this.cpu.IX.Word == expectedRegisters[(int)Register.IX].Word; + var iy = this.cpu.IY.Word == expectedRegisters[(int)Register.IY].Word; + + var sp = this.cpu.SP.Word == expectedRegisters[(int)Register.SP].Word; + var pc = this.cpu.PC.Word == expectedRegisters[(int)Register.PC].Word; + + var memptr = this.cpu.MEMPTR.Word == expectedRegisters[(int)Register.MEMPTR].Word; + + var iv = this.cpu.IV == expectedState.I; + var refresh = this.cpu.REFRESH == expectedState.R; + var iff1 = this.cpu.IFF1 == expectedState.IFF1; + var iff2 = this.cpu.IFF2 == expectedState.IFF2; + var im = this.cpu.IM == expectedState.IM; + + // And back again, so the following works as expected... + this.cpu.Exx(); + this.cpu.ExxAF(); + + var success = + af && bc && de && hl + && af_ && bc_ && de_ && hl_ + && ix && iy + && sp && pc + && iv && refresh + && iff1 && iff2 + && im + && memptr; + + if (!success) + { + this.Failed = true; + System.Console.Error.WriteLine($"**** Failed test (Register): {this.test.Description}"); + + if (!af) + { + var expectedWord = expectedRegisters[(int)Register.AF]; + var actualWord = this.cpu.AF; + DumpDifference("A", "F", expectedWord, actualWord); + } + + if (!bc) + { + var expectedWord = expectedRegisters[(int)Register.BC]; + var actualWord = this.cpu.BC; + DumpDifference("B", "C", expectedWord, actualWord); + } + + if (!de) + { + var expectedWord = expectedRegisters[(int)Register.DE]; + var actualWord = this.cpu.DE; + DumpDifference("D", "E", expectedWord, actualWord); + } + + if (!hl) + { + var expectedWord = expectedRegisters[(int)Register.HL]; + var actualWord = this.cpu.HL; + DumpDifference("H", "L", expectedWord, actualWord); + } + + if (!ix) + { + var expectedWord = expectedRegisters[(int)Register.IX]; + var actualWord = this.cpu.IX; + DumpDifference("IXH", "IXL", actualWord, expectedWord); + } + + if (!iy) + { + var expectedWord = expectedRegisters[(int)Register.IY]; + var actualWord = this.cpu.IY; + DumpDifference("IYH", "IYL", actualWord, expectedWord); + } + + if (!sp) + { + var expectedWord = expectedRegisters[(int)Register.SP]; + var actualWord = this.cpu.SP; + DumpDifference("SPH", "SPL", expectedWord, actualWord); + } + + if (!pc) + { + var expectedWord = expectedRegisters[(int)Register.PC]; + var actualWord = this.cpu.PC; + DumpDifference("PCH", "PCL", expectedWord, actualWord); + } + + if (!memptr) + { + var expectedWord = expectedRegisters[(int)Register.MEMPTR]; + var actualWord = this.cpu.MEMPTR; + DumpDifference("MEMPTRH", "MEMPTRL", actualWord, expectedWord); + } + + this.cpu.ExxAF(); + this.cpu.Exx(); + + if (!af_) + { + var expectedWord = expectedRegisters[(int)Register.AF]; + var actualWord = this.cpu.AF; + DumpDifference("A'", "F'", expectedWord, actualWord); + } + + if (!bc_) + { + var expectedWord = expectedRegisters[(int)Register.BC_]; + var actualWord = this.cpu.BC; + DumpDifference("B'", "C'", actualWord, expectedWord); + } + + if (!de_) + { + var expectedWord = expectedRegisters[(int)Register.DE_]; + var actualWord = this.cpu.DE; + DumpDifference("D'", "E'", actualWord, expectedWord); + } + + if (!hl_) + { + var expectedWord = expectedRegisters[(int)Register.HL_]; + var actualWord = this.cpu.HL; + DumpDifference("H'", "L'", actualWord, expectedWord); + } + + if (!iv) + { + var output = $"**** IV, Expected: {expectedState.I:X2}, Got: {this.cpu.IV:X2}"; + System.Console.Error.WriteLine(output); + } + + if (!refresh) + { + var output = $"**** R, Expected: {expectedState.R:X2}, Got: {this.cpu.REFRESH:X2}"; + System.Console.Error.WriteLine(output); + } + + if (!iff1) + { + var output = $"**** IFF1, Expected: {expectedState.IFF1}, Got: {this.cpu.IFF1}"; + System.Console.Error.WriteLine(output); + } + + if (!iff2) + { + var output = $"**** IFF2, Expected: {expectedState.IFF2}, Got: {this.cpu.IFF2}"; + System.Console.Error.WriteLine(output); + } + + if (!im) + { + var output = $"**** IM, Expected: {expectedState.IM}, Got: {this.cpu.IM}"; + System.Console.Error.WriteLine(output); + } + } + } + + private void CheckMemory() + { + var first = true; + + foreach (var memoryDatum in this.result.MemoryData) + { + var address = memoryDatum.Address; + foreach (var expected in memoryDatum.Bytes) + { + var actual = this.Peek(address); + if (expected != actual) + { + this.Failed = true; + if (first) + { + first = false; + System.Console.Error.WriteLine($"**** Failed test (Memory): {this.test.Description}"); + } + + System.Console.Error.WriteLine($"**** Difference: Address: {address:x4} Expected: {expected:x2} Actual: {actual:x2}"); + } + + ++address; + } + } + } + } +} diff --git a/Z80/Z80.FuseTest/TestSuite.cs b/Z80/Z80.FuseTest/TestSuite.cs new file mode 100644 index 0000000..e7de7e8 --- /dev/null +++ b/Z80/Z80.FuseTest/TestSuite.cs @@ -0,0 +1,58 @@ +// +// Copyright (c) Adrian Conlon. All rights reserved. +// +namespace Fuse +{ + public class TestSuite + { + private readonly Tests tests; + private readonly Results results; + + public TestSuite(string path) + { + this.tests = new Tests(path + ".in"); + this.results = new Results(path + ".expected"); + } + + public void Read() + { + this.tests.Read(); + this.results.Read(); + } + + public void Parse() + { + this.tests.Parse(); + this.results.Parse(); + } + + public void Run() + { + var failedCount = 0; + var unimplementedCount = 0; + foreach (var test in this.tests.Container) + { + var key = test.Key; + System.Console.Out.WriteLine($"** Checking: {key}"); + + var input = test.Value; + var result = this.results.Container[key]; + var runner = new TestRunner(input, result); + + runner.Run(); + if (runner.Failed) + { + ++failedCount; + } + + if (runner.Unimplemented) + { + ++unimplementedCount; + } + } + + System.Console.Out.WriteLine($"+++ Failed test count: {failedCount}"); + System.Console.Out.WriteLine($"+++ Unimplemented test count: {unimplementedCount}"); + } + } +} diff --git a/Z80/Z80.FuseTest/Z80.FuseTest.csproj b/Z80/Z80.FuseTest/Z80.FuseTest.csproj new file mode 100644 index 0000000..a1e218b --- /dev/null +++ b/Z80/Z80.FuseTest/Z80.FuseTest.csproj @@ -0,0 +1,70 @@ + + + + + Debug + AnyCPU + {0233FF6C-DB1A-4353-8E42-D22717770226} + Exe + Z80.FuseTest + Z80.FuseTest + v4.8 + 512 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + {6ebf8857-62a3-4ef4-af21-c1844031d7e4} + EightBit + + + {28e65032-5dff-406f-9385-0ee1422a7f4a} + Fuse + + + {c00648c1-bac1-4efb-816f-e87c091619d7} + Z80 + + + + \ No newline at end of file diff --git a/Z80/Z80.FuseTest/fuse-tests/README b/Z80/Z80.FuseTest/fuse-tests/README new file mode 100644 index 0000000..33c6309 --- /dev/null +++ b/Z80/Z80.FuseTest/fuse-tests/README @@ -0,0 +1,79 @@ +File formats +============ + +tests.in +-------- + +Each test has the format: + + +AF BC DE HL AF' BC' DE' HL' IX IY SP PC MEMPTR +I R IFF1 IFF2 IM + + specifies whether the Z80 is halted. + specifies the number of tstates to run the test for, in + decimal; the number actually executed may be higher, as the final + instruction is allowed to complete. + +Then followed by lines specifying the initial memory setup. Each has +the format: + + ... -1 + +eg + +1234 56 78 9a -1 + +says to put 0x56 at 0x1234, 0x78 at 0x1235 and 0x9a at 0x1236. + +Finally, -1 to end the test. Blank lines may follow before the next test. + +tests.expected +-------------- + +Each test output starts with the test description, followed by a list +of 'events': each has the format + +