-- TestFTP.mesa
-- HGM,  October 18, 1980  1:21 PM  
-- MAS,  July 9, 1980  8:44 PM  

DIRECTORY
  FTPDefs USING [
    VirtualFilename, FileInfo, FtpError, Intent, FTPError, FTPInitialize,
    FTPFinalize, FTPUser, FTPCreateUser, FTPSetCredentials, FTPDestroyUser,
    FTPOpenConnection, FTPCloseConnection, FTPRenewConnection, FTPEnumerateFiles,
    FTPDeleteFile, FTPRenameFile, FTPRetrieveFile, FTPNoteFilenameUsed,
    FTPStoreFile, FTPTransferFile, FTPInventoryDumpFile, FTPBeginDumpFile,
    FTPEndDumpFile, AltoFilePrimitives, PupCommunicationPrimitives,
    FTPSetBufferSize],
  PupDefs USING [
    UseAltoChecksumMicrocode, UseNullChecksumMicrocode,
    UsePrincOpsChecksumMicrocode, UseSoftwareChecksumMicrocode],
  Inline USING [LowHalf],
  ImageDefs USING [StopMesa],
  Put USING [CR, Char, Line, LongDecimal, Text],
  OsStaticDefs USING [OsStatics],
  Runtime USING [CallDebugger],
  SegmentDefs USING [
    MemoryConfig, GetMemoryConfig, FileHandle, Write, Read, OldFileOnly,
    FileNameError, NewFile, DestroyFile, GetFileTimes, GetEndOfFile],
  StreamDefs USING [
    StreamHandle, CreateByteStream, FileLength, GetPosition, SetPosition,
    ReadBlock],
  StringDefs USING [BcplToMesaString, EquivalentStrings],
  Storage USING [Pages, FreePages],
  Window USING [Handle];

TestFTP: PROGRAM
  IMPORTS
    Inline, ImageDefs, Put, Runtime, SegmentDefs, StringDefs, StreamDefs, Storage,
    FTPDefs, PupDefs =
  BEGIN OPEN FTPDefs;

  -- Fiddle these to switch to another server
  -- There are also a few specific names used for testing access and timings
  user: STRING ← [40];
  password: STRING ← [40];
  isSapsford: BOOLEAN = IsSapsford[];

  wh: Window.Handle = NIL;

  IsSapsford: PROCEDURE RETURNS [BOOLEAN] =
    BEGIN
    StringDefs.BcplToMesaString[OsStaticDefs.OsStatics.UserName, user];
    RETURN[StringDefs.EquivalentStrings[user, "Sapsford"L]];
    END;

  defaultServer: STRING = IF isSapsford THEN "Igor" ELSE "Idun";
  mesaServer: STRING = "Iris";
  currentServer: STRING;


  remoteScratch1: STRING = "Scratch.1$";
  remoteScratch2: STRING = "Scratch.2$";
  remoteScratch3: STRING = "Scratch.3$";
  remoteScratches: STRING = "Scratch.*!*";
  localScratch: STRING = "Foo.$";
  localScratch2: STRING = "Baz.$";

  ftpuser: FTPUser;

  clock: POINTER TO INTEGER = LOOPHOLE[430B];
  msPerTick: CARDINAL = 39;
  ticksPerSecond: CARDINAL = 1000/msPerTick;
  when: INTEGER;

  StartTiming: PROCEDURE [s: STRING] = BEGIN Put.Text[wh, s]; when ← clock↑; END;

  StopTiming: PROCEDURE [s: STRING, bytes: LONG CARDINAL ← 0] =
    BEGIN
    ms: LONG CARDINAL ← LONG[clock↑ - when]*msPerTick;
    IF bytes # 0 THEN Put.LongDecimal[wh, bytes];
    Put.Text[wh, s];
    Put.Text[wh, ", "];
    Put.LongDecimal[wh, ms];
    Put.Text[wh, " ms"];
    IF bytes # 0 THEN
      BEGIN
      Put.Text[wh, ", "];
      Put.LongDecimal[wh, 8*(bytes*1000/ms)];
      Put.Text[wh, " bits/sec"];
      END;
    Put.Line[wh, "."];
    when ← clock↑;
    END;

  Pause: PROCEDURE [seconds: INTEGER] =
    BEGIN
    when ← clock↑;
    UNTIL (clock↑ - when) > (seconds*ticksPerSecond) DO ENDLOOP;
    END;

  Start: PROCEDURE [server: STRING, quiet: BOOLEAN ← FALSE] =
    BEGIN
    serverText: STRING = [100];
    currentServer ← server;
    IF ~quiet THEN
      BEGIN
      StartTiming["Opening FTP connection to "];
      Put.Text[wh, server];
      Put.Text[wh, " ..."];
      END;
    FTPInitialize[];
    ftpuser ← FTPCreateUser[AltoFilePrimitives[], PupCommunicationPrimitives[]];
    StringDefs.BcplToMesaString[OsStaticDefs.OsStatics.UserName, user];
    StringDefs.BcplToMesaString[OsStaticDefs.OsStatics.UserPassword, password];
    FTPSetCredentials[ftpuser, primary, user, password];
    FTPOpenConnection[ftpuser, server, files, serverText];
    IF ~quiet THEN BEGIN StopTiming[" ok"]; Put.Line[wh, serverText]; END;
    END;

  ReOpen: PROCEDURE =
    BEGIN
    serverText: STRING = [100];
    FTPCloseConnection[ftpuser];
    FTPOpenConnection[ftpuser, currentServer, files, serverText];
    END;

  List: PROCEDURE [remote: STRING] =
    BEGIN
    Lister: PROCEDURE [
      x: UNSPECIFIED, name: STRING, y: VirtualFilename, info: FileInfo] =
      BEGIN
      Put.Text[wh, "  "];
      Put.Text[wh, name];
      IF info.creationDate # NIL THEN
	BEGIN
	THROUGH [name.length..25) DO Put.Char[wh, ' ]; ENDLOOP;
	Put.Text[wh, info.creationDate];
	END;
      Put.CR[wh];
      END;
    StartTiming["Listing "];
    Put.Text[wh, remote];
    Put.Line[wh, " ..."];
    FTPEnumerateFiles[ftpuser, remote, enumeration, Lister, NIL];
    StopTiming["End of listing"];
    END;

  ListViaTemp: PROCEDURE [remote: STRING] =
    BEGIN
    Lister: PROCEDURE [
      x: UNSPECIFIED, name: STRING, y: VirtualFilename, info: FileInfo] =
      BEGIN
      Put.Text[wh, "  "];
      Put.Text[wh, name];
      IF info.creationDate # NIL THEN
	BEGIN
	THROUGH [name.length..25) DO Put.Char[wh, ' ]; ENDLOOP;
	Put.Text[wh, info.creationDate];
	END;
      Put.CR[wh];
      END;
    StartTiming["Listing "];
    Put.Text[wh, remote];
    Put.Line[wh, " ..."];
    FTPEnumerateFiles[ftpuser, remote, unspecified, Lister, NIL];
    StopTiming["End of listing"];
    END;

  ListDump: PROCEDURE [remote: STRING] =
    BEGIN
    Lister: PROCEDURE [x: UNSPECIFIED, name: STRING, y, z: UNSPECIFIED] =
      BEGIN Put.Text[wh, "  "]; Put.Line[wh, name]; END;
    StartTiming["Listing contents of "];
    Put.Text[wh, remote];
    Put.Line[wh, " ..."];
    FTPInventoryDumpFile[ftpuser, remote, enumeration, Lister, NIL];
    StopTiming["End of listing"];
    END;

  RetrieveStar: PROCEDURE [discard, remote: STRING] =
    BEGIN
    Snarf: PROCEDURE [
      x: UNSPECIFIED, name: STRING, y: VirtualFilename, info: FileInfo] =
      BEGIN Put.Text[wh, "  "]; Retrieve[discard, name]; END;
    Put.Text[wh, "Multiple Retrieving "];
    Put.Text[wh, remote];
    Put.Line[wh, " ..."];
    FTPEnumerateFiles[ftpuser, remote, retrieval, Snarf, NIL];
    Put.Line[wh, "End of Multiple Retrieve."];
    END;

  Retrieve: PROCEDURE [local, remote: STRING] =
    BEGIN
    bc: LONG CARDINAL;
    StartTiming[local];
    Put.Text[wh, " <= ["];
    Put.Text[wh, currentServer];
    Put.Text[wh, "]"];
    Put.Text[wh, remote];
    Put.Text[wh, " ... "];
    bc ← FTPRetrieveFile[ftpuser, local, remote, unknown];
    StopTiming[" bytes", bc];
    END;

  SingleRetrieve: PROCEDURE [server, local, remote: STRING] =
    BEGIN
    bc: LONG CARDINAL;
    user: STRING ← [40];
    password: STRING ← [40];
    serverText: STRING = [100];
    StartTiming["Single retrieve: "];
    Put.Text[wh, local];
    Put.Text[wh, " <= ["];
    Put.Text[wh, server];
    Put.Text[wh, "]"];
    Put.Text[wh, remote];
    Put.Text[wh, " ... "];
    FTPInitialize[];
    ftpuser ← FTPCreateUser[AltoFilePrimitives[], PupCommunicationPrimitives[]];
    StringDefs.BcplToMesaString[OsStaticDefs.OsStatics.UserName, user];
    StringDefs.BcplToMesaString[OsStaticDefs.OsStatics.UserPassword, password];
    FTPSetCredentials[ftpuser, primary, user, password];
    FTPOpenConnection[ftpuser, server, files, serverText];
    bc ← FTPRetrieveFile[ftpuser, local, remote, unknown];
    FTPCloseConnection[ftpuser];
    FTPDestroyUser[ftpuser];
    FTPFinalize[];
    StopTiming[" bytes", bc];
    END;

  Store: PROCEDURE [local, remote: STRING] =
    BEGIN
    bc: LONG CARDINAL;
    StartTiming[local];
    Put.Text[wh, " => ["];
    Put.Text[wh, currentServer];
    Put.Text[wh, "]"];
    Put.Text[wh, remote];
    Put.Text[wh, " ... "];
    bc ← FTPStoreFile[ftpuser, local, remote, binary];
    StopTiming[" bytes", bc];
    END;

  SingleStore: PROCEDURE [server, local, remote: STRING] =
    BEGIN
    bc: LONG CARDINAL;
    user: STRING ← [40];
    password: STRING ← [40];
    serverText: STRING = [100];
    StartTiming["Single store: "];
    StartTiming[local];
    Put.Text[wh, " => ["];
    Put.Text[wh, server];
    Put.Text[wh, "]"];
    Put.Text[wh, remote];
    Put.Text[wh, " ... "];
    FTPInitialize[];
    ftpuser ← FTPCreateUser[AltoFilePrimitives[], PupCommunicationPrimitives[]];
    StringDefs.BcplToMesaString[OsStaticDefs.OsStatics.UserName, user];
    StringDefs.BcplToMesaString[OsStaticDefs.OsStatics.UserPassword, password];
    FTPSetCredentials[ftpuser, primary, user, password];
    FTPOpenConnection[ftpuser, server, files, serverText];
    bc ← FTPStoreFile[ftpuser, local, remote, binary];
    FTPCloseConnection[ftpuser];
    FTPDestroyUser[ftpuser];
    FTPFinalize[];
    StopTiming[" bytes", bc];
    END;

  DeleteStar: PROCEDURE [remote: STRING] =
    BEGIN
    Kill: PROCEDURE [
      x: UNSPECIFIED, name: STRING, y: VirtualFilename, info: FileInfo] =
      BEGIN Put.Text[wh, "  "]; Delete[name]; END;
    Put.Text[wh, "Multiple Delete of "];
    Put.Text[wh, remote];
    Put.Line[wh, " ..."];
    FTPEnumerateFiles[ftpuser, remote, deletion, Kill, NIL];
    Put.Line[wh, "End of deleting."];
    END;

  Delete: PROCEDURE [remote: STRING] =
    BEGIN
    StartTiming["Deleting "];
    Put.Text[wh, remote];
    Put.Text[wh, " ..."];
    FTPDeleteFile[ftpuser, remote];
    StopTiming[" ok"];
    END;

  Rename: PROCEDURE [old, new: STRING] =
    BEGIN
    StartTiming["Renaming "];
    Put.Text[wh, old];
    Put.Text[wh, " to be "];
    Put.Text[wh, new];
    Put.Text[wh, " ..."];
    FTPRenameFile[ftpuser, old, new];
    StopTiming[" ok"];
    END;

  StartDumping: PROCEDURE [where: STRING] =
    BEGIN
    Put.Text[wh, "Dumping things into "];
    Put.Text[wh, where];
    Put.Text[wh, " ..."];
    FTPBeginDumpFile[ftpuser, where];
    Put.CR[wh];
    END;

  StopDumping: PROCEDURE =
    BEGIN FTPEndDumpFile[ftpuser]; Put.Line[wh, "End of Dumping."]; END;

  Load: PROCEDURE [local, remote: STRING] =
    BEGIN
    Loader: PROCEDURE [x: UNSPECIFIED, name: STRING, y, z: UNSPECIFIED] =
      BEGIN
      Put.Text[wh, "  "];
      IF local # NIL THEN Retrieve[local, name] -- all into one

      ELSE Retrieve[name, name]; -- use name from dump file

      END;
    Put.Text[wh, "Loading "];
    Put.Text[wh, remote];
    Put.Line[wh, " ..."];
    FTPInventoryDumpFile[ftpuser, remote, retrieval, Loader, NIL];
    Put.Line[wh, "End of load"];
    END;

  Transfer: PROCEDURE [from, source, destination: STRING] =
    BEGIN
    bc: LONG CARDINAL;
    user: STRING ← [40];
    password: STRING ← [40];
    serverText: STRING = [100];
    temp: FTPUser;
    FTPInitialize[];
    temp ← FTPCreateUser[AltoFilePrimitives[], PupCommunicationPrimitives[]];
    StringDefs.BcplToMesaString[OsStaticDefs.OsStatics.UserName, user];
    StringDefs.BcplToMesaString[OsStaticDefs.OsStatics.UserPassword, password];
    FTPSetCredentials[temp, primary, user, password];
    FTPOpenConnection[temp, from, files, serverText];
    Put.Line[wh, serverText];
    StartTiming["Transfer: ["];
    Put.Text[wh, from];
    Put.Text[wh, "]"];
    Put.Text[wh, source];
    Put.Text[wh, " => ["];
    Put.Text[wh, currentServer];
    Put.Text[wh, "]"];
    Put.Text[wh, destination];
    Put.Text[wh, " ... "];
    bc ← FTPTransferFile[temp, source, ftpuser, destination, unknown, NIL, NIL];
    StopTiming[" bytes", bc];
    FTPCloseConnection[temp];
    FTPDestroyUser[temp];
    FTPFinalize[];
    END;

  Stop: PROCEDURE [quiet: BOOLEAN ← FALSE] =
    BEGIN
    IF ~quiet THEN StartTiming["Closing down ..."];
    FTPCloseConnection[ftpuser];
    FTPDestroyUser[ftpuser];
    FTPFinalize[];
    IF ~quiet THEN StopTiming[" ok"];
    END;

  SimpleTest: PROCEDURE [localFile: STRING] =
    BEGIN
    Store[localFile, remoteScratch1];
    Retrieve[localScratch, remoteScratch1];
    List[remoteScratch1];
    Delete[remoteScratch1];
    CompareFiles[localFile, localScratch];
    END;

  FancyTest: PROCEDURE [localFile: STRING] =
    BEGIN
    remoteName: STRING = [100];
    Store[localFile, remoteScratch1];
    Rename[remoteScratch1, remoteScratch2];
    Retrieve[localScratch, remoteScratch2];
    FTPNoteFilenameUsed[ftpuser, remoteName, NIL];
    Put.Text[wh, "Remote name is: "];
    Put.Line[wh, remoteName];
    FTPRenewConnection[ftpuser];
    ReOpen[];
    List[remoteScratches];
    ListViaTemp[remoteScratches];
    CompareFiles[localFile, localScratch];
    Transfer[currentServer, remoteScratch2, remoteScratch3];
    Retrieve[localScratch, remoteScratch3];
    List[remoteScratches];
    CompareFiles[localFile, localScratch];
    END;

  SimpleDumpTest: PROCEDURE [localFile: STRING] =
    BEGIN
    StartDumping[remoteScratch2];
    Put.Text[wh, "  "];
    Store[localFile, remoteScratch1];
    StopDumping[];
    ListDump[remoteScratch2];
    Load[localScratch, remoteScratch2];
    CompareFiles[localFile, localScratch];
    END;

  FancyDumpTest: PROCEDURE =
    BEGIN
    StartDumping[remoteScratch2];
    Store["User.cm", "User.cm$"];
    Store["Com.cm", "Com.cm$"];
    Store["Binder.bcd", "Binder.bcd$"];
    Store["Rem.cm", "Rem.cm$"];
    Store["User.cm", "User.cm$$"];
    StopDumping[];
    ListDump[remoteScratch2];
    Load[NIL, remoteScratch2];
    CompareFiles["User.cm", "User.cm$"];
    CompareFiles["Com.cm", "Com.cm$"];
    CompareFiles["Binder.bcd", "Binder.bcd$"];
    CompareFiles["Rem.cm", "Rem.cm$"];
    CompareFiles["User.cm", "User.cm$$"];
    DeleteLocalFile["User.cm$"];
    DeleteLocalFile["Com.cm$"];
    DeleteLocalFile["Binder.bcd$"];
    DeleteLocalFile["Rem.cm$"];
    DeleteLocalFile["User.cm$$"];
    END;

  TimingTest: PROCEDURE [where: STRING, twice: BOOLEAN ← FALSE] =
    BEGIN
    localFile: STRING = "Compiler.image";
    IF isSapsford AND where # defaultServer THEN {
      Put.Line[wh, "... oops, Sapsford => no test"]; RETURN};
    Start[where];
    Store[localFile, remoteScratch1];
    Retrieve[localScratch, remoteScratch1];
    IF twice THEN Retrieve[localScratch, remoteScratch1];
    Delete[remoteScratch1];
    Stop[];
    CompareFiles[localFile, localScratch];
    DeleteLocalFile[localScratch];
    END;

  BufferTest: PROCEDURE [pages: CARDINAL] =
    BEGIN
    localFile: STRING = "Compiler.image";
    FTPSetBufferSize[pages];
    Start[defaultServer];
    Store[localFile, remoteScratch1];
    Retrieve[localScratch, remoteScratch1];
    Retrieve[localScratch, remoteScratch1];
    Delete[remoteScratch1];
    CompareFiles[localFile, localScratch];
    DeleteLocalFile[localScratch];
    Stop[];
    END;

  TimeTransfer: PROCEDURE [to: STRING] =
    BEGIN
    remoteFile: STRING = "<Mesa>Compiler.image";
    IF isSapsford AND to # defaultServer THEN {
      Put.Line[wh, "... oops, Sapsford => no test"]; RETURN};
    Start[to];
    Transfer[mesaServer, remoteFile, remoteScratch1];
    Retrieve[localScratch, remoteScratch1];
    Delete[remoteScratch1];
    Stop[];
    SingleRetrieve[mesaServer, localScratch2, remoteFile];
    CompareFiles[localScratch, localScratch2];
    DeleteLocalFile[localScratch];
    DeleteLocalFile[localScratch2];
    END;

  AccessDeniedTester: PROCEDURE =
    BEGIN
    -- You have to create the files and turn off access to them by hand.
    IF isSapsford THEN {Put.Line[wh, "... oops, Sapsford => no test"]; RETURN};
    TestAccessDenied["Ivy", "trash$", "YouCantSeeMe"];
    TestAccessDenied["Idun", "trash$", "YouCantSeeMe"];
    TestAccessDeniedStar["Idun", "trash$", "YouCantSeeMe"];
    TestAccessDeniedStar["Idun", "trash$", "YouCantSee*"];
    END;

  TestAccessDenied: PROCEDURE [where, discard, remote: STRING] =
    BEGIN
    ENABLE
      FTPError =>
	BEGIN
	IF ftpError = requestedAccessDenied THEN
	  BEGIN
	  Put.CR[wh];
	  Put.Text[wh, "    Access denied: "];
	  Put.Line[wh, message];
	  CONTINUE;
	  END;
	END;
    SingleRetrieve[where, discard, remote];
    Runtime.CallDebugger["We didn't get Rejected."];
    END;

  TestAccessDeniedStar: PROCEDURE [where, discard, remote: STRING] =
    BEGIN
    Snarf: PROCEDURE [
      x: UNSPECIFIED, name: STRING, y: VirtualFilename, info: FileInfo] =
      BEGIN
      ENABLE
	FTPError =>
	  BEGIN
	  IF ftpError = requestedAccessDenied THEN
	    BEGIN
	    Put.CR[wh];
	    Put.Text[wh, "    Access denied: "];
	    Put.Line[wh, message];
	    CONTINUE;
	    END;
	  END;
      Retrieve[discard, name];
      Runtime.CallDebugger["We didn't get Rejected."];
      END;
    Start[where, TRUE];
    Put.Text[wh, "Multiple Retrieving "];
    Put.Text[wh, remote];
    Put.Line[wh, " ..."];
    FTPEnumerateFiles[ftpuser, remote, retrieval, Snarf, NIL];
    Stop[TRUE];
    END;

  NotFoundTester: PROCEDURE =
    BEGIN
    TestNotFound["ThisFileShouldntExist", enumeration];
    TestNotFound["ThisFileShouldntExist", retrieval];
    TestNotFound["ThisFileShouldntExist", deletion];
    TestNotFound["ThisFileShouldntExist", unspecified];
    TestNotFound["ThisFileShouldntExist*", enumeration];
    TestNotFound["ThisFileShouldntExist*", retrieval];
    TestNotFound["ThisFileShouldntExist*", deletion];
    TestNotFound["ThisFileShouldntExist*", unspecified];
    TestNotFoundStar["ThisFileShouldntExist", enumeration];
    TestNotFoundStar["ThisFileShouldntExist", retrieval];
    TestNotFoundStar["ThisFileShouldntExist", deletion];
    TestNotFoundStar["ThisFileShouldntExist", unspecified];
    TestNotFoundStar["ThisFileShouldntExist*", enumeration];
    TestNotFoundStar["ThisFileShouldntExist*", retrieval];
    TestNotFoundStar["ThisFileShouldntExist*", deletion];
    TestNotFoundStar["ThisFileShouldntExist*", unspecified];
    END;

  TestNotFound: PROCEDURE [remote: STRING, why: Intent] =
    BEGIN
    Snarf: PROCEDURE [
      x: UNSPECIFIED, name: STRING, y: VirtualFilename, info: FileInfo] =
      BEGIN Runtime.CallDebugger["We shoudn't find any files."]; END;
    BEGIN
    ENABLE
      FTPError =>
	IF ftpError = noSuchFile THEN {Put.Line[wh, message]; CONTINUE; };
    SELECT why FROM
      enumeration => Put.Text[wh, "List "];
      retrieval => Put.Text[wh, "Retrieve "];
      deletion => Put.Text[wh, "Delete "];
      unspecified => Put.Text[wh, "Enumerate "];
      ENDCASE => ERROR;
    Put.Text[wh, remote];
    Put.Text[wh, " => "];
    SELECT why FROM
      retrieval => [] ← FTPRetrieveFile[ftpuser, "trash$", remote, unknown];
      deletion => FTPDeleteFile[ftpuser, remote];
      enumeration, unspecified =>
	FTPEnumerateFiles[ftpuser, remote, why, Snarf, NIL];
      ENDCASE => ERROR;
    Runtime.CallDebugger["We shoudn't find any files."];
    END;
    END;

  TestNotFoundStar: PROCEDURE [remote: STRING, why: Intent] =
    BEGIN
    Snarf: PROCEDURE [
      x: UNSPECIFIED, name: STRING, y: VirtualFilename, info: FileInfo] =
      BEGIN Runtime.CallDebugger["We shoudn't find any files."]; END;
    SELECT why FROM
      enumeration => Put.Text[wh, "List* "];
      retrieval => Put.Text[wh, "Retrieve* "];
      deletion => Put.Text[wh, "Delete* "];
      unspecified => Put.Text[wh, "Enumerate* "];
      ENDCASE => ERROR;
    Put.Text[wh, remote];
    Put.Text[wh, " => "];
    BEGIN
    ENABLE
      FTPError =>
	IF ftpError = noSuchFile THEN BEGIN Put.Line[wh, message]; CONTINUE; END;
    FTPEnumerateFiles[ftpuser, remote, why, Snarf, NIL];
    Runtime.CallDebugger["We didn't get rejected."];
    END;
    END;

  FunnyNameTester: PROCEDURE =
    BEGIN
    TestFunnyName["<ThisDirectoryDoesntExist>Foo", noSuchFile, enumeration];
    TestFunnyName["<ThisDirectoryDoesntExist>Foo", illegalFilename, retrieval];
    TestFunnyName["<ThisDirectoryDoesntExist>Foo", illegalFilename, deletion];
    TestFunnyName["<ThisDirectoryDoesntExist>Foo", noSuchFile, unspecified];
    TestFunnyNameStar["<ThisDirectoryDoesntExist>Foo", noSuchFile, enumeration];
    TestFunnyNameStar[
      "<ThisDirectoryDoesntExist>Foo", illegalFilename, retrieval];
    TestFunnyNameStar["<ThisDirectoryDoesntExist>Foo", illegalFilename, deletion];
    TestFunnyNameStar["<ThisDirectoryDoesntExist>Foo", noSuchFile, unspecified];
    TestFunnyNameStar["<ThisDirectoryDoesntExist>Foo*", noSuchFile, enumeration];
    TestFunnyNameStar["<ThisDirectoryDoesntExist>Foo*", noSuchFile, retrieval];
    TestFunnyNameStar["<ThisDirectoryDoesntExist>Foo*", noSuchFile, deletion];
    TestFunnyNameStar["<ThisDirectoryDoesntExist>Foo*", noSuchFile, unspecified];
    TestFunnyName["Illegal character", illegalFilename, enumeration];
    TestFunnyName["Illegal character", illegalFilename, retrieval];
    TestFunnyName["Illegal character", illegalFilename, deletion];
    TestFunnyName["Illegal character", illegalFilename, unspecified];
    TestFunnyNameStar["Illegal character", illegalFilename, enumeration];
    TestFunnyNameStar["Illegal character", illegalFilename, retrieval];
    TestFunnyNameStar["Illegal character", illegalFilename, deletion];
    TestFunnyNameStar["Illegal character", illegalFilename, unspecified];
    END;

  TestFunnyName: PROCEDURE [remote: STRING, expected: FtpError, why: Intent] =
    BEGIN
    Snarf: PROCEDURE [
      x: UNSPECIFIED, name: STRING, y: VirtualFilename, info: FileInfo] =
      BEGIN Runtime.CallDebugger["We shoudn't find any files."]; END;
    SELECT why FROM
      enumeration => Put.Text[wh, "List "];
      retrieval => Put.Text[wh, "Retrieve "];
      deletion => Put.Text[wh, "Delete "];
      unspecified => Put.Text[wh, "Enumerate "];
      ENDCASE => ERROR;
    Put.Text[wh, remote];
    Put.Text[wh, " => "];
    BEGIN
    ENABLE
      FTPError =>
	BEGIN Put.Line[wh, message]; IF ftpError = expected THEN CONTINUE; END;
    SELECT why FROM
      retrieval => [] ← FTPRetrieveFile[ftpuser, "trash$", remote, unknown];
      deletion => FTPDeleteFile[ftpuser, remote];
      enumeration, unspecified =>
	FTPEnumerateFiles[ftpuser, remote, why, Snarf, NIL];
      ENDCASE => ERROR;
    Runtime.CallDebugger["We should have been rejected."];
    END;
    END;

  TestFunnyNameStar: PROCEDURE [remote: STRING, expected: FtpError, why: Intent] =
    BEGIN
    Snarf: PROCEDURE [
      x: UNSPECIFIED, name: STRING, y: VirtualFilename, info: FileInfo] =
      BEGIN Runtime.CallDebugger["We shoudn't find any files."]; END;
    SELECT why FROM
      enumeration => Put.Text[wh, "List* "];
      retrieval => Put.Text[wh, "Retrieve* "];
      deletion => Put.Text[wh, "Delete* "];
      unspecified => Put.Text[wh, "Enumerate* "];
      ENDCASE => ERROR;
    Put.Text[wh, remote];
    Put.Text[wh, " => "];
    BEGIN
    ENABLE
      FTPError =>
	BEGIN Put.Line[wh, message]; IF ftpError = expected THEN CONTINUE; END;
    FTPEnumerateFiles[ftpuser, remote, why, Snarf, NIL];
    Runtime.CallDebugger["We should have been rejected."];
    END;
    END;

  RejectTester: PROCEDURE =
    BEGIN
    FTPSetCredentials[ftpuser, primary, NIL, NIL];
    TestReject[credentialsMissing, "User name/pwd required"];
    FTPSetCredentials[ftpuser, primary, "Horse Shit", NIL];
    TestReject[noSuchPrimaryUser, "Invalid user name"];
    FTPSetCredentials[ftpuser, primary, user, "Horse Shit"];
    TestReject[incorrectPrimaryPassword, "Invalid user password"];
    FTPSetCredentials[ftpuser, primary, user, password]; -- put it back
    FTPSetCredentials[ftpuser, secondary, "Horse Shit", "Horse Shit"];
    TestReject[noSuchSecondaryUser, "Invalid connect name"];
    FTPSetCredentials[ftpuser, secondary, "Mesa", "Horse Shit"];
    TestReject[incorrectSecondaryPassword, "Invalid connect password"];
    END;

  TestReject: PROCEDURE [expected: FtpError, text: STRING] =
    BEGIN
    BEGIN
    ENABLE
      FTPError =>
	BEGIN
	IF ftpError = expected THEN
	  BEGIN
	  Put.Text[wh, "    "];
	  Put.Text[wh, text];
	  Put.Text[wh, " (while listing) "];
	  Put.Line[wh, message];
	  CONTINUE;
	  END;
	END;
    List["Compiler.image"];
    Runtime.CallDebugger["We didn't get Rejected while listing."];
    END;
    BEGIN
    ENABLE
      FTPError =>
	BEGIN
	IF ftpError = expected THEN
	  BEGIN
	  Put.CR[wh];
	  Put.Text[wh, "    "];
	  Put.Text[wh, text];
	  Put.Text[wh, " (while reading) "];
	  Put.Line[wh, message];
	  CONTINUE;
	  END;
	END;
    Retrieve["Trash$", "Compiler.image"];
    Runtime.CallDebugger["We didn't get Rejected while retrieving."];
    END;
    BEGIN
    ENABLE
      FTPError =>
	BEGIN
	IF ftpError = expected THEN
	  BEGIN
	  Put.Text[wh, "    "];
	  Put.Text[wh, text];
	  Put.Text[wh, " (while read*ing) "];
	  Put.Line[wh, message];
	  CONTINUE;
	  END;
	END;
    RetrieveStar["Trash$", "Compiler.image!*"];
    Runtime.CallDebugger["We didn't get Rejected while retrieving *."];
    END;
    BEGIN
    ENABLE
      FTPError =>
	BEGIN
	IF ftpError = expected THEN
	  BEGIN
	  Put.CR[wh];
	  Put.Text[wh, "    "];
	  Put.Text[wh, text];
	  Put.Text[wh, " (while storing) "];
	  Put.Line[wh, message];
	  CONTINUE;
	  END;
	END;
    Store["Compiler.image", "Please-Tell-HGM-About-This-$$$"];
    Runtime.CallDebugger["We didn't get Rejected while storing."];
    END;
    END;

  VersionTester: PROCEDURE =
    BEGIN
    VersionTest["User.cm", "Version$!1"];
    Delete["Version$!1"];
    VersionTest["User.cm", "Version$!1"];
    VersionTest["Binder.bcd", "Version$!2"];
    VersionTest["Com.cm", "Version$!3"];
    VersionTest["User.cm", "Version$!4"];
    VersionTest["Com.cm", "Version$!1"];
    VersionTest["Com.cm", "Version$!4"];
    List["Version$"];
    Delete["Version$!1"];
    Delete["Version$!2"];
    Delete["Version$!3"];
    Delete["Version$!4"];
    END;

  VersionTest: PROCEDURE [localFile, remoteFile: STRING] =
    BEGIN
    Store[localFile, remoteFile];
    Retrieve[localScratch, remoteFile];
    List[remoteFile];
    CompareFiles[localFile, localScratch];
    END;

  UnwindTester: PROCEDURE =
    BEGIN
    Start[defaultServer];
    TestUnwind["*.mesa", enumeration];
    TestUnwind["*.mesa", retrieval];
    TestUnwind["*.mesa", deletion];
    TestUnwind["*.mesa", renaming];
    TestUnwind["*.mesa", unspecified];
    Stop[];
    END;

  TestUnwind: PROCEDURE [remote: STRING, why: Intent] =
    BEGIN
    GetOutOfHere: SIGNAL = CODE;
    Foo: PROCEDURE [
      x: UNSPECIFIED, name: STRING, y: VirtualFilename, info: FileInfo] =
      BEGIN SIGNAL GetOutOfHere; END;
    SELECT why FROM
      enumeration => Put.Text[wh, "List "];
      retrieval => Put.Text[wh, "Retrieve "];
      deletion => Put.Text[wh, "Delete "];
      renaming => Put.Text[wh, "Rename "];
      unspecified => Put.Text[wh, "Enumerate "];
      ENDCASE => ERROR;
    Put.Text[wh, remote];
    Put.Text[wh, " => "];
    BEGIN
    ENABLE GetOutOfHere => {Put.Line[wh, " UNWINDing..."]; CONTINUE; };
    FTPEnumerateFiles[ftpuser, remote, why, Foo, NIL];
    Runtime.CallDebugger["We didn't get UNWINDed."];
    END;
    END;

  TestExtraRetrieve: PROCEDURE =
    BEGIN
    BEGIN
    ENABLE
      FTPError =>
	BEGIN
	IF ftpError = fileGroupDesignatorUnexpected THEN
	  BEGIN
	  Put.Text[wh, "    Extra files on retrieve => "];
	  Put.Line[wh, message];
	  CONTINUE;
	  END;
	END;
    Retrieve[localScratch, remoteScratches];
    Runtime.CallDebugger["We didn't get Rejected while retrieving."];
    END;
    END;

  TestExtraDelete: PROCEDURE =
    BEGIN
    BEGIN
    ENABLE
      FTPError =>
	BEGIN
	IF ftpError = fileGroupDesignatorUnexpected THEN
	  BEGIN
	  Put.Text[wh, "    Extra files on delete => "];
	  Put.Line[wh, message];
	  CONTINUE;
	  END;
	END;
    Delete[remoteScratches];
    Runtime.CallDebugger["We didn't get Rejected while deleting."];
    END;
    END;

  -- Local file system interactions

  DeleteLocalFile: PROCEDURE [fileName: STRING] =
    BEGIN OPEN SegmentDefs;
    file: FileHandle ← NIL;
    file ← NewFile[fileName, Write, OldFileOnly ! FileNameError => CONTINUE];
    IF file # NIL THEN DestroyFile[file];
    END;

  CompareFiles: PROCEDURE [one, two: STRING] =
    BEGIN
    pages: CARDINAL = 20;
    bufferSize: CARDINAL = pages*256;
    oneFile, twoFile: SegmentDefs.FileHandle;
    oneCreate, twoCreate: LONG CARDINAL;
    oneStream, twoStream: StreamDefs.StreamHandle;
    finger, length, words: LONG CARDINAL;
    oneBuffer: POINTER TO ARRAY [0..bufferSize) OF WORD;
    twoBuffer: POINTER TO ARRAY [0..bufferSize) OF WORD;
    n1, n2, tail: CARDINAL;
    StartTiming["Checking contents ..."];
    oneFile ← SegmentDefs.NewFile[one, SegmentDefs.Read, SegmentDefs.OldFileOnly];
    twoFile ← SegmentDefs.NewFile[two, SegmentDefs.Read];
    [read:, write:, create: oneCreate] ← SegmentDefs.GetFileTimes[oneFile];
    [read:, write:, create: twoCreate] ← SegmentDefs.GetFileTimes[twoFile];
    IF oneCreate # twoCreate THEN Runtime.CallDebugger["Create dates differ..."];
    IF SegmentDefs.GetEndOfFile[oneFile] # SegmentDefs.GetEndOfFile[twoFile] THEN
      Runtime.CallDebugger["File lengths differ."];
    oneStream ← StreamDefs.CreateByteStream[oneFile, SegmentDefs.Read];
    twoStream ← StreamDefs.CreateByteStream[twoFile, SegmentDefs.Read];
    IF StreamDefs.FileLength[oneStream] # StreamDefs.FileLength[twoStream] THEN
      Runtime.CallDebugger["engths differ."];
    length ← StreamDefs.GetPosition[oneStream];
    StreamDefs.SetPosition[oneStream, 0];
    StreamDefs.SetPosition[twoStream, 0];
    oneBuffer ← Storage.Pages[pages];
    twoBuffer ← Storage.Pages[pages];
    words ← length/2;
    FOR finger ← 0, finger + bufferSize WHILE words > finger + bufferSize DO
      n1 ← StreamDefs.ReadBlock[oneStream, oneBuffer, bufferSize];
      n2 ← StreamDefs.ReadBlock[twoStream, twoBuffer, bufferSize];
      IF n1 # bufferSize OR n2 # bufferSize THEN
	Runtime.CallDebugger["ReadBlock mixup."];
      FOR i: CARDINAL IN [0..bufferSize) DO
	IF oneBuffer[i] # twoBuffer[i] THEN
	  Runtime.CallDebugger["Data words differ."];
	ENDLOOP;
      ENDLOOP;
    IF (tail ← Inline.LowHalf[words - finger]) # 0 THEN
      BEGIN
      n1 ← StreamDefs.ReadBlock[oneStream, oneBuffer, tail];
      n2 ← StreamDefs.ReadBlock[twoStream, twoBuffer, tail];
      IF n1 # tail OR n2 # tail THEN Runtime.CallDebugger["ReadBlock mixup."];
      FOR i: CARDINAL IN [0..tail) DO
	IF oneBuffer[i] # twoBuffer[i] THEN
	  Runtime.CallDebugger["Data words differ."];
	ENDLOOP;
      END;
    IF words*2 # length AND oneStream.get[oneStream] # twoStream.get[twoStream]
      THEN Runtime.CallDebugger["Data bytes differ."];
    oneStream.destroy[oneStream];
    twoStream.destroy[twoStream];
    Storage.FreePages[oneBuffer];
    Storage.FreePages[twoBuffer];
    StopTiming[" bytes", length];
    END;

  TestSpeed: PROCEDURE =
    BEGIN
    Put.CR[wh];
    TimingTest["Idun"];
    Put.CR[wh];
    TimingTest["Ibis"];
    Put.CR[wh];
    TimingTest["Ivy"];
    Put.CR[wh];
    TimingTest["Isis"];
    Put.CR[wh];
    TimeTransfer[defaultServer]; -- Iris => Idun
    Put.CR[wh];
    TimeTransfer[mesaServer]; -- Iris => Iris

    END;

  TestChecksums: PROCEDURE =
    BEGIN
    config: SegmentDefs.MemoryConfig ← SegmentDefs.GetMemoryConfig[];
    Put.Line[wh, "Using NULL checksums ..."];
    PupDefs.UseNullChecksumMicrocode[];
    TimingTest[defaultServer, TRUE];
    Put.CR[wh];
    Put.Line[wh, "Using Software checksums ..."];
    PupDefs.UseSoftwareChecksumMicrocode[];
    TimingTest[defaultServer, TRUE];
    Put.CR[wh];
    IF config.AltoType = AltoIIXM AND
      (config.controlStore = RamandRom OR config.controlStore = Ram3k) THEN
      BEGIN
      Put.Line[wh, "Using (Alto) Microcode checksums ..."];
      PupDefs.UseAltoChecksumMicrocode[];
      TimingTest[defaultServer, TRUE];
      Put.CR[wh];
      END;
    IF config.AltoType = D0 OR config.AltoType = Dorado THEN
      BEGIN
      Put.Line[wh, "Using Microcode checksums ..."];
      PupDefs.UsePrincOpsChecksumMicrocode[];
      TimingTest[defaultServer, TRUE];
      Put.CR[wh];
      END;
    END;

  TestBufferSizes: PROCEDURE =
    BEGIN
    FTPInitialize[];
    Put.Line[wh, "Using 1 page buffers ..."];
    BufferTest[1];
    Put.CR[wh];
    Put.Line[wh, "Using default (4 page) buffers ..."];
    BufferTest[0];
    Put.CR[wh];
    Put.Line[wh, "Using 10 page buffers ..."];
    BufferTest[10];
    Put.CR[wh];
    Put.Line[wh, "Using 25 page buffers ..."];
    BufferTest[25];
    Put.CR[wh];
    FTPFinalize[];
    END;

  -- Main line testing.......

  Put.Line[wh, "FTP Test kludge..."];
  Put.CR[wh];

  Put.CR[wh];
  Put.Line[wh, "Testing checksum options ..."];
  TestChecksums[];

  Put.CR[wh];
  Put.Line[wh, "Testing buffer sizes ..."];
  TestBufferSizes[];

  Put.CR[wh];
  Put.CR[wh];
  Put.Line[wh, "Various timing tests ..."];
  TestSpeed[];
  Put.CR[wh];

  Put.Line[wh, "Basic tests ..."];
  Start[defaultServer];
  Stop[];
  Start[defaultServer];
  SimpleTest["User.cm"]; -- medium
  SimpleTest["Com.cm"]; -- small
  SimpleTest["Binder.bcd"]; -- reasonably large
  SimpleTest["Rem.cm"]; -- probably empty
  Stop[];
  Put.CR[wh];

  Put.Line[wh, "Use existing connection ..."];
  Start[defaultServer];
  Store["User.cm", remoteScratch1];
  Store["User.cm", remoteScratch1];
  Store["User.cm", remoteScratch1];
  Retrieve[localScratch, remoteScratch1];
  Retrieve[localScratch, remoteScratch1];
  Retrieve[localScratch, remoteScratch1];
  Stop[];
  Put.CR[wh];

  Put.Line[wh, "Make/break connection each time (pause between tries) ..."];
  Pause[10];
  SingleStore[defaultServer, "User.cm", remoteScratch1];
  Pause[10];
  SingleStore[defaultServer, "User.cm", remoteScratch1];
  Pause[10];
  SingleStore[defaultServer, "User.cm", remoteScratch1];
  Pause[10];
  SingleRetrieve[defaultServer, localScratch, remoteScratch1];
  Pause[10];
  SingleRetrieve[defaultServer, localScratch, remoteScratch1];
  Pause[10];
  SingleRetrieve[defaultServer, localScratch, remoteScratch1];
  Pause[10];
  Put.CR[wh];

  Start[defaultServer];
  FancyTest["User.cm"]; -- medium
  FancyTest["Com.cm"]; -- small
  FancyTest["Binder.bcd"]; -- reasonably large
  FancyTest["Rem.cm"]; -- probably empty

  SimpleDumpTest["User.cm"]; -- medium
  SimpleDumpTest["Com.cm"]; -- small
  SimpleDumpTest["Binder.bcd"]; -- reasonably large
  SimpleDumpTest["Rem.cm"]; -- probably empty
  FancyDumpTest[];

  -- At this point, we have 8 versions of Scratch.2$, and 4 versions of Scratch.3$
  TestExtraRetrieve[];
  TestExtraDelete[];

  RetrieveStar[localScratch, remoteScratches];
  DeleteStar[remoteScratches]; -- Delete them all
  Stop[];
  Put.CR[wh];

  -- Test hairy UNWIND cases
  Put.Line[wh, "Testing hairy UNWIND cases ..."];
  UnwindTester[];
  Put.CR[wh];

  Put.Line[wh, "Testing access denied ..."];
  AccessDeniedTester[];
  Put.CR[wh];

  Put.Line[wh, "Testing file not found (and UNWINDing) ..."];
  Start[defaultServer];
  NotFoundTester[];
  Stop[];
  Put.CR[wh];

  Put.Line[wh, "Testing funny file names ..."];
  Start[defaultServer];
  FunnyNameTester[];
  Stop[];
  Put.CR[wh];

  Put.Line[wh, "Testing credentials ..."];
  Start[mesaServer];
  RejectTester[];
  Stop[];
  Put.CR[wh];

  Put.Line[wh, "Testing version overwriting ..."];
  Start[defaultServer];
  VersionTester[];
  Stop[];
  Put.CR[wh];

  Put.Line[wh, "Testing other strange cases ..."];
  SingleStore[defaultServer, "TestFTP.bcd", remoteScratch1]; -- file is in use
  Put.CR[wh];

  Put.Line[wh, "Testing local disk full ..."];
  Start[mesaServer];
  BEGIN
  ENABLE
    FTPError =>
      BEGIN
      IF ftpError = noRoomForFile THEN
	BEGIN
	Put.CR[wh];
	Put.Text[wh, "    Disk Full: "];
	Put.Line[wh, message];
	CONTINUE;
	END;
      END;
  Retrieve["foo$0$", "<Mesa>Compiler.image"];
  Retrieve["foo$1$", "<Mesa>Compiler.image"];
  Retrieve["foo$2$", "<Mesa>Compiler.image"];
  Retrieve["foo$3$", "<Mesa>Compiler.image"];
  Retrieve["foo$4$", "<Mesa>Compiler.image"];
  Retrieve["foo$5$", "<Mesa>Compiler.image"];
  Retrieve["foo$6$", "<Mesa>Compiler.image"];
  Retrieve["foo$7$", "<Mesa>Compiler.image"];
  Retrieve["foo$8$", "<Mesa>Compiler.image"];
  Retrieve["foo$9$", "<Mesa>Compiler.image"];
  Runtime.CallDebugger["I give up, your disk is too big....."];
  END;
  Stop[];

  StartTiming["Deleting trashy files ..."];
  DeleteLocalFile["foo$0$"];
  DeleteLocalFile["foo$1$"];
  DeleteLocalFile["foo$2$"];
  DeleteLocalFile["foo$3$"];
  DeleteLocalFile["foo$4$"];
  DeleteLocalFile["foo$5$"];
  DeleteLocalFile["foo$6$"];
  DeleteLocalFile["foo$7$"];
  DeleteLocalFile["foo$8$"];
  DeleteLocalFile["foo$9$"];
  StopTiming[" done"];

  Put.Line[wh, "Testing remote disk full ..."];
  IF isSapsford THEN Put.Line[wh, "... oops, Sapsford => no test"]
  ELSE
    BEGIN
    Start["Iris"]; -- HGM is known to have a small allocation
    BEGIN
    ENABLE
      FTPError =>
	BEGIN
	IF ftpError = noRoomForFile THEN
	  BEGIN
	  Put.CR[wh];
	  Put.Text[wh, "    Disk Full: "];
	  Put.Line[wh, message];
	  CONTINUE;
	  END;
	END;
    THROUGH [0..6) DO Store["Compiler.image", "foo$0$"]; ENDLOOP;
    Runtime.CallDebugger["I give up, your allocation is too big....."];
    END;
    DeleteStar["foo$0$!*"];
    Stop[];
    END;

  Put.CR[wh];
  ImageDefs.StopMesa[];

  END.