Merge pull request #55 from badvision/votrax

Restoring tests
This commit is contained in:
Brendan Robert 2024-08-02 22:53:13 -05:00 committed by GitHub
commit f34ba40ff0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 2855 additions and 6 deletions

View File

@ -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()));

View File

@ -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);
}
}
}

View File

@ -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);
}

Binary file not shown.

View File

@ -13,4 +13,4 @@ public abstract class AbstractFXTest {
Platform.startup(() -> {});
}
}
}
}

View File

@ -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 <<programLocation>> 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;
}
}

View File

@ -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<String> lines = new ArrayList<>();
Consumer<Byte> timerHandler;
Consumer<Byte> tickCountHandler;
Consumer<Byte> errorHandler;
Consumer<Byte> stopHandler;
Consumer<Byte> progressHandler;
Consumer<Byte> 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<String> errors = new ArrayList<>();
List<Integer> 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);
}
}

View File

@ -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;
}
};
}
}

View File

@ -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();
}
}

View File

@ -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 $300
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 #<ret
beq +
""", "X = LSB of return address-1")
.run();
}
}

View File

@ -0,0 +1,80 @@
package jace.apple2e;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import org.junit.Before;
import org.junit.Test;
import jace.AbstractFXTest;
import javafx.scene.image.WritableImage;
// This is mostly to provide execution coverage to catch null pointer or index out of range exceptions
public class VideoDHGRTest extends AbstractFXTest {
WritableImage image = new WritableImage(560, 192);
private VideoDHGR video;
@Before
public void setUp() {
video = new VideoDHGR();
}
@Test
public void testInitHgrDhgrTables() {
// Test the initialization of HGR_TO_DHGR and HGR_TO_DHGR_BW tables
assertNotNull(video.HGR_TO_DHGR);
assertNotNull(video.HGR_TO_DHGR_BW);
// Add more assertions here
}
@Test
public void testInitCharMap() {
// Test the initialization of CHAR_MAP1, CHAR_MAP2, and CHAR_MAP3 arrays
assertNotNull(video.CHAR_MAP1);
assertNotNull(video.CHAR_MAP2);
assertNotNull(video.CHAR_MAP3);
// Add more assertions here
}
private void writeToScreen() {
video.getCurrentWriter().displayByte(image, 0, 0, 0, 0);
video.getCurrentWriter().displayByte(image, 0, 4, 0, 0);
video.getCurrentWriter().displayByte(image, 0, 190, 0, 0);
video.getCurrentWriter().displayByte(image, -1, 0, 0, 0);
video.getCurrentWriter().actualWriter().displayByte(image, 0, 0, 0, 0);
}
@Test
public void testGetYOffset() {
// Run through all possible combinations of soft switches to ensure the correct Y offset is returned each time
SoftSwitches[] switches = {SoftSwitches.HIRES, SoftSwitches.TEXT, SoftSwitches.PAGE2, SoftSwitches._80COL, SoftSwitches.DHIRES, SoftSwitches.MIXED};
for (int i=0; i < Math.pow(2.0, switches.length); i++) {
String state = "";
for (int j=0; j < switches.length; j++) {
switches[j].getSwitch().setState((i & (1 << j)) != 0);
state += switches[j].getSwitch().getName() + "=" + (switches[j].getSwitch().getState() ? "1" : "0") + " ";
}
video.configureVideoMode();
int address = video.getCurrentWriter().getYOffset(0);
int expected = SoftSwitches.TEXT.isOn() || SoftSwitches.HIRES.isOff() ? (SoftSwitches.PAGE2.isOn() ? 0x0800 : 0x0400)
: (SoftSwitches.PAGE2.isOn() ? 0x04000 : 0x02000);
assertEquals("Address for mode not correct: " + state, expected, address);
}
}
@Test
public void testDisplayByte() {
// Run through all possible combinations of soft switches to ensure the video writer executes without error
SoftSwitches[] switches = {SoftSwitches.HIRES, SoftSwitches.TEXT, SoftSwitches.PAGE2, SoftSwitches._80COL, SoftSwitches.DHIRES, SoftSwitches.MIXED};
for (int i=0; i < Math.pow(2.0, switches.length); i++) {
for (int j=0; j < switches.length; j++) {
switches[j].getSwitch().setState((i & (1 << j)) != 0);
}
video.configureVideoMode();
writeToScreen();
}
}
// Add more test cases for other methods in the VideoDHGR class
}

View File

@ -0,0 +1,85 @@
package jace.apple2e;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import org.junit.Before;
import org.junit.Test;
import jace.AbstractFXTest;
import javafx.scene.image.WritableImage;
// This is mostly to provide execution coverage to catch null pointer or index out of range exceptions
public class VideoNTSCTest extends AbstractFXTest {
WritableImage image = new WritableImage(560, 192);
private VideoNTSC video;
@Before
public void setUp() {
video = new VideoNTSC();
}
@Test
public void testInitHgrDhgrTables() {
// Test the initialization of HGR_TO_DHGR and HGR_TO_DHGR_BW tables
assertNotNull(video.HGR_TO_DHGR);
assertNotNull(video.HGR_TO_DHGR_BW);
// Add more assertions here
}
@Test
public void testInitCharMap() {
// Test the initialization of CHAR_MAP1, CHAR_MAP2, and CHAR_MAP3 arrays
assertNotNull(video.CHAR_MAP1);
assertNotNull(video.CHAR_MAP2);
assertNotNull(video.CHAR_MAP3);
// Add more assertions here
}
private void writeToScreen() {
video.getCurrentWriter().displayByte(image, 0, 0, 0, 0);
video.getCurrentWriter().displayByte(image, 0, 4, 0, 0);
video.getCurrentWriter().displayByte(image, 0, 190, 0, 0);
video.getCurrentWriter().displayByte(image, -1, 0, 0, 0);
video.getCurrentWriter().actualWriter().displayByte(image, 0, 0, 0, 0);
}
@Test
public void testGetYOffset() {
// Run through all possible combinations of soft switches to ensure the correct Y offset is returned each time
SoftSwitches[] switches = {SoftSwitches.HIRES, SoftSwitches.TEXT, SoftSwitches.PAGE2, SoftSwitches._80COL, SoftSwitches.DHIRES, SoftSwitches.MIXED};
for (int i=0; i < Math.pow(2.0, switches.length); i++) {
String state = "";
for (int j=0; j < switches.length; j++) {
switches[j].getSwitch().setState((i & (1 << j)) != 0);
state += switches[j].getSwitch().getName() + "=" + (switches[j].getSwitch().getState() ? "1" : "0") + " ";
}
video.configureVideoMode();
int address = video.getCurrentWriter().getYOffset(0);
int expected = SoftSwitches.TEXT.isOn() || SoftSwitches.HIRES.isOff() ? (SoftSwitches.PAGE2.isOn() ? 0x0800 : 0x0400)
: (SoftSwitches.PAGE2.isOn() ? 0x04000 : 0x02000);
assertEquals("Address for mode not correct: " + state, expected, address);
}
}
@Test
public void testDisplayByte() {
// Run through all possible combinations of soft switches to ensure the video writer executes without error
SoftSwitches[] switches = {SoftSwitches.HIRES, SoftSwitches.TEXT, SoftSwitches.PAGE2, SoftSwitches._80COL, SoftSwitches.DHIRES, SoftSwitches.MIXED};
for (int i=0; i < Math.pow(2.0, switches.length); i++) {
for (int j=0; j < switches.length; j++) {
switches[j].getSwitch().setState((i & (1 << j)) != 0);
}
video.configureVideoMode();
writeToScreen();
}
}
@Test
public void testDisplayModes() {
for (VideoNTSC.VideoMode mode : VideoNTSC.VideoMode.values()) {
VideoNTSC.setVideoMode(mode, false);
}
}
}

View File

@ -0,0 +1,38 @@
package jace.applesoft;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import org.junit.Test;
public class ApplesoftTest {
@Test
public void fromStringTest() {
String programSource = "10 PRINT \"Hello, World!\"\n20 PRINT \"Goodbye!\"";
ApplesoftProgram program = ApplesoftProgram.fromString(programSource);
assertNotNull(program);
assertEquals(2, program.lines.size());
Line line1 = program.lines.get(0);
assertEquals(10, line1.getNumber());
assertEquals(1, line1.getCommands().size());
Command command1 = line1.getCommands().get(0);
assertEquals(0xBA, command1.parts.get(0).getByte() & 0x0ff);
String match = "";
for (int idx=1; idx < command1.parts.size(); idx++) {
match += command1.parts.get(idx).toString();
}
assertEquals("\"Hello, World!\"", match);
}
@Test
public void toStringTest() {
Line line1 = Line.fromString("10 print \"Hello, world!\"");
Line line2 = Line.fromString("20 print \"Goodbye!\"");
ApplesoftProgram program = new ApplesoftProgram();
program.lines.add(line1);
program.lines.add(line2);
String programSource = program.toString();
assertEquals("10 PRINT \"Hello, world!\"\n20 PRINT \"Goodbye!\"\n", programSource);
}
}

View File

@ -0,0 +1,156 @@
/*
* 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.core;
import static jace.TestUtils.initComputer;
import static jace.TestUtils.runAssemblyCode;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.BeforeClass;
import org.junit.Test;
import jace.Emulator;
import jace.apple2e.MOS65C02;
import jace.apple2e.RAM128k;
/**
* Test that memory listeners fire appropriately.
* @author brobert
*/
public class MemoryTest {
static Computer computer;
static MOS65C02 cpu;
static RAM128k ram;
@BeforeClass
public static void setupClass() {
initComputer();
SoundMixer.MUTE = true;
computer = Emulator.withComputer(c->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());
}
}

View File

@ -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();
}
}

View File

@ -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<String> search = Arrays.asList("banana", "orange", "apple pie");
String bestMatch = Utility.findBestMatch(match, search);
assertEquals("apple pie", bestMatch);
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}
}

View File

@ -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]);
// }
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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