DebugReportWriter.java

1
package com.rox.debug.report;
2
3
import com.rox.ByteUtil;
4
import com.rox.cpu.mos6502.MOS6502Snapshot;
5
import com.rox.cpu.mos6502.assembler.DisassembledInstruction;
6
import com.rox.debug.cpu.CpuStateText;
7
import com.rox.debug.memory.HexDump;
8
import com.rox.ppu.PpuSnapshot;
9
import com.rox.save.SaveFileFormat;
10
import com.rox.save.SaveMetadata;
11
import com.rox.save.SaveType;
12
import com.rox.save.SystemSnapshot;
13
14
import javax.imageio.ImageIO;
15
import java.awt.image.BufferedImage;
16
import java.io.IOException;
17
import java.nio.charset.StandardCharsets;
18
import java.nio.file.Files;
19
import java.nio.file.Path;
20
import java.time.ZoneId;
21
import java.time.format.DateTimeFormatter;
22
import java.util.Map;
23
24
/**
25
 * Writes a {@link DebugReport} as a folder of files a person (or an AI assistant) can dig through:
26
 * <ul>
27
 *     <li>{@code screenshot.png} - the last complete frame (absent if none had been shown yet)</li>
28
 *     <li>{@code framebuffer-partial.png} - the PPU's part-drawn frame at the moment of capture</li>
29
 *     <li>{@code description.txt} - what the person flagging the issue saw</li>
30
 *     <li>{@code state.txt} - CPU registers/flags, the next instructions, PPU and mapper state</li>
31
 *     <li>{@code ram.bin}/{@code ram.hex.txt} - the 2KB of CPU RAM ({@code $0000-$07FF}, mirrored through {@code $1FFF})</li>
32
 *     <li>{@code oam.bin}, {@code nametables.bin}, {@code palette.bin}, {@code prg-ram.bin}, and
33
 *     {@code chr-ram.bin} (CHR-RAM boards only) - raw dumps</li>
34
 *     <li>{@code <folder name>.sav} - the restorable {@link SystemSnapshot}</li>
35
 * </ul>
36
 */
37
public final class DebugReportWriter {
38
    private static final int SCREEN_WIDTH = 256;
39
    private static final int SCREEN_HEIGHT = 240;
40
    private static final DateTimeFormatter HEADER_TIMESTAMP = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
41
42
    private DebugReportWriter(){
43
    }
44
45
    /** Writes every file into {@code directory} (created if needed), naming the {@code .sav} after the directory. */
46
    public static void write(final Path directory, final DebugReport report) throws IOException {
47
        Files.createDirectories(directory);
48
        final SystemSnapshot snapshot = report.snapshot();
49
        final PpuSnapshot ppu = snapshot.ppu();
50
51 1 1. write : negated conditional → KILLED
        if (report.screenshotRgb() != null){
52 1 1. write : removed call to com/rox/debug/report/DebugReportWriter::writePng → KILLED
            writePng(directory.resolve("screenshot.png"), report.screenshotRgb());
53
        }
54 1 1. write : removed call to com/rox/debug/report/DebugReportWriter::writePng → KILLED
        writePng(directory.resolve("framebuffer-partial.png"), report.partialFrameRgb());
55
        Files.writeString(directory.resolve("description.txt"), report.description(), StandardCharsets.UTF_8);
56
        Files.writeString(directory.resolve("state.txt"), formatState(report), StandardCharsets.UTF_8);
57
58
        final int[] ram = snapshot.ram();
59
        Files.write(directory.resolve("ram.bin"), ByteUtil.toBytes(ram));
60
        Files.writeString(directory.resolve("ram.hex.txt"), hexDump(ram), StandardCharsets.UTF_8);
61
        Files.write(directory.resolve("oam.bin"), ByteUtil.toBytes(ppu.oam()));
62
        Files.write(directory.resolve("nametables.bin"), ByteUtil.toBytes(ppu.nametableRam()));
63
        Files.write(directory.resolve("palette.bin"), ByteUtil.toBytes(ppu.paletteRam()));
64
        Files.write(directory.resolve("prg-ram.bin"), ByteUtil.toBytes(snapshot.mapper().prgRam()));
65 2 1. write : changed conditional boundary → KILLED
2. write : negated conditional → KILLED
        if (snapshot.mapper().chrRam().length > 0){
66
            Files.write(directory.resolve("chr-ram.bin"), ByteUtil.toBytes(snapshot.mapper().chrRam()));
67
        }
68
69
        final SaveMetadata metadata = new SaveMetadata(SaveType.LIVE_SNAPSHOT,
70
                report.capturedAt().atZone(ZoneId.systemDefault()).toInstant(), 0);
71 1 1. write : removed call to com/rox/save/SaveFileFormat::writeLiveSnapshot → KILLED
        SaveFileFormat.writeLiveSnapshot(directory.resolve(directory.getFileName() + ".sav"), metadata, snapshot);
72
    }
73
74
    static String formatState(final DebugReport report){
75
        final MOS6502Snapshot cpu = report.snapshot().cpu();
76
        final PpuSnapshot ppu = report.snapshot().ppu();
77
        final StringBuilder out = new StringBuilder();
78
        out.append("EmuRoxLab debug snapshot\n");
79
        out.append("ROM:      ").append(report.romName()).append('\n');
80
        out.append("Captured: ").append(HEADER_TIMESTAMP.format(report.capturedAt())).append("\n\n");
81
82
        out.append("--- CPU ---\n");
83
        out.append(CpuStateText.registers(cpu)).append('\n');
84
        out.append("Flags: ").append(CpuStateText.flags(cpu)).append("  (NV-BDIZC, uppercase = set)\n");
85
        out.append(CpuStateText.interruptLines(cpu)).append("\n\n");
86
87
        out.append("--- Next instructions ---\n");
88
        boolean first = true;
89
        for (final DisassembledInstruction instruction : report.upcomingInstructions()){
90
            out.append(CpuStateText.instruction(instruction, first)).append('\n');
91
            first = false;
92
        }
93
        out.append('\n');
94
95
        final PpuSnapshot.Timing timing = ppu.timing();
96
        final PpuSnapshot.Registers registers = ppu.registers();
97
        out.append("--- PPU ---\n");
98
        out.append(String.format("Scanline: %d  Dot: %d  VBlank: %s%n", timing.scanline(), timing.dot(), yesNo(timing.vblankFlag())));
99
        out.append(String.format("PPUCTRL: $%02X  PPUMASK: $%02X  OAMADDR: $%02X%n",
100
                registers.control(), registers.mask(), registers.oamAddress()));
101
        out.append(String.format("v: $%04X  t: $%04X  fine X: %d  write toggle: %d%n", registers.currentVramAddress(),
102 1 1. formatState : negated conditional → KILLED
                registers.temporaryVramAddress(), registers.fineXScroll(), registers.writeToggle() ? 1 : 0));
103
        out.append("Sprite overflow: ").append(yesNo(ppu.sprites().spriteOverflow()))
104
                .append("  Sprite 0 hit: ").append(yesNo(ppu.sprites().spriteZeroHitFlag())).append("\n\n");
105
106
        out.append("--- Mapper ---\n");
107 1 1. formatState : negated conditional → KILLED
        if (report.mapperState().isEmpty()){
108
            out.append("(fixed mapping, no bank registers)\n");
109
        }
110
        for (final Map.Entry<String, String> entry : report.mapperState().entrySet()){
111
            out.append(entry.getKey()).append(": ").append(entry.getValue()).append('\n');
112
        }
113 1 1. formatState : replaced return value with "" for com/rox/debug/report/DebugReportWriter::formatState → KILLED
        return out.toString();
114
    }
115
116
    private static String yesNo(final boolean value){
117 2 1. yesNo : negated conditional → KILLED
2. yesNo : replaced return value with "" for com/rox/debug/report/DebugReportWriter::yesNo → KILLED
        return value ? "yes" : "no";
118
    }
119
120
    /** 16 bytes per row, each row prefixed with its address: {@code 0010: 00 01 ... 0F}. */
121
    static String hexDump(final int[] bytes){
122 3 1. hexDump : replaced return value with "" for com/rox/debug/report/DebugReportWriter::hexDump → KILLED
2. lambda$hexDump$0 : replaced int return with 0 for com/rox/debug/report/DebugReportWriter::lambda$hexDump$0 → KILLED
3. hexDump : Replaced integer subtraction with addition → KILLED
        return HexDump.format(address -> bytes[address], 0, bytes.length - 1, false);
123
    }
124
125
    private static void writePng(final Path path, final int[] rgb) throws IOException {
126
        final BufferedImage image = new BufferedImage(SCREEN_WIDTH, SCREEN_HEIGHT, BufferedImage.TYPE_INT_RGB);
127 1 1. writePng : removed call to java/awt/image/BufferedImage::setRGB → KILLED
        image.setRGB(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, rgb, 0, SCREEN_WIDTH);
128
        ImageIO.write(image, "png", path.toFile());
129
    }
130
}

Mutations

51

1.1
Location : write
Killed by : com.rox.debug.report.DebugReportWriterTest.[engine:junit-jupiter]/[class:com.rox.debug.report.DebugReportWriterTest]/[method:skipsChrRamOnAChrRomBoard(java.nio.file.Path)]
negated conditional → KILLED

52

1.1
Location : write
Killed by : com.rox.debug.report.DebugReportWriterTest.[engine:junit-jupiter]/[class:com.rox.debug.report.DebugReportWriterTest]/[method:writesBothFramesAsPngs(java.nio.file.Path)]
removed call to com/rox/debug/report/DebugReportWriter::writePng → KILLED

54

1.1
Location : write
Killed by : com.rox.debug.report.DebugReportWriterTest.[engine:junit-jupiter]/[class:com.rox.debug.report.DebugReportWriterTest]/[method:skipsTheScreenshotWhenNoFrameHadBeenShownYet(java.nio.file.Path)]
removed call to com/rox/debug/report/DebugReportWriter::writePng → KILLED

65

1.1
Location : write
Killed by : com.rox.debug.report.DebugReportWriterTest.[engine:junit-jupiter]/[class:com.rox.debug.report.DebugReportWriterTest]/[method:skipsChrRamOnAChrRomBoard(java.nio.file.Path)]
changed conditional boundary → KILLED

2.2
Location : write
Killed by : com.rox.debug.report.DebugReportWriterTest.[engine:junit-jupiter]/[class:com.rox.debug.report.DebugReportWriterTest]/[method:skipsChrRamOnAChrRomBoard(java.nio.file.Path)]
negated conditional → KILLED

71

1.1
Location : write
Killed by : com.rox.debug.report.DebugReportWriterTest.[engine:junit-jupiter]/[class:com.rox.debug.report.DebugReportWriterTest]/[method:writesTheRestorableSnapshotNamedAfterTheFolder(java.nio.file.Path)]
removed call to com/rox/save/SaveFileFormat::writeLiveSnapshot → KILLED

102

1.1
Location : formatState
Killed by : com.rox.debug.report.DebugReportWriterTest.[engine:junit-jupiter]/[class:com.rox.debug.report.DebugReportWriterTest]/[method:stateSummaryCoversCpuInstructionsPpuAndMapper()]
negated conditional → KILLED

107

1.1
Location : formatState
Killed by : com.rox.debug.report.DebugReportWriterTest.[engine:junit-jupiter]/[class:com.rox.debug.report.DebugReportWriterTest]/[method:stateSummaryShowsTheOtherHalfOfEveryFlagAndToggle()]
negated conditional → KILLED

113

1.1
Location : formatState
Killed by : com.rox.debug.report.DebugReportWriterTest.[engine:junit-jupiter]/[class:com.rox.debug.report.DebugReportWriterTest]/[method:stateSummaryCoversCpuInstructionsPpuAndMapper()]
replaced return value with "" for com/rox/debug/report/DebugReportWriter::formatState → KILLED

117

1.1
Location : yesNo
Killed by : com.rox.debug.report.DebugReportWriterTest.[engine:junit-jupiter]/[class:com.rox.debug.report.DebugReportWriterTest]/[method:stateSummaryCoversCpuInstructionsPpuAndMapper()]
negated conditional → KILLED

2.2
Location : yesNo
Killed by : com.rox.debug.report.DebugReportWriterTest.[engine:junit-jupiter]/[class:com.rox.debug.report.DebugReportWriterTest]/[method:stateSummaryCoversCpuInstructionsPpuAndMapper()]
replaced return value with "" for com/rox/debug/report/DebugReportWriter::yesNo → KILLED

122

1.1
Location : hexDump
Killed by : com.rox.debug.report.DebugReportWriterTest.[engine:junit-jupiter]/[class:com.rox.debug.report.DebugReportWriterTest]/[method:hexDumpPrefixesEveryRowWithItsAddressAndHandlesAShortLastRow()]
replaced return value with "" for com/rox/debug/report/DebugReportWriter::hexDump → KILLED

2.2
Location : lambda$hexDump$0
Killed by : com.rox.debug.report.DebugReportWriterTest.[engine:junit-jupiter]/[class:com.rox.debug.report.DebugReportWriterTest]/[method:hexDumpPrefixesEveryRowWithItsAddressAndHandlesAShortLastRow()]
replaced int return with 0 for com/rox/debug/report/DebugReportWriter::lambda$hexDump$0 → KILLED

3.3
Location : hexDump
Killed by : com.rox.debug.report.DebugReportWriterTest.[engine:junit-jupiter]/[class:com.rox.debug.report.DebugReportWriterTest]/[method:hexDumpPrefixesEveryRowWithItsAddressAndHandlesAShortLastRow()]
Replaced integer subtraction with addition → KILLED

127

1.1
Location : writePng
Killed by : com.rox.debug.report.DebugReportWriterTest.[engine:junit-jupiter]/[class:com.rox.debug.report.DebugReportWriterTest]/[method:writesBothFramesAsPngs(java.nio.file.Path)]
removed call to java/awt/image/BufferedImage::setRGB → KILLED

Active mutators

Tests examined


Report generated by PIT 1.22.1