From 0ff75050f23c2a04073eb9b1d0149281b6ba3e72 Mon Sep 17 00:00:00 2001 From: Simeon Warner Date: Thu, 19 Jun 2014 09:23:48 -0400 Subject: [PATCH] Towards dump implementation. Add logging to dump.py. Use and clean up temp dir for dump tests --- resync/dump.py | 140 +++++++++++++++++++++-------- resync/resource_dump.py | 10 +++ resync/test/test_dump.py | 161 ++++++++++++++++++++++++++++++++-- resync/test/testdata/a_to_z/a | 1 + resync/test/testdata/a_to_z/b | 18 ++++ resync/test/testdata/a_to_z/c | 1 + resync/test/testdata/a_to_z/d | 1 + resync/test/testdata/a_to_z/e | 1 + resync/test/testdata/a_to_z/f | 25 ++++++ resync/test/testdata/a_to_z/g | 1 + resync/test/testdata/a_to_z/h | 1 + resync/test/testdata/a_to_z/i | 1 + resync/test/testdata/a_to_z/j | 1 + resync/test/testdata/a_to_z/k | 24 +++++ resync/test/testdata/a_to_z/l | 1 + resync/test/testdata/a_to_z/m | 1 + 16 files changed, 345 insertions(+), 43 deletions(-) create mode 100644 resync/test/testdata/a_to_z/a create mode 100644 resync/test/testdata/a_to_z/b create mode 100644 resync/test/testdata/a_to_z/c create mode 100644 resync/test/testdata/a_to_z/d create mode 100644 resync/test/testdata/a_to_z/e create mode 100644 resync/test/testdata/a_to_z/f create mode 100644 resync/test/testdata/a_to_z/g create mode 100644 resync/test/testdata/a_to_z/h create mode 100644 resync/test/testdata/a_to_z/i create mode 100644 resync/test/testdata/a_to_z/j create mode 100644 resync/test/testdata/a_to_z/k create mode 100644 resync/test/testdata/a_to_z/l create mode 100644 resync/test/testdata/a_to_z/m diff --git a/resync/dump.py b/resync/dump.py index ddff6af..ca98c1c 100644 --- a/resync/dump.py +++ b/resync/dump.py @@ -1,8 +1,9 @@ """Dump handler for ResourceSync""" +import logging import os.path from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED -from sitemap import Sitemap +from resync.resource_dump_manifest import ResourceDumpManifest class DumpError(Exception): pass @@ -10,49 +11,79 @@ class DumpError(Exception): class Dump(object): """Dump of content for a Resource Dump or Change Dump - The resource_list must be comprised of Resource objects + The resources itearable must be comprised of Resource objects which have the path attributes set to indicate the local location of the copies of the resources. + + rl = ResourceList() + # ... add items by whatever means, may have >50k items and/or + # >100MB total size of files ... + d = Dump(rl) + d.write(dumpfile="/tmp/rd") """ - def __init__(self, format=None, compress=True): + def __init__(self, resources=None, format=None, compress=True): + self.resources = resources self.format = ('zip' if (format is None) else format) self.compress = compress + self.manifest_class = ResourceDumpManifest #FIXME self.max_size = 100*1024*1024 #100MB self.max_files = 50000 self.path_prefix = None + self.logger = logging.getLogger('dump') - def write(self, resource_list=None, dumpfile=None): - """Wrapper to Write a dump file""" - self.check_files(resource_list) - if (self.format == 'zip'): - self.write_zip(resource_list,dumpfile) - elif (self.format == 'warc'): - self.write_warc(resource_list,dumpfile) - else: - raise DumpError("Unknown dump format '%s'" % (self.format)) + def write(self, basename=None, write_separate_manifests=True): + """Write one or more dump files to complete this dump - def write_zip(self, resource_list=None, dumpfile=None): - """Write a ZIP format dump file""" + Returns the number of dump/archive files written. + """ + self.check_files() + n=0 + for manifest in self.partition_dumps(): + dumpbase="%s%05d" % (basename,n) + dumpfile="%s.%s" % (dumpbase,self.format) + if (write_separate_manifests): + manifest.write(basename=dumpbase+'.xml') + if (self.format == 'zip'): + self.write_zip(manifest.resources,dumpfile) + elif (self.format == 'warc'): + self.write_warc(manifest.resources,dumpfile) + else: + raise DumpError("Unknown dump format requested (%s)" % (self.format)) + n+=1 + self.logger.info("Wrote %d dump files" % (n)) + return(n) + + def write_zip(self, resources=None, dumpfile=None): + """Write a ZIP format dump file + + Writes a ZIP file containing the resources in the iterable resources along with + a manifest file manifest.xml (written first). No checks on the size of files + or total size are performed, this is expected to have been done beforehand. + """ compression = ( ZIP_DEFLATED if self.compress else ZIP_STORED ) zf = ZipFile(dumpfile, mode="w", compression=compression, allowZip64=True) - # Write resource_list first - s = Sitemap() + # Write resources first + rdm = ResourceDumpManifest(resources=resources) real_path = {} - for resource in resource_list: + for resource in resources: archive_path = self.archive_path(resource.path) real_path[archive_path] = resource.path resource.path = archive_path - zf.writestr('manifest.xml',s.resources_as_xml(resource_list)) - # Add all files in the resource_list - for resource in resource_list: + zf.writestr('manifest.xml',rdm.as_xml()) + # Add all files in the resources + for resource in resources: zf.write(real_path[resource.path], arcname=resource.path) zf.close() zipsize = os.path.getsize(dumpfile) - print "Wrote ZIP file dump %s with size %d bytes" % (dumpfile,zipsize) + self.logger.info("Wrote ZIP file dump %s with size %d bytes" % (dumpfile,zipsize)) - def write_warc(self, resource_list=None, dumpfile=None): - """Write a WARC dump file""" + def write_warc(self, resources=None, dumpfile=None): + """Write a WARC dump file + + WARC support is not part of ResourceSync v1.0 (Z39.99 2014) but is left + in this library for experimentation. + """ # Load library late as we want to be able to run rest of code # without this installed try: @@ -60,8 +91,8 @@ class Dump(object): except: raise DumpError("Failed to load WARC library") wf = WARCFile(dumpfile, mode="w", compress=self.compress) - # Add all files in the resource_list - for resource in resource_list: + # Add all files in the resources + for resource in resources: wh = WARCHeader({}) wh.url = resource.uri wh.ip_address = None @@ -73,19 +104,21 @@ class Dump(object): wf.write_record( WARCRecord( header=wh, payload=resource.path ) ) wf.close() warcsize = os.path.getsize(dumpfile) - print "Wrote WARC file dump %s with size %d bytes" % (dumpfile,warcsize) + self.logging.info("Wrote WARC file dump %s with size %d bytes" % (dumpfile,warcsize)) - def check_files(self,resource_list): - """Go though and check all files in resource_list, add up size, find common path + def check_files(self,set_length=True,check_length=True): + """Go though and check all files in self.resources, add up size, and find + longest common path that can be used when writing the dump file. Saved in + self.path_prefix. - Will find the longest common path prefix that can be used when writing the - dump file. Saved in self.path_prefix. + Parameters set_length and check_length control control whether then set_length + attribute should be set from the file size if not specified, and whether any + length specified should be checked. By default both are True. In any event, the + total size calculated is the size of files on disk. """ - if (len(resource_list) > self.max_files): - raise DumpError("Number of files to dump (%d) exceeds maximum (%d)" % (len(resource_list),self.max_files)) total_size = 0 #total size of all files in bytes path_prefix = None - for resource in resource_list: + for resource in self.resources: if (resource.path is None): #explicit test because exception raised by getsize otherwise confusing raise DumpError("No file path defined for resource %s" % resource.uri) @@ -93,12 +126,45 @@ class Dump(object): path_prefix = os.path.dirname(resource.path) else: path_prefix = os.path.commonprefix( [ path_prefix, os.path.dirname(resource.path) ]) - total_size += os.path.getsize(resource.path) + size = os.path.getsize(resource.path) + if (resource.length is not None): + if (check_length and resource.length!=size): + raise DumpError("Size of resource %s is %d on disk, not %d as specified" % + (resource.uri, size, resource.length) ) + elif (set_length): + resource.length = size + if (size > self.max_size): + raise DumpError("Size of file (%s, %d) exceeds maximum (%d) dump size" % (resource.path,size,self.max_size)) + total_size += size self.path_prefix = path_prefix self.total_size = total_size - print "Total size of files to include in dump %d bytes" % (total_size) - if (total_size > self.max_size): - raise DumpError("Size of files to dump (%d) exceeds maximum (%d)" % (total_size,self.max_size)) + self.logger.info("Total size of files to include in dump %d bytes" % (total_size)) + return True + + def partition_dumps(self): + """Yeild a set of manifest object that parition the dumps + + Simply adds resources/files to a manifest until their are either the + the correct number of files or the size limit is exceeded, then yields + that manifest. + """ + manifest=self.manifest_class() + manifest_size=0 + manifest_files=0 + for resource in self.resources: + manifest.add(resource) + manifest_size+=resource.length + manifest_files+=1 + if (manifest_size>=self.max_size or + manifest_files>=self.max_files): + yield(manifest) + # Need to start a new manifest + manifest=self.manifest_class() + manifest_size=0 + manifest_files=0 + if (manifest_files>0): + yield(manifest) + def archive_path(self,real_path): """Return the archive path for file with real_path diff --git a/resync/resource_dump.py b/resync/resource_dump.py index e8b1a29..e0cc112 100644 --- a/resync/resource_dump.py +++ b/resync/resource_dump.py @@ -17,9 +17,19 @@ class ResourceDump(ResourceList): A Resource Dump comprises a set of content packages that are the resources listed. Properties similar to a ResourceList and implemented as a sub-class of ResourceList. + + See the Dump class for details of how to create a Resource + Dump from a ResourceList. """ def __init__(self, resources=None, md=None, ln=None, uri=None, allow_multifile=None, mapper=None): super(ResourceDump, self).__init__(resources=resources, md=md, ln=ln, uri=uri, mapper=mapper) self.capability_name='resourcedump' + + def write(self, basename="/tmp/resource_dump.xml", dumpfile=None): + """Write out a Resource Dump document and optionally also the actual dump files + + Dump files will be written if the dumpfile argument is set to a base filename. + """ + super(ResourceDump,self).write(basename) diff --git a/resync/test/test_dump.py b/resync/test/test_dump.py index 2c3c56b..e538d3d 100644 --- a/resync/test/test_dump.py +++ b/resync/test/test_dump.py @@ -1,19 +1,168 @@ +import os.path import unittest +import tempfile +import shutil +import sys +import zipfile from resync.dump import Dump, DumpError from resync.resource_list import ResourceList +from resync.change_list import ChangeList from resync.resource import Resource +from resync.resource_dump_manifest import ResourceDumpManifest +from resync.change_dump_manifest import ChangeDumpManifest class TestDump(unittest.TestCase): - def test00_dump_creation(self): - i=ResourceList() - i.add( Resource('http://ex.org/a', length=1, path='resync/test/testdata/a') ) - i.add( Resource('http://ex.org/b', length=2, path='resync/test/testdata/b') ) + _tmpdir=None + + @classmethod + def setUpClass(cls): + # Create tmp dir to write to and check + cls._tmpdir=tempfile.mkdtemp() + if (not os.path.isdir(cls._tmpdir)): + raise Exception("Failed to create tempdir to use for dump tests") + + @classmethod + def tearDownClass(cls): + # Cleanup + if (not os.path.isdir(cls._tmpdir)): + raise Exception("Ooops, no tempdir (%s) to clean up?" % (tmpdir)) + shutil.rmtree(cls._tmpdir) + + @property + def tmpdir(self): + # read-only access to _tmpdir, just in case... The rmtree scares me + return(self._tmpdir) + + def test00_dump_zip_resource_list(self): + rl=ResourceDumpManifest() + rl.add( Resource('http://ex.org/a', length=7, path='resync/test/testdata/a') ) + rl.add( Resource('http://ex.org/b', length=21, path='resync/test/testdata/b') ) d=Dump() - d.check_files(resource_list=i) + zipf=os.path.join(self.tmpdir,"test00_dump.zip") + d.write_zip(resources=rl,dumpfile=zipf) # named args + self.assertTrue( os.path.exists(zipf) ) + self.assertTrue( zipfile.is_zipfile(zipf) ) + zo=zipfile.ZipFile(zipf,'r') + self.assertEqual( len(zo.namelist()), 3 ) + zo.close() + os.unlink(zipf) + + def test01_dump_zip_change_list(self): + cl=ChangeDumpManifest() + cl.add( Resource('http://ex.org/a', length=7, path='resync/test/testdata/a', change="updated") ) + cl.add( Resource('http://ex.org/b', length=21, path='resync/test/testdata/b', change="updated") ) + d=Dump() + zipf=os.path.join(self.tmpdir,"test01_dump.zip") + d.write_zip(cl,zipf) # positional args + self.assertTrue( os.path.exists(zipf) ) + self.assertTrue( zipfile.is_zipfile(zipf) ) + zo=zipfile.ZipFile(zipf,'r') + self.assertEqual( len(zo.namelist()), 3 ) + zo.close() + os.unlink(zipf) + + def test02_dump_check_files(self): + cl=ChangeList() + cl.add( Resource('http://ex.org/a', length=7, path='resync/test/testdata/a', change="updated") ) + cl.add( Resource('http://ex.org/b', length=21, path='resync/test/testdata/b', change="updated") ) + d=Dump(resources=cl) + self.assertTrue(d.check_files()) self.assertEqual(d.total_size, 28) - #FIXME -- need some code to actually write and read dump + def test03_dump_multi_file_max_size(self): + rl=ResourceList() + for letter in map(chr,xrange(ord('a'),ord('l')+1)): + uri='http://ex.org/%s' % (letter) + fname='resync/test/testdata/a_to_z/%s' % (letter) + rl.add( Resource(uri, path=fname) ) + self.assertEqual( len(rl), 12 ) + #d=Dump(rl) + #tmpdir=tempfile.mkdtemp() + #tmpbase=os.path.join(tmpdir,'base') + #d.max_size=2000 # start new zip after size exceeds 2000 bytes + #n=d.write(tmpbase) + #self.assertEqual( n, 2, 'expect to write 2 dump files' ) + # + # Now repeat with large size limit but small number of files limit + d2=Dump(rl) + tmpbase=os.path.join(self.tmpdir,'test03_') + d2.max_files=4 + n=d2.write(tmpbase) + self.assertEqual( n, 3, 'expect to write 3 dump files' ) + self.assertTrue( os.path.isfile(tmpbase+'00000.zip') ) + self.assertTrue( os.path.isfile(tmpbase+'00001.zip') ) + self.assertTrue( os.path.isfile(tmpbase+'00002.zip') ) + # Look at the first file in detail + zipf=tmpbase+'00000.zip' + zo=zipfile.ZipFile(zipf,'r') + self.assertEqual( zo.namelist(), ['manifest.xml','a','b','c','d'] ) + #self.assertEqual( zo.getinfo('manifest.xml').file_size, 470 ) + self.assertEqual( zo.getinfo('a').file_size, 9 ) + self.assertEqual( zo.getinfo('b').file_size, 1116 ) + self.assertEqual( zo.getinfo('c').file_size, 32 ) + self.assertEqual( zo.getinfo('d').file_size, 13 ) + zo.close() + os.unlink(zipf) + # Check second and third files have expected contents + zipf=tmpbase+'00001.zip' + zo=zipfile.ZipFile(zipf,'r') + self.assertEqual( zo.namelist(), ['manifest.xml','e','f','g','h'] ) + zo.close() + os.unlink(zipf) + zipf=tmpbase+'00002.zip' + zo=zipfile.ZipFile(zipf,'r') + self.assertEqual( zo.namelist(), ['manifest.xml','i','j','k','l'] ) + zo.close() + os.unlink(zipf) + + def test04_dump_multi_file_max_size(self): + rl=ResourceList() + for letter in map(chr,xrange(ord('a'),ord('l')+1)): + uri='http://ex.org/%s' % (letter) + fname='resync/test/testdata/a_to_z/%s' % (letter) + rl.add( Resource(uri, path=fname) ) + self.assertEqual( len(rl), 12 ) + d2=Dump(rl) + tmpbase=os.path.join(self.tmpdir,'test0f_') + d2.max_size=2000 + n=d2.write(tmpbase) + self.assertEqual( n, 2, 'expect to write 2 dump files' ) + self.assertTrue( os.path.isfile(tmpbase+'00000.zip') ) + self.assertTrue( os.path.isfile(tmpbase+'00001.zip') ) + # Look at the first file in detail + zipf=tmpbase+'00000.zip' + zo=zipfile.ZipFile(zipf,'r') + self.assertEqual( zo.namelist(), ['manifest.xml','a','b','c','d','e','f'] ) + #self.assertEqual( zo.getinfo('manifest.xml').file_size, 470 ) + self.assertEqual( zo.getinfo('a').file_size, 9 ) + self.assertEqual( zo.getinfo('b').file_size, 1116 ) + self.assertEqual( zo.getinfo('c').file_size, 32 ) + self.assertEqual( zo.getinfo('d').file_size, 13 ) + self.assertEqual( zo.getinfo('e').file_size, 20 ) + self.assertEqual( zo.getinfo('f').file_size, 1625 ) + zo.close() + os.unlink(zipf) + # Check second and third files have expected contents + zipf=tmpbase+'00001.zip' + zo=zipfile.ZipFile(zipf,'r') + self.assertEqual( zo.namelist(), ['manifest.xml','g','h','i','j','k','l'] ) + zo.close() + os.unlink(zipf) + + def test10_no_path(self): + rl=ResourceList() + rl.add( Resource('http://ex.org/a', length=7, path='resync/test/testdata/a') ) + rl.add( Resource('http://ex.org/b', length=21 ) ) + d=Dump(rl) + self.assertRaises( DumpError, d.check_files ) + + def test11_bad_size(self): + rl=ResourceList() + rl.add( Resource('http://ex.org/a', length=9999, path='resync/test/testdata/a') ) + d=Dump(rl) + self.assertTrue( d.check_files(check_length=False) ) + self.assertRaises( DumpError, d.check_files ) if __name__ == '__main__': suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestDump) diff --git a/resync/test/testdata/a_to_z/a b/resync/test/testdata/a_to_z/a new file mode 100644 index 0000000..9606d7d --- /dev/null +++ b/resync/test/testdata/a_to_z/a @@ -0,0 +1 @@ +aaaaaaaaa \ No newline at end of file diff --git a/resync/test/testdata/a_to_z/b b/resync/test/testdata/a_to_z/b new file mode 100644 index 0000000..742ded7 --- /dev/null +++ b/resync/test/testdata/a_to_z/b @@ -0,0 +1,18 @@ +bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb diff --git a/resync/test/testdata/a_to_z/c b/resync/test/testdata/a_to_z/c new file mode 100644 index 0000000..935af58 --- /dev/null +++ b/resync/test/testdata/a_to_z/c @@ -0,0 +1 @@ +ccccccccccccccccccccccccccccccc diff --git a/resync/test/testdata/a_to_z/d b/resync/test/testdata/a_to_z/d new file mode 100644 index 0000000..b7a5ef5 --- /dev/null +++ b/resync/test/testdata/a_to_z/d @@ -0,0 +1 @@ +dddddddddddd diff --git a/resync/test/testdata/a_to_z/e b/resync/test/testdata/a_to_z/e new file mode 100644 index 0000000..a41d7e9 --- /dev/null +++ b/resync/test/testdata/a_to_z/e @@ -0,0 +1 @@ +eeeeeeeeeeeeeeeeeee diff --git a/resync/test/testdata/a_to_z/f b/resync/test/testdata/a_to_z/f new file mode 100644 index 0000000..086e67e --- /dev/null +++ b/resync/test/testdata/a_to_z/f @@ -0,0 +1,25 @@ +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff diff --git a/resync/test/testdata/a_to_z/g b/resync/test/testdata/a_to_z/g new file mode 100644 index 0000000..101e504 --- /dev/null +++ b/resync/test/testdata/a_to_z/g @@ -0,0 +1 @@ +ggggggg diff --git a/resync/test/testdata/a_to_z/h b/resync/test/testdata/a_to_z/h new file mode 100644 index 0000000..0fd2680 --- /dev/null +++ b/resync/test/testdata/a_to_z/h @@ -0,0 +1 @@ +hhhhhhhhhhhhhhhhhh diff --git a/resync/test/testdata/a_to_z/i b/resync/test/testdata/a_to_z/i new file mode 100644 index 0000000..a9534b1 --- /dev/null +++ b/resync/test/testdata/a_to_z/i @@ -0,0 +1 @@ +iiii diff --git a/resync/test/testdata/a_to_z/j b/resync/test/testdata/a_to_z/j new file mode 100644 index 0000000..51a877a --- /dev/null +++ b/resync/test/testdata/a_to_z/j @@ -0,0 +1 @@ +jjjjjjjj diff --git a/resync/test/testdata/a_to_z/k b/resync/test/testdata/a_to_z/k new file mode 100644 index 0000000..f33060d --- /dev/null +++ b/resync/test/testdata/a_to_z/k @@ -0,0 +1,24 @@ +kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk +kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk +kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk +kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk +kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk +kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk +kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk +kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk +kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk +kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk +kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk +kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk +kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk +kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk +kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk +kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk +kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk +kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk +kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk +kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk +kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk +kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk +kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk +kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk diff --git a/resync/test/testdata/a_to_z/l b/resync/test/testdata/a_to_z/l new file mode 100644 index 0000000..a4a8dab --- /dev/null +++ b/resync/test/testdata/a_to_z/l @@ -0,0 +1 @@ +lllll diff --git a/resync/test/testdata/a_to_z/m b/resync/test/testdata/a_to_z/m new file mode 100644 index 0000000..28ce6a8 --- /dev/null +++ b/resync/test/testdata/a_to_z/m @@ -0,0 +1 @@ +m