Skip to content

Commit da22f2c

Browse files
authored
fix: Tests should not leave resources behind. (#2176)
1 parent 472a995 commit da22f2c

147 files changed

Lines changed: 3110 additions & 3704 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/copilot-instructions.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,12 @@ dotnet build
4040
# Run with executable
4141
dotnet run --project src/Spice86 -- -e path\to\program.exe
4242
43-
# Run tests
44-
dotnet test tests/Spice86.Tests
43+
# Run tests (excluding SingleStepTest - see note below)
44+
dotnet test tests/Spice86.Tests --filter "FullyQualifiedName!~SingleStepTest"
4545
```
4646

47+
> **SingleStepTest exclusion rule**: `SingleStepTest` runs millions of CPU instruction test cases and take an extremely long time to complete. **Always exclude it** using `--filter "FullyQualifiedName!~SingleStepTest"` unless the change being tested directly touches CPU instruction decoding, execution, or flag handling (e.g. changes to `CfgCpu`, instruction parsers, ALU operations, or flag computation). When in doubt, exclude it.
48+
4749
### Debugging Workflow
4850
- **GDB Integration**: Server runs on port 10000 by default (`--GdbPort 10000`)
4951
- Use `--Debug` to pause at startup for breakpoint setup
@@ -158,7 +160,6 @@ Variants: `MemoryBasedDataStructureWithCsBaseAddress`, `MemoryBasedDataStructure
158160
}
159161
```
160162
- **NEVER use generic `catch (Exception)`, `catch (Exception e)`, or empty `catch`**
161-
- Never use SegmentedAddress.Linear for address computations. Segmented addresses can rollover and Linear doesn't handle this correctly.
162163
- Each exception type must be caught explicitly
163164
- This is non-negotiable - the .editorconfig enforces this rule
164165

@@ -242,7 +243,7 @@ Variants: `MemoryBasedDataStructureWithCsBaseAddress`, `MemoryBasedDataStructure
242243
- **Function Tracking**: `FunctionHandler` intercepts calls/rets for CFG building and override dispatch
243244

244245
## Common Gotchas
245-
- **Segmented addressing**: Use `SegmentedAddress` not raw offsets; linear address = segment * 16 + offset
246+
- **Segmented addressing**: Use `SegmentedAddress` not raw offsets; linear address = segment * 16 + offset. Avoid computing addresses from `SegmentedAddress.Linear` because segmented addresses can roll over and `.Linear` does not handle this correctly. Use the `IMmu` (`TranslateAddress`) for address translation instead, as it handles rollover and access validation. Passing `.Linear` to an API that already expects a linear address is fine.
246247
- **A20 Gate**: Memory wrapping at 1MB boundary controlled by `A20Gate` (toggle via `--A20Gate` flag)
247248
- **EMS/XMS**: Enabled by default; disable with `--Xms false` / `--Ems false`
248249
- **Time handling**: Real-time vs instruction-based via `--InstructionTimeScale` or `--TimeMultiplier`

src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/JccParser.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ private CfgInstruction ParseJcc(ParsingContext context, int conditionCode, BitWi
6363

6464
(int offsetValue, FieldWithValue offsetField) = ReadSignedOffset(offsetWidth);
6565

66-
CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
66+
CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1) { Kind = InstructionKind.Jump };
6767
instr.AddField(offsetField);
6868
instr.MaxSuccessorsCount = 2;
6969
ushort targetIp = (ushort)(instr.NextInMemoryAddress32.Offset + offsetValue);

src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/JcxzParser.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public JcxzParser(ParsingTools parsingTools) : base(parsingTools) {
1818

1919
public CfgInstruction Parse(ParsingContext context) {
2020
(int offsetValue, FieldWithValue offsetField) = ReadSignedOffset(BitWidth.BYTE_8);
21-
CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
21+
CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1) { Kind = InstructionKind.Jump };
2222
instr.AddField(offsetField);
2323
instr.MaxSuccessorsCount = 2;
2424
ushort targetIp = (ushort)(instr.NextInMemoryAddress32.Offset + offsetValue);

src/Spice86.Core/Emulator/CPU/CfgCpu/Parser/SpecificParsers/LoopParser.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ private CfgInstruction ParseLoop(ParsingContext context, InstructionOperation di
3131
BitWidth addressWidth = context.AddressWidthFromPrefixes;
3232
(int offsetValue, FieldWithValue offsetField) = ReadSignedOffset(BitWidth.BYTE_8);
3333

34-
CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1);
34+
CfgInstruction instr = new(context.Address, context.OpcodeField, context.Prefixes, 1) { Kind = InstructionKind.Jump };
3535
instr.AddField(offsetField);
3636
instr.MaxSuccessorsCount = 2;
3737

src/Spice86.Core/Emulator/Gdb/GdbCommandBreakPointHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public GdbCommandBreakpointHandler(
3535
EmulatorBreakpointsManager emulatorBreakpointsManager,
3636
IPauseHandler pauseHandler, GdbIo gdbIo, ILoggerService loggerService,
3737
CPU.State state, IMemory memory) {
38-
_loggerService = loggerService.WithLogLevel(LogEventLevel.Verbose);
38+
_loggerService = loggerService;
3939
_emulatorBreakpointsManager = emulatorBreakpointsManager;
4040
_pauseHandler = pauseHandler;
4141
_pauseHandler.Paused += OnPauseFromEmulator;

src/Spice86.Core/Emulator/Gdb/GdbIo.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public sealed class GdbIo : IDisposable {
2727
/// <param name="port">The port number to listen on.</param>
2828
/// <param name="loggerService">The logger service implementation.</param>
2929
public GdbIo(int port, ILoggerService loggerService) {
30-
_loggerService = loggerService.WithLogLevel(LogEventLevel.Debug);
30+
_loggerService = loggerService;
3131
// Listen to connections in IPv4 or IPv6
3232
_tcpListener = new TcpListener(IPAddress.IPv6Any, port);
3333
_tcpListener.Server.DualMode = true;

src/Spice86.Core/Emulator/Memory/MemoryMap.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,4 @@ public static class MemoryMap {
7979
/// <see cref="VirtualDeviceBase"/>
8080
/// </summary>
8181
public const ushort DeviceDriversSegment = 0xF800;
82-
83-
/// <summary>
84-
/// Segment for DOS Current Directory Structure (CDS).
85-
/// </summary>
86-
public const ushort DosCdsSegment = 0x108;
8782
}

src/Spice86.Core/Emulator/OperatingSystem/Dos.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,8 @@ public Dos(Configuration configuration, IMemory memory,
193193
MemoryUtils.ToPhysicalAddress(DosSysVars.Segment, 0x0));
194194

195195
DosSysVars.ConsoleDeviceHeaderPointer = ((IVirtualDevice)dosDevices[1]).Header.BaseAddress;
196-
DosSysVars.CurrentDirectoryStructureListPointer = DosTables.CurrentDirectoryStructure.BaseAddress;
196+
SegmentedAddress cdsAddress = DosTables.CdsSegmentedAddress;
197+
DosSysVars.CurrentDirectoryStructureListPointer = (uint)(cdsAddress.Segment << 16 | cdsAddress.Offset);
197198
DosSysVars.CurrentDirectoryStructureCount = 26;
198199

199200
DosSwappableDataArea = new(_memory,

src/Spice86.Core/Emulator/OperatingSystem/DosPathResolver.cs

Lines changed: 46 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -286,46 +286,12 @@ internal DosPathBuilderResult GetFullDosPathIncludingRoot(string? dosPath, out s
286286
/// <param name="dosPath">The DOS path to convert.</param>
287287
/// <returns>A string containing the full file path in the host file system, or <see langword="null"/> if nothing was found or the DOS path cannot be resolved.</returns>
288288
public string? GetFullHostPathFromDosOrDefault(string? dosPath) {
289-
if (string.IsNullOrWhiteSpace(dosPath)) {
290-
return null;
291-
}
292-
293-
dosPath = GetFullDosPathIncludingRoot(dosPath);
294-
if (dosPath is null) {
295-
return null;
296-
}
297-
298-
(string? hostPrefix, string dosRelativePath) = DeconstructDosPath(dosPath);
299-
if (hostPrefix is null) {
300-
return null;
301-
}
302-
303-
if (string.IsNullOrWhiteSpace(dosRelativePath)) {
304-
return ConvertUtils.ToSlashPath(hostPrefix);
305-
}
306-
307-
string slashedRelative = ConvertUtils.ToSlashPath(dosRelativePath);
308-
int lastSlash = slashedRelative.LastIndexOf('/');
309-
string dirPart = lastSlash >= 0 ? slashedRelative[..lastSlash] : string.Empty;
310-
string lastSegment = lastSlash >= 0 ? slashedRelative[(lastSlash + 1)..] : slashedRelative;
311-
312-
string? resolvedHostDir = ResolveCaseInsensitiveDirectory(hostPrefix, dirPart);
313-
if (string.IsNullOrWhiteSpace(resolvedHostDir)) {
289+
(string resolvedHostDir, string lastSegment)? components = ResolveDosPathComponents(dosPath);
290+
if (components is null) {
314291
return null;
315292
}
316293

317-
if (string.IsNullOrWhiteSpace(lastSegment)) {
318-
return ConvertUtils.ToSlashPath(resolvedHostDir);
319-
}
320-
321-
EnumerationOptions options = new EnumerationOptions {
322-
RecurseSubdirectories = false,
323-
MatchCasing = MatchCasing.CaseInsensitive,
324-
ReturnSpecialDirectories = false
325-
};
326-
327-
string? firstMatch = FindFilesUsingWildCmp(resolvedHostDir, lastSegment, options).FirstOrDefault();
328-
return string.IsNullOrWhiteSpace(firstMatch) ? null : ConvertUtils.ToSlashPath(firstMatch);
294+
return ResolveFileInDirectory(components.Value.resolvedHostDir, components.Value.lastSegment);
329295
}
330296

331297
/// <summary>
@@ -377,6 +343,29 @@ internal DosPathBuilderResult GetFullDosPathIncludingRoot(string? dosPath, out s
377343
/// <param name="dosPath">The DOS path to convert.</param>
378344
/// <returns>A string containing the full file path in the host file system, or <see langword="null"/> if nothing was found or the DOS path cannot be resolved.</returns>
379345
public string? GetFullHostExecutablePathFromDosOrDefault(string? dosPath) {
346+
(string resolvedHostDir, string lastSegment)? components = ResolveDosPathComponents(dosPath);
347+
if (components is null) {
348+
return null;
349+
}
350+
351+
string resolvedHostDir = components.Value.resolvedHostDir;
352+
string lastSegment = components.Value.lastSegment;
353+
354+
string? result = ResolveFileInDirectory(resolvedHostDir, lastSegment);
355+
if (result is not null) {
356+
return result;
357+
}
358+
359+
string? extensionProbeMatch = TryResolveExecutableWithoutExtension(resolvedHostDir, lastSegment);
360+
return string.IsNullOrWhiteSpace(extensionProbeMatch) ? null : ConvertUtils.ToSlashPath(extensionProbeMatch);
361+
}
362+
363+
/// <summary>
364+
/// Resolves the DOS path into its host directory and filename components.
365+
/// Returns <see langword="null"/> when the path cannot be resolved (invalid, empty, or missing directory).
366+
/// When the path refers to a directory (no filename), <c>lastSegment</c> is empty.
367+
/// </summary>
368+
private (string resolvedHostDir, string lastSegment)? ResolveDosPathComponents(string? dosPath) {
380369
if (string.IsNullOrWhiteSpace(dosPath)) {
381370
return null;
382371
}
@@ -392,7 +381,7 @@ internal DosPathBuilderResult GetFullDosPathIncludingRoot(string? dosPath, out s
392381
}
393382

394383
if (string.IsNullOrWhiteSpace(dosRelativePath)) {
395-
return ConvertUtils.ToSlashPath(hostPrefix);
384+
return (ConvertUtils.ToSlashPath(hostPrefix), string.Empty);
396385
}
397386

398387
string slashedRelative = ConvertUtils.ToSlashPath(dosRelativePath);
@@ -405,6 +394,14 @@ internal DosPathBuilderResult GetFullDosPathIncludingRoot(string? dosPath, out s
405394
return null;
406395
}
407396

397+
return (resolvedHostDir, lastSegment);
398+
}
399+
400+
/// <summary>
401+
/// Resolves a filename within a host directory, trying an exact case-insensitive match first
402+
/// to avoid 8.3 truncation false positives, then falling back to DOS wildcard comparison.
403+
/// </summary>
404+
private string? ResolveFileInDirectory(string resolvedHostDir, string lastSegment) {
408405
if (string.IsNullOrWhiteSpace(lastSegment)) {
409406
return ConvertUtils.ToSlashPath(resolvedHostDir);
410407
}
@@ -415,16 +412,20 @@ internal DosPathBuilderResult GetFullDosPathIncludingRoot(string? dosPath, out s
415412
ReturnSpecialDirectories = false
416413
};
417414

418-
string? firstMatch = FindFilesUsingWildCmp(resolvedHostDir, lastSegment, options).FirstOrDefault();
419-
if (!string.IsNullOrWhiteSpace(firstMatch)) {
420-
return ConvertUtils.ToSlashPath(firstMatch);
415+
// Try exact case-insensitive match first to avoid 8.3 truncation false positives
416+
// (e.g. bios_int70_wait.com and bios_int1a.com both truncating to BIOS_INT.COM).
417+
string? exactMatch = Directory
418+
.EnumerateFileSystemEntries(resolvedHostDir, lastSegment, options)
419+
.FirstOrDefault();
420+
if (!string.IsNullOrWhiteSpace(exactMatch)) {
421+
return ConvertUtils.ToSlashPath(exactMatch);
421422
}
422423

423-
string? extensionProbeMatch = TryResolveExecutableWithoutExtension(resolvedHostDir, lastSegment, options);
424-
return string.IsNullOrWhiteSpace(extensionProbeMatch) ? null : ConvertUtils.ToSlashPath(extensionProbeMatch);
424+
string? firstMatch = FindFilesUsingWildCmp(resolvedHostDir, lastSegment, options).FirstOrDefault();
425+
return string.IsNullOrWhiteSpace(firstMatch) ? null : ConvertUtils.ToSlashPath(firstMatch);
425426
}
426427

427-
private string? TryResolveExecutableWithoutExtension(string resolvedHostDir, string lastSegment, EnumerationOptions options) {
428+
private string? TryResolveExecutableWithoutExtension(string resolvedHostDir, string lastSegment) {
428429
if (string.IsNullOrWhiteSpace(lastSegment)) {
429430
return null;
430431
}
@@ -434,7 +435,7 @@ internal DosPathBuilderResult GetFullDosPathIncludingRoot(string? dosPath, out s
434435
}
435436

436437
return ExecutableExtensionLookupOrder
437-
.Select(extension => FindFilesUsingWildCmp(resolvedHostDir, $"{lastSegment}{extension}", options).FirstOrDefault())
438+
.Select(extension => ResolveFileInDirectory(resolvedHostDir, $"{lastSegment}{extension}"))
438439
.FirstOrDefault(match => !string.IsNullOrWhiteSpace(match));
439440
}
440441

src/Spice86.Core/Emulator/OperatingSystem/Structures/DosTables.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ namespace Spice86.Core.Emulator.OperatingSystem.Structures;
22

33
using Spice86.Core.Emulator.Memory;
44
using Spice86.Core.Emulator.Memory.ReaderWriter;
5+
using Spice86.Core.Emulator.OperatingSystem;
56
using Spice86.Shared.Emulator.Memory;
67
using Spice86.Shared.Utils;
78

@@ -26,6 +27,11 @@ public class DosTables {
2627
/// </summary>
2728
public CurrentDirectoryStructure CurrentDirectoryStructure { get; private set; }
2829

30+
/// <summary>
31+
/// Gets the segmented address where the CDS is allocated.
32+
/// </summary>
33+
public SegmentedAddress CdsSegmentedAddress { get; private set; }
34+
2935
/// <summary>
3036
/// Gets the Double Byte Character Set (DBCS) lead-byte table.
3137
/// </summary>
@@ -36,8 +42,13 @@ public class DosTables {
3642
/// </summary>
3743
/// <param name="memory">The memory interface to write structures to.</param>
3844
public DosTables(IByteReaderWriter memory) {
39-
uint cdsAddress = MemoryUtils.ToPhysicalAddress(MemoryMap.DosCdsSegment, 0);
40-
CurrentDirectoryStructure = new CurrentDirectoryStructure(memory, cdsAddress);
45+
// CDS needs one entry per drive (A-Z), rounded up to the nearest paragraph.
46+
const int bytesPerParagraph = 16;
47+
int cdsSizeInBytes = DosDriveManager.MaxDriveCount * CurrentDirectoryStructure.CdsEntrySize;
48+
ushort cdsParagraphs = (ushort)((cdsSizeInBytes + bytesPerParagraph - 1) / bytesPerParagraph);
49+
ushort cdsSegment = ReserveDosPrivateSegment(cdsParagraphs);
50+
CdsSegmentedAddress = new SegmentedAddress(cdsSegment, 0);
51+
CurrentDirectoryStructure = new CurrentDirectoryStructure(memory, CdsSegmentedAddress.Linear);
4152

4253
ushort currentMemorySegment = ReserveDosPrivateSegment(DosDoubleByteCharacterSet.DbcsTableSizeInParagraphs);
4354
ushort doubleByteCharacterSetSegment = (ushort)(currentMemorySegment + 1);

0 commit comments

Comments
 (0)