Added single step tests and fixed a lot of CPU bugs

This commit is contained in:
Badvision 2024-08-25 23:07:51 -05:00
parent 9c71dec304
commit 15e0133e4b
8 changed files with 479 additions and 97 deletions

View File

@ -189,6 +189,12 @@
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<scope>test</scope>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>org.xerial.thirdparty</groupId>
<artifactId>nestedvm</artifactId>

View File

@ -136,7 +136,7 @@ public class MOS65C02 extends CPU {
BBS6(0x0ef, COMMAND.BBS6, MODE.ZP_REL, 5, true),
BBS7(0x0ff, COMMAND.BBS7, MODE.ZP_REL, 5, true),
BEQ_REL0(0x00F0, COMMAND.BEQ, MODE.RELATIVE, 2),
BIT_IMM(0x0089, COMMAND.BIT, MODE.IMMEDIATE, 2, true),
BIT_IMM(0x0089, COMMAND.BIT, MODE.IMMEDIATE, 3, true),
BIT_ZP(0x0024, COMMAND.BIT, MODE.ZEROPAGE, 3),
BIT_ZP_X(0x0034, COMMAND.BIT, MODE.ZEROPAGE_X, 4, true),
BIT_AB(0x002C, COMMAND.BIT, MODE.ABSOLUTE, 4),
@ -193,7 +193,8 @@ public class MOS65C02 extends CPU {
INX(0x00E8, COMMAND.INX, MODE.IMPLIED, 2),
INY(0x00C8, COMMAND.INY, MODE.IMPLIED, 2),
JMP_AB(0x004C, COMMAND.JMP, MODE.ABSOLUTE, 3, false, false),
JMP_IND(0x006C, COMMAND.JMP, MODE.INDIRECT, 5),
// JMP_IND(0x006C, COMMAND.JMP, MODE.INDIRECT_BUGGY, 6),
JMP_IND(0x006C, COMMAND.JMP, MODE.INDIRECT, 6),
JMP_IND_X(0x007C, COMMAND.JMP, MODE.INDIRECT_X, 6, true),
JSR_AB(0x0020, COMMAND.JSR, MODE.ABSOLUTE, 6, false, false),
LDA_IMM(0x00A9, COMMAND.LDA, MODE.IMMEDIATE, 2),
@ -386,6 +387,10 @@ public class MOS65C02 extends CPU {
ZEROPAGE(2, "$~1", (cpu) -> cpu.getMemory().read(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false) & 0x00FF),
ZEROPAGE_X(2, "$~1,X", (cpu) -> 0x0FF & (cpu.getMemory().read(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false) + cpu.X)),
ZEROPAGE_Y(2, "$~1,Y", (cpu) -> 0x0FF & (cpu.getMemory().read(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false) + cpu.Y)),
INDIRECT_BUGGY(3, "$(~2~1)", (cpu) -> {
int address = cpu.getMemory().readWord(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false);
return cpu.getMemory().readWordPageWraparound(address, TYPE.READ_DATA, true, false);
}),
INDIRECT(3, "$(~2~1)", (cpu) -> {
int address = cpu.getMemory().readWord(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false);
return cpu.getMemory().readWord(address, TYPE.READ_DATA, true, false);
@ -396,15 +401,15 @@ public class MOS65C02 extends CPU {
}),
INDIRECT_ZP(2, "$(~1)", (cpu) -> {
int address = cpu.getMemory().read(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false);
return cpu.getMemory().readWord(address & 0x0FF, TYPE.READ_DATA, true, false);
return cpu.getMemory().readWordPageWraparound(address & 0x0FF, TYPE.READ_DATA, true, false);
}),
INDIRECT_ZP_X(2, "$(~1,X)", (cpu) -> {
int address = cpu.getMemory().read(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false) + cpu.X;
return cpu.getMemory().readWord(address & 0x0FF, TYPE.READ_DATA, true, false);
return cpu.getMemory().readWordPageWraparound(address & 0x0FF, TYPE.READ_DATA, true, false);
}),
INDIRECT_ZP_Y(2, "$(~1),Y", (cpu) -> {
int address = 0x00FF & cpu.getMemory().read(cpu.getProgramCounter() + 1, TYPE.READ_OPERAND, cpu.readAddressTriggersEvent, false);
address = cpu.getMemory().readWord(address, TYPE.READ_DATA, true, false);
address = cpu.getMemory().readWordPageWraparound(address, TYPE.READ_DATA, true, false);
int address2 = address + cpu.Y;
cpu.setPageBoundaryPenalty((address & 0x00ff00) != (address2 & 0x00ff00));
cpu.setPageBoundaryApplied(true);
@ -526,7 +531,8 @@ public class MOS65C02 extends CPU {
public void processCommand(int address, int value, MODE addressMode, MOS65C02 cpu) {
if ((value & (1 << bit)) == 0) {
cpu.setProgramCounter(address);
cpu.addWaitCycles(cpu.pageBoundaryPenalty ? 2 : 1);
cpu.setPageBoundaryApplied(true);
cpu.addWaitCycles(1);
}
}
}
@ -666,9 +672,9 @@ public class MOS65C02 extends CPU {
BIT((address, value, addressMode, cpu) -> {
int result = (cpu.A & value);
cpu.Z = result == 0;
cpu.N = (value & 0x080) != 0;
// As per http://www.6502.org/tutorials/vflag.html
if (addressMode != MODE.IMMEDIATE) {
cpu.N = (value & 0x080) != 0;
cpu.V = (value & 0x040) != 0;
}
}),
@ -831,7 +837,7 @@ public class MOS65C02 extends CPU {
cpu.push((byte) cpu.A);
}),
PHP((address, value, addressMode, cpu) -> {
cpu.push((cpu.getStatus()));
cpu.push((byte) (cpu.getStatus() | 0x10));
}),
PHX((address, value, addressMode, cpu) -> {
cpu.push((byte) cpu.X);
@ -969,11 +975,11 @@ public class MOS65C02 extends CPU {
cpu.setNZ(cpu.Y);
}),
TRB((address, value, addressMode, cpu) -> {
cpu.C = (value & cpu.A) != 0 ? 1 : 0;
cpu.Z = (value & cpu.A) == 0;
cpu.getMemory().write(address, (byte) (value & ~cpu.A), true, false);
}),
TSB((address, value, addressMode, cpu) -> {
cpu.C = (value & cpu.A) != 0 ? 1 : 0;
cpu.Z = (value & cpu.A) == 0;
cpu.getMemory().write(address, (byte) (value | cpu.A), true, false);
}),
TSX((address, value, addressMode, cpu) -> {
@ -1061,10 +1067,6 @@ public class MOS65C02 extends CPU {
bytes = 2;
wait = 2;
}
case 3, 7, 0x0b, 0x0f -> {
wait = 1;
bytes = 1;
}
case 4 -> {
bytes = 2;
if ((op & 0x0f0) == 0x040) {
@ -1081,8 +1083,12 @@ public class MOS65C02 extends CPU {
wait = 4;
}
}
default -> bytes = 2;
default -> {
wait = 1;
bytes = 1;
}
}
wait--;
incrementProgramCounter(bytes);
addWaitCycles(wait);
@ -1129,7 +1135,7 @@ public class MOS65C02 extends CPU {
return getMemory().read(0x0100 + STACK, TYPE.READ_DATA, true, false);
}
private byte getStatus() {
public byte getStatus() {
return (byte) ((N ? 0x080 : 0)
| (V ? 0x040 : 0)
| 0x020
@ -1140,10 +1146,17 @@ public class MOS65C02 extends CPU {
| ((C > 0) ? 0x01 : 0));
}
private void setStatus(byte b) {
public void setStatus(byte b) {
setStatus(b, false);
}
public void setStatus(byte b, boolean setBreakFlag) {
N = (b & 0x080) != 0;
V = (b & 0x040) != 0;
// B flag is unaffected in this way.
// B flag is normally unaffected in this way, can be bypassed for unit testing
if (setBreakFlag) {
B = (b & 0x010) != 0;
}
D = (b & 0x08) != 0;
I = (b & 0x04) != 0;
Z = (b & 0x02) != 0;
@ -1171,10 +1184,13 @@ public class MOS65C02 extends CPU {
LOG.log(Level.WARNING, "BRK at ${0}", Integer.toString(getProgramCounter(), 16));
dumpTrace();
}
B = true;
programCounter++;
pushPC();
push((byte) (getStatus() | 0x010));
// 65c02 clears D flag on BRK
I = true;
D = false;
interruptSignalled = true;
setProgramCounter(getMemory().readWord(INT_VECTOR, TYPE.READ_DATA, true, false));
}
// Hardware IRQ generated
@ -1191,10 +1207,10 @@ public class MOS65C02 extends CPU {
}
interruptSignalled = false;
if (!I || B) {
I = false;
I = true;
pushWord(getProgramCounter());
push(getStatus());
I = true;
B = false;
int newPC = getMemory().readWord(INT_VECTOR, TYPE.READ_DATA, true, false);
// System.out.println("Interrupt generated, setting PC to (" + Integer.toString(INT_VECTOR, 16) + ") = " + Integer.toString(newPC, 16));
setProgramCounter(newPC);
@ -1206,6 +1222,7 @@ public class MOS65C02 extends CPU {
public void reset() {
pushWord(getProgramCounter());
push(getStatus());
setWaitCycles(0);
// STACK = 0x0ff;
// B = false;
B = true;
@ -1217,7 +1234,7 @@ public class MOS65C02 extends CPU {
// Z = true;
int resetVector = getMemory().readWord(RESET_VECTOR, TYPE.READ_DATA, true, false);
int newPC = resetVector;
LOG.log(Level.WARNING, "Reset called, setting PC to ({0}) = {1}", new Object[]{Integer.toString(RESET_VECTOR, 16), Integer.toString(newPC, 16)});
// LOG.log(Level.WARNING, "Reset called, setting PC to ({0}) = {1}", new Object[]{Integer.toString(RESET_VECTOR, 16), Integer.toString(newPC, 16)});
setProgramCounter(newPC);
}

View File

@ -65,6 +65,11 @@ public abstract class Device implements Reconfigurable {
return _ram;
}
// NOTE: This is for unit testing only, don't actually use this for anything else or expect things to be weird.
public void setMemory(RAM ram) {
_ram = ram;
}
Device parentDevice = null;
public Device getParent() {
return parentDevice;

View File

@ -28,6 +28,7 @@ import java.util.function.Consumer;
import jace.Emulator;
import jace.apple2e.SoftSwitches;
import jace.config.Reconfigurable;
import jace.core.RAMEvent.TYPE;
/**
* RAM is a 64K address space of paged memory. It also manages sets of memory
@ -164,6 +165,14 @@ public abstract class RAM implements Reconfigurable {
return msb + lsb;
}
// This is used by opcodes that wrap around page boundaries
public int readWordPageWraparound(int address, TYPE eventType, boolean triggerEvent, boolean requireSynchronization) {
int lsb = 0x00ff & read(address, eventType, triggerEvent, requireSynchronization);
int addr1 = ((address + 1) & 0x0ff) | (address & 0x0ff00);
int msb = (0x00ff & read(addr1, eventType, triggerEvent, requireSynchronization)) << 8;
return msb + lsb;
}
private synchronized void mapListener(RAMListener l, int address) {
if ((address & 0x0FF00) == 0x0C000) {
int index = address & 0x0FF;

View File

@ -4,12 +4,17 @@ import static org.lwjgl.openal.AL10.AL_NO_ERROR;
import static org.lwjgl.openal.AL10.alGetError;
import java.io.InputStream;
import java.nio.ShortBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import org.lwjgl.openal.EXTEfx;
import javax.sound.sampled.Mixer;
import org.lwjgl.BufferUtils;
import static org.lwjgl.openal.AL11.*;
import static org.lwjgl.openal.EXTEfx.*;
import jace.core.SoundMixer;
import jace.core.SoundMixer.SoundBuffer;
@ -64,7 +69,7 @@ public class Votrax extends TimedDevice {
// 1b: UH3 (nUt)
// 1c: ER (bIRd)
// 1d: R (Roof)
// 1e: R1 (Rug)
// 1e: R01 (Rug)
// 1f: R2 (mutteR -- German)
// 20: L (Lift)
// 21: L1 (pLay)
@ -131,43 +136,17 @@ public class Votrax extends TimedDevice {
this.sampleCounter = 0.0;
this.samplesPerClock = clockFrequency / sampleRate;
}
public Optional<Double> tick() {
sampleCounter += samplesPerClock;
if (sampleCounter >= 1.0) {
sampleCounter -= 1.0;
public abstract int getBufferDuration();
return Optional.of(doGenerate());
} else {
return Optional.empty();
public void fillBuffer(ShortBuffer buffer) {
for (int i = 0; i < getBufferDuration(); i++) {
buffer.put((short) (doGenerate() * 32767));
}
}
public abstract double doGenerate();
}
public static class Mixer extends Generator {
public List<Generator> inputs = new ArrayList<>();
public List<Double> gains = new ArrayList<>();
double volume=0.0;
public void addInput(Generator input) {
inputs.add(input);
gains.add(1.0);
}
public void setGain(int index, double gain) {
gains.set(index, gain);
}
public double doGenerate() {
double sample = 0.0;
for (int i = 0; i < inputs.size(); i++) {
sample += inputs.get(i).doGenerate() * gains.get(i);
}
return sample;
}
}
public static class SawGenerator extends Generator {
double pitch=440.0;
double sample = 0.0;
@ -186,6 +165,12 @@ public class Votrax extends TimedDevice {
changePerSample = direction * 2.0 * pitch / sampleRate;
}
@Override
// Let's generate 10 loops of the sawtooth wave
public int getBufferDuration() {
return (int) (10.0 * sampleRate / pitch);
}
public double doGenerate() {
sample += changePerSample;
if (sample < -1.0) {
@ -202,29 +187,51 @@ public class Votrax extends TimedDevice {
public double doGenerate() {
return Math.random() * 2.0 - 1.0;
}
@Override
// Let's generate 10 seconds of noise
public int getBufferDuration() {
return sampleRate * 10;
}
}
public float mixerGain = 32767.0f;
public int[] filters = new int[5];
public SawGenerator formantGenerator = new SawGenerator();
public NoiseGenerator noiseGenerator = new NoiseGenerator();
public Mixer mixer = new Mixer();
public static int FORMANT = 0;
public static int NOISE = 1;
private Thread playbackThread = null;
// private Thread playbackThread = null;
public Votrax() throws Exception {
// loadPhonemeData();
formantGenerator.setSampleRate(44100);
formantGenerator.setPitch(100);
noiseGenerator.setSampleRate(44100);
mixer.addInput(formantGenerator);
mixer.addInput(noiseGenerator);
mixer.setGain(FORMANT, 0.5);
mixer.setGain(NOISE, 0.1);
}
int formantSource = -1;
int noiseSource = -1;
public void resume() {
// Create a buffer for the sawtooth wave
ShortBuffer formantLoop = BufferUtils.createShortBuffer(formantGenerator.getBufferDuration());
// Create a buffer for the noise
ShortBuffer noiseLoop = BufferUtils.createShortBuffer(noiseGenerator.getBufferDuration());
// Fill the buffers
formantGenerator.fillBuffer(formantLoop);
noiseGenerator.fillBuffer(noiseLoop);
// Create a source for the formant generator
formantSource = alGenSources();
alSourcei(formantSource, AL_BUFFER, formantLoop.get(0));
alSourcei(formantSource, AL_LOOPING, AL_TRUE);
alSourcePlay(formantSource);
// Create a source for the noise generator
noiseSource = alGenSources();
alSourcei(noiseSource, AL_BUFFER, noiseLoop.get(0));
alSourcei(noiseSource, AL_LOOPING, AL_TRUE);
alSourcePlay(noiseSource);
try {
createFilters();
} catch (Exception e) {
@ -232,33 +239,33 @@ public class Votrax extends TimedDevice {
suspend();
}
super.resume();
if (playbackThread != null && !playbackThread.isAlive()) {
return;
}
playbackThread = new Thread(() -> {
SoundBuffer soundBuffer = null;
try {
soundBuffer = SoundMixer.createBuffer(false);
while (isRunning()) {
try {
soundBuffer.playSample((short) (mixer.doGenerate() * mixerGain));
} catch (Exception e) {
e.printStackTrace();
suspend();
}
}
} catch (InterruptedException | ExecutionException | SoundError e) {
e.printStackTrace();
suspend();
} finally {
try {
soundBuffer.shutdown();
} catch (InterruptedException | ExecutionException | SoundError e) {
e.printStackTrace();
}
}
});
playbackThread.start();
// if (playbackThread != null && !playbackThread.isAlive()) {
// return;
// }
// playbackThread = new Thread(() -> {
// SoundBuffer soundBuffer = null;
// try {
// soundBuffer = SoundMixer.createBuffer(false);
// while (isRunning()) {
// try {
// soundBuffer.playSample((short) (mixer.doGenerate() * mixerGain));
// } catch (Exception e) {
// e.printStackTrace();
// suspend();
// }
// }
// } catch (InterruptedException | ExecutionException | SoundError e) {
// e.printStackTrace();
// suspend();
// } finally {
// try {
// soundBuffer.shutdown();
// } catch (InterruptedException | ExecutionException | SoundError e) {
// e.printStackTrace();
// }
// }
// });
// playbackThread.start();
}
private void createFilters() throws Exception {
@ -269,19 +276,22 @@ public class Votrax extends TimedDevice {
for (int i = 0; i < 5; i++) {
alGetError();
filters[i] = EXTEfx.alGenFilters();
filters[i] = alGenFilters();
if (alGetError() != AL_NO_ERROR) {
throw new Exception("Failed to create filter " + i);
}
if (EXTEfx.alIsFilter(filters[i])) {
if (alIsFilter(filters[i])) {
// Set Filter type to Band-Pass and set parameters
EXTEfx.alFilteri(filters[i], EXTEfx.AL_FILTER_TYPE, EXTEfx.AL_FILTER_BANDPASS);
alFilteri(filters[i], AL_FILTER_TYPE, AL_FILTER_BANDPASS);
if (alGetError() != AL_NO_ERROR) {
System.out.println("Band pass filter not supported.");
} else {
EXTEfx.alFilterf(filters[i], EXTEfx.AL_BANDPASS_GAIN, 0.5f);
EXTEfx.alFilterf(filters[i], EXTEfx.AL_BANDPASS_GAINHF, 0.5f);
alFilterf(filters[i], AL_BANDPASS_GAIN, 0.5f);
alFilterf(filters[i], AL_BANDPASS_GAINHF, 0.5f);
System.out.println("Band pass filter "+i+" created.");
// Now add an aux send for the noise and formant sources to go to this filter
// Inspiration: https://github.com/LWJGL/lwjgl3/blob/master/modules/samples/src/test/java/org/lwjgl/demo/openal/EFXTest.java
alSource3i(formantSource, AL_AUXILIARY_SEND_FILTER, filters[i], 0, 0);
}
}
}
@ -290,14 +300,14 @@ public class Votrax extends TimedDevice {
public boolean suspend() {
destroyFilters();
playbackThread = null;
// playbackThread = null;
return super.suspend();
}
private void destroyFilters() {
for (int i = 0; i < 5; i++) {
if (EXTEfx.alIsFilter(filters[i])) {
EXTEfx.alDeleteFilters(filters[i]);
if (alIsFilter(filters[i])) {
alDeleteFilters(filters[i]);
}
}
}

View File

@ -40,6 +40,7 @@ module jace {
requires org.lwjgl.openal;
requires org.lwjgl.stb;
requires org.lwjgl.glfw;
requires org.lwjgl;
// requires org.reflections;

View File

@ -15,9 +15,14 @@
*/
package jace;
import java.io.IOException;
import java.util.Arrays;
import jace.core.CPU;
import jace.core.Computer;
import jace.core.Device;
import jace.core.RAM;
import jace.core.RAMEvent.TYPE;
import jace.core.Utility;
import jace.ide.HeadlessProgram;
import jace.ide.Program;
@ -36,6 +41,72 @@ public class TestUtils {
Emulator.withComputer(Computer::reconfigure);
}
public static class FakeRAM extends RAM {
byte[] memory = new byte[65536];
public byte read(int address, TYPE eventType, boolean triggerEvent, boolean requireSyncronization) {
return memory[address & 0x0ffff];
}
public byte readRaw(int address) {
return memory[address & 0x0ffff];
}
public void write(int address, byte value, boolean triggerEvent, boolean requireSyncronization) {
memory[address & 0x0ffff] = value;
}
@Override
public String getName() {
return "Fake ram";
}
@Override
public String getShortName() {
return "ram";
}
@Override
public void reconfigure() {
}
@Override
public void configureActiveMemory() {
}
@Override
protected void loadRom(String path) throws IOException {
}
@Override
public void attach() {
}
@Override
public void performExtendedCommand(int i) {
}
@Override
public String getState() {
return "";
}
@Override
public void resetState() {
}
}
public static void clearFakeRam(RAM ram) {
Arrays.fill(((FakeRAM) ram).memory, (byte) 0);
}
public static RAM initFakeRam() {
RAM ram = new FakeRAM();
Emulator.withComputer(c -> {
c.setMemory(ram);
c.getCpu().setMemory(ram);
});
return ram;
}
public static void assemble(String code, int addr) throws Exception {
runAssemblyCode(code, addr, 0);
}

View File

@ -0,0 +1,263 @@
package jace.apple2e;
import static jace.TestUtils.*;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
import jace.Emulator;
import jace.core.Computer;
import jace.core.RAM;
import jace.core.SoundMixer;
import jace.core.RAMEvent.TYPE;
public class CpuUnitTest {
// This will loop through each of the files in 65x02_unit_tests/wdc65c02 and run the tests in each file
// The goal is to produce an output report that shows the number of tests that passed and failed
// The output should be reported in a format compatible with junit but also capture multiple potential failures, not just the first faliure
static Computer computer;
static MOS65C02 cpu;
static RAM ram;
public static enum Operation {
read, write
}
TypeToken<Collection<TestRecord>> testCollectionType = new TypeToken<Collection<TestRecord>>(){};
record TestResult(String source, String testName, boolean passed, String message) {}
// Note cycles are a mix of int and string so the parser doesn't like to serialize that into well-formed objects
record TestRecord(String name, @SerializedName("initial") MachineState initialState, @SerializedName("final") MachineState finalState, List<List<String>> cycles) {}
record MachineState(int pc, int s, int a, int x, int y, byte p, List<int[]> ram) {}
public static boolean BREAK_ON_FAIL = false;
@BeforeClass
public static void setUp() {
initComputer();
SoundMixer.MUTE = true;
computer = Emulator.withComputer(c->c, null);
cpu = (MOS65C02) computer.getCpu();
ram = initFakeRam();
}
@Before
public void resetState() {
// Reinit memory on each test to avoid weird side effects
cpu.reset();
cpu.resume();
}
// Make a list of tests to skip
public static String[] SKIP_TESTS = new String[] {
"cb", "db"
};
public static String TEST_FOLDER = "/65x02_unit_tests/wdc65c02/v1";
@Test
public void testAll() throws IOException, URISyntaxException {
// Read all the files in the directory
// For each file, read the contents and run the tests
List<TestResult> results = new ArrayList<>();
// Path testFolder = Paths.get(getClass().getResource("/65x02_unit_tests_wdc65c02/v1").toURI());
for (String path : getSorted(getResourceListing(TEST_FOLDER))) {
boolean skip = false;
for (String skipPattern : SKIP_TESTS) {
if (path.contains(skipPattern)) {
skip = true;
}
}
if (skip) {
continue;
}
String file = TEST_FOLDER + "/" + path;
results.addAll(runTest(file));
if (BREAK_ON_FAIL && results.stream().anyMatch(r->!r.passed())) {
break;
}
}
// Report results
int passed = 0;
Set<String> failedTests = new HashSet<>();
for (TestResult result : results) {
if (result.passed()) {
passed++;
} else {
failedTests.add(result.testName());
if (failedTests.size() < 20) {
System.err.println(result.source() + ";" + result.testName() + " " + "FAILED" + ": " + result.message());
}
}
}
System.err.println("Passed: " + passed + " Failed: " + failedTests.size());
if (failedTests.size() > 0) {
throw new RuntimeException("One or more tests failed, see log for details");
}
}
private String getStatusBits(int status) {
StringBuilder sb = new StringBuilder();
sb.append((status & 0x80) != 0 ? "N" : "-");
sb.append((status & 0x40) != 0 ? "V" : "-");
sb.append((status & 0x20) != 0 ? "-" : "?");
sb.append((status & 0x10) != 0 ? "B" : "-");
sb.append((status & 0x08) != 0 ? "D" : "-");
sb.append((status & 0x04) != 0 ? "I" : "-");
sb.append((status & 0x02) != 0 ? "Z" : "-");
sb.append((status & 0x01) != 0 ? "C" : "-");
return sb.toString();
}
private Collection<? extends TestResult> runTest(String file) {
Gson gson = new Gson();
List<TestResult> results = new ArrayList<>();
// Read the file which is a JSON file and parse it.
try {
// Given the JSON data in source, parse it to a usable list of tests
// For each test, run the test
Collection<TestRecord> tests = gson.fromJson(new InputStreamReader(getClass().getResourceAsStream(file)), testCollectionType.getType());
for (TestRecord t : tests) {
String name = t.name() + "_%d cycles_%04X->%04X".formatted(t.cycles().size(), t.initialState().pc(), t.finalState().pc());
// Set up the initial state by setting CPU registers and RAM
cpu.reset();
cpu.setProgramCounter(t.initialState().pc());
cpu.STACK = t.initialState().s();
cpu.A = t.initialState().a();
cpu.X = t.initialState().x();
cpu.Y = t.initialState().y();
cpu.setStatus(t.initialState().p(), true);
// Set up the memory values
for (int[] mem : t.initialState().ram()) {
ram.write(mem[0], (byte) mem[1], false, false);
}
// Step the CPU for each cycle
for (List<String> c : t.cycles()) {
if (BREAK_ON_FAIL) {
cpu.traceLength = 100;
cpu.setTraceEnabled(true);
}
cpu.doTick();
// TODO: Check the memory accesses
}
// Check the final state
boolean passed = true;
if (cpu.getProgramCounter() != t.finalState().pc()) {
results.add(new TestResult(file.toString(), name, false, "Program Counter mismatch, expected %04X but got %04X".formatted(t.finalState().pc(), cpu.getProgramCounter())));
passed = false;
}
if (cpu.STACK != t.finalState().s()) {
results.add(new TestResult(file.toString(), name, false, "Stack Pointer mismatch, expected %02X but got %02X".formatted(t.finalState().s(), cpu.STACK)));
passed = false;
}
if (cpu.A != t.finalState().a()) {
results.add(new TestResult(file.toString(), name, false, "Accumulator mismatch, expected %02X but got %02X".formatted(t.finalState().a(), cpu.A)));
passed = false;
}
if (cpu.X != t.finalState().x()) {
results.add(new TestResult(file.toString(), name, false, "X Register mismatch, expected %02X but got %02X".formatted(t.finalState().x(), cpu.X)));
passed = false;
}
if (cpu.Y != t.finalState().y()) {
results.add(new TestResult(file.toString(), name, false, "Y Register mismatch, expected %02X but got %02X".formatted(t.finalState().y(), cpu.Y)));
passed = false;
}
if (cpu.getStatus() != t.finalState().p()) {
results.add(new TestResult(file.toString(), name, false, "Status Register mismatch, expected %s but got %s".formatted(getStatusBits(t.finalState().p()),getStatusBits(cpu.getStatus()))));
passed = false;
}
// Check the memory values
for (int[] mem : t.finalState().ram()) {
byte value = ram.read(mem[0], TYPE.EXECUTE, false, false);
if (value != (byte) mem[1]) {
results.add(new TestResult(file.toString(), name, false, "Memory mismatch at address %04X, expected %02X but got %02X".formatted(mem[0], mem[1], value)));
// results.add(new TestResult(file.toString(), name, false, "Memory mismatch, expected %s but got %s".formatted(getStatusBits(mem[1]),getStatusBits(value))));
passed = false;
}
}
if (passed) {
results.add(new TestResult(file.toString(), t.name(), true, "All checks passed"));
} else if (BREAK_ON_FAIL) {
break;
}
// Clear out the memory for the next test
for (int[] mem : t.finalState().ram()) {
ram.write(mem[0], (byte) 0, false, false);
}
}
} catch (Exception e) {
results.add(new TestResult(file.toString(), "<INIT>", false, "Unable to read file: " + e.getMessage()));
return results;
}
return results;
}
private String[] getSorted(String[] values) {
Set<String> set = new TreeSet<>();
for (String value : values) {
set.add(value);
}
return set.toArray(new String[0]);
}
private String[] getResourceListing(String path) throws URISyntaxException, IOException {
URL dirURL = getClass().getResource(path);
if (dirURL != null && dirURL.getProtocol().equals("file")) {
/* A file path: easy enough */
return new File(dirURL.toURI()).list();
}
if (dirURL == null) {
/*
* In case of a jar file, we can't actually find a directory.
* Have to assume the same jar as clazz.
*/
String me = getClass().getName().replace(".", "/")+".class";
dirURL = getClass().getClassLoader().getResource(me);
}
if (dirURL.getProtocol().equals("jar")) {
/* A JAR path */
String jarPath = dirURL.getPath().substring(5, dirURL.getPath().indexOf("!")); //strip out only the JAR file
try (JarFile jar = new JarFile(URLDecoder.decode(jarPath, "UTF-8"))) {
Enumeration<JarEntry> entries = jar.entries(); //gives ALL entries in jar
Set<String> result = new HashSet<String>(); //avoid duplicates in case it is a subdirectory
while(entries.hasMoreElements()) {
String name = entries.nextElement().getName();
if (name.startsWith(path)) { //filter according to the path
String entry = name.substring(path.length());
int checkSubdir = entry.indexOf("/");
if (checkSubdir >= 0) {
// if it is a subdirectory, we just return the directory name
entry = entry.substring(0, checkSubdir);
}
result.add(entry);
}
}
return result.toArray(new String[result.size()]);
}
}
throw new IOException("Unable to locate resource folder for path: " + path);
}
}