Move some scripts that were in development/tools/scripts back in this project
They were moved into sdk/scripts when sdk was split from development. Change-Id: I8404ae5fdeb9060adb76357f29b42c4c8e2054ee
This commit is contained in:
489
scripts/divide_and_compress_test.py
Executable file
489
scripts/divide_and_compress_test.py
Executable file
@@ -0,0 +1,489 @@
|
||||
#!/usr/bin/python2.4
|
||||
#
|
||||
# Copyright (C) 2008 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
"""Tests for divide_and_compress.py.
|
||||
|
||||
TODO(jmatt): Add tests for module methods.
|
||||
"""
|
||||
|
||||
__author__ = 'jmatt@google.com (Justin Mattson)'
|
||||
|
||||
import os
|
||||
import stat
|
||||
import unittest
|
||||
import zipfile
|
||||
|
||||
import divide_and_compress
|
||||
import mox
|
||||
|
||||
|
||||
class BagOfParts(object):
|
||||
"""Just a generic class that I can use to assign random attributes to."""
|
||||
|
||||
def NoOp(self):
|
||||
x = 1
|
||||
|
||||
|
||||
class ValidAndRemoveTests(unittest.TestCase):
|
||||
"""Test the ArchiveIsValid and RemoveLastFile methods."""
|
||||
|
||||
def setUp(self):
|
||||
"""Prepare the test.
|
||||
|
||||
Construct some mock objects for use with the tests.
|
||||
"""
|
||||
self.my_mox = mox.Mox()
|
||||
file1 = BagOfParts()
|
||||
file1.filename = 'file1.txt'
|
||||
file1.contents = 'This is a test file'
|
||||
file2 = BagOfParts()
|
||||
file2.filename = 'file2.txt'
|
||||
file2.contents = ('akdjfk;djsf;kljdslkfjslkdfjlsfjkdvn;kn;2389rtu4i'
|
||||
'tn;ghf8:89H*hp748FJw80fu9WJFpwf39pujens;fihkhjfk'
|
||||
'sdjfljkgsc n;iself')
|
||||
self.files = {'file1': file1, 'file2': file2}
|
||||
|
||||
def tearDown(self):
|
||||
"""Remove any stubs we've created."""
|
||||
self.my_mox.UnsetStubs()
|
||||
|
||||
def testArchiveIsValid(self):
|
||||
"""Test the DirectoryZipper.ArchiveIsValid method.
|
||||
|
||||
Run two tests, one that we expect to pass and one that we expect to fail
|
||||
"""
|
||||
test_file_size = 1056730
|
||||
self.my_mox.StubOutWithMock(os, 'stat')
|
||||
os.stat('/foo/0.zip').AndReturn([test_file_size])
|
||||
self.my_mox.StubOutWithMock(stat, 'ST_SIZE')
|
||||
stat.ST_SIZE = 0
|
||||
os.stat('/baz/0.zip').AndReturn([test_file_size])
|
||||
mox.Replay(os.stat)
|
||||
test_target = divide_and_compress.DirectoryZipper('/foo/', 'bar',
|
||||
test_file_size - 1, True)
|
||||
|
||||
self.assertEqual(False, test_target.ArchiveIsValid(),
|
||||
msg=('ERROR: Test failed, ArchiveIsValid should have '
|
||||
'returned false, but returned true'))
|
||||
|
||||
test_target = divide_and_compress.DirectoryZipper('/baz/', 'bar',
|
||||
test_file_size + 1, True)
|
||||
self.assertEqual(True, test_target.ArchiveIsValid(),
|
||||
msg=('ERROR: Test failed, ArchiveIsValid should have'
|
||||
' returned true, but returned false'))
|
||||
|
||||
def testRemoveLastFile(self):
|
||||
"""Test DirectoryZipper.RemoveLastFile method.
|
||||
|
||||
Construct a ZipInfo mock object with two records, verify that write is
|
||||
only called once on the new ZipFile object.
|
||||
"""
|
||||
source = self.CreateZipSource()
|
||||
dest = self.CreateZipDestination()
|
||||
source_path = ''.join([os.getcwd(), '/0-old.zip'])
|
||||
dest_path = ''.join([os.getcwd(), '/0.zip'])
|
||||
test_target = divide_and_compress.DirectoryZipper(
|
||||
''.join([os.getcwd(), '/']), 'dummy', 1024*1024, True)
|
||||
self.my_mox.StubOutWithMock(test_target, 'OpenZipFileAtPath')
|
||||
test_target.OpenZipFileAtPath(source_path, mode='r').AndReturn(source)
|
||||
test_target.OpenZipFileAtPath(dest_path,
|
||||
compress=zipfile.ZIP_DEFLATED,
|
||||
mode='w').AndReturn(dest)
|
||||
self.my_mox.StubOutWithMock(os, 'rename')
|
||||
os.rename(dest_path, source_path)
|
||||
self.my_mox.StubOutWithMock(os, 'unlink')
|
||||
os.unlink(source_path)
|
||||
|
||||
self.my_mox.ReplayAll()
|
||||
test_target.RemoveLastFile()
|
||||
self.my_mox.VerifyAll()
|
||||
|
||||
def CreateZipSource(self):
|
||||
"""Create a mock zip sourec object.
|
||||
|
||||
Read should only be called once, because the second file is the one
|
||||
being removed.
|
||||
|
||||
Returns:
|
||||
A configured mocked
|
||||
"""
|
||||
|
||||
source_zip = self.my_mox.CreateMock(zipfile.ZipFile)
|
||||
source_zip.infolist().AndReturn([self.files['file1'], self.files['file1']])
|
||||
source_zip.infolist().AndReturn([self.files['file1'], self.files['file1']])
|
||||
source_zip.read(self.files['file1'].filename).AndReturn(
|
||||
self.files['file1'].contents)
|
||||
source_zip.close()
|
||||
return source_zip
|
||||
|
||||
def CreateZipDestination(self):
|
||||
"""Create mock destination zip.
|
||||
|
||||
Write should only be called once, because there are two files in the
|
||||
source zip and we expect the second to be removed.
|
||||
|
||||
Returns:
|
||||
A configured mocked
|
||||
"""
|
||||
|
||||
dest_zip = mox.MockObject(zipfile.ZipFile)
|
||||
dest_zip.writestr(self.files['file1'].filename,
|
||||
self.files['file1'].contents)
|
||||
dest_zip.close()
|
||||
return dest_zip
|
||||
|
||||
|
||||
class FixArchiveTests(unittest.TestCase):
|
||||
"""Tests for the DirectoryZipper.FixArchive method."""
|
||||
|
||||
def setUp(self):
|
||||
"""Create a mock file object."""
|
||||
self.my_mox = mox.Mox()
|
||||
self.file1 = BagOfParts()
|
||||
self.file1.filename = 'file1.txt'
|
||||
self.file1.contents = 'This is a test file'
|
||||
|
||||
def tearDown(self):
|
||||
"""Unset any mocks that we've created."""
|
||||
self.my_mox.UnsetStubs()
|
||||
|
||||
def _InitMultiFileData(self):
|
||||
"""Create an array of mock file objects.
|
||||
|
||||
Create three mock file objects that we can use for testing.
|
||||
"""
|
||||
self.multi_file_dir = []
|
||||
|
||||
file1 = BagOfParts()
|
||||
file1.filename = 'file1.txt'
|
||||
file1.contents = 'kjaskl;jkdjfkja;kjsnbvjnvnbuewklriujalvjsd'
|
||||
self.multi_file_dir.append(file1)
|
||||
|
||||
file2 = BagOfParts()
|
||||
file2.filename = 'file2.txt'
|
||||
file2.contents = ('He entered the room and there in the center, it was.'
|
||||
' Looking upon the thing, suddenly he could not remember'
|
||||
' whether he had actually seen it before or whether'
|
||||
' his memory of it was merely the effect of something'
|
||||
' so often being imagined that it had long since become '
|
||||
' manifest in his mind.')
|
||||
self.multi_file_dir.append(file2)
|
||||
|
||||
file3 = BagOfParts()
|
||||
file3.filename = 'file3.txt'
|
||||
file3.contents = 'Whoa, what is \'file2.txt\' all about?'
|
||||
self.multi_file_dir.append(file3)
|
||||
|
||||
def testSingleFileArchive(self):
|
||||
"""Test behavior of FixArchive when the archive has a single member.
|
||||
|
||||
We expect that when this method is called with an archive that has a
|
||||
single member that it will return False and unlink the archive.
|
||||
"""
|
||||
test_target = divide_and_compress.DirectoryZipper(
|
||||
''.join([os.getcwd(), '/']), 'dummy', 1024*1024, True)
|
||||
self.my_mox.StubOutWithMock(test_target, 'OpenZipFileAtPath')
|
||||
test_target.OpenZipFileAtPath(
|
||||
''.join([os.getcwd(), '/0.zip']), mode='r').AndReturn(
|
||||
self.CreateSingleFileMock())
|
||||
self.my_mox.StubOutWithMock(os, 'unlink')
|
||||
os.unlink(''.join([os.getcwd(), '/0.zip']))
|
||||
self.my_mox.ReplayAll()
|
||||
self.assertEqual(False, test_target.FixArchive('SIZE'))
|
||||
self.my_mox.VerifyAll()
|
||||
|
||||
def CreateSingleFileMock(self):
|
||||
"""Create a mock ZipFile object for testSingleFileArchive.
|
||||
|
||||
We just need it to return a single member infolist twice
|
||||
|
||||
Returns:
|
||||
A configured mock object
|
||||
"""
|
||||
mock_zip = self.my_mox.CreateMock(zipfile.ZipFile)
|
||||
mock_zip.infolist().AndReturn([self.file1])
|
||||
mock_zip.infolist().AndReturn([self.file1])
|
||||
mock_zip.close()
|
||||
return mock_zip
|
||||
|
||||
def testMultiFileArchive(self):
|
||||
"""Test behavior of DirectoryZipper.FixArchive with a multi-file archive.
|
||||
|
||||
We expect that FixArchive will rename the old archive, adding '-old' before
|
||||
'.zip', read all the members except the last one of '-old' into a new
|
||||
archive with the same name as the original, and then unlink the '-old' copy
|
||||
"""
|
||||
test_target = divide_and_compress.DirectoryZipper(
|
||||
''.join([os.getcwd(), '/']), 'dummy', 1024*1024, True)
|
||||
self.my_mox.StubOutWithMock(test_target, 'OpenZipFileAtPath')
|
||||
test_target.OpenZipFileAtPath(
|
||||
''.join([os.getcwd(), '/0.zip']), mode='r').AndReturn(
|
||||
self.CreateMultiFileMock())
|
||||
self.my_mox.StubOutWithMock(test_target, 'RemoveLastFile')
|
||||
test_target.RemoveLastFile(''.join([os.getcwd(), '/0.zip']))
|
||||
self.my_mox.StubOutWithMock(os, 'stat')
|
||||
os.stat(''.join([os.getcwd(), '/0.zip'])).AndReturn([49302])
|
||||
self.my_mox.StubOutWithMock(stat, 'ST_SIZE')
|
||||
stat.ST_SIZE = 0
|
||||
self.my_mox.ReplayAll()
|
||||
self.assertEqual(True, test_target.FixArchive('SIZE'))
|
||||
self.my_mox.VerifyAll()
|
||||
|
||||
def CreateMultiFileMock(self):
|
||||
"""Create mock ZipFile object for use with testMultiFileArchive.
|
||||
|
||||
The mock just needs to return the infolist mock that is prepared in
|
||||
InitMultiFileData()
|
||||
|
||||
Returns:
|
||||
A configured mock object
|
||||
"""
|
||||
self._InitMultiFileData()
|
||||
mock_zip = self.my_mox.CreateMock(zipfile.ZipFile)
|
||||
mock_zip.infolist().AndReturn(self.multi_file_dir)
|
||||
mock_zip.close()
|
||||
return mock_zip
|
||||
|
||||
|
||||
class AddFileToArchiveTest(unittest.TestCase):
|
||||
"""Test behavior of method to add a file to an archive."""
|
||||
|
||||
def setUp(self):
|
||||
"""Setup the arguments for the DirectoryZipper object."""
|
||||
self.my_mox = mox.Mox()
|
||||
self.output_dir = '%s/' % os.getcwd()
|
||||
self.file_to_add = 'file.txt'
|
||||
self.input_dir = '/foo/bar/baz/'
|
||||
|
||||
def tearDown(self):
|
||||
self.my_mox.UnsetStubs()
|
||||
|
||||
def testAddFileToArchive(self):
|
||||
"""Test the DirectoryZipper.AddFileToArchive method.
|
||||
|
||||
We are testing a pretty trivial method, we just expect it to look at the
|
||||
file its adding, so that it possible can through out a warning.
|
||||
"""
|
||||
test_target = divide_and_compress.DirectoryZipper(self.output_dir,
|
||||
self.input_dir,
|
||||
1024*1024, True)
|
||||
self.my_mox.StubOutWithMock(test_target, 'OpenZipFileAtPath')
|
||||
archive_mock = self.CreateArchiveMock()
|
||||
test_target.OpenZipFileAtPath(
|
||||
''.join([self.output_dir, '0.zip']),
|
||||
compress=zipfile.ZIP_DEFLATED).AndReturn(archive_mock)
|
||||
self.StubOutOsModule()
|
||||
self.my_mox.ReplayAll()
|
||||
test_target.AddFileToArchive(''.join([self.input_dir, self.file_to_add]),
|
||||
zipfile.ZIP_DEFLATED)
|
||||
self.my_mox.VerifyAll()
|
||||
|
||||
def StubOutOsModule(self):
|
||||
"""Create a mock for the os.path and os.stat objects.
|
||||
|
||||
Create a stub that will return the type (file or directory) and size of the
|
||||
object that is to be added.
|
||||
"""
|
||||
self.my_mox.StubOutWithMock(os.path, 'isfile')
|
||||
os.path.isfile(''.join([self.input_dir, self.file_to_add])).AndReturn(True)
|
||||
self.my_mox.StubOutWithMock(os, 'stat')
|
||||
os.stat(''.join([self.input_dir, self.file_to_add])).AndReturn([39480])
|
||||
self.my_mox.StubOutWithMock(stat, 'ST_SIZE')
|
||||
stat.ST_SIZE = 0
|
||||
|
||||
def CreateArchiveMock(self):
|
||||
"""Create a mock ZipFile for use with testAddFileToArchive.
|
||||
|
||||
Just verify that write is called with the file we expect and that the
|
||||
archive is closed after the file addition
|
||||
|
||||
Returns:
|
||||
A configured mock object
|
||||
"""
|
||||
archive_mock = self.my_mox.CreateMock(zipfile.ZipFile)
|
||||
archive_mock.write(''.join([self.input_dir, self.file_to_add]),
|
||||
self.file_to_add)
|
||||
archive_mock.close()
|
||||
return archive_mock
|
||||
|
||||
|
||||
class CompressDirectoryTest(unittest.TestCase):
|
||||
"""Test the master method of the class.
|
||||
|
||||
Testing with the following directory structure.
|
||||
/dir1/
|
||||
/dir1/file1.txt
|
||||
/dir1/file2.txt
|
||||
/dir1/dir2/
|
||||
/dir1/dir2/dir3/
|
||||
/dir1/dir2/dir4/
|
||||
/dir1/dir2/dir4/file3.txt
|
||||
/dir1/dir5/
|
||||
/dir1/dir5/file4.txt
|
||||
/dir1/dir5/file5.txt
|
||||
/dir1/dir5/file6.txt
|
||||
/dir1/dir5/file7.txt
|
||||
/dir1/dir6/
|
||||
/dir1/dir6/file8.txt
|
||||
|
||||
file1.txt., file2.txt, file3.txt should be in 0.zip
|
||||
file4.txt should be in 1.zip
|
||||
file5.txt, file6.txt should be in 2.zip
|
||||
file7.txt will not be stored since it will be too large compressed
|
||||
file8.txt should b in 3.zip
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""Setup all the mocks for this test."""
|
||||
self.my_mox = mox.Mox()
|
||||
|
||||
self.base_dir = '/dir1'
|
||||
self.output_path = '/out_dir/'
|
||||
self.test_target = divide_and_compress.DirectoryZipper(
|
||||
self.output_path, self.base_dir, 1024*1024, True)
|
||||
|
||||
self.InitArgLists()
|
||||
self.InitOsDotPath()
|
||||
self.InitArchiveIsValid()
|
||||
self.InitWriteIndexRecord()
|
||||
self.InitAddFileToArchive()
|
||||
|
||||
def tearDown(self):
|
||||
self.my_mox.UnsetStubs()
|
||||
|
||||
def testCompressDirectory(self):
|
||||
"""Test the DirectoryZipper.CompressDirectory method."""
|
||||
self.my_mox.ReplayAll()
|
||||
for arguments in self.argument_lists:
|
||||
self.test_target.CompressDirectory(None, arguments[0], arguments[1])
|
||||
self.my_mox.VerifyAll()
|
||||
|
||||
def InitAddFileToArchive(self):
|
||||
"""Setup mock for DirectoryZipper.AddFileToArchive.
|
||||
|
||||
Make sure that the files are added in the order we expect.
|
||||
"""
|
||||
self.my_mox.StubOutWithMock(self.test_target, 'AddFileToArchive')
|
||||
self.test_target.AddFileToArchive('/dir1/file1.txt', zipfile.ZIP_DEFLATED)
|
||||
self.test_target.AddFileToArchive('/dir1/file2.txt', zipfile.ZIP_DEFLATED)
|
||||
self.test_target.AddFileToArchive('/dir1/dir2/dir4/file3.txt',
|
||||
zipfile.ZIP_DEFLATED)
|
||||
self.test_target.AddFileToArchive('/dir1/dir5/file4.txt',
|
||||
zipfile.ZIP_DEFLATED)
|
||||
self.test_target.AddFileToArchive('/dir1/dir5/file4.txt',
|
||||
zipfile.ZIP_DEFLATED)
|
||||
self.test_target.AddFileToArchive('/dir1/dir5/file5.txt',
|
||||
zipfile.ZIP_DEFLATED)
|
||||
self.test_target.AddFileToArchive('/dir1/dir5/file5.txt',
|
||||
zipfile.ZIP_DEFLATED)
|
||||
self.test_target.AddFileToArchive('/dir1/dir5/file6.txt',
|
||||
zipfile.ZIP_DEFLATED)
|
||||
self.test_target.AddFileToArchive('/dir1/dir5/file7.txt',
|
||||
zipfile.ZIP_DEFLATED)
|
||||
self.test_target.AddFileToArchive('/dir1/dir5/file7.txt',
|
||||
zipfile.ZIP_DEFLATED)
|
||||
self.test_target.AddFileToArchive('/dir1/dir6/file8.txt',
|
||||
zipfile.ZIP_DEFLATED)
|
||||
|
||||
def InitWriteIndexRecord(self):
|
||||
"""Setup mock for DirectoryZipper.WriteIndexRecord."""
|
||||
self.my_mox.StubOutWithMock(self.test_target, 'WriteIndexRecord')
|
||||
|
||||
# we are trying to compress 8 files, but we should only attempt to
|
||||
# write an index record 7 times, because one file is too large to be stored
|
||||
self.test_target.WriteIndexRecord().AndReturn(True)
|
||||
self.test_target.WriteIndexRecord().AndReturn(False)
|
||||
self.test_target.WriteIndexRecord().AndReturn(False)
|
||||
self.test_target.WriteIndexRecord().AndReturn(True)
|
||||
self.test_target.WriteIndexRecord().AndReturn(True)
|
||||
self.test_target.WriteIndexRecord().AndReturn(False)
|
||||
self.test_target.WriteIndexRecord().AndReturn(True)
|
||||
|
||||
def InitArchiveIsValid(self):
|
||||
"""Mock out DirectoryZipper.ArchiveIsValid and DirectoryZipper.FixArchive.
|
||||
|
||||
Mock these methods out such that file1, file2, and file3 go into one
|
||||
archive. file4 then goes into the next archive, file5 and file6 in the
|
||||
next, file 7 should appear too large to compress into an archive, and
|
||||
file8 goes into the final archive
|
||||
"""
|
||||
self.my_mox.StubOutWithMock(self.test_target, 'ArchiveIsValid')
|
||||
self.my_mox.StubOutWithMock(self.test_target, 'FixArchive')
|
||||
self.test_target.ArchiveIsValid().AndReturn(True)
|
||||
self.test_target.ArchiveIsValid().AndReturn(True)
|
||||
self.test_target.ArchiveIsValid().AndReturn(True)
|
||||
|
||||
# should be file4.txt
|
||||
self.test_target.ArchiveIsValid().AndReturn(False)
|
||||
self.test_target.FixArchive('SIZE').AndReturn(True)
|
||||
self.test_target.ArchiveIsValid().AndReturn(True)
|
||||
|
||||
# should be file5.txt
|
||||
self.test_target.ArchiveIsValid().AndReturn(False)
|
||||
self.test_target.FixArchive('SIZE').AndReturn(True)
|
||||
self.test_target.ArchiveIsValid().AndReturn(True)
|
||||
self.test_target.ArchiveIsValid().AndReturn(True)
|
||||
|
||||
# should be file7.txt
|
||||
self.test_target.ArchiveIsValid().AndReturn(False)
|
||||
self.test_target.FixArchive('SIZE').AndReturn(True)
|
||||
self.test_target.ArchiveIsValid().AndReturn(False)
|
||||
self.test_target.FixArchive('SIZE').AndReturn(False)
|
||||
self.test_target.ArchiveIsValid().AndReturn(True)
|
||||
|
||||
def InitOsDotPath(self):
|
||||
"""Mock out os.path.isfile.
|
||||
|
||||
Mock this out so the things we want to appear as files appear as files and
|
||||
the things we want to appear as directories appear as directories. Also
|
||||
make sure that the order of file visits is as we expect (which is why
|
||||
InAnyOrder isn't used here).
|
||||
"""
|
||||
self.my_mox.StubOutWithMock(os.path, 'isfile')
|
||||
os.path.isfile('/dir1/dir2').AndReturn(False)
|
||||
os.path.isfile('/dir1/dir5').AndReturn(False)
|
||||
os.path.isfile('/dir1/dir6').AndReturn(False)
|
||||
os.path.isfile('/dir1/file1.txt').AndReturn(True)
|
||||
os.path.isfile('/dir1/file2.txt').AndReturn(True)
|
||||
os.path.isfile('/dir1/dir2/dir3').AndReturn(False)
|
||||
os.path.isfile('/dir1/dir2/dir4').AndReturn(False)
|
||||
os.path.isfile('/dir1/dir2/dir4/file3.txt').AndReturn(True)
|
||||
os.path.isfile('/dir1/dir5/file4.txt').AndReturn(True)
|
||||
os.path.isfile('/dir1/dir5/file4.txt').AndReturn(True)
|
||||
os.path.isfile('/dir1/dir5/file5.txt').AndReturn(True)
|
||||
os.path.isfile('/dir1/dir5/file5.txt').AndReturn(True)
|
||||
os.path.isfile('/dir1/dir5/file6.txt').AndReturn(True)
|
||||
os.path.isfile('/dir1/dir5/file7.txt').AndReturn(True)
|
||||
os.path.isfile('/dir1/dir5/file7.txt').AndReturn(True)
|
||||
os.path.isfile('/dir1/dir6/file8.txt').AndReturn(True)
|
||||
|
||||
def InitArgLists(self):
|
||||
"""Create the directory path => directory contents mappings."""
|
||||
self.argument_lists = []
|
||||
self.argument_lists.append(['/dir1',
|
||||
['file1.txt', 'file2.txt', 'dir2', 'dir5',
|
||||
'dir6']])
|
||||
self.argument_lists.append(['/dir1/dir2', ['dir3', 'dir4']])
|
||||
self.argument_lists.append(['/dir1/dir2/dir3', []])
|
||||
self.argument_lists.append(['/dir1/dir2/dir4', ['file3.txt']])
|
||||
self.argument_lists.append(['/dir1/dir5',
|
||||
['file4.txt', 'file5.txt', 'file6.txt',
|
||||
'file7.txt']])
|
||||
self.argument_lists.append(['/dir1/dir6', ['file8.txt']])
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user