Towards dump implementation. Add logging to dump.py. Use and clean up temp dir for dump tests

This commit is contained in:
Simeon Warner 2014-06-19 09:23:48 -04:00
parent 874f8ba891
commit 0ff75050f2
16 changed files with 345 additions and 43 deletions

View File

@ -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

View File

@ -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)

View File

@ -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)

1
resync/test/testdata/a_to_z/a vendored Normal file
View File

@ -0,0 +1 @@
aaaaaaaaa

18
resync/test/testdata/a_to_z/b vendored Normal file
View File

@ -0,0 +1,18 @@
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb

1
resync/test/testdata/a_to_z/c vendored Normal file
View File

@ -0,0 +1 @@
ccccccccccccccccccccccccccccccc

1
resync/test/testdata/a_to_z/d vendored Normal file
View File

@ -0,0 +1 @@
dddddddddddd

1
resync/test/testdata/a_to_z/e vendored Normal file
View File

@ -0,0 +1 @@
eeeeeeeeeeeeeeeeeee

25
resync/test/testdata/a_to_z/f vendored Normal file
View File

@ -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

1
resync/test/testdata/a_to_z/g vendored Normal file
View File

@ -0,0 +1 @@
ggggggg

1
resync/test/testdata/a_to_z/h vendored Normal file
View File

@ -0,0 +1 @@
hhhhhhhhhhhhhhhhhh

1
resync/test/testdata/a_to_z/i vendored Normal file
View File

@ -0,0 +1 @@
iiii

1
resync/test/testdata/a_to_z/j vendored Normal file
View File

@ -0,0 +1 @@
jjjjjjjj

24
resync/test/testdata/a_to_z/k vendored Normal file
View File

@ -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

1
resync/test/testdata/a_to_z/l vendored Normal file
View File

@ -0,0 +1 @@
lllll

1
resync/test/testdata/a_to_z/m vendored Normal file
View File

@ -0,0 +1 @@
m