diff --git a/src/main/java/jace/config/ConfigurationUIController.java b/src/main/java/jace/config/ConfigurationUIController.java index bdfe2a2..da6886d 100644 --- a/src/main/java/jace/config/ConfigurationUIController.java +++ b/src/main/java/jace/config/ConfigurationUIController.java @@ -31,6 +31,7 @@ import javafx.scene.input.MouseEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.scene.text.Text; +import javafx.stage.FileChooser; import javafx.util.StringConverter; public class ConfigurationUIController { @@ -221,9 +222,9 @@ public class ConfigurationUIController { return Optional.of(row); } - private void editKeyboardShortcut(ConfigNode node, String actionName, Text widget) { - throw new UnsupportedOperationException("Not supported yet."); - } + // private void editKeyboardShortcut(ConfigNode node, String actionName, Text widget) { + // throw new UnsupportedOperationException("Not supported yet."); + // } @SuppressWarnings("all") private Node buildEditField(ConfigNode node, String settingName, Serializable value) { @@ -247,13 +248,40 @@ public class ConfigurationUIController { return buildTextField(node, settingName, value, null); } } else if (type.equals(File.class)) { - // TODO: Add file support! + return buildFileField(node, settingName, value); } else if (ISelection.class.isAssignableFrom(type)) { return buildDynamicSelectComponent(node, settingName, value); } return null; } + // NOTE: This was written but not tested/used currently. Test before using! + private Node buildFileField(ConfigNode node, String settingName, Serializable value) { + // Create a label that shows the name of the file and lets you select a file when the label is clicked. + HBox hbox = new HBox(); + Label label = new Label(value == null ? "" : ((File) value).getName()); + label.setMinWidth(150.0); + label.getStyleClass().add("setting-file-label"); + label.setOnMouseClicked((e) -> { + FileChooser fileChooser = new FileChooser(); + File file = fileChooser.showOpenDialog(label.getScene().getWindow()); + if (file != null) { + node.setFieldValue(settingName, file); + label.setText(file.getName()); + } + }); + hbox.getChildren().add(label); + // Add a button that lets you clear the file selection. + Label clearButton = new Label("Clear"); + clearButton.getStyleClass().add("setting-file-clear"); + clearButton.setOnMouseClicked((e) -> { + node.setFieldValue(settingName, null); + label.setText(""); + }); + return hbox; + + } + private Node buildTextField(ConfigNode node, String settingName, Serializable value, String validationPattern) { TextField widget = new TextField(String.valueOf(value)); widget.textProperty().addListener((e) -> node.setFieldValue(settingName, widget.getText())); diff --git a/src/main/java/jace/hardware/Votrax.java b/src/main/java/jace/hardware/Votrax.java new file mode 100644 index 0000000..1bbbdfb --- /dev/null +++ b/src/main/java/jace/hardware/Votrax.java @@ -0,0 +1,103 @@ +package jace.hardware; + +import java.io.InputStream; + +public class Votrax { + // This is a speech synthesizer based on the Votrax SC-02 + // There are 2 sound generators, one for the voice and one for the noise + // The voice generator is a saw-tooth wave generator at a frequency determined by the voice frequency register + // The noise generator is a pseudo-random noise generator + + // The Votrax has 5 filters that can be applied to the voice generator, controlled by a filter frequency register + // The fifth filter takes both the voice and noise generators as input, but other filters only take the voice generator + // There is also a final high-pass filter that can be applied to the output of the voice and noise generators + + // There is a phoneme register which controls the phoneme to be spoken (0-63) + // There is a duration register which controls the duration of the phoneme (0-3) + // There is a rate register which controls the rate of speech (0-15) + // There is an inflection register which controls the inflection of the voice (0-15) + + // For each phoneme there are 8 bytes that control the filters and sound generator levels + byte[] phonemeData = new byte[64 * 8]; + // Phoneme chart: + // 00: PA (pause) + // 01: E (mEEt) + // 02: E1 (bEnt) + // 03: Y (bEfore) + // 04: Y1 (Year) + // 05: AY (plEAse) + // 06: IE (anY) + // 07: I (sIx) + // 08: A (mAde) + // 09: A1 (cAre) + // 0a: EH (nEst) + // 0b: EH1 (bElt) + // 0c: AE (dAd) + // 0d: AE1 (After) + // 0e: AH (gOt) + // 0f: AH1 (fAther) + // 10: AW (Office) + // 11: O (stOre) + // 12: OU (bOAt) + // 13: OO (lOOk) + // 14: IU (yOU) + // 15: IU1 (cOUld) + // 16: U (tUne) + // 17: U1 (cartOOn) + // 18: UH (wOnder) + // 19: UH1 (lOve) + // 1a: UH2 (whAt) + // 1b: UH3 (nUt) + // 1c: ER (bIRd) + // 1d: R (Roof) + // 1e: R1 (Rug) + // 1f: R2 (mutteR -- German) + // 20: L (Lift) + // 21: L1 (pLay) + // 22: LF (faLL) + // 23: W (Water) + // 24: B (Bag) + // 25: D (paiD) + // 26: KV (taG) + // 27: P (Pen) + // 28: T (Tart) + // 29: K (Kit) + // 2a: HV - Hold Vocal + // 2b: HVC - Hold Vocal Closure + // 2c: HF - (Heart) + // 2d: HFC - Hold Frictave Closure + // 2e: HN - Hold Nasal + // 2f: Z (Zero) + // 30: S (Same) + // 31: J (meaSure) + // 32: SCH (SHip) + // 33: V (Very) + // 34: F (Four) + // 35: THV (THere) + // 36: TH (wiTH) + // 37: M (More) + // 38: N (NiNe) + // 39: NG (raNG) + // 3a: :A (mAErchen -- German) + // 3b: :OH (lOwe - French) + // 3c: :U (fUEnf -- German) + // 3d: :UH (menU -- French) + // 3e: E2 (bittE -- German) + // 3f: LB (Lube) + + public void loadPhonemeData() { + InputStream romFile = Votrax.class.getResourceAsStream("/jace/data/sc01a.bin"); + if (romFile == null) { + throw new RuntimeException("Cannot find Votrax SC-01A ROM"); + } + // Load into phonemeData + try { + if (romFile.read(phonemeData) != phonemeData.length) { + throw new RuntimeException("Bad Votrax SC-01A ROM size"); + } + } catch (Exception ex) { + throw new RuntimeException("Error loading Votrax SC-01A ROM", ex); + } + } + +} \ No newline at end of file diff --git a/src/main/java/jace/hardware/massStorage/DirectoryNode.java b/src/main/java/jace/hardware/massStorage/DirectoryNode.java index 8989066..a5a1b75 100644 --- a/src/main/java/jace/hardware/massStorage/DirectoryNode.java +++ b/src/main/java/jace/hardware/massStorage/DirectoryNode.java @@ -152,7 +152,6 @@ public class DirectoryNode extends DiskNode implements FileFilter { end = start + ENTRIES_PER_BLOCK; } for (int i = start; i < end && i < directoryEntries.size(); i++, offset += FILE_ENTRY_SIZE) { - // TODO: Add any parts that are not file entries. // System.out.println("Entry "+i+": "+children.get(i).getName()+"; offset "+offset); generateFileEntry(buffer, offset, i); } diff --git a/src/main/resources/jace/data/sc01a.bin b/src/main/resources/jace/data/sc01a.bin new file mode 100644 index 0000000..da13e04 Binary files /dev/null and b/src/main/resources/jace/data/sc01a.bin differ diff --git a/src/test/java/jace/AbstractFXTest.java b/src/test/java/jace/AbstractFXTest.java index 9b99a0b..b0581c7 100644 --- a/src/test/java/jace/AbstractFXTest.java +++ b/src/test/java/jace/AbstractFXTest.java @@ -13,4 +13,4 @@ public abstract class AbstractFXTest { Platform.startup(() -> {}); } } -} \ No newline at end of file +} diff --git a/src/test/java/jace/ProgramException.java b/src/test/java/jace/ProgramException.java new file mode 100644 index 0000000..adac21f --- /dev/null +++ b/src/test/java/jace/ProgramException.java @@ -0,0 +1,44 @@ +package jace; + +import jace.apple2e.MOS65C02; + +public class ProgramException extends Exception { + int breakpointNumber; + String processorStats; + String programLocation; + public ProgramException(String message, int breakpointNumber) { + super(message.replaceAll("<<.*>>", "")); + this.breakpointNumber = breakpointNumber; + this.processorStats = Emulator.withComputer(c-> ((MOS65C02) c.getCpu()).getState(), "N/A"); + // Look for a string pattern <> in the message and extract if found + int start = message.indexOf("<<"); + if (start != -1) { + int end = message.indexOf(">>", start); + if (end != -1) { + this.programLocation = message.substring(start + 2, end); + } + } else { + this.programLocation = "N/A"; + } + } + public int getBreakpointNumber() { + return breakpointNumber; + } + public String getProcessorStats() { + return processorStats; + } + public String getProgramLocation() { + return programLocation; + } + public String getMessage() { + String message = super.getMessage(); + if (getBreakpointNumber() >= 0) { + message += " at breakpoint " + getBreakpointNumber(); + } + message += " \nStats: " + getProcessorStats(); + if (getProgramLocation() != null) { + message += " \n at " + getProgramLocation(); + } + return message; + } +} diff --git a/src/test/java/jace/TestProgram.java b/src/test/java/jace/TestProgram.java new file mode 100644 index 0000000..17298be --- /dev/null +++ b/src/test/java/jace/TestProgram.java @@ -0,0 +1,465 @@ +package jace; + +import static jace.TestUtils.runAssemblyCode; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import jace.apple2e.Full65C02Test; +import jace.apple2e.MOS65C02; +import jace.core.Computer; + +public class TestProgram { + // Tests could be run in any order so it is really important that all registers/flags are preserved! + public static enum Flag { + CARRY_SET("BCS +", "Carry should be set"), + CARRY_CLEAR("BCC +", "Carry should be clear"), + ZERO_SET("BEQ +", "Zero should be set"), + IS_ZERO("BEQ +", "Zero should be clear"), + ZERO_CLEAR("BNE +", "Zero should be clear"), + NOT_ZERO("BNE +", "Zero should be clear"), + NEGATIVE("BMI +", "Negative should be set"), + POSITIVE("BPL +", "Negative should be clear"), + OVERFLOW_SET("BVS +", "Overflow should be set"), + OVERFLOW_CLEAR("BVC +", "Overflow should be clear"), + DECIMAL_SET(""" + PHP + PHA + PHP + PLA + BIT #%00001000 + BEQ ++ + PLA + PLP + BRA + + ++ ; Error + """, "Decimal should be set"), + DECIMAL_CLEAR(""" + PHP + PHA + PHP + PLA + BIT #%00001000 + BNE ++ + PLA + PLP + BRA + + ++ ; Error + """, "Decimal should be clear"), + INTERRUPT_SET(""" + PHP + PHA + PHP + PLA + BIT #%00000100 + BEQ ++ + PLA + PLP + BRA + + ++ ; Error + """, "Interrupt should be set"), + INTERRUPT_CLEAR(""" + PHP + PHA + PHP + PLA + BIT #%00000100 + BNE ++ + PLA + PLP + BRA + + ++ ; Error + """, "Interrupt should be clear"),; + String code; + String condition; + Flag(String code, String condition) { + this.code = code; + this.condition = condition; + } + } + + ArrayList lines = new ArrayList<>(); + Consumer timerHandler; + Consumer tickCountHandler; + Consumer errorHandler; + Consumer stopHandler; + Consumer progressHandler; + Consumer traceHandler; + + public TestProgram() { + lines.add(TestProgram.UNIT_TEST_MACROS); + } + + public TestProgram(String line1) { + this(); + lines.add(line1); + } + + public TestProgram(String line1, int tickCount) { + this(); + assertTimed(line1, tickCount); + } + + int tickCount = 0; + + int timerStart = 0; + int timerLastMark = 0; + int timerEnd = 0; + int timerLastEllapsed = 0; + int lastBreakpoint = -1; + int maxTicks = 10000; + boolean programCompleted = false; + boolean programReportedError = false; + boolean programRunning = false; + List errors = new ArrayList<>(); + List timings = new ArrayList<>(); + + ProgramException lastError = null; + public static String UNIT_TEST_MACROS = """ + !cpu 65c02 + !macro extendedOp .code, .val {!byte $FC, .code, .val} + !macro startTimer {+extendedOp $10, $80} + !macro markTimer {+extendedOp $10, $81} + !macro stopTimer {+extendedOp $10, $82} + !macro assertTicks .ticks {+extendedOp $11, .ticks} + !macro stop .p1, .p2 { + +extendedOp .p1, .p2 + +extendedOp $13, $ff + +traceOff + - + JMP - + } + !macro throwError .errorCode {+stop $12, .errorCode} + !macro recordError .errorCode {+extendedOp $12, .errorCode} + !macro success {+stop $14, $ff} + !macro breakpoint .num {+extendedOp $14, .num} + !macro traceOn {+extendedOp $15, $01} + !macro traceOff {+extendedOp $15, $00} + !macro resetRegs { + LDA #0 + LDX #0 + LDY #0 + PHA + PLP + TXS + PHP + } + +resetRegs + """; + public static String INDENT = " "; + + public void attach() { + timerHandler = this::handleTimer; + tickCountHandler = this::countAndCompareTicks; + errorHandler = this::recordError; + stopHandler = b->stop(); + progressHandler = this::recordProgress; + traceHandler = this::handleTrace; + + Emulator.withComputer(c-> { + MOS65C02 cpu = (MOS65C02) c.getCpu(); + cpu.registerExtendedCommandHandler(0x10, timerHandler); + cpu.registerExtendedCommandHandler(0x11, tickCountHandler); + cpu.registerExtendedCommandHandler(0x12, errorHandler); + cpu.registerExtendedCommandHandler(0x13, stopHandler); + cpu.registerExtendedCommandHandler(0x14, progressHandler); + cpu.registerExtendedCommandHandler(0x15, traceHandler); + }); + } + + public void detach() { + Emulator.withComputer(c-> { + MOS65C02 cpu = (MOS65C02) c.getCpu(); + cpu.unregisterExtendedCommandHandler(timerHandler); + cpu.unregisterExtendedCommandHandler(tickCountHandler); + cpu.unregisterExtendedCommandHandler(errorHandler); + cpu.unregisterExtendedCommandHandler(stopHandler); + cpu.unregisterExtendedCommandHandler(progressHandler); + cpu.unregisterExtendedCommandHandler(traceHandler); + }); + } + + private void handleTimer(byte val) { + switch (val) { + case (byte)0x80: + timerStart = tickCount; + timerLastMark = tickCount; + break; + case (byte)0x82: + timerEnd = tickCount; + // Fall through + case (byte)0x81: + // Don't count the time spent on the timer commands! + timerLastEllapsed = (tickCount - timerLastMark) - 4; + timerLastMark = tickCount; + break; + default: + lastError = new ProgramException("Unknown timer command %s".formatted(val), lastBreakpoint); + stop(); + } + } + + private void countAndCompareTicks(byte val) { + int expectedTickCountNum = val; + int expectedTickCount = timings.get(expectedTickCountNum); + String errorMessage = lastError != null ? lastError.getProgramLocation() : ""; + if (timerLastEllapsed != expectedTickCount) { + lastError = new ProgramException("Expected %s ticks, instead counted %s <<%s>>".formatted(expectedTickCount, timerLastEllapsed, errorMessage), lastBreakpoint); + stop(); + } + } + + private void recordError(byte v) { + int val = v & 0x0ff; + + if (val >= 0 && val < errors.size()) { + lastError = new ProgramException(errors.get(val), lastBreakpoint); + } else if (val == 255) { + lastError = null; + } else { + lastError = new ProgramException("Error %s".formatted(val), lastBreakpoint); + } + } + + public TestProgram defineError(String error) { + errors.add(error); + return this; + } + + private void stop() { + programReportedError = lastError != null; + programRunning = false; + } + + private void recordProgress(byte val) { + if (val == (byte)0xff) { + programCompleted = true; + return; + } else { + lastBreakpoint = val; + } + } + + private void handleTrace(byte val) { + if (val == (byte)0x01) { + System.out.println("Trace on"); + Full65C02Test.cpu.setTraceEnabled(true); + } else { + System.out.println("Trace off"); + Full65C02Test.cpu.setTraceEnabled(false); + } + } + + public TestProgram assertTimed(String line, int ticks) { + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + String caller = stackTrace[2].toString(); + int errNum = errors.size(); + errors.add("Expected %s ticks for %s <<%s>>".formatted(ticks, line, caller)); + int timingNum = timings.size(); + timings.add(ticks); + lines.add(INDENT+"+startTimer"); + lines.add(line); + lines.add(INDENT+"+markTimer"); + lines.add(INDENT+"+recordError %s".formatted(errNum)); + lines.add(INDENT+"+assertTicks %s ; Check for %s cycles".formatted(timingNum, ticks)); + lines.add(INDENT+"+recordError %s".formatted(255)); + return this; + } + + public TestProgram add(String line) { + lines.add(line); + return this; + } + + public TestProgram assertFlags(TestProgram.Flag... flags) { + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + String caller = stackTrace[2].toString(); + for (TestProgram.Flag flag : flags) + _test(TestProgram.INDENT + flag.code, flag.condition + "<<" + caller + ">>"); + return this; + } + + /* Test the A register for a specific value */ + public TestProgram assertA(int val) { + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + String caller = stackTrace[2].toString(); + String condition = "A != %s <<%s>>".formatted(Integer.toHexString(val), caller); + _test(""" + PHP + CMP #%s + BNE ++ + PLP + BRA + + ++ ; Error + """.formatted(val), condition + " in " + caller); + return this; + } + + /* Test X register for a specific value */ + public TestProgram assertX(int val) { + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + String caller = stackTrace[2].toString(); + String condition = "X != %s <<%s>>".formatted(Integer.toHexString(val), caller); + _test(""" + PHP + CPX #%s + BNE ++ + PLP + BRA + + ++ ; Error + """.formatted(val), condition); + return this; + } + + /* Test Y register for a specific value */ + public TestProgram assertY(int val) { + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + String caller = stackTrace[2].toString(); + String condition = "Y != %s <<%s>>".formatted(Integer.toHexString(val), caller); + _test(""" + PHP + CPY #%s + BNE ++ + PLP + BRA + + ++ ; Error + """.formatted(val), condition); + return this; + } + + /* Test an address for a specific value. If the value is incorrect, leave it in A for inspection */ + public TestProgram assertAddrVal(int address, int val) { + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + String caller = stackTrace[2].toString(); + String condition = "$%s != %s <<%s>>".formatted(Integer.toHexString(address), Integer.toHexString(val), caller); + _test(""" + PHP + PHA + LDA $%s + CMP #%s + BNE ++ + PLA + PLP + BRA + + ++ ; Error + LDA $%s + """.formatted(Integer.toHexString(address), val, val), condition); + return this; + } + + /* Use provided code to test a condition. If successful it should jump or branch to + + * If unsuccessful the error condition will be reported. + * + * @param code The code to test + * @param condition The condition to report if the test fails + */ + public TestProgram test(String code, String condition) { + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + String caller = stackTrace[2].toString(); + _test(code, condition + "<<" + caller + ">>"); + return this; + } + + private void _test(String code, String condition) { + int errorNum = errors.size(); + errors.add(condition); + lines.add(""" + ; << Test %s + %s + +throwError %s + + ; >> Test """.formatted(condition, code, errorNum)); + } + + /** + * Note the current breakpoint, helpful in understanding error locations + * + * @param num Error number to report in any error that occurs + */ + public TestProgram breakpoint(int num) { + lines.add(INDENT + "+breakpoint %s".formatted(num)); + return this; + } + + /** + * Reset the registers to 0, clear the stack, and clear flags + * + * @return + */ + public TestProgram resetRegisters() { + lines.add(INDENT + "+resetRegs"); + return this; + } + + /** + * Turn on or off tracing + * + * @param state True to turn on tracing, false to turn it off + */ + public TestProgram setTrace(boolean state) { + lines.add(INDENT +"+trace%s".formatted(state ? "On" : "Off")); + return this; + } + + /** + * Render the program as unassembled code + * + * @return The program as a string + */ + public String build() { + lines.add(INDENT +"+success"); + return String.join("\n", lines); + } + + /** + * Run the program for a specific number of ticks, or until it completes (whichever comes first) + * + * @param ticks The number of ticks to run the program + * @throws ProgramException If the program reports an error + */ + public void runForTicks(int ticks) throws ProgramException { + this.maxTicks = ticks; + run(); + } + + /** + * Run the program until it completes, reports an error, or reaches the maximum number of ticks + * + * @throws ProgramException If the program reports an error + */ + public void run() throws ProgramException { + Computer computer = Emulator.withComputer(c->c, null); + MOS65C02 cpu = (MOS65C02) computer.getCpu(); + + attach(); + programRunning = true; + String program = build(); + // We have to run the program more carefully so just load it first + try { + runAssemblyCode(program, 0); + } catch (Exception e) { + throw new ProgramException(e.getMessage() + "\n" + program, -1); + } + cpu.resume(); + try { + for (int i=0; i < maxTicks; i++) { + cpu.doTick(); + tickCount++; + if (programReportedError) { + throw lastError; + } + if (!programRunning) { + break; + } + } + } finally { + cpu.suspend(); + detach(); + } + assertFalse("Test reported an error", programReportedError); + assertFalse("Program never ended fully after " + tickCount + " ticks; got to breakpoint " + lastBreakpoint, programRunning); + assertTrue("Test did not complete fully", programCompleted); + } +} \ No newline at end of file diff --git a/src/test/java/jace/TestUtils.java b/src/test/java/jace/TestUtils.java new file mode 100644 index 0000000..6313a07 --- /dev/null +++ b/src/test/java/jace/TestUtils.java @@ -0,0 +1,83 @@ +/* + * Copyright 2023 org.badvision. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package jace; + +import jace.core.CPU; +import jace.core.Computer; +import jace.core.Device; +import jace.core.Utility; +import jace.ide.HeadlessProgram; +import jace.ide.Program; + +/** + * + * @author brobert + */ +public class TestUtils { + private TestUtils() { + // Utility class has no constructor + } + + public static void initComputer() { + Utility.setHeadlessMode(true); + Emulator.withComputer(Computer::reconfigure); + } + + public static void assemble(String code, int addr) throws Exception { + runAssemblyCode(code, addr, 0); + } + + public static void runAssemblyCode(String code, int ticks) throws Exception { + runAssemblyCode(code, 0x6000, ticks); + } + + public static void runAssemblyCode(String code, int addr, int ticks) throws Exception { + CPU cpu = Emulator.withComputer(c->c.getCpu(), null); + HeadlessProgram program = new HeadlessProgram(Program.DocumentType.assembly); + program.setValue("*=$"+Integer.toHexString(addr)+"\n "+code+"\n NOP\n RTS"); + program.execute(); + if (ticks > 0) { + cpu.resume(); + for (int i=0; i < ticks; i++) { + cpu.doTick(); + } + cpu.suspend(); + } + } + + public static Device createSimpleDevice(Runnable r, String name) { + return new Device() { + @Override + public void tick() { + r.run(); + } + + @Override + public String getShortName() { + return name; + } + + @Override + public void reconfigure() { + } + + @Override + protected String getDeviceName() { + return name; + } + }; + } +} diff --git a/src/test/java/jace/apple2e/CycleCountTest.java b/src/test/java/jace/apple2e/CycleCountTest.java new file mode 100644 index 0000000..7ad5cc7 --- /dev/null +++ b/src/test/java/jace/apple2e/CycleCountTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2023 org.badvision. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package jace.apple2e; + +import static jace.TestUtils.initComputer; + +import org.junit.BeforeClass; +import org.junit.Test; + +import jace.ProgramException; +import jace.TestProgram; +import jace.core.SoundMixer; + + +/** + * More advanced cycle counting tests. These help ensure CPU runs correctly so things + * like vapor lock and speaker sound work as expected. + * @author brobert + */ +public class CycleCountTest { + + @BeforeClass + public static void setupClass() { + initComputer(); + SoundMixer.MUTE = true; + } + + /** + * Composite test which ensures the speaker beep is the right pitch. + * Test that the wait routine for beep cycles correctly. + * Calling WAIT with A=#$c (12) should take 535 cycles + * according to the tech ref notes: =1/2*(26+27*A+5*A^2) where A = 12 (0x0c) + * The BELL routine has an additional 12 cycles per iteration plus 1 extra cycle in the first iteration. + * e.g. 2 iterations take 1093 cycles + * + * @throws ProgramException + */ + @Test + public void testDirectBeeperCycleCount() throws ProgramException { + new TestProgram(""" + SPKR = $C030 + jmp BELL + WAIT sec + WAIT2 pha + WAIT3 sbc #$01 + bne WAIT3 + pla + sbc #$01 + bne WAIT2 + rts + BELL +markTimer + ldy #$02 + BELL2 lda #$0c + jsr WAIT + lda SPKR + dey + bne BELL2 + """, 1093).run(); + } + +} diff --git a/src/test/java/jace/apple2e/Full65C02Test.java b/src/test/java/jace/apple2e/Full65C02Test.java new file mode 100644 index 0000000..6fb9bd8 --- /dev/null +++ b/src/test/java/jace/apple2e/Full65C02Test.java @@ -0,0 +1,804 @@ +/* + * Copyright 2024 Brendan Robert + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package jace.apple2e; + +import static jace.TestProgram.Flag.CARRY_CLEAR; +import static jace.TestProgram.Flag.CARRY_SET; +import static jace.TestProgram.Flag.DECIMAL_CLEAR; +import static jace.TestProgram.Flag.DECIMAL_SET; +import static jace.TestProgram.Flag.INTERRUPT_CLEAR; +import static jace.TestProgram.Flag.INTERRUPT_SET; +import static jace.TestProgram.Flag.IS_ZERO; +import static jace.TestProgram.Flag.NEGATIVE; +import static jace.TestProgram.Flag.NOT_ZERO; +import static jace.TestProgram.Flag.OVERFLOW_CLEAR; +import static jace.TestProgram.Flag.OVERFLOW_SET; +import static jace.TestProgram.Flag.POSITIVE; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import jace.Emulator; +import jace.ProgramException; +import jace.TestProgram; +import jace.TestUtils; +import jace.core.Computer; +import jace.core.RAMEvent.TYPE; +import jace.core.SoundMixer; + +/** + * Basic test functionality to assert correct 6502 decode and execution. + * + * @author blurry + */ +public class Full65C02Test { + + static Computer computer; + public static MOS65C02 cpu; + static RAM128k ram; + + @BeforeClass + public static void setupClass() { + TestUtils.initComputer(); + SoundMixer.MUTE = true; + computer = Emulator.withComputer(c->c, null); + cpu = (MOS65C02) computer.getCpu(); + ram = (RAM128k) computer.getMemory(); + } + + @AfterClass + public static void teardownClass() { + } + + @Before + public void setup() { + computer.pause(); + cpu.clearState(); + } + + @Test + /* ADC: All CPU flags/modes */ + public void testAdditionCPUFlags() throws ProgramException { + new TestProgram() + // Add 1 w/o carry; 1+1 = 2 + .assertTimed("ADC #1", 2) + .assertA(1) + .assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE) + // Add 1 w/ carry 1+0+c = 2 + .add("SEC") + .assertTimed("ADC #0", 2) + .assertA(2) + .assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE) + // Decimal: ADD 8 w/o carry; 2+8 = 10 + .add("SED") + .assertTimed("ADC #8", 3) + .assertA(0x10) + .assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE) + // Decimal: ADD 9 w/ carry; 10+9+c = 20 + .add("SEC") + .assertTimed("ADC #09", 3) + .assertA(0x20) + .assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE) + // Decimal: Overflow check; 20 + 99 + C = 20 (carry, no overflow) + .add(""" + SED + SEC + LDA #$20 + ADC #$99 + """) + .assertA(0x20) + .assertFlags(CARRY_SET, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE) + // Decimal: Overflow check; 20 + 64 + C = 85 (overflow) + .add("ADC #$64") + .assertA(0x85) + .assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_SET, NEGATIVE) + // Overflow check; 0x7F + 0x01 = 0x80 (overflow) + .add("CLD") + .add("LDA #$7F") + .assertTimed("ADC #1", 2) + .assertA(0x80) + .assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_SET, NEGATIVE) + .run(); + } + + @Test + /* ADC: All addressing modes -- these are used in many opcodes so we mostly just need to test here */ + public void testAdditionAddressingModes() throws ProgramException { + // Start test by filling zero page and $1000...$10FF with 0...255 + new TestProgram(""" + LDX #0 + - TXA + STA $00,X + STA $1000,X + INX + BNE - + LDA #1 + """) + // 1: AB,X + .add("LDX #$7F") + .assertTimed("ADC $1000,X", 4) + .assertA(0x80) + .assertFlags(OVERFLOW_SET, CARRY_CLEAR, NEGATIVE) + // 2: AB,Y + .add("LDY #$20") + .assertTimed("ADC $1000,Y", 4) + .assertA(0xA0) + .assertFlags(OVERFLOW_CLEAR, CARRY_CLEAR, NEGATIVE) + // 3: izp ($09) == 0x100f ==> A+=f + .assertTimed("ADC ($0f)", 5) + .assertA(0xAF) + // 4: izx ($00,x) where X=f = 0x100f ==> A+=f + .add("LDX #$0F") + .assertTimed("ADC ($00,x)", 6) + .assertA(0xBE) + // 5: izy ($00),y where Y=20 = 0x102F ==> A+=2F + .add("LDY #$21") + .assertTimed("ADC ($0F),y", 5) + .assertA(0xEE) + // 6: zpx $00,x where X=10 ==> A+=10 + .add("LDX #$10") + .assertTimed("ADC $00,x", 4) + .assertA(0xFE) + // 7: zp $01 ==> A+=01 + .assertTimed("ADC $01", 3) + .assertA(0xFF) + // 8: abs $1001 ==> A+=01 + .assertTimed("ADC $1001", 4) + .assertA(0x00) + .assertFlags(IS_ZERO, OVERFLOW_CLEAR, CARRY_SET, POSITIVE) + // Now check boundary conditions on indexed addressing for timing differences + .add("LDX #$FF") + .assertTimed("ADC $10FF,X", 5) + .add("LDY #$FF") + .assertTimed("ADC $10FF,Y", 5) + .assertTimed("ADC ($0F),Y", 6) + .run(); + } + + @Test + /* ABS: All CPU flags/modes */ + public void testSubtraction() throws ProgramException { + new TestProgram("SEC") + // 0-1 = -1 + .assertTimed("SBC #1", 2) + .assertA(0xFF) + .assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, NEGATIVE) + // 127 - -1 = 128 (Overflow) + .add("SEC") + .add("LDA #$7F") + .assertTimed("SBC #$FF", 2) + .assertA(0x80) + .assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_SET, NEGATIVE) + // -128 - 1 = -129 (overflow) + .add("SEC") + .add("LDA #$80") + .assertTimed("SBC #$01", 2) + .assertA(0x7F) + .assertFlags(CARRY_SET, NOT_ZERO, OVERFLOW_SET, POSITIVE) + // 20-10=10 (no overflow) + .add("LDA #$30") + .assertTimed("SBC #$10", 2) + .assertA(0x20) + .assertFlags(CARRY_SET, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE) + // Decimal: 20-5=15 + .add("SED") + .assertTimed("SBC #$05", 3) + .assertA(0x15) + .assertFlags(CARRY_SET, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE) + // Decimal: 0x15-0x05=0x10 + .assertTimed("SBC #$05", 3) + .assertA(0x10) + .assertFlags(CARRY_SET, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE) + // Decimal: 99-19=80 + .add("LDA #$99") + .assertTimed("SBC #$19", 3) + .assertA(0x80) + .assertFlags(CARRY_SET, NOT_ZERO, OVERFLOW_CLEAR, NEGATIVE) + // Decimal: 99-50=49 (unintuitively causes overflow) + .add("LDA #$99") + .assertTimed("SBC #$50", 3) + .assertA(0x49) + .assertFlags(CARRY_SET, NOT_ZERO, OVERFLOW_SET, POSITIVE) + // Decimal: 19 - 22 = 97 (arithmetic underflow clears carry) + .add("LDA #$19") + .assertTimed("SBC #$22", 3) + .assertA(0x97) + .assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, NEGATIVE) + // Test cycle counts for other addressing modes + .add("CLD") + .assertTimed("SBC $1000", 4) + .assertTimed("SBC $1000,x", 4) + .assertTimed("SBC $1000,y", 4) + .assertTimed("SBC ($00)", 5) + .assertTimed("SBC ($00,X)", 6) + .assertTimed("SBC ($00),Y", 5) + .assertTimed("SBC $00", 3) + .assertTimed("SBC $00,X", 4) + .run(); + } + + @Test + /* Full test of ADC and SBC with binary coded decimal mode */ + public void testBCD() throws ProgramException, URISyntaxException, IOException { + Path resource = Paths.get(getClass().getResource("/jace/bcd_test.asm").toURI()); + String testCode = Files.readString(resource); + TestProgram test = new TestProgram(testCode); + test.defineError("Error when performing ADC operation"); + test.defineError("Error when performing SBC operation"); + try { + test.runForTicks(50000000); + } catch (ProgramException e) { + // Dump memory from 0x0006 to 0x0015 + for (int i = 0x0006; i <= 0x0015; i++) { + System.out.printf("%04X: %02X\n", i, ram.read(i, TYPE.READ_DATA, false, false)); + } + throw e; + } + } + + @Test + /* Test of the processor flags */ + public void testFlags() throws ProgramException { + new TestProgram() + // Test explicit flag set/clear commands (and the tests by way of this cover all branch instructions) + .assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR) + .assertTimed("SEC", 2) + .assertFlags(CARRY_SET, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR) + .assertTimed("CLC", 2) + .assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR) + .assertTimed("SED", 2) + .assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE, DECIMAL_SET, INTERRUPT_CLEAR) + .assertTimed("CLD", 2) + .assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR) + .assertTimed("SEI", 2) + .assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE, DECIMAL_CLEAR, INTERRUPT_SET) + .assertTimed("CLI", 2) + .assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR) + // Set overflow flag by hacking the P register + .add(""" + LDA #%01000000 + PHA + PLP + """) + .assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_SET, POSITIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR) + .assertTimed("CLV", 2) + .assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR) + // Test Zero and Negative flags (within reason, the ADC/SBC tests cover these more thoroughly) + .assertTimed("LDA #0",2 ) + .assertFlags(CARRY_CLEAR, IS_ZERO, OVERFLOW_CLEAR, POSITIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR) + .assertTimed("LDA #$ff", 2) + .assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, NEGATIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR) + .assertTimed("LDY #0", 2) + .assertFlags(CARRY_CLEAR, IS_ZERO, OVERFLOW_CLEAR, POSITIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR) + .assertTimed("LDY #$ff", 2) + .assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, NEGATIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR) + .assertTimed("LDX #0", 2) + .assertFlags(CARRY_CLEAR, IS_ZERO, OVERFLOW_CLEAR, POSITIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR) + .assertTimed("LDX #$ff", 2) + .assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, NEGATIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR) + .run(); + } + + /* Test stack operations */ + @Test + public void testStack() throws ProgramException { + new TestProgram() + .assertTimed("TSX", 2) + .assertX(0xFF) + .assertTimed("LDA #255", 2) + .assertTimed("PHA", 3) + .assertTimed("LDX #11", 2) + .assertTimed("PHX", 3) + .assertTimed("LDY #12", 2) + .assertTimed("PHY", 3) + .assertTimed("PHP", 3) + .assertTimed("TSX", 2) + .assertX(0xFB) + .assertTimed("PLP", 4) + .add("LDA #0") + .assertFlags(IS_ZERO, POSITIVE) + .assertTimed("PLA", 4) + .assertA(12) + .assertFlags(NOT_ZERO, POSITIVE) + .add("LDY #$FF") + .assertFlags(NOT_ZERO, NEGATIVE) + .assertTimed("PLY", 4) + .assertY(11) + .assertFlags(NOT_ZERO, POSITIVE) + .assertTimed("PLX", 4) + .assertX(255) + .assertFlags(NOT_ZERO, NEGATIVE) + .run(); + } + + /* Test logic operations */ + @Test + public void testLogic() throws ProgramException { + // OR, AND, EOR + new TestProgram() + .assertTimed("LDA #0x55", 2) + .assertTimed("AND #0xAA", 2) + .assertA(0x00) + .assertFlags(IS_ZERO, POSITIVE) + .assertTimed("LDA #0x55", 2) + .assertTimed("ORA #0xAA", 2) + .assertA(0xFF) + .assertFlags(NOT_ZERO, NEGATIVE) + .assertTimed("LDA #0x55", 2) + .assertTimed("EOR #0xFF", 2) + .assertA(0xAA) + .assertFlags(NOT_ZERO, NEGATIVE) + .run(); + TestProgram cycleCounts = new TestProgram(); + for (String opcode : new String[] {"AND", "ORA", "EOR"}) { + cycleCounts.assertTimed(opcode + " $1000", 4) + .assertTimed(opcode + " $1000,x", 4) + .assertTimed(opcode + " $1000,y", 4) + .assertTimed(opcode + " ($00)", 5) + .assertTimed(opcode + " ($00,X)", 6) + .assertTimed(opcode + " ($00),Y", 5) + .assertTimed(opcode + " $00", 3) + .assertTimed(opcode + " $00,X", 4); + } + cycleCounts.run(); + // ASL + new TestProgram() + .assertTimed("LDA #0x55", 2) + .add("STA $1000") + .assertTimed("ASL", 2) + .assertA(0xAA) + .assertFlags(NOT_ZERO, NEGATIVE, CARRY_CLEAR) + .assertTimed("ASL", 2) + .assertA(0x54) + .assertFlags(NOT_ZERO, POSITIVE, CARRY_SET) + .assertTimed("ASL $1000", 6) + .assertAddrVal(0x1000, 0xAA) + .assertFlags(NOT_ZERO, NEGATIVE, CARRY_CLEAR) + .assertTimed("ASL $1000,x", 7) + .assertTimed("ASL $00", 5) + .assertTimed("ASL $00,x", 6) + .run(); + // LSR + new TestProgram() + .assertTimed("LDA #0x55", 2) + .add("STA $1000") + .assertTimed("LSR", 2) + .assertA(0x2A) + .assertFlags(NOT_ZERO, POSITIVE, CARRY_SET) + .assertTimed("LSR", 2) + .assertA(0x15) + .assertFlags(NOT_ZERO, POSITIVE, CARRY_CLEAR) + .assertTimed("LSR $1000", 6) + .assertAddrVal(0x1000, 0x2A) + .assertFlags(NOT_ZERO, POSITIVE, CARRY_SET) + .assertTimed("LSR $1000,x", 7) + .assertTimed("LSR $00", 5) + .assertTimed("LSR $00,x", 6) + .run(); + // BIT + new TestProgram() + .add("LDA #$FF") + .add("STA $FF") + .assertTimed("BIT #0", 2) + .assertFlags(IS_ZERO, POSITIVE, OVERFLOW_CLEAR) + .assertTimed("BIT #$FF", 2) + .assertFlags(NOT_ZERO, NEGATIVE, OVERFLOW_CLEAR) + .assertTimed("BIT $FF", 3) + .assertFlags(NOT_ZERO, NEGATIVE, OVERFLOW_SET) + .add("CLV") + .add("LDA #$40") + .assertTimed("BIT #$40", 2) + .assertFlags(NOT_ZERO, POSITIVE, OVERFLOW_CLEAR) + .assertTimed("BIT #$80", 2) + .assertFlags(IS_ZERO, NEGATIVE, OVERFLOW_CLEAR) + .assertTimed("BIT $1000", 4) + .assertTimed("BIT $1000,x", 4) + .assertTimed("BIT $00,X", 4) + .run(); + // ROL + new TestProgram() + .add("LDA #0x55") + .add("STA $1000") + .assertTimed("ROL", 2) + .assertA(0xAA) + .assertFlags(NOT_ZERO, NEGATIVE, CARRY_CLEAR) + .assertTimed("ROL", 2) + .assertA(0x54) + .assertFlags(NOT_ZERO, POSITIVE, CARRY_SET) + .add("CLC") + .assertTimed("ROL $1000", 6) + .assertAddrVal(0x1000, 0xAA) + .assertFlags(NOT_ZERO, NEGATIVE, CARRY_CLEAR) + .assertTimed("ROL $1000,x", 7) + .assertTimed("ROL $00", 5) + .assertTimed("ROL $00,x", 6) + .run(); + // ROR + new TestProgram() + .add("LDA #0x55") + .add("STA $1000") + .assertTimed("ROR", 2) + .assertA(0x2A) + .assertFlags(NOT_ZERO, POSITIVE, CARRY_SET) + .assertTimed("ROR", 2) + .assertA(0x95) + .assertFlags(NOT_ZERO, NEGATIVE, CARRY_CLEAR) + .assertTimed("ROR $1000", 6) + .assertAddrVal(0x1000, 0x2A) + .assertFlags(NOT_ZERO, POSITIVE, CARRY_SET) + .assertTimed("ROR $1000,x", 7) + .assertTimed("ROR $00", 5) + .assertTimed("ROR $00,x", 6) + .run(); + } + + /* Increment/Decrement instructions */ + @Test + public void testIncDec() throws ProgramException { + new TestProgram() + .add("LDA #0") + .add("STA $1000") + .assertTimed("INC", 2) + .assertA(1) + .assertFlags(NOT_ZERO, POSITIVE) + .assertTimed("DEC", 2) + .assertA(0) + .assertFlags(IS_ZERO, POSITIVE) + .add("LDA #$FF") + .assertTimed("INC", 2) + .assertA(0) + .assertFlags(IS_ZERO, POSITIVE) + .assertTimed("DEC", 2) + .assertA(0xFF) + .assertFlags(NOT_ZERO, NEGATIVE) + .assertTimed("INC $1000", 6) + .assertAddrVal(0x1000, 1) + .assertFlags(NOT_ZERO, POSITIVE) + .assertTimed("DEC $1000", 6) + .assertAddrVal(0x1000, 0) + .assertFlags(IS_ZERO, POSITIVE) + .assertTimed("INC $1000,x", 7) + .assertTimed("DEC $1000,x", 7) + .assertTimed("INC $00", 5) + .assertTimed("DEC $00", 5) + .assertTimed("INC $00,x", 6) + .assertTimed("DEC $00,x", 6) + // INX/DEX/INY/DEY + .add("LDX #0") + .assertTimed("INX", 2) + .assertX(1) + .assertFlags(NOT_ZERO, POSITIVE) + .assertTimed("DEX", 2) + .assertX(0) + .assertFlags(IS_ZERO, POSITIVE) + .assertTimed("DEX", 2) + .assertX(0xFF) + .assertFlags(NOT_ZERO, NEGATIVE) + .assertTimed("INX", 2) + .assertX(0) + .assertFlags(IS_ZERO, POSITIVE) + .add("LDY #0") + .assertTimed("INY", 2) + .assertY(1) + .assertFlags(NOT_ZERO, POSITIVE) + .assertTimed("DEY", 2) + .assertY(0) + .assertFlags(IS_ZERO, POSITIVE) + .assertTimed("DEY", 2) + .assertY(0xFF) + .assertFlags(NOT_ZERO, NEGATIVE) + .assertTimed("INY", 2) + .assertY(0) + .assertFlags(IS_ZERO, POSITIVE) + + .run(); + } + + /* All compare instructions CMP, CPX, CPY */ + @Test + public void testComparisons() throws ProgramException { + new TestProgram() + .add("LDA #0") + .assertTimed("CMP #0", 2) + .assertFlags(IS_ZERO, POSITIVE, CARRY_SET) + .assertTimed("CMP #1", 2) + .assertFlags(NOT_ZERO, NEGATIVE, CARRY_CLEAR) + .add("LDA #$FF") + .assertTimed("CMP #0", 2) + .assertFlags(NOT_ZERO, NEGATIVE, CARRY_SET) + .assertTimed("CMP #$FF", 2) + .assertFlags(IS_ZERO, POSITIVE, CARRY_SET) + .add("LDX #0") + .assertTimed("CPX #0", 2) + .assertFlags(IS_ZERO, POSITIVE, CARRY_SET) + .assertTimed("CPX #1", 2) + .assertFlags(NOT_ZERO, NEGATIVE, CARRY_CLEAR) + .add("LDX #$FF") + .assertTimed("CPX #0", 2) + .assertFlags(NOT_ZERO, NEGATIVE, CARRY_SET) + .assertTimed("CPX #$FF", 2) + .assertFlags(IS_ZERO, POSITIVE, CARRY_SET) + .add("LDY #0") + .assertTimed("CPY #0", 2) + .assertFlags(IS_ZERO, POSITIVE, CARRY_SET) + .assertTimed("CPY #1", 2) + .assertFlags(NOT_ZERO, NEGATIVE, CARRY_CLEAR) + .add("LDY #$FF") + .assertTimed("CPY #0", 2) + .assertFlags(NOT_ZERO, NEGATIVE, CARRY_SET) + .assertTimed("CPY #$FF", 2) + .assertFlags(IS_ZERO, POSITIVE, CARRY_SET) + // Cycle count other modes + .assertTimed("CMP $1000", 4) + .assertTimed("CMP $1000,x", 4) + .assertTimed("CMP $1000,y", 4) + .assertTimed("CMP ($00)", 5) + .assertTimed("CMP ($00,X)", 6) + .assertTimed("CMP ($00),Y", 5) + .assertTimed("CMP $00", 3) + .assertTimed("CMP $00,X", 4) + .assertTimed("CPX $1000", 4) + .assertTimed("CPX $10", 3) + .assertTimed("CPY $1000", 4) + .assertTimed("CPY $10", 3) + .run(); + } + + /* Load/Store/Transfer operations */ + @Test + public void testLoadStore() throws ProgramException { + new TestProgram() + .assertTimed("LDA #0", 2) + .assertA(0) + .assertFlags(IS_ZERO, POSITIVE) + .add("LDA #$FF") + .assertA(0xFF) + .assertFlags(NOT_ZERO, NEGATIVE) + .assertTimed("LDX #0", 2) + .assertX(0) + .assertFlags(IS_ZERO, POSITIVE) + .add("LDX #$FE") + .assertX(0xFE) + .assertFlags(NOT_ZERO, NEGATIVE) + .assertTimed("LDY #0", 2) + .assertY(0) + .assertFlags(IS_ZERO, POSITIVE) + .add("LDY #$FD") + .assertY(0xFD) + .assertFlags(NOT_ZERO, NEGATIVE) + .assertTimed("STA $1000", 4) + .assertAddrVal(0x1000, 0xFF) + .assertTimed("STX $1001", 4) + .assertAddrVal(0x1001, 0xFE) + .assertTimed("STY $1002", 4) + .assertAddrVal(0x1002, 0xFD) + .assertTimed("LDA $1002", 4) + .assertA(0xFD) + .assertFlags(NOT_ZERO, NEGATIVE) + .assertTimed("LDX $1000", 4) + .assertX(0xFF) + .assertFlags(NOT_ZERO, NEGATIVE) + .assertTimed("LDY $1001", 4) + .assertY(0xFE) + .assertFlags(NOT_ZERO, NEGATIVE) + // Cycle count for other LDA modes + .assertTimed("LDA $1000,x", 4) + .assertTimed("LDA $1000,y", 4) + .assertTimed("LDA ($00)", 5) + .assertTimed("LDA ($00,X)", 6) + .assertTimed("LDA ($00),Y", 5) + .assertTimed("LDA $00", 3) + .assertTimed("LDA $00,X", 4) + // Cycle counts for other STA modes + .assertTimed("STA $1000,x", 5) + .assertTimed("STA $1000,y", 5) + .assertTimed("STA ($00)", 5) + .assertTimed("STA ($00,X)", 6) + .assertTimed("STA ($00),Y", 6) + .assertTimed("STA $00", 3) + .assertTimed("STA $00,X", 4) + // Cycle counts for other LDX and LDY modes + .assertTimed("LDX $1000", 4) + .assertTimed("LDX $1000,y", 4) + .assertTimed("LDX $00", 3) + .assertTimed("LDX $00,y", 4) + .assertTimed("LDY $1000", 4) + .assertTimed("LDY $1000,x", 4) + .assertTimed("LDY $00", 3) + .assertTimed("LDY $00,x", 4) + // Cycle counts for other STX and STY modes + .assertTimed("STX $1000", 4) + .assertTimed("STX $00", 3) + .assertTimed("STX $00, Y", 4) + .assertTimed("STY $1000", 4) + .assertTimed("STY $00", 3) + .assertTimed("STY $00, X", 4) + // STZ + .assertTimed("STZ $1000", 4) + .assertAddrVal(0x1000, 0) + .assertTimed("STZ $1000,x", 5) + .assertTimed("STZ $00", 3) + .assertTimed("STZ $00,x", 4) + .run(); + // Now test the transfer instructions + new TestProgram() + .add("LDA #10") + .add("LDX #20") + .add("LDY #30") + .assertTimed("TAX", 2) + .assertX(10) + .assertFlags(NOT_ZERO, POSITIVE) + .assertTimed("TAY", 2) + .assertY(10) + .assertFlags(NOT_ZERO, POSITIVE) + .add("LDA #$FF") + .assertTimed("TAX", 2) + .assertX(0xFF) + .assertFlags(NOT_ZERO, NEGATIVE) + .assertTimed("TAY", 2) + .assertY(0xFF) + .assertFlags(NOT_ZERO, NEGATIVE) + .add("LDA #0") + .assertTimed("TXA", 2) + .assertA(0xFF) + .assertFlags(NOT_ZERO, NEGATIVE) + .add("LDA #0") + .assertTimed("TYA", 2) + .assertA(0xFF) + .assertFlags(NOT_ZERO, NEGATIVE) + .assertTimed("TSX", 2) + .assertX(0xFF) + .assertTimed("TXS", 2) + .run(); + } + + /* Test branch instructions */ + @Test + public void testBranches() throws ProgramException { + new TestProgram() + // Zero and Negative flags + .add("LDA #0") + .assertTimed("BEQ *+2",3) + .assertTimed("BNE *+2",2) + .assertTimed("BPL *+2",3) + .assertTimed("BMI *+2",2) + .add("LDA #$FF") + .assertTimed("BEQ *+2",2) + .assertTimed("BNE *+2",3) + .assertTimed("BPL *+2",2) + .assertTimed("BMI *+2",3) + .assertTimed("BRA *+2", 3) + // Carry flag + .assertTimed("BCC *+2", 3) + .assertTimed("BCS *+2", 2) + .add("SEC") + .assertTimed("BCC *+2", 2) + .assertTimed("BCS *+2", 3) + // Overflow flag + .add("CLV") + .assertTimed("BVC *+2", 3) + .assertTimed("BVS *+2", 2) + .add(""" + lda #$40 + sta $1000 + bit $1000 + """) + .assertTimed("BVC *+2", 2) + .assertTimed("BVS *+2", 3) + .assertTimed("NOP", 2) + .run(); + } + + /* Test JMP */ + @Test + public void testJmp() throws ProgramException { + new TestProgram() + .add("LDA #0") + .assertTimed(""" + JMP + + LDA #$FF + NOP + NOP + + + """, 3) + .assertA(0) + // Testing indirect jump using self-modifying code + // Load the address of jmp target and store at $300 + .add(""" + LDA #jmpTarget + STA $301 + LDX #$FF + """) + .assertTimed("JMP ($300)", 6) + .add(""" + LDX #0 + jmpTarget LDA #$88 + """) + .assertA(0x88) + .assertX(0xFF) + // Perform similar test using indirect,x addressing + .add(""" + LDA #<(jmpTargetX) + STA $310 + LDA #>(jmpTargetX) + STA $311 + LDX #$10 + LDY #$FF + """) + .assertTimed("JMP ($300,X)", 6) + .add(""" + LDY #88 + jmpTargetX LDA #$88 + """) + .assertA(0x88) + .assertY(0xFF) + .run(); + } + + /* Test JSR */ + @Test + public void testJsr() throws ProgramException { + // "Easy" test that JSR + RTS work as expected and together take 12 cycles + new TestProgram() + .add(""" + jmp + + sub1 rts + +throwError 69 + + + """) + .assertTimed(""" + JSR sub1 + """, 12) + .run(); + // Check that JSR pushes the expected PC values to the stack + new TestProgram() + .add(""" + jmp start + test + plx + ply + phy + phx + rts + """) + .test("", "RTS did not return to the correct address") + .add(""" + start + jsr test + ret + """) + .test(""" + cpy #>ret + beq + + """, "Y = MSB of return address") + .test(""" + inx + cpx #c, null); + cpu = (MOS65C02) computer.getCpu(); + ram = (RAM128k) computer.getMemory(); + } + + @Test + public void assertMemoryConfiguredCorrectly() { + assertEquals("Active read bank 3 should be main memory page 3", + ram.mainMemory.getMemoryPage(3), + ram.activeRead.getMemoryPage(3)); + + assertEquals("Active write bank 3 should be main memory page 3", + ram.mainMemory.getMemoryPage(3), + ram.activeWrite.getMemoryPage(3)); + } + + @Test + public void testListenerRelevance() throws Exception { + AtomicInteger anyEventCaught = new AtomicInteger(); + RAMListener anyListener = new RAMListener("Execution test", RAMEvent.TYPE.ANY, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) { + @Override + protected void doConfig() { + setScopeStart(0x0100); + } + + @Override + protected void doEvent(RAMEvent e) { + anyEventCaught.incrementAndGet(); + } + }; + + AtomicInteger readAnyEventCaught = new AtomicInteger(); + RAMListener readAnyListener = new RAMListener("Execution test 1", RAMEvent.TYPE.READ, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) { + @Override + protected void doConfig() { + setScopeStart(0x0100); + } + + @Override + protected void doEvent(RAMEvent e) { + readAnyEventCaught.incrementAndGet(); + } + }; + + AtomicInteger writeEventCaught = new AtomicInteger(); + RAMListener writeListener = new RAMListener("Execution test 2", RAMEvent.TYPE.WRITE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) { + @Override + protected void doConfig() { + setScopeStart(0x0100); + } + + @Override + protected void doEvent(RAMEvent e) { + writeEventCaught.incrementAndGet(); + } + }; + + AtomicInteger executeEventCaught = new AtomicInteger(); + RAMListener executeListener = new RAMListener("Execution test 3", RAMEvent.TYPE.EXECUTE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY) { + @Override + protected void doConfig() { + setScopeStart(0x0100); + } + + @Override + protected void doEvent(RAMEvent e) { + executeEventCaught.incrementAndGet(); + } + }; + + + RAMEvent readDataEvent = new RAMEvent(RAMEvent.TYPE.READ_DATA, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY, 0x100, 0, 0); + RAMEvent readOperandEvent = new RAMEvent(RAMEvent.TYPE.READ_OPERAND, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY, 0x100, 0, 0); + RAMEvent executeEvent = new RAMEvent(RAMEvent.TYPE.EXECUTE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY, 0x100, 0, 0); + RAMEvent writeEvent = new RAMEvent(RAMEvent.TYPE.WRITE, RAMEvent.SCOPE.ADDRESS, RAMEvent.VALUE.ANY, 0x100, 0, 0); + + // Any listener + assertTrue("Any listener should handle all events", anyListener.isRelevant(readDataEvent)); + assertTrue("Any listener should handle all events", anyListener.isRelevant(readOperandEvent)); + assertTrue("Any listener should handle all events", anyListener.isRelevant(executeEvent)); + assertTrue("Any listener should handle all events", anyListener.isRelevant(writeEvent)); + + // Read listener + assertTrue("Read listener should handle all read events", readAnyListener.isRelevant(readDataEvent)); + assertTrue("Read listener should handle all read events", readAnyListener.isRelevant(readOperandEvent)); + assertTrue("Read listener should handle all read events", readAnyListener.isRelevant(executeEvent)); + assertFalse("Read listener should ignore write events", readAnyListener.isRelevant(writeEvent)); + + // Write listener + assertFalse("Write listener should ignore all read events", writeListener.isRelevant(readDataEvent)); + assertFalse("Write listener should ignore all read events", writeListener.isRelevant(readOperandEvent)); + assertFalse("Write listener should ignore all read events", writeListener.isRelevant(executeEvent)); + assertTrue("Write listener should handle write events", writeListener.isRelevant(writeEvent)); + + // Execution listener + assertTrue("Execute listener should only catch execution events", executeListener.isRelevant(executeEvent)); + assertFalse("Execute listener should only catch execution events", executeListener.isRelevant(readDataEvent)); + assertFalse("Execute listener should only catch execution events", executeListener.isRelevant(readOperandEvent)); + assertFalse("Execute listener should only catch execution events", executeListener.isRelevant(writeEvent)); + + ram.addListener(anyListener); + ram.addListener(executeListener); + ram.addListener(readAnyListener); + ram.addListener(writeListener); + + runAssemblyCode("NOP", 0x0100, 2); + + assertEquals("Should have no writes for 0x0100", 0, writeEventCaught.get()); + assertEquals("Should have read event for 0x0100", 1, readAnyEventCaught.get()); + assertEquals("Should have execute for 0x0100", 1, executeEventCaught.get()); + } +} diff --git a/src/test/java/jace/core/SoundTest.java b/src/test/java/jace/core/SoundTest.java new file mode 100644 index 0000000..b3e97d1 --- /dev/null +++ b/src/test/java/jace/core/SoundTest.java @@ -0,0 +1,84 @@ +package jace.core; + +import static org.junit.Assert.assertEquals; + +import java.util.concurrent.ExecutionException; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import jace.AbstractFXTest; +import jace.core.SoundMixer.SoundBuffer; +import jace.core.SoundMixer.SoundError; + +public class SoundTest extends AbstractFXTest { + @Before + public void setUp() { + System.out.println("Init sound"); + Utility.setHeadlessMode(false); + SoundMixer.initSound(); + } + + @After + public void tearDown() { + Utility.setHeadlessMode(true); + } + + @Test + //(Only use this to ensure the sound engine produces audible output, it's otherwise annoying to hear all the time) + public void soundGenerationTest() throws SoundError { + try { + System.out.println("Performing sound test..."); + SoundMixer mixer = new SoundMixer(); + System.out.println("Attach mixer"); + mixer.attach(); + System.out.println("Allocate buffer"); + SoundBuffer buffer = SoundMixer.createBuffer(false); + System.out.println("Generate sound"); + // for (int i = 0; i < 100000; i++) { + for (int i = 0; i < 100; i++) { + // Gerate a sin wave with a frequency sweep so we can tell if the buffer is being fully processed + double x = Math.sin(i*i * 0.0001); + buffer.playSample((short) (Short.MAX_VALUE * x)); + } + System.out.println("Closing buffer"); + buffer.shutdown(); + System.out.println("Deactivating sound"); + mixer.detach(); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + } + } + + @Test + // Commented out because it's annoying to hear all the time, but it worked without issues + public void mixerTortureTest() throws SoundError, InterruptedException, ExecutionException { + System.out.println("Performing speaker tick test..."); + SoundMixer.initSound(); + System.out.println("Create mixer"); + SoundMixer mixer = new SoundMixer(); + System.out.println("Attach mixer"); + mixer.attach(); + // We want to create and destroy lots of buffers to make sure we don't have any memory leaks + // for (int i = 0; i < 10000; i++) { + for (int i = 0; i < 1000; i++) { + // Print status every 1000 iterations + if (i % 1000 == 0) { + System.out.println("Iteration %d".formatted(i)); + } + SoundBuffer buffer = SoundMixer.createBuffer(false); + for (int j = 0; j < SoundMixer.BUFFER_SIZE*2; j++) { + // Gerate a sin wave with a frequency sweep so we can tell if the buffer is being fully processed + double x = Math.sin(j*j * 0.0001); + buffer.playSample((short) (Short.MAX_VALUE * x)); + } + buffer.flush(); + buffer.shutdown(); + } + // Assert buffers are empty + assertEquals("All buffers should be empty", 0, mixer.getActiveBuffers()); + System.out.println("Deactivating sound"); + mixer.detach(); + } +} diff --git a/src/test/java/jace/core/UtilityTest.java b/src/test/java/jace/core/UtilityTest.java new file mode 100644 index 0000000..ca65212 --- /dev/null +++ b/src/test/java/jace/core/UtilityTest.java @@ -0,0 +1,42 @@ +package jace.core; +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.Collection; + +import org.junit.Test; + +public class UtilityTest { + + @Test + public void testLevenshteinDistance() { + String s1 = "kitten"; + String s2 = "sitting"; + int distance = Utility.levenshteinDistance(s1, s2); + assertEquals(3, distance); + } + + @Test + public void testAdjustedLevenshteinDistance() { + String s1 = "kitten"; + String s2 = "sitting"; + int adjustedDistance = Utility.adjustedLevenshteinDistance(s1, s2); + assertEquals(4, adjustedDistance); + } + + @Test + public void testRankMatch() { + String s1 = "apple"; + String s2 = "banana"; + double score = Utility.rankMatch(s1, s2, 3); + assertEquals(0, score, 0.001); + } + + @Test + public void testFindBestMatch() { + String match = "apple"; + Collection search = Arrays.asList("banana", "orange", "apple pie"); + String bestMatch = Utility.findBestMatch(match, search); + assertEquals("apple pie", bestMatch); + } +} \ No newline at end of file diff --git a/src/test/java/jace/hardware/CardAppleMouseTest.java b/src/test/java/jace/hardware/CardAppleMouseTest.java new file mode 100644 index 0000000..b2127c2 --- /dev/null +++ b/src/test/java/jace/hardware/CardAppleMouseTest.java @@ -0,0 +1,121 @@ +package jace.hardware; + +import static org.junit.Assert.assertEquals; + +import org.junit.Before; +import org.junit.Test; + +import jace.AbstractFXTest; +import jace.core.RAMEvent; +import jace.core.RAMEvent.SCOPE; +import jace.core.RAMEvent.TYPE; +import jace.core.RAMEvent.VALUE; +import javafx.geometry.Rectangle2D; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; + +public class CardAppleMouseTest extends AbstractFXTest { + + private CardAppleMouse cardAppleMouse; + + @Before + public void setUp() { + cardAppleMouse = new CardAppleMouse(); + } + + @Test + public void testGetDeviceName() { + assertEquals("Apple Mouse", cardAppleMouse.getDeviceName()); + } + + @Test + public void testReset() { + cardAppleMouse.mode = 1; + cardAppleMouse.clampWindow = new Rectangle2D(10, 10, 100, 100); + cardAppleMouse.detach(); + + cardAppleMouse.reset(); + + assertEquals(0, cardAppleMouse.mode); + assertEquals(new Rectangle2D(0, 0, 0x03ff, 0x03ff), cardAppleMouse.clampWindow); + } + + @Test + // Test mouseHandler responses to mouse events + public void testMouseHandler() { + MouseEvent clickEvent = new MouseEvent(MouseEvent.MOUSE_CLICKED,0,0,0,0,MouseButton.PRIMARY,1,false,false,false, false, false, false, false, false, false, false, null); + MouseEvent releaseEvent = new MouseEvent(MouseEvent.MOUSE_RELEASED,0,0,0,0,MouseButton.PRIMARY,1,false,false,false, false, false, false, false, false, false, false, null); + MouseEvent dragEvent = new MouseEvent(MouseEvent.MOUSE_DRAGGED,0,0,0,0,MouseButton.PRIMARY,1,false,false,false, false, false, false, false, false, false, false, null); + + cardAppleMouse.mode = 1; + cardAppleMouse.mouseHandler.handle(clickEvent); + cardAppleMouse.mouseHandler.handle(dragEvent); + cardAppleMouse.mouseHandler.handle(releaseEvent); + assertEquals(1, cardAppleMouse.mode); + } + + @Test + // Test firmware entry points + public void testFirmware() { + // Test reads + RAMEvent event = new RAMEvent(TYPE.READ, SCOPE.ANY, VALUE.ANY, 0, 0, 0); + cardAppleMouse.handleFirmwareAccess(0x80, TYPE.EXECUTE, 0, event); + assertEquals(0x60, event.getNewValue()); + event.setNewValue(0x00); + cardAppleMouse.handleFirmwareAccess(0x81, TYPE.EXECUTE, 0, event); + assertEquals(0x60, event.getNewValue()); + event.setNewValue(0x00); + cardAppleMouse.handleFirmwareAccess(0x82, TYPE.EXECUTE, 0, event); + assertEquals(0x60, event.getNewValue()); + event.setNewValue(0x00); + cardAppleMouse.handleFirmwareAccess(0x83, TYPE.EXECUTE, 0, event); + assertEquals(0x60, event.getNewValue()); + event.setNewValue(0x00); + cardAppleMouse.handleFirmwareAccess(0x84, TYPE.EXECUTE, 0, event); + assertEquals(0x60, event.getNewValue()); + event.setNewValue(0x00); + cardAppleMouse.handleFirmwareAccess(0x85, TYPE.EXECUTE, 0, event); + assertEquals(0x60, event.getNewValue()); + event.setNewValue(0x00); + cardAppleMouse.handleFirmwareAccess(0x86, TYPE.EXECUTE, 0, event); + assertEquals(0x60, event.getNewValue()); + event.setNewValue(0x00); + cardAppleMouse.handleFirmwareAccess(0x87, TYPE.EXECUTE, 0, event); + assertEquals(0x60, event.getNewValue()); + event.setNewValue(0x00); + cardAppleMouse.handleFirmwareAccess(0x88, TYPE.EXECUTE, 0, event); + assertEquals(0x60, event.getNewValue()); + event.setNewValue(0x00); + cardAppleMouse.handleFirmwareAccess(0x05, TYPE.READ, 0, event); + assertEquals(0x38, event.getNewValue()); + cardAppleMouse.handleFirmwareAccess(0x07, TYPE.READ, 0, event); + assertEquals(0x18, event.getNewValue()); + cardAppleMouse.handleFirmwareAccess(0x08, TYPE.READ, 0, event); + assertEquals(0x01, event.getNewValue()); + cardAppleMouse.handleFirmwareAccess(0x0B, TYPE.READ, 0, event); + assertEquals(0x01, event.getNewValue()); + cardAppleMouse.handleFirmwareAccess(0x0C, TYPE.READ, 0, event); + assertEquals(0x20, event.getNewValue()); + cardAppleMouse.handleFirmwareAccess(0x11, TYPE.READ, 0, event); + assertEquals(0x00, event.getNewValue()); + cardAppleMouse.handleFirmwareAccess(0x12, TYPE.READ, 0, event); + assertEquals(0x080, event.getNewValue()); + cardAppleMouse.handleFirmwareAccess(0x13, TYPE.READ, 0, event); + assertEquals(0x081, event.getNewValue()); + cardAppleMouse.handleFirmwareAccess(0x14, TYPE.READ, 0, event); + assertEquals(0x082, event.getNewValue()); + cardAppleMouse.handleFirmwareAccess(0x15, TYPE.READ, 0, event); + assertEquals(0x083, event.getNewValue()); + cardAppleMouse.handleFirmwareAccess(0x16, TYPE.READ, 0, event); + assertEquals(0x084, event.getNewValue()); + cardAppleMouse.handleFirmwareAccess(0x17, TYPE.READ, 0, event); + assertEquals(0x085, event.getNewValue()); + cardAppleMouse.handleFirmwareAccess(0x18, TYPE.READ, 0, event); + assertEquals(0x086, event.getNewValue()); + cardAppleMouse.handleFirmwareAccess(0x19, TYPE.READ, 0, event); + assertEquals(0x087, event.getNewValue()); + cardAppleMouse.handleFirmwareAccess(0x1A, TYPE.READ, 0, event); + assertEquals(0x088, event.getNewValue()); + } + +} \ No newline at end of file diff --git a/src/test/java/jace/hardware/CardSSCTest.java b/src/test/java/jace/hardware/CardSSCTest.java new file mode 100644 index 0000000..cbd213e --- /dev/null +++ b/src/test/java/jace/hardware/CardSSCTest.java @@ -0,0 +1,54 @@ +package jace.hardware; + +import static jace.hardware.CardSSC.ACIA_Command; +import static jace.hardware.CardSSC.ACIA_Control; +import static jace.hardware.CardSSC.ACIA_Data; +import static jace.hardware.CardSSC.ACIA_Status; +import static jace.hardware.CardSSC.SW1; +import static jace.hardware.CardSSC.SW2_CTS; +import static org.junit.Assert.assertEquals; + +import org.junit.Before; +import org.junit.Test; + +import jace.AbstractFXTest; +import jace.core.RAMEvent; +import jace.core.RAMEvent.SCOPE; +import jace.core.RAMEvent.TYPE; +import jace.core.RAMEvent.VALUE; + +public class CardSSCTest extends AbstractFXTest { + + private CardSSC cardSSC; + + @Before + public void setUp() { + cardSSC = new CardSSC(); + } + + @Test + public void testGetDeviceName() { + assertEquals("Super Serial Card", cardSSC.getDeviceName()); + } + + @Test + public void testSetSlot() { + cardSSC.setSlot(1); + // assertEquals("Slot 1", cardSSC.activityIndicator.getText()); + } + + @Test + public void testReset() { + cardSSC.reset(); + } + + @Test + public void testIOAccess() { + RAMEvent event = new RAMEvent(TYPE.READ_DATA, SCOPE.ANY, VALUE.ANY, 0, 0, 0); + int[] registers = {SW1, SW2_CTS, ACIA_Data, ACIA_Control, ACIA_Status, ACIA_Command}; + for (int register : registers) { + cardSSC.handleIOAccess(register, TYPE.READ_DATA, 0, event); + cardSSC.handleIOAccess(register, TYPE.WRITE, 0, event); + } + } +} \ No newline at end of file diff --git a/src/test/java/jace/hardware/FloppyDiskTest.java b/src/test/java/jace/hardware/FloppyDiskTest.java new file mode 100644 index 0000000..8debc8b --- /dev/null +++ b/src/test/java/jace/hardware/FloppyDiskTest.java @@ -0,0 +1,62 @@ +package jace.hardware; +import static jace.hardware.FloppyDisk.PRODOS_SECTOR_ORDER; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; + +import org.junit.Before; +import org.junit.Test; + +import jace.AbstractFXTest; +import jace.hardware.FloppyDisk.SectorOrder; + +public class FloppyDiskTest extends AbstractFXTest { + + private FloppyDisk floppyDisk; + + @Before + public void setUp() throws IOException { + floppyDisk = new FloppyDisk(); + } + + @Test + public void readDisk_ValidDiskFile_Success() throws IOException { + // Create a sample disk file + byte[] diskData = new byte[232960]; + File diskFile = File.createTempFile("test_disk", ".dsk"); + diskFile.deleteOnExit(); + ByteArrayInputStream diskInputStream = new ByteArrayInputStream(diskData); + + // Read the disk file + floppyDisk.readDisk(diskInputStream, SectorOrder.DOS); + + // Verify the disk properties + assert(floppyDisk.isNibblizedImage); + assertEquals(254, floppyDisk.volumeNumber); + assertEquals(0, floppyDisk.headerLength); + assertEquals(232960, floppyDisk.nibbles.length); + assertEquals("Sector order not null", true, null != floppyDisk.currentSectorOrder); + assertNull(floppyDisk.diskPath); + } + + @Test + public void nibblize_ValidNibbles_Success() throws IOException { + // Create a sample nibbles array + byte[] nibbles = new byte[FloppyDisk.DISK_NIBBLE_LENGTH]; + for (int i = 0; i < nibbles.length; i++) { + nibbles[i] = (byte) (i % 256); + } + floppyDisk.currentSectorOrder = PRODOS_SECTOR_ORDER; + // Nibblize the nibbles array + byte[] nibblizedData = floppyDisk.nibblize(nibbles); + + // Verify the nibblized data + assertEquals(FloppyDisk.DISK_NIBBLE_LENGTH, nibblizedData.length); +// for (int i = 0; i < nibblizedData.length; i++) { +// assertEquals((i % 256) >> 2, nibblizedData[i]); +// } + } +} \ No newline at end of file diff --git a/src/test/java/jace/hardware/PassportMidiInterfaceTest.java b/src/test/java/jace/hardware/PassportMidiInterfaceTest.java new file mode 100644 index 0000000..184a227 --- /dev/null +++ b/src/test/java/jace/hardware/PassportMidiInterfaceTest.java @@ -0,0 +1,44 @@ +package jace.hardware; + +import static jace.hardware.PassportMidiInterface.ACIA_RECV; +import static jace.hardware.PassportMidiInterface.ACIA_STATUS; +import static jace.hardware.PassportMidiInterface.TIMER1_LSB; +import static jace.hardware.PassportMidiInterface.TIMER1_MSB; +import static jace.hardware.PassportMidiInterface.TIMER2_LSB; +import static jace.hardware.PassportMidiInterface.TIMER2_MSB; +import static jace.hardware.PassportMidiInterface.TIMER3_LSB; +import static jace.hardware.PassportMidiInterface.TIMER3_MSB; +import static jace.hardware.PassportMidiInterface.TIMER_CONTROL_1; +import static jace.hardware.PassportMidiInterface.TIMER_CONTROL_2; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; + +import jace.AbstractFXTest; +import jace.core.RAMEvent; +import jace.core.RAMEvent.SCOPE; +import jace.core.RAMEvent.TYPE; +import jace.core.RAMEvent.VALUE; + + +public class PassportMidiInterfaceTest extends AbstractFXTest { + PassportMidiInterface midi = new PassportMidiInterface(); + + @Test + public void testDeviceSelection() { + assertNotNull(PassportMidiInterface.preferredMidiDevice.getSelections()); + assertNotEquals(0, PassportMidiInterface.preferredMidiDevice.getSelections().size()); + } + + @Test + public void testIOAccess() { + RAMEvent event = new RAMEvent(TYPE.READ_DATA, SCOPE.ANY, VALUE.ANY, 0, 0, 0); + int[] registers = {ACIA_STATUS, ACIA_RECV, TIMER_CONTROL_1, TIMER_CONTROL_2, TIMER1_LSB, TIMER1_MSB, TIMER2_LSB, TIMER2_MSB, TIMER3_LSB, TIMER3_MSB}; + for (int register : registers) { + midi.handleIOAccess(register, TYPE.READ_DATA, 0, event); + midi.handleIOAccess(register, TYPE.WRITE, 0, event); + } + } + +} diff --git a/src/test/java/jace/hardware/mockingboard/PSGTest.java b/src/test/java/jace/hardware/mockingboard/PSGTest.java new file mode 100644 index 0000000..9650a33 --- /dev/null +++ b/src/test/java/jace/hardware/mockingboard/PSGTest.java @@ -0,0 +1,50 @@ +package jace.hardware.mockingboard; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Before; +import org.junit.Test; + +public class PSGTest { + + private PSG psg; + + @Before + public void setUp() { + psg = new PSG(0, 100, 44100, "name", 255); + } + + @Test + public void setControl_InactiveCommand_NoAction() { + psg.setControl(0); // Set control to inactive + // Assert that no action is taken + } + + @Test + public void setControl_LatchCommand_SelectedRegUpdated() { + psg.setControl(1); // Set control to latch + // Assert that selectedReg is updated correctly + // Add your assertions here + } + + @Test + public void setControl_ReadCommand_BusUpdated() { + psg.setControl(2); // Set control to read + // Assert that bus is updated correctly + // Add your assertions here + } + + @Test + public void setControl_WriteCommand_RegUpdated() { + psg.setControl(3); // Set control to write + // Assert that the corresponding register is updated correctly + // Add your assertions here + } + + @Test + public void updateTest() { + AtomicInteger out = new AtomicInteger(); + psg.update(out, false, out, false, out, false); + psg.update(out, true, out, true, out, true); + } +} \ No newline at end of file diff --git a/src/test/java/jace/hardware/mockingboard/R6522Test.java b/src/test/java/jace/hardware/mockingboard/R6522Test.java new file mode 100644 index 0000000..d7bea9b --- /dev/null +++ b/src/test/java/jace/hardware/mockingboard/R6522Test.java @@ -0,0 +1,49 @@ +package jace.hardware.mockingboard; + +import org.junit.Test; + +public class R6522Test { + R6522 r6522 = new R6522() { + @Override + public String getShortName() { + return "name"; + } + + @Override + public void sendOutputA(int value) { + // No-op + } + + @Override + public void sendOutputB(int value) { + // No-op + } + + @Override + public int receiveOutputA() { + return -1; + } + + @Override + public int receiveOutputB() { + return -1; + } + }; + + @Test + public void testWriteRegs() { + for (R6522.Register reg : R6522.Register.values()) { + r6522.writeRegister(reg.val, 0); + } + } + + @Test + public void testReadRegs() { + for (R6522.Register reg : R6522.Register.values()) { + r6522.readRegister(reg.val); + } + } +} + + + diff --git a/src/test/java/jace/ide/ApplesoftTest.java b/src/test/java/jace/ide/ApplesoftTest.java new file mode 100644 index 0000000..d734e96 --- /dev/null +++ b/src/test/java/jace/ide/ApplesoftTest.java @@ -0,0 +1,83 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package jace.ide; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; + +import org.junit.After; +import org.junit.AfterClass; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import static jace.TestUtils.initComputer; +import jace.applesoft.ApplesoftProgram; + +/** + * + * @author blurry + */ +public class ApplesoftTest { + + public ApplesoftTest() { + } + + static Byte[] lemonadeStandBinary; + + @BeforeClass + public static void setUpClass() throws URISyntaxException, IOException { + initComputer(); + byte[] lemonadeStand = readBinary("/jace/lemonade_stand.bin"); + lemonadeStandBinary = ApplesoftProgram.toObjects(lemonadeStand); + } + + public static byte[] readBinary(String path) throws IOException, URISyntaxException { + Path resource = Paths.get(ApplesoftTest.class.getResource(path).toURI()); + return Files.readAllBytes(resource); + } + + @AfterClass + public static void tearDownClass() { + } + + @Before + public void setUp() { + } + + @After + public void tearDown() { + } + + @Test + public void deserializeBinaryTest() { + ApplesoftProgram program = ApplesoftProgram.fromBinary(Arrays.asList(lemonadeStandBinary), 0x0801); + assertNotNull(program); + assertNotSame("", program.toString()); + assertEquals("Lemonade stand has 380 lines", 380, program.getLength()); + assertTrue("Should have last line 31114", program.toString().contains("31114 ")); + } + + @Test + public void roundTripStringComparisonTest() { + ApplesoftProgram program = ApplesoftProgram.fromBinary(Arrays.asList(lemonadeStandBinary), 0x0801); + String serialized = program.toString(); + ApplesoftProgram deserialized = ApplesoftProgram.fromString(serialized); + String[] serializedLines = serialized.split("\\n"); + String[] researializedLines = deserialized.toString().split("\\n"); + assertEquals("Lemonade stand has 380 lines", 380, deserialized.getLength()); + assertArrayEquals("Program listing should be not change if re-keyed in as printed", serializedLines, researializedLines); + } +} diff --git a/src/test/resources/jace/bcd_test.asm b/src/test/resources/jace/bcd_test.asm new file mode 100644 index 0000000..c9e132b --- /dev/null +++ b/src/test/resources/jace/bcd_test.asm @@ -0,0 +1,301 @@ +; Modified and adapted from http://www.6502.org/tutorials/decimal_mode.html#B +; +; Verify decimal mode behavior +; Written by Bruce Clark. This code is public domain. +; +; Returns: +; ERROR = 0 if the test passed +; ERROR = 1 if the test failed +; +; This routine requires 17 bytes of RAM -- 1 byte each for: +; AR, CF, DA, DNVZC, ERROR, HA, HNVZC, N1, N1H, N1L, N2, N2L, NF, VF, and ZF +; and 2 bytes for N2H +; +; Variables: +; N1 and N2 are the two numbers to be added or subtracted +; N1H, N1L, N2H, and N2L are the upper 4 bits and lower 4 bits of N1 and N2 +; DA and DNVZC are the actual accumulator and flag results in decimal mode +; HA and HNVZC are the accumulator and flag results when N1 and N2 are +; added or subtracted using binary arithmetic +; AR, NF, VF, ZF, and CF are the predicted decimal mode accumulator and +; flag results, calculated using binary arithmetic +; +; This program takes approximately 1 minute at 1 MHz (a few seconds more on +; a 65C02 than a 6502 or 65816) +; + AR = $06 + NF = $07 + VF = $08 + CF = $09 + ZF = $0a + DA = $0b + DNVZC = $0c + HA = $0d + HNVZC = $0e + N1 = $0f + N1H = $10 + N1L = $11 + N2 = $12 + N2H = $13 + N2L = $15 + +TEST ;LDY #1 ; initialize Y (used to loop through carry flag values) + ;STY ERROR ; store 1 in ERROR until the test passes + LDA #0 ; initialize N1 and N2 + STA N1 + STA N2 +LOOP1 LDA N2 ; N2L = N2 & $0F + AND #$0F ; [1] see text + STA N2L + LDA N2 ; N2H = N2 & $F0 + AND #$F0 ; [2] see text + STA N2H + ORA #$0F ; N2H+1 = (N2 & $F0) + $0F + STA N2H+1 +LOOP2 LDA N1 ; N1L = N1 & $0F + AND #$0F ; [3] see text + STA N1L + LDA N1 ; N1H = N1 & $F0 + AND #$F0 ; [4] see text + STA N1H + JSR ADD + JSR A65C02 + JSR COMPARE + BNE ADD_ERROR + JSR SUB + JSR S65C02 + JSR COMPARE + BNE SUB_ERROR + INC N1 ; [5] see text + BNE LOOP2 ; loop through all 256 values of N1 + INC N2 ; [6] see text + BNE LOOP1 ; loop through all 256 values of N2 + DEY + BPL LOOP1 ; loop through both values of the carry flag +SUCCESS +success +ADD_ERROR + CPY #1 ; Set carry based on Y reg + LDA DA + LDX N1 + LDY N2 + +throwError 0 +SUB_ERROR + CPY #1 ; Set carry based on Y reg + LDA DA + LDX N1 + LDY N2 + +throwError 1 + +; Calculate the actual decimal mode accumulator and flags, the accumulator +; and flag results when N1 is added to N2 using binary arithmetic, the +; predicted accumulator result, the predicted carry flag, and the predicted +; V flag +; +ADD SED ; decimal mode + CPY #1 ; set carry if Y = 1, clear carry if Y = 0 + LDA N1 + ADC N2 + STA DA ; actual accumulator result in decimal mode + PHP + PLA + STA DNVZC ; actual flags result in decimal mode + CLD ; binary mode + CPY #1 ; set carry if Y = 1, clear carry if Y = 0 + LDA N1 + ADC N2 + STA HA ; accumulator result of N1+N2 using binary arithmetic + + PHP + PLA + STA HNVZC ; flags result of N1+N2 using binary arithmetic + CPY #1 + LDA N1L + ADC N2L + CMP #$0A + LDX #0 + BCC A1 + INX + ADC #5 ; add 6 (carry is set) + AND #$0F + SEC +A1 ORA N1H +; +; if N1L + N2L < $0A, then add N2 & $F0 +; if N1L + N2L >= $0A, then add (N2 & $F0) + $0F + 1 (carry is set) +; + ADC N2H,X + PHP + BCS A2 + CMP #$A0 + BCC A3 +A2 ADC #$5F ; add $60 (carry is set) + SEC +A3 STA AR ; predicted accumulator result + PHP + PLA + STA CF ; predicted carry result + PLA +; +; note that all 8 bits of the P register are stored in VF +; + STA VF ; predicted V flags + RTS + +; Calculate the actual decimal mode accumulator and flags, and the +; accumulator and flag results when N2 is subtracted from N1 using binary +; arithmetic +; +SUB SED ; decimal mode + CPY #1 ; set carry if Y = 1, clear carry if Y = 0 + LDA N1 + SBC N2 + STA DA ; actual accumulator result in decimal mode + PHP + PLA + STA DNVZC ; actual flags result in decimal mode + CLD ; binary mode + CPY #1 ; set carry if Y = 1, clear carry if Y = 0 + LDA N1 + SBC N2 + STA HA ; accumulator result of N1-N2 using binary arithmetic + + PHP + PLA + STA HNVZC ; flags result of N1-N2 using binary arithmetic + RTS + +; Calculate the predicted SBC accumulator result for the 6502 and 65816 + +; +SUB1 CPY #1 ; set carry if Y = 1, clear carry if Y = 0 + LDA N1L + SBC N2L + LDX #0 + BCS S11 + INX + SBC #5 ; subtract 6 (carry is clear) + AND #$0F + CLC +S11 ORA N1H +; +; if N1L - N2L >= 0, then subtract N2 & $F0 +; if N1L - N2L < 0, then subtract (N2 & $F0) + $0F + 1 (carry is clear) +; + SBC N2H,X + BCS S12 + SBC #$5F ; subtract $60 (carry is clear) +S12 STA AR + RTS + +; Calculate the predicted SBC accumulator result for the 6502 and 65C02 + +; +SUB2 CPY #1 ; set carry if Y = 1, clear carry if Y = 0 + LDA N1L + SBC N2L + LDX #0 + BCS S21 + INX + AND #$0F + CLC +S21 ORA N1H +; +; if N1L - N2L >= 0, then subtract N2 & $F0 +; if N1L - N2L < 0, then subtract (N2 & $F0) + $0F + 1 (carry is clear) +; + SBC N2H,X + BCS S22 + SBC #$5F ; subtract $60 (carry is clear) +S22 CPX #0 + BEQ S23 + SBC #6 +S23 STA AR ; predicted accumulator result + RTS + +; Compare accumulator actual results to predicted results +; +; Return: +; Z flag = 1 (BEQ branch) if same +; Z flag = 0 (BNE branch) if different +; +COMPARE LDA DA + CMP AR + +breakpoint 1 + BNE C1 + LDA DNVZC ; [7] see text + EOR NF + AND #$80 ; mask off N flag + +breakpoint 2 + BNE C1 + LDA DNVZC ; [8] see text + EOR VF + AND #$40 ; mask off V flag + +breakpoint 3 + BNE C1 ; [9] see text + LDA DNVZC + EOR ZF ; mask off Z flag + AND #2 + +breakpoint 4 + BNE C1 ; [10] see text + LDA DNVZC + EOR CF + AND #1 ; mask off C flag + +breakpoint 5 +C1 RTS + +; These routines store the predicted values for ADC and SBC for the 6502, +; 65C02, and 65816 in AR, CF, NF, VF, and ZF + +A6502 LDA VF +; +; since all 8 bits of the P register were stored in VF, bit 7 of VF contains +; the N flag for NF +; + STA NF + LDA HNVZC + STA ZF + RTS + +S6502 JSR SUB1 + LDA HNVZC + STA NF + STA VF + STA ZF + STA CF + RTS + +A65C02 LDA AR + PHP + PLA + STA NF + STA ZF + RTS + +S65C02 JSR SUB2 + LDA AR + PHP + PLA + STA NF + STA ZF + LDA HNVZC + STA VF + STA CF + RTS + +A65816 LDA AR + PHP + PLA + STA NF + STA ZF + RTS + +S65816 JSR SUB1 + LDA AR + PHP + PLA + STA NF + STA ZF + LDA HNVZC + STA VF + STA CF + RTS \ No newline at end of file