unit TestDisAss;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, fpcunit, testutils, testregistry, LCLProc,
  GDBMIDebugger, Debugger, DebugManager, maps;

type
  TTestDisAssRegion = record
    FirstAddr, LastAddr: TDBGPtr;
    FirstOutAddr, LastOutAddr: TDBGPtr; // continue output to last_out addr
    FuncName: String;
    FileName: String;
    BaseLine, InstrPerLine: Integer;
    InstrLen: Integer
  end;

  { TTestBrkGDBMIDebugger }

  TTestBrkGDBMIDebugger = class(TGDBMIDebugger)
  protected
    FTestCmdLine: String;
    procedure SendCmdLn(const ACommand: String); override;  overload;
    function  ReadLine(const APeek: Boolean; ATimeOut: Integer = - 1): String; override; overload;
    function CreateDebugProcess(const AOptions: String): Boolean; override;
    function GetDebugProcessRunning: Boolean; override;
  protected
    function TestRespodDisass(AStartAddr, AEndAddr: TDBGPtr; AWithSrc: Boolean): String;
    function TestMemDump(AStartAddr: TDBGPtr; AWSize, ARowCnt, AColCnt: Integer): String;
  public
    TestDisAssRegions: array of TTestDisAssRegion;
    TestDefaultRegion: TTestDisAssRegion;
    TestFailMemDump: Boolean;
    TestIsFailed: Boolean;
    procedure TestSetState(const AValue: TDBGState);
  end;

  { TTestDisAss }

  TTestDisAss = class(TTestCase)
  protected
    FCallStack: TCallStackMonitor;
    FExceptions: TIDEExceptions;
    FSignals: TIDESignals;
    //FBreakPoints: TIDEBreakPoints;
    //FBreakPointGroups: TIDEBreakPointGroups;
    FLocals: TLocalsMonitor;
    FLineInfo: TIDELineInfo;
    FWatches: TWatchesMonitor;
    FThreads: TThreadsMonitor;
    FRegisters: TIDERegisters;
  published
    procedure RangeMap;
    procedure Disassemble;
  end;

implementation

{ TTestBrkGDBMIDebugger }

procedure TTestBrkGDBMIDebugger.SendCmdLn(const ACommand: String);
begin
  debugln(['############', ACommand]);
  FTestCmdLine := ACommand;
end;

function TTestBrkGDBMIDebugger.ReadLine(const APeek: Boolean; ATimeOut: Integer): String;
  procedure SkipSpaces(var pos: Integer);
  begin
    while (pos <= length(FTestCmdLine)) and (FTestCmdLine[pos] = ' ') do inc(pos);
  end;
  function CheckString(var pos: Integer; const txt: String; ASkipSpaces: Boolean=True): Boolean;
  begin
    Result := copy(FTestCmdLine, pos, length(txt)) = txt;
    if Result then inc(pos, length(txt));
    if Result and ASkipSpaces then SkipSpaces(pos);
  end;
  function GetNum(var pos: Integer; out num: Integer; ASkipSpaces: Boolean=True): Boolean;
  var p1: Integer;
  begin
    p1 := pos;
    if not (FTestCmdLine[pos] in ['0'..'9']) then exit(False);
    while (pos <= length(FTestCmdLine)) and (FTestCmdLine[pos] in ['0'..'9']) do inc(pos);
    if (pos <= length(FTestCmdLine)) and not (FTestCmdLine[pos] in [' ']) then exit(False);
    num := StrToIntDef(copy(FTestCmdLine, p1, pos-p1), -1);
    if ASkipSpaces then SkipSpaces(pos);
    Result := True;
  end;


var
  i,j : Integer;
  StartAddr, EndAddr, WSize, RowCnt, ColCnt: Integer;
  WithSrc: Boolean;
begin
  if FTestCmdLine = '' then exit('(gdb) ');

  i := 1;
  if CheckString(i, '-data-disassemble ')
  then if CheckString(i, '-s')
  then if GetNum(i, StartAddr)
  then if CheckString(i, '-e')
  then if GetNum(i, EndAddr)
  then begin
    WithSrc := False;
    if CheckString(i, '--')
    then begin
      WithSrc := GetNum(i, j);
      WithSrc := j = 1;
    end;
debugln([StartAddr]);
debugln([EndAddr]);
    FTestCmdLine := '';
    Result := TestRespodDisass(StartAddr, EndAddr, WithSrc);
    debugln(['###>>>',Result,'###<<<']);
    exit;
  end;

  // -data-read-memory 65608 x 1 1 389
  i:=1;
  if CheckString(i, '-data-read-memory ')
  then if GetNum(i, StartAddr)
  then if CheckString(i, 'x') // only hex supported
  then if GetNum(i, WSize) // word-size: 1=byte
  then if GetNum(i, RowCnt) // rows
  then if GetNum(i, ColCnt) // col
  then begin
    FTestCmdLine := '';
    if TestFailMemDump then exit('(gdb) ');
    Result := TestMemDump(StartAddr, WSize, RowCnt, ColCnt);
    debugln(['###>>>',Result,'###<<<']);
    exit;
  end;


  TestIsFailed := True; // unknow command
  FTestCmdLine := '';
  Result := '';
end;

function TTestBrkGDBMIDebugger.CreateDebugProcess(const AOptions: String): Boolean;
begin
  Result := True;
end;

function TTestBrkGDBMIDebugger.GetDebugProcessRunning: Boolean;
begin
  Result := True;
end;

function TTestBrkGDBMIDebugger.TestRespodDisass(AStartAddr, AEndAddr: TDBGPtr;
  AWithSrc: Boolean): String;

  function GetDisAssRegion(AnAddr: TDBGPtr): TTestDisAssRegion;
  var
    i: Integer;
  begin
    Result := TestDefaultRegion;
    for i := 0 to high(TestDisAssRegions) do
      if (AnAddr >= TestDisAssRegions[i].FirstAddr) and
         (AnAddr <= TestDisAssRegions[i].LastAddr)
      then begin
        Result := (TestDisAssRegions[i]);
        break;
      end;
    if Result.InstrLen < 1 then Result.InstrLen := 4;
    if Result.InstrPerLine < 1 then Result.InstrPerLine := 8;
  end;

  function GetMIInstr(AnAddr: TDBGPtr; ARgn: TTestDisAssRegion): String;
  begin
    Result := Format('{address="0x%x",', [AnAddr]);
    if ARgn.FuncName <> '' then
      Result := Result + Format('func-name="%s",offset="%d",', [ARgn.FuncName, AnAddr -ARgn.FirstAddr]);
    Result := Result + 'inst="nop"}'
  end;

var
  CurAddr: TDBGPtr;
  Rgn: TTestDisAssRegion;
  CurLine, lc: Integer;
begin
  Result := '';
  Rgn := GetDisAssRegion(AStartAddr);
  if (Rgn.FileName = '') then AWithSrc := False;
  if Rgn.FirstOutAddr <> 0 then AStartAddr := Rgn.FirstOutAddr;
  if (Rgn.LastOutAddr <> 0) and (AEndAddr < Rgn.LastOutAddr) then AEndAddr := Rgn.LastOutAddr;

  CurAddr := AStartAddr;
  CurLine := Rgn.BaseLine + (CurAddr - Rgn.FirstAddr) div Rgn.InstrPerLine;
  Result := '^done,asm_insns=[';
  lc := 0;
  if AWithSrc then begin
    Result := Result + Format('src_and_asm_line={line="%d",file="%s",line_asm_insn=[ ',
                              [CurLine, Rgn.FileName]);
  end;

  while CurAddr <= AEndAddr do begin;
    if AWithSrc and (lc >= Rgn.InstrPerLine) then begin
      Result := Result + ']}';
      Result := Result + Format(',src_and_asm_line={line="%d",file="%s",line_asm_insn=[ ',
                                [CurLine, Rgn.FileName]);
      lc := 0;
    end;
    inc(lc);

    if CurAddr > Rgn.LastOutAddr then begin
      Rgn := GetDisAssRegion(CurAddr);
    end;
    if (Result <> '') and not (Result[length(Result)] in ['{', '[']) then Result := Result + ',';;
    Result := Result + GetMIInstr(CurAddr, Rgn);
    CurAddr := CurAddr + Rgn.InstrLen;
  end;

  if AWithSrc then
    Result := Result + ']}';
  Result := Result + ']';//+LineEnding;
end;

function TTestBrkGDBMIDebugger.TestMemDump(AStartAddr: TDBGPtr; AWSize, ARowCnt,
  AColCnt: Integer): String;
var
  i: Integer;
begin
  // only supports single row
  Result := Format('^done,'
                  +'addr="0x%x",nr-bytes="%d",total-bytes="%d",'
                  +'next-row="0x%x",prev-row="0x%x",'
                  +'next-page="0x%x",prev-page="0x%x",'
                  +'memory=[{addr="0x%x",data=["0x00"', // first entry
                  [AStartAddr, AColCnt, AColCnt,
                   AStartAddr+AColCnt, AStartAddr-AColCnt,
                   AStartAddr+AColCnt, AStartAddr-AColCnt,
                   AStartAddr
                  ]);
  for i := 2 to AColCnt do
    Result := Result + ',"0x00"';
  Result := Result + ']}]';//+LineEnding;
end;

procedure TTestBrkGDBMIDebugger.TestSetState(const AValue: TDBGState);
begin
  SetState(AValue);
end;

procedure TTestDisAss.RangeMap;
var Errors: String;

  function AddRangeItem(ARange: TDBGDisassemblerEntryRange; AnAddr: TDBGPtr): TDisassemblerEntry;
  begin
    Result.Addr := AnAddr;
    ARange.Append(@Result);
  end;

  function CreateRange(AFirst, ALast, AItemEnd: TDBGPtr; AList: Array of TDBGPtr): TDBGDisassemblerEntryRange;
  var
    i: Integer;
  begin
    Result := TDBGDisassemblerEntryRange.Create;
    Result.RangeStartAddr   := AFirst;
    Result.RangeEndAddr     := ALast;
    Result.LastEntryEndAddr := AItemEnd;
    for i := low(AList) to high(AList) do
      AddRangeItem(Result, AList[i]);
  end;

  procedure TestValue(AName: String; AExp, AGot: String); overload;
  begin
    if AExp <> AGot then Errors := Format('%s%s: ExP: %s Got %s%s', [Errors, AName, AExp, AGot, LineEnding]);;
  end;
  procedure TestValue(AName: String; AExp, AGot: TDBGPtr); overload;
  begin
    if AExp <> AGot then Errors := Format('%s%s: ExP: %d Got %d%s', [Errors, AName, AExp, AGot, LineEnding]);;
  end;
  procedure TestRange(AName: String; ARange: TDBGDisassemblerEntryRange; AFirst, ALast, AItemEnd: TDBGPtr; AList: Array of TDBGPtr);
  var
    i: Integer;
  begin
    try
      if ARange = nil then begin
        Errors := Format('%s%s: No Range found%s', [Errors, AName, LineEnding]);
      end;
      TestValue(AName + 'RangeStartAddr',   ARange.RangeStartAddr, AFirst);
      TestValue(AName + 'RangeEndAddr',     ARange.RangeEndAddr, ALast);
      TestValue(AName + 'LastEntryEndAddr', ARange.LastEntryEndAddr, AItemEnd);
      TestValue(AName + 'Count',            ARange.Count, high(AList)-low(AList)+1);
        for i := 0 to ARange.Count-1 do debugln([i,': ',ARange.EntriesPtr[i]^.Addr]);
      for i := low(AList) to high(AList) do
        TestValue(AName + 'Item '+IntToStr(i), ARange.EntriesPtr[i]^.Addr, AList[i]);
    except
      on e: exception do Errors := Format('%s%s: Exception: %s %s%s', [Errors, AName, e.ClassName, e.Message, LineEnding]);
    end;
  end;

var
  EntryRanges: TDBGDisassemblerEntryMap;
  Range: TDBGDisassemblerEntryRange;
begin
  //TDBGDisassemblerEntryMapIterator
  EntryRanges := TDBGDisassemblerEntryMap.Create(itu8, SizeOf(TDBGDisassemblerEntryRange));

  EntryRanges.AddRange(CreateRange(100, 120, 120, [100, 108, 116]));
  TestValue('Map Count', 1, EntryRanges.Count);
  Range := EntryRanges.GetRangeForAddr(100, False);
  TestRange('R1', Range, 100, 120, 120, [100, 108, 116]);

  EntryRanges.AddRange(CreateRange(120, 140, 140, [120, 128, 136]));
  TestValue('Map Count (merged to end)', 1, EntryRanges.Count);
  Range := EntryRanges.GetRangeForAddr(100, False);
  TestRange('R1 (merged to end)', Range, 100, 140, 140, [100, 108, 116, 120, 128, 136]);

  EntryRanges.AddRange(CreateRange(80, 100, 100, [80, 90]));
  TestValue('Map Count (merged to start)', 1, EntryRanges.Count);
  Range := EntryRanges.GetRangeForAddr(100, False);
  TestRange('R1 (merged to end)', Range, 80, 140, 140, [80, 90, 100, 108, 116, 120, 128, 136]);

  FreeAndNil(EntryRanges);

  AssertEquals('', Errors);
end;

procedure TTestDisAss.Disassemble;
var
  IdeDisAss: TIDEDisassembler;
  Gdb: TTestBrkGDBMIDebugger;

  procedure Init;
  begin
    FreeAndNil(IdeDisAss);
    FreeAndNil(Gdb);
    Gdb := TTestBrkGDBMIDebugger.Create('');
    IdeDisAss := TIDEDisassembler.Create;
    IdeDisAss.Master := Gdb.Disassembler;

    FWatches := TWatchesMonitor.Create;
    FThreads := TThreadsMonitor.Create;
    FExceptions := TIDEExceptions.Create;
    FSignals := TIDESignals.Create;
    FLocals := TLocalsMonitor.Create;
    FLineInfo := TIDELineInfo.Create;
    FCallStack := TCallStackMonitor.Create;
    FRegisters := TIDERegisters.Create;

    //TManagedBreakpoints(FBreakpoints).Master := FDebugger.BreakPoints;
    FWatches.Supplier := Gdb.Watches;
    FThreads.Supplier := Gdb.Threads;
    FLocals.Supplier := Gdb.Locals;
    FLineInfo.Master := Gdb.LineInfo;
    FCallStack.Supplier := Gdb.CallStack;
    FExceptions.Master := Gdb.Exceptions;
    FSignals.Master := Gdb.Signals;
    FRegisters.Master := Gdb.Registers;

    Gdb.TestSetState(dsPause);
    Gdb.TestIsFailed := False;;
    Gdb.TestFailMemDump := False;

  end;
  procedure CleanGdb;
  begin
    FreeAndNil(Gdb);
    FWatches.Supplier := nil;
    FThreads.Supplier := nil;
    //FLocals.Master := nil;
    //FLineInfo.Master := nil;
    FCallStack.Supplier := nil;
    //FExceptions.Master := nil;
    //FSignals.Master := nil;
    //FRegisters.Master := nil;

    FreeAndNil(FWatches);
    FreeAndNil(FThreads);
    //FreeAndNil(FBreakPoints);
    //FreeAndNil(FBreakPointGroups);
    FreeAndNil(FCallStack);
    FreeAndNil(FExceptions);
    FreeAndNil(FSignals);
    FreeAndNil(FLocals);
    FreeAndNil(FLineInfo);
    FreeAndNil(FRegisters);
  end;

  procedure Test(Name: String;Addr: TDBGPtr; MinBefore, MinAfter: Integer);
  var
    t: String;
    a, b: TDBGPtr;
    i: Integer;
  begin
    t := '';
    if Gdb.TestIsFailed then t := t + ' - Unknown Command';
    if IdeDisAss.BaseAddr <> Addr then t := t + Format(' - BaseAddr, Exp %x, got %x', [Addr, IdeDisAss.BaseAddr]);
    if IdeDisAss.CountBefore < MinBefore then t := t + Format(' - CountBefore, Exp %d, got %d', [MinBefore, IdeDisAss.CountBefore]);
    if IdeDisAss.CountAfter < MinAfter then t := t + Format(' - CountAfter, Exp %d, got %d', [MinAfter, IdeDisAss.CountAfter]);

    if IdeDisAss.Entries[0].Addr <> Addr then t := t + Format(' - Entries[0].Addr, Exp %x, got %x', [Addr, IdeDisAss.Entries[0].Addr]);

    a := IdeDisAss.Entries[-IdeDisAss.CountBefore].Addr;
    for i := -IdeDisAss.CountBefore + 1 to IdeDisAss.CountAfter - 1 do begin
      b := IdeDisAss.Entries[i].Addr;
      if b <= a then t := t + Format(' - Entries[%d].Addr went back, Exp greater %x, got %x', [i, a, b]);
      a := b;
    end;;

    if t <> '' then t := Name+LineEnding+t;
    AssertEquals('', t);
  end;

  procedure TestSrc(Name: String; StartAddr, EndAddr: TDBGPtr);
  var
    t: String;
    b: TDBGPtr;
    i: Integer;
    itm: TDisassemblerEntry;
  begin
    t := '';
    for i := -IdeDisAss.CountBefore to IdeDisAss.CountAfter - 1 do begin
      itm := IdeDisAss.Entries[i];
      b := itm.Addr;
      if (b >= StartAddr) and (b <= EndAddr) and
         not(itm.SrcFileName <> '')
      then t := t + Format(' - Entries[%d] Addr(%x) has no source', [i, b]);
    end;;

    if t <> '' then t := Name+LineEnding+t;
    AssertEquals('', t);
  end;

begin
  Gdb := nil; IdeDisAss := nil;

  {%region NO SOURCE}
    //{%region simple block}
    //Init;
    //IdeDisAss.PrepareRange($20100, 10, 20);
    //Test('no src, no offset, 1 block', $20100, 10, 19);
    //{%endregion}

    //{%region multi simple block}
    //Init;
    //SetLength(Gdb.TestDisAssRegions, 2);
    //with Gdb.TestDisAssRegions[0] do begin
    //  FirstAddr := $30100-184; LastAddr := $30100-25; InstrLen := 8;
    //  FuncName := 'abc';
    //end;
    //with Gdb.TestDisAssRegions[1] do begin
    //  FirstAddr := $30100-24; LastAddr := $30100+200; InstrLen := 8;
    //  FuncName := 'def';
    //end;
    //IdeDisAss.PrepareRange($30100, 10, 20);
    //Test('no src, multi block', $30100, 10, 19);
    //{%endregion}

    {%region multi simple block, overlap}
    // The first block disassembles 4 bytes into the 2nd
    // The 2nd block must be re-done
    Init;
    SetLength(Gdb.TestDisAssRegions, 2);
    with Gdb.TestDisAssRegions[0] do begin
      FirstAddr := $30100-180; LastAddr := $30100-20; InstrLen := 8;
      FuncName := 'abc';
    end;
    with Gdb.TestDisAssRegions[1] do begin
      FirstAddr := $30100-24; LastAddr := $30100+200; InstrLen := 8;
      FuncName := 'def';
    end;
    IdeDisAss.PrepareRange($30100, 10, 20);
    Test('no src, multi block, overlap of 4', $30100, 10, 19);
    CleanGdb;
    {%endregion}
  {%endregion NO SOURCE}


    if false then begin
  {%region simple block, src}
  Init;
  SetLength(Gdb.TestDisAssRegions, 1);
  with Gdb.TestDisAssRegions[0] do begin
    FirstAddr := $30100-400; LastAddr := $30100+400; InstrLen := 8;
    FuncName := 'abc';       FileName := 'foo.pp';   InstrPerLine := 5;
  end;
  IdeDisAss.PrepareRange($30100, 10, 20);
  Test('src, 1 block', $30100, 10, 19);
  TestSrc('src, 1 block', $30100-400, $30100+400);
  CleanGdb;
  {%endregion}

  {%region 2 block, part src}
  Init;
  SetLength(Gdb.TestDisAssRegions, 2);
  with Gdb.TestDisAssRegions[0] do begin
    FirstAddr := $30100-400; LastAddr := $30100-9; InstrLen := 8;
    FuncName := 'abc';
  end;
  with Gdb.TestDisAssRegions[1] do begin
    FirstAddr := $30100-8; LastAddr := $30100+400; InstrLen := 8;
    FuncName := 'def';       FileName := 'foo.pp';   InstrPerLine := 5;
  end;
  IdeDisAss.PrepareRange($30100, 10, 20);
  Test('part-src, 1 block', $30100, 10, 19);
  TestSrc('part-src, 1 block', $30100-8, $30100+400);
  CleanGdb;
  {%endregion}





  {%region fail mem dump}
  Init;
  Gdb.TestFailMemDump := True;
  IdeDisAss.PrepareRange($10100, 10, 20);
  // just enough, if it din't crash => go error state.
  CleanGdb;
  {%endregion}
end;//xxxxxxxxxxxx
  FreeAndNil(IdeDisAss);
  FreeAndNil(Gdb);
end;



initialization

  RegisterTest(TTestDisAss); 
end.

