From 690f5a7ba429ec5c4f9505b820dc8b6b11031058 Mon Sep 17 00:00:00 2001 From: Andy McFadden Date: Thu, 2 Nov 2023 15:35:09 -0700 Subject: [PATCH] Add more clipboard infrastructure tests Pull from a filesystem, from the volume dir and a subdir. --- AppCommon/AddFileWorker.cs | 45 +-------- AppCommon/ClipFileEntry.cs | 1 + AppCommon/ClipFileSet.cs | 41 +++++++-- AppCommon/ClipPasteWorker.cs | 2 +- DiskArc/DAExtensions.cs | 47 +++++++++- cp2/Tests/Controller.cs | 4 + cp2/Tests/TestClip.cs | 173 +++++++++++++++++++++++++++++++++-- 7 files changed, 252 insertions(+), 61 deletions(-) diff --git a/AppCommon/AddFileWorker.cs b/AppCommon/AddFileWorker.cs index 9bb06ed..a683125 100644 --- a/AppCommon/AddFileWorker.cs +++ b/AppCommon/AddFileWorker.cs @@ -409,7 +409,7 @@ public void AddFilesToDisk(IFileSystem fileSystem, IFileEntry targetDir, // directory if we remembered the path / entry from the previous iteration. string storageDir = doStripPaths ? string.Empty : addEnt.StorageDir; IFileEntry subDirEnt; - subDirEnt = CreateSubdirectories(fileSystem, targetDirEnt, storageDir, + subDirEnt = fileSystem.CreateSubdirectories(targetDirEnt, storageDir, addEnt.StorageDirSep); // Add the new file to subDirEnt. See if it already exists. @@ -524,49 +524,6 @@ public void AddFilesToDisk(IFileSystem fileSystem, IFileEntry targetDir, isCancelled = false; } - /// - /// Ensures that all of the directories in the path exist. If they don't exist, they - /// will be created. - /// - /// Filesystem to modify. - /// Base directory. - /// Partial pathname, directories only. - /// Directory separator character used in storage dir. - /// File entry for destination directory. - /// Something failed. - internal static IFileEntry CreateSubdirectories(IFileSystem fileSystem, - IFileEntry targetDirEnt, string storageDir, char storageDirSep) { - if (string.IsNullOrEmpty(storageDir)) { - return targetDirEnt; - } - IFileEntry subDirEnt = targetDirEnt; - string[] dirStrings = storageDir.Split(storageDirSep); - foreach (string dirName in dirStrings) { - // Adjust this directory name to be compatible with the target filesystem. - string adjDirName = fileSystem.AdjustFileName(dirName); - - // See if it exists. If it does, and it's not a directory, is very bad. - if (fileSystem.TryFindFileEntry(subDirEnt, adjDirName, - out IFileEntry nextDirEnt)) { - if (!nextDirEnt.IsDirectory) { - throw new IOException("Error: path component '" + adjDirName + - "' (" + dirName + ") is not a directory"); - } - subDirEnt = nextDirEnt; - } else { - // Not found, create new. - try { - subDirEnt = fileSystem.CreateFile(subDirEnt, adjDirName, - CreateMode.Directory); - } catch (IOException ex) { - throw new IOException("Error: unable to create directory '" + - adjDirName + "': " + ex.Message); - } - } - } - return subDirEnt; - } - /// /// Copies a part (i.e fork) of a file to a disk image file stream. The file source may /// be a plain or structured file. diff --git a/AppCommon/ClipFileEntry.cs b/AppCommon/ClipFileEntry.cs index 7fa709f..5dfcf33 100644 --- a/AppCommon/ClipFileEntry.cs +++ b/AppCommon/ClipFileEntry.cs @@ -445,6 +445,7 @@ public ClipFileEntry(object archiveOrFileSystem, IFileEntry entry, IFileEntry ad Type? expectedType, AppHook appHook) { Debug.Assert(!string.IsNullOrEmpty(attribs.FileNameOnly)); Debug.Assert(!string.IsNullOrEmpty(attribs.FullPathName)); + mStreamGen = new StreamGenerator(archiveOrFileSystem, entry, adfEntry, part, attribs, preserveMode, exportSpec, defaultSpecs, expectedType, appHook); diff --git a/AppCommon/ClipFileSet.cs b/AppCommon/ClipFileSet.cs index 13d2def..a48ec33 100644 --- a/AppCommon/ClipFileSet.cs +++ b/AppCommon/ClipFileSet.cs @@ -81,7 +81,7 @@ public class ClipFileSet { private bool mUseRawData; // Keep track of directories we've already added to the output. - private Dictionary mAddedDirs = + private Dictionary mAddedFiles = new Dictionary(); @@ -316,8 +316,13 @@ private void GenerateFromDisk(IFileSystem fs, List entries) { /// Generates an entry in the ClipFileEntry set for the specified entry. If the entry is /// a directory, we recursively generate clip objects for the entry's children. /// + /// + /// We need to avoid creating two entries for the same file. This can happen if + /// the entry list includes "dir1" and "dir1/foo.txt"; note either of these could + /// appear first. + /// /// Filesystem reference. - /// File entry to process. + /// File entry to process. May be a file or directory. /// Entry above the root, used to limit the partial path /// prefix. private void GenerateDiskEntries(IFileSystem fs, IFileEntry entry, @@ -329,7 +334,9 @@ private void GenerateDiskEntries(IFileSystem fs, IFileEntry entry, // create empty directories, and a way to pass the directory file dates along. AddMissingDirectories(fs, entry, aboveRootEntry); } - if (entry.IsDirectory) { + if (mAddedFiles.ContainsKey(entry)) { + // Already added. + } else if (entry.IsDirectory) { // Current directory, if not stripped, was added above. foreach (IFileEntry child in entry) { GenerateDiskEntries(fs, child, aboveRootEntry); @@ -350,6 +357,10 @@ private void GenerateDiskEntries(IFileSystem fs, IFileEntry entry, // Generate file attributes. The same object will be used for both forks. FileAttribs attrs = new FileAttribs(entry); attrs.FullPathName = ReRootedPathName(entry, aboveRootEntry); + if (string.IsNullOrEmpty(attrs.FullPathName)) { + mAppHook.LogW("Not adding below-root entry: " + entry); + return; + } if (mStripPaths) { extractPath = Path.GetFileName(extractPath); attrs.FullPathName = @@ -361,10 +372,10 @@ private void GenerateDiskEntries(IFileSystem fs, IFileEntry entry, } else { CreateForExport(fs, entry, IFileEntry.NO_ENTRY, attrs, extractPath); } + mAddedFiles.Add(entry, entry); } } - /// /// Adds entries for the directory hierarchy that contains the specified entry, if /// they haven't yet been added. If the entry is itself a directory, it will be added. @@ -381,10 +392,12 @@ private void AddMissingDirectories(IFileSystem fs, IFileEntry entry, } // Recursively check parents. AddMissingDirectories(fs, entry.ContainingDir, aboveRootEntry); - if (entry.IsDirectory && !mAddedDirs.ContainsKey(entry)) { + // Add this one if it's not already present. + if (entry.IsDirectory && !mAddedFiles.ContainsKey(entry)) { // Add this directory to the output list. FileAttribs attrs = new FileAttribs(entry); attrs.FullPathName = ReRootedPathName(entry, aboveRootEntry); + Debug.Assert(!string.IsNullOrEmpty(attrs.FullPathName)); attrs.FullPathSep = entry.DirectorySeparatorChar; attrs.FileNameOnly = PathName.GetFileName(attrs.FullPathName, attrs.FullPathSep); Debug.Assert(attrs.IsDirectory); @@ -395,7 +408,7 @@ private void AddMissingDirectories(IFileSystem fs, IFileEntry entry, mAppHook)); XferEntries.Add(new ClipFileEntry(fs, entry, IFileEntry.NO_ENTRY, FilePart.DataFork, attrs, mAppHook)); - mAddedDirs.Add(entry, entry); + mAddedFiles.Add(entry, entry); } } @@ -407,7 +420,23 @@ private void AddMissingDirectories(IFileSystem fs, IFileEntry entry, /// File entry. /// Entry above the root directory. For the volume dir, /// or non-hierarchical filesystems, this will be NO_ENTRY. + /// Partial pathname, or the empty string if the above-root entry is not actually + /// above the entry. private static string ReRootedPathName(IFileEntry entry, IFileEntry aboveRootEntry) { + // Test to see if entry is at or above the root. If it's at the same level as the + // root, or above it, it shouldn't be part of the set. + if (entry.ContainingDir == aboveRootEntry) { + return string.Empty; + } + IFileEntry scanEntry = entry; + while (scanEntry.ContainingDir != aboveRootEntry) { + scanEntry = scanEntry.ContainingDir; + if (scanEntry == IFileEntry.NO_ENTRY) { + // Walked to the top without finding it. + return string.Empty; + } + } + StringBuilder sb = new StringBuilder(); ReRootedPathName(entry, aboveRootEntry, sb); return sb.ToString(); diff --git a/AppCommon/ClipPasteWorker.cs b/AppCommon/ClipPasteWorker.cs index 4d8c1ee..317574c 100644 --- a/AppCommon/ClipPasteWorker.cs +++ b/AppCommon/ClipPasteWorker.cs @@ -409,7 +409,7 @@ public void AddFilesToDisk(IFileSystem fileSystem, IFileEntry targetDir, string storageName = PathName.GetFileName(clipEntry.Attribs.FullPathName, clipEntry.Attribs.FullPathSep); IFileEntry subDirEnt; - subDirEnt = AddFileWorker.CreateSubdirectories(fileSystem, targetDirEnt, storageDir, + subDirEnt = fileSystem.CreateSubdirectories(targetDirEnt, storageDir, clipEntry.Attribs.FullPathSep); // Add the new file to subDirEnt. See if it already exists. diff --git a/DiskArc/DAExtensions.cs b/DiskArc/DAExtensions.cs index 56f0730..8bcbce7 100644 --- a/DiskArc/DAExtensions.cs +++ b/DiskArc/DAExtensions.cs @@ -157,7 +157,8 @@ public static bool CheckStorageName(this IArchive archive, string storageName) { /// /// Formats an IDiskImage. This should only be used on a newly-created IDiskImage, /// when the ChunkAccess property is non-null and the FileSystem property is null. - /// Query the FileSystem out of the IDiskImage when this returns. + /// The disk will be analyzed, so the caller can access the filesystem object via + /// . /// /// /// The chunks will be zeroed out before the new filesystem is written. @@ -570,6 +571,50 @@ public static IFileEntry CreateFile(this IFileSystem fs, IFileEntry dirEntry, return entry; } + /// + /// Ensures that all of the directories in the path exist. If they don't exist, they + /// will be created. + /// + /// Base directory. + /// Partial pathname, with directories only. + /// Directory separator character used in storage dir. + /// File entry for destination directory. + /// I/O failure, or part of the path existed but was not + /// a regular file. + public static IFileEntry CreateSubdirectories(this IFileSystem fs, + IFileEntry baseDir, string partialPath, char dirSep) { + if (string.IsNullOrEmpty(partialPath)) { + return baseDir; + } + Debug.Assert(fs.Characteristics.IsHierarchical); + IFileEntry subDirEnt = baseDir; + string[] dirStrings = partialPath.Split(dirSep); + foreach (string dirName in dirStrings) { + // Adjust this directory name to be compatible with the target filesystem. + string adjDirName = fs.AdjustFileName(dirName); + + // See if it exists. If it does, and it's not a directory, is very bad. + if (fs.TryFindFileEntry(subDirEnt, adjDirName, + out IFileEntry nextDirEnt)) { + if (!nextDirEnt.IsDirectory) { + throw new IOException("Error: path component '" + adjDirName + + "' (" + dirName + ") is not a directory"); + } + subDirEnt = nextDirEnt; + } else { + // Not found, create new. + try { + subDirEnt = fs.CreateFile(subDirEnt, adjDirName, + IFileSystem.CreateMode.Directory); + } catch (IOException ex) { + throw new IOException("Error: unable to create directory '" + + adjDirName + "': " + ex.Message); + } + } + } + return subDirEnt; + } + //public static DiskFileStream OpenDataForkRO(this IFileSystem fs, IFileEntry entry) { // return fs.OpenFile(entry, IFileSystem.FileAccessMode.ReadOnly, Defs.FilePart.DataFork); //} diff --git a/cp2/Tests/Controller.cs b/cp2/Tests/Controller.cs index ae5ea56..0584bab 100644 --- a/cp2/Tests/Controller.cs +++ b/cp2/Tests/Controller.cs @@ -85,6 +85,10 @@ public static bool HandleRunTests(string cmdName, string[] args, ParamsBag parms ResetConsole(); } + // Give any finalizer-based checks a chance to run. + GC.Collect(); + GC.WaitForPendingFinalizers(); + Console.WriteLine("Success"); return true; } diff --git a/cp2/Tests/TestClip.cs b/cp2/Tests/TestClip.cs index e9ede4d..266d71d 100644 --- a/cp2/Tests/TestClip.cs +++ b/cp2/Tests/TestClip.cs @@ -24,10 +24,11 @@ using DiskArc.Disk; using DiskArc.FS; using static DiskArc.Defs; +using static DiskArc.IFileSystem; namespace cp2.Tests { /// - /// Test clipboard copy/paste infrastructure. This does not use the system clipboard. + /// Test clipboard copy/paste infrastructure. This does not actually use the system clipboard. /// /// /// This is primarily a test of the AppCommon infrastructure rather than a test of cp2 @@ -40,10 +41,12 @@ public static void RunTest(ParamsBag parms) { // // Basic plan: - // - Use ClipFileSet to generate a list of ClipFileEntry objects, then use - // ClipFileWorker to add the entries to a different archive or to extract to the - // filesystem. A custom stream generator is used in place of the system clipboard - // mechanism. + // - Use ClipFileSet to generate a list of ClipFileEntry objects. + // - Simple test: compare the set to a set of expected results to confirm that + // the set is being generated correctly. + // - Full test: use ClipFileWorker to add the entries to a different archive or to + // extract to the filesystem. A custom stream generator is used in place of the + // system clipboard mechanism. // // Some things to test: // - Direct file transfer of entries with data, rsrc, data+rsrc. @@ -58,9 +61,12 @@ public static void RunTest(ParamsBag parms) { parms.Raw = false; parms.MacZip = true; - IArchive testArchive = CreateTestArchive(sSampleFiles, parms); + using IArchive testArchive = CreateTestArchive(sSampleFiles, parms); + using IDiskImage testProFS = CreateTestProDisk(sSampleFiles, parms); - TestForeignExtract(testArchive, sForeignExtractNAPS, parms); + TestForeignExtract(testArchive, parms); + TestBasicXfer((IFileSystem)testProFS.Contents!, parms); + TestRerootXfer((IFileSystem)testProFS.Contents!, parms); Controller.RemoveTestTmp(parms); } @@ -129,6 +135,46 @@ private static IArchive CreateTestArchive(SampleFile[] specs, ParamsBag parms) { return arc; } + private static IDiskImage CreateTestProDisk(SampleFile[] specs, ParamsBag parms) { + // Create a disk image in a memory stream. + UnadornedSector disk = UnadornedSector.CreateBlockImage(new MemoryStream(), + 1600, parms.AppHook); + disk.FormatDisk(FileSystemType.ProDOS, "Test", 0, true, parms.AppHook); + IFileSystem fs = (IFileSystem)disk.Contents!; + fs.PrepareFileAccess(true); + + foreach (SampleFile sample in specs) { + string dirName = PathName.GetDirectoryName(sample.PathName, DIR_SEP); + string fileName = PathName.GetFileName(sample.PathName, DIR_SEP); + + IFileEntry subDir = fs.CreateSubdirectories(fs.GetVolDirEntry(), dirName, DIR_SEP); + IFileEntry newEntry = fs.CreateFile(subDir, fs.AdjustFileName(fileName), + sample.RsrcLength >= 0 ? CreateMode.Extended : CreateMode.File); + newEntry.FileName = fileName; + newEntry.FileType = PRODOS_FILETYPE; + newEntry.AuxType = PRODOS_AUXTYPE; + newEntry.CreateWhen = CREATE_WHEN; + newEntry.ModWhen = MOD_WHEN; + if (sample.DataLength >= 0) { + using (Stream stream = fs.OpenFile(newEntry, FileAccessMode.ReadWrite, + FilePart.DataFork)) { + for (int i = 0; i < sample.DataLength; i++) { + stream.WriteByte(0x33); + } + } + } + if (sample.RsrcLength >= 0) { + using (Stream stream = fs.OpenFile(newEntry, FileAccessMode.ReadWrite, + FilePart.RsrcFork)) { + for (int i = 0; i < sample.RsrcLength; i++) { + stream.WriteByte(0x44); + } + } + } + } + return disk; + } + private class ExpectedForeign { public string PathName { get; private set; } public int MinLength { get; private set; } @@ -157,8 +203,7 @@ public ExpectedForeign(string fileName, int minLength, int maxLength) { DATA_ONLY_DLEN), }; - private static void TestForeignExtract(IArchive arc, ExpectedForeign[] expected, - ParamsBag parms) { + private static void TestForeignExtract(IArchive arc, ParamsBag parms) { List entries = new List(); foreach (IFileEntry entry in arc) { entries.Add(entry); @@ -167,6 +212,11 @@ private static void TestForeignExtract(IArchive arc, ExpectedForeign[] expected, parms.Preserve, parms.Raw, parms.StripPaths, parms.MacZip, null, null, parms.AppHook); + TestForeign(clipSet, sForeignExtractNAPS, parms); + } + + private static void TestForeign(ClipFileSet clipSet, ExpectedForeign[] expected, + ParamsBag parms) { if (clipSet.ForeignEntries.Count != expected.Length) { throw new Exception("Foreign extract count mismatch: expected=" + expected.Length + ", actual=" + clipSet.ForeignEntries.Count); @@ -197,5 +247,110 @@ private static void TestForeignExtract(IArchive arc, ExpectedForeign[] expected, } } } + + private class ExpectedXfer { + public string PathName { get; private set; } + public FilePart Part { get; private set; } + public int Length { get; private set; } + + public ExpectedXfer(string pathName, FilePart part, int length) { + PathName = pathName; + Part = part; + Length = length; + } + } + + private static ExpectedXfer[] sXferProDOS = new ExpectedXfer[] { + new ExpectedXfer(DATA_ONLY_NAME, FilePart.DataFork, DATA_ONLY_DLEN), + new ExpectedXfer(RSRC_ONLY_NAME, FilePart.DataFork, 0), + new ExpectedXfer(RSRC_ONLY_NAME, FilePart.RsrcFork, RSRC_ONLY_RLEN), + new ExpectedXfer(DATA_RSRC_NAME, FilePart.DataFork, DATA_RSRC_DLEN), + new ExpectedXfer(DATA_RSRC_NAME, FilePart.RsrcFork, DATA_RSRC_RLEN), + new ExpectedXfer(SUBDIR1, FilePart.Unknown, -1), + new ExpectedXfer(SUBDIR1 + '/' + DATA_ONLY_NAME, FilePart.DataFork, DATA_ONLY_DLEN), + new ExpectedXfer(SUBDIR1 + '/' + SUBDIR2, FilePart.Unknown, -1), + new ExpectedXfer(SUBDIR1 + '/' + SUBDIR2 + '/' + DATA_ONLY_NAME, FilePart.DataFork, + DATA_ONLY_DLEN), + }; + + private static void TestBasicXfer(IFileSystem srcFs, ParamsBag parms) { + List entries = GatherDiskEntries(srcFs, srcFs.GetVolDirEntry()); + + ClipFileSet clipSet = new ClipFileSet(srcFs, entries, IFileEntry.NO_ENTRY, + parms.Preserve, parms.Raw, parms.StripPaths, parms.MacZip, + null, null, parms.AppHook); + + TestXfer(clipSet, sXferProDOS, parms); + } + + private static ExpectedXfer[] sXferProDOSReroot = new ExpectedXfer[] { + new ExpectedXfer(DATA_ONLY_NAME, FilePart.DataFork, DATA_ONLY_DLEN), + new ExpectedXfer(SUBDIR2, FilePart.Unknown, -1), + new ExpectedXfer(SUBDIR2 + '/' + DATA_ONLY_NAME, FilePart.DataFork, DATA_ONLY_DLEN), + }; + + private static void TestRerootXfer(IFileSystem srcFs, ParamsBag parms) { + IFileEntry rootDir = srcFs.GetVolDirEntry(); + IFileEntry baseDir = srcFs.FindFileEntry(rootDir, SUBDIR1); + // Deliberately use rootDir here rather than baseDir to exercise exclusion of + // entries that live below the root. + List entries = GatherDiskEntries(srcFs, rootDir); + + ClipFileSet clipSet = new ClipFileSet(srcFs, entries, baseDir, + parms.Preserve, parms.Raw, parms.StripPaths, parms.MacZip, + null, null, parms.AppHook); + + TestXfer(clipSet, sXferProDOSReroot, parms); + } + + private static List GatherDiskEntries(IFileSystem fs, IFileEntry rootDir) { + List entryList = new List(); + GetDiskEntries(fs, rootDir, entryList); + return entryList; + } + + private static void GetDiskEntries(IFileSystem fs, IFileEntry dir, + List entryList) { + foreach (IFileEntry entry in dir) { + entryList.Add(entry); + if (entry.IsDirectory) { + GetDiskEntries(fs, entry, entryList); + } + } + } + + private static void TestXfer(ClipFileSet clipSet, ExpectedXfer[] expected, ParamsBag parms) { + if (clipSet.XferEntries.Count != expected.Length) { + throw new Exception("Xfer extract count mismatch: expected=" + expected.Length + + ", actual=" + clipSet.XferEntries.Count); + } + for (int i = 0; i < clipSet.XferEntries.Count; i++) { + ClipFileEntry clipEntry = clipSet.XferEntries[i]; + ExpectedXfer exp = expected[i]; + if (clipEntry.ExtractPath != exp.PathName) { + throw new Exception("Xfer mismatch " + i + ": expected='" + exp.PathName + + "', actual='" + clipEntry.ExtractPath + "'"); + } + if (clipEntry.Attribs.IsDirectory) { + continue; + } + if (clipEntry.Part != exp.Part) { + throw new Exception("Xfer part mismatch " + i + ": expected=" + exp.Part + + ", actual=" + clipEntry.Part); + } + if (clipEntry.Attribs.FileType != PRODOS_FILETYPE || + clipEntry.Attribs.AuxType != PRODOS_AUXTYPE || + clipEntry.Attribs.CreateWhen != CREATE_WHEN || + clipEntry.Attribs.ModWhen != MOD_WHEN) { + throw new Exception("Xfer attribute mismatch " + i + ": " + + clipEntry.ExtractPath); + } + // Test reported length. In many cases the output length can't be known. + if (clipEntry.OutputLength >= 0 && clipEntry.OutputLength != exp.Length) { + throw new Exception("Xfer output length mismatch " + i + ": expected=" + + exp.Length + ", actual=" + clipEntry.OutputLength); + } + } + } } }