~ubuntu-branches/debian/jessie/armory/jessie

« back to all changes in this revision

Viewing changes to pytest/testPyBtcWallet.py

  • Committer: Package Import Robot
  • Author(s): Joseph Bisch
  • Date: 2014-10-07 10:22:45 UTC
  • Revision ID: package-import@ubuntu.com-20141007102245-2s3x3rhjxg689hek
Tags: upstream-0.92.3
ImportĀ upstreamĀ versionĀ 0.92.3

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
'''
 
2
Created on Aug 14, 2013
 
3
 
 
4
@author: Andy
 
5
'''
 
6
import sys
 
7
sys.path.append('..')
 
8
from pytest.Tiab import TiabTest, FIRST_WLT_NAME, SECOND_WLT_NAME
 
9
import os
 
10
import unittest
 
11
 
 
12
from armoryengine.MultiSigUtils import readLockboxesFile
 
13
from CppBlockUtils import SecureBinaryData
 
14
from armoryengine.ArmoryUtils import convertKeyDataToAddress, \
 
15
   hash256, binary_to_hex, hex_to_binary, CLI_OPTIONS, \
 
16
   WalletLockError, InterruptTestError, MULTISIG_FILE_NAME
 
17
from armoryengine.PyBtcWallet import PyBtcWallet
 
18
from armoryengine.BDM import TheBDM
 
19
 
 
20
 
 
21
sys.argv.append('--nologging')
 
22
 
 
23
 
 
24
WALLET_ROOT_ADDR = '5da74ed60a43a7ff11f0ba56cb0192b03518cc56'
 
25
NEW_UNUSED_ADDR = 'fb80e6fd042fa24178b897a6a70e1ae7eb56a20a'
 
26
 
 
27
class PyBtcWalletTest(TiabTest):
 
28
 
 
29
   def setUp(self):
 
30
      self.shortlabel = 'TestWallet1'
 
31
      self.wltID ='3VB8XSoY'
 
32
      
 
33
      self.fileA    = os.path.join(self.armoryHomeDir, 'armory_%s_.wallet' % self.wltID)
 
34
      self.fileB    = os.path.join(self.armoryHomeDir, 'armory_%s_backup.wallet' % self.wltID)
 
35
      self.fileAupd = os.path.join(self.armoryHomeDir, 'armory_%s_backup_unsuccessful.wallet' % self.wltID)
 
36
      self.fileBupd = os.path.join(self.armoryHomeDir, 'armory_%s_update_unsuccessful.wallet' % self.wltID)
 
37
 
 
38
      self.removeFileList([self.fileA, self.fileB, self.fileAupd, self.fileBupd])
 
39
   
 
40
      # We need a controlled test, so we script the all the normally-random stuff
 
41
      self.privKey   = SecureBinaryData('\xaa'*32)
 
42
      self.privKey2  = SecureBinaryData('\x33'*32)
 
43
      self.chainstr  = SecureBinaryData('\xee'*32)
 
44
      theIV     = SecureBinaryData(hex_to_binary('77'*16))
 
45
      self.passphrase  = SecureBinaryData('A self.passphrase')
 
46
      self.passphrase2 = SecureBinaryData('A new self.passphrase')
 
47
      
 
48
      self.wlt = PyBtcWallet().createNewWallet(withEncrypt=False, \
 
49
                                          plainRootKey=self.privKey, \
 
50
                                          chaincode=self.chainstr,   \
 
51
                                          IV=theIV, \
 
52
                                          shortLabel=self.shortlabel,
 
53
                                          armoryHomeDir = self.armoryHomeDir)
 
54
      
 
55
   def tearDown(self):
 
56
      self.removeFileList([self.fileA, self.fileB, self.fileAupd, self.fileBupd])
 
57
      
 
58
 
 
59
   # *********************************************************************
 
60
   # Testing deterministic, encrypted wallet features'
 
61
   # *********************************************************************
 
62
   def removeFileList(self, fileList):
 
63
      for f in fileList:
 
64
         if os.path.exists(f):
 
65
            os.remove(f)
 
66
 
 
67
   def testBackupWallet(self):
 
68
      backupTestPath = os.path.join(self.armoryHomeDir, 'armory_%s_.wallet.backup.test' % self.wltID)
 
69
      # Remove backupTestPath in case it exists
 
70
      backupFileList = [backupTestPath, self.fileB]
 
71
      self.removeFileList(backupFileList)
 
72
      # Remove the backup test path that is to be created after tear down.
 
73
      self.addCleanup(self.removeFileList, backupFileList)
 
74
      self.wlt.backupWalletFile(backupTestPath)
 
75
      self.assertTrue(os.path.exists(backupTestPath))
 
76
      self.wlt.backupWalletFile()
 
77
      self.assertTrue(os.path.exists(self.fileB))
 
78
            
 
79
   def testIsWltSigningAnyLockbox(self):
 
80
      lockboxList = readLockboxesFile(os.path.join(self.armoryHomeDir, MULTISIG_FILE_NAME))
 
81
      self.assertFalse(self.wlt.isWltSigningAnyLockbox(lockboxList))
 
82
      
 
83
      lboxWltAFile   = os.path.join(self.armoryHomeDir,'armory_%s_.wallet' % FIRST_WLT_NAME)
 
84
      lboxWltA = PyBtcWallet().readWalletFile(lboxWltAFile, doScanNow=True)
 
85
      self.assertTrue(lboxWltA.isWltSigningAnyLockbox(lockboxList))
 
86
      
 
87
      lboxWltBFile   = os.path.join(self.armoryHomeDir,'armory_%s_.wallet' % SECOND_WLT_NAME)
 
88
      lboxWltB = PyBtcWallet().readWalletFile(lboxWltBFile, doScanNow=True)
 
89
      self.assertTrue(lboxWltB.isWltSigningAnyLockbox(lockboxList))
 
90
      
 
91
   # Remove wallet files, need fresh dir for this test
 
92
   def testPyBtcWallet(self):
 
93
 
 
94
      self.wlt.addrPoolSize = 5
 
95
      # No block chain loaded so this should return -1
 
96
      # self.assertEqual(self.wlt.detectHighestUsedIndex(True), -1)
 
97
      self.assertEqual(self.wlt.kdfKey, None)
 
98
      self.assertEqual(binary_to_hex(self.wlt.addrMap['ROOT'].addrStr20), WALLET_ROOT_ADDR )
 
99
 
 
100
      #############################################################################
 
101
      # (1) Getting a new address:
 
102
      newAddr = self.wlt.getNextUnusedAddress()
 
103
      self.wlt.pprint(indent=' '*5)
 
104
      self.assertEqual(binary_to_hex(newAddr.addrStr20), NEW_UNUSED_ADDR)
 
105
   
 
106
      # (1) Re-reading wallet from file, compare the two wallets
 
107
      wlt2 = PyBtcWallet().readWalletFile(self.wlt.walletPath)
 
108
      self.assertTrue(self.wlt.isEqualTo(wlt2))
 
109
      
 
110
      #############################################################################
 
111
      # Test locking an unencrypted wallet does not lock
 
112
      self.assertFalse(self.wlt.useEncryption)
 
113
      self.wlt.lock()
 
114
      self.assertFalse(self.wlt.isLocked)
 
115
      # (2)Testing unencrypted wallet import-address'
 
116
      originalLength = len(self.wlt.linearAddr160List)
 
117
      self.wlt.importExternalAddressData(privKey=self.privKey2)
 
118
      self.assertEqual(len(self.wlt.linearAddr160List), originalLength+1)
 
119
      
 
120
      # (2) Re-reading wallet from file, compare the two wallets
 
121
      wlt2 = PyBtcWallet().readWalletFile(self.wlt.walletPath)
 
122
      self.assertTrue(self.wlt.isEqualTo(wlt2))
 
123
   
 
124
      # (2a)Testing deleteImportedAddress
 
125
      # Wallet size before delete:',  os.path.getsize(self.wlt.walletPath)
 
126
      # Addresses before delete:', len(self.wlt.linearAddr160List)
 
127
      toDelete160 = convertKeyDataToAddress(self.privKey2)
 
128
      self.wlt.deleteImportedAddress(toDelete160)
 
129
      self.assertEqual(len(self.wlt.linearAddr160List), originalLength)
 
130
      
 
131
   
 
132
      # (2a) Reimporting address for remaining tests
 
133
      # Wallet size before reimport:',  os.path.getsize(self.wlt.walletPath)
 
134
      self.wlt.importExternalAddressData(privKey=self.privKey2)
 
135
      self.assertEqual(len(self.wlt.linearAddr160List), originalLength+1)
 
136
      
 
137
   
 
138
      # (2b)Testing ENCRYPTED wallet import-address
 
139
      privKey3  = SecureBinaryData('\xbb'*32)
 
140
      privKey4  = SecureBinaryData('\x44'*32)
 
141
      self.chainstr2  = SecureBinaryData('\xdd'*32)
 
142
      theIV2     = SecureBinaryData(hex_to_binary('66'*16))
 
143
      self.passphrase2= SecureBinaryData('hello')
 
144
      wltE = PyBtcWallet().createNewWallet(withEncrypt=True, \
 
145
                                          plainRootKey=privKey3, \
 
146
                                          securePassphrase=self.passphrase2, \
 
147
                                          chaincode=self.chainstr2,   \
 
148
                                          IV=theIV2, \
 
149
                                          shortLabel=self.shortlabel,
 
150
                                          armoryHomeDir = self.armoryHomeDir)
 
151
      
 
152
      #  We should have thrown an error about importing into a  locked wallet...
 
153
      self.assertRaises(WalletLockError, wltE.importExternalAddressData, privKey=self.privKey2)
 
154
 
 
155
 
 
156
   
 
157
      wltE.unlock(securePassphrase=self.passphrase2)
 
158
      wltE.importExternalAddressData(privKey=self.privKey2)
 
159
   
 
160
      # (2b) Re-reading wallet from file, compare the two wallets
 
161
      wlt2 = PyBtcWallet().readWalletFile(wltE.walletPath)
 
162
      self.assertTrue(wltE.isEqualTo(wlt2))
 
163
   
 
164
      # (2b) Unlocking wlt2 after re-reading locked-import-wallet
 
165
      wlt2.unlock(securePassphrase=self.passphrase2)
 
166
      self.assertFalse(wlt2.isLocked)
 
167
 
 
168
      #############################################################################
 
169
      # Now play with encrypted wallets
 
170
      # *********************************************************************'
 
171
      # (3)Testing conversion to encrypted wallet
 
172
   
 
173
      kdfParams = self.wlt.computeSystemSpecificKdfParams(0.1)
 
174
      self.wlt.changeKdfParams(*kdfParams)
 
175
   
 
176
      self.assertEqual(self.wlt.kdf.getSalt(), kdfParams[2])
 
177
      self.wlt.changeWalletEncryption( securePassphrase=self.passphrase )
 
178
      self.assertEqual(self.wlt.kdf.getSalt(), kdfParams[2])
 
179
      
 
180
      # (3) Re-reading wallet from file, compare the two wallets'
 
181
      wlt2 = PyBtcWallet().readWalletFile(self.wlt.getWalletPath())
 
182
      self.assertTrue(self.wlt.isEqualTo(wlt2))
 
183
      # NOTE:  this isEqual operation compares the serializations
 
184
      #        of the wallet addresses, which only contains the 
 
185
      #        encrypted versions of the private keys.  However,
 
186
      #        self.wlt is unlocked and contains the plaintext keys, too
 
187
      #        while wlt2 does not.
 
188
      self.wlt.lock()
 
189
      for key in self.wlt.addrMap:
 
190
         self.assertTrue(self.wlt.addrMap[key].isLocked)
 
191
         self.assertEqual(self.wlt.addrMap[key].binPrivKey32_Plain.toHexStr(), '')
 
192
   
 
193
      #############################################################################
 
194
      # (4)Testing changing self.passphrase on encrypted wallet',
 
195
   
 
196
      self.wlt.unlock( securePassphrase=self.passphrase )
 
197
      for key in self.wlt.addrMap:
 
198
         self.assertFalse(self.wlt.addrMap[key].isLocked)
 
199
         self.assertNotEqual(self.wlt.addrMap[key].binPrivKey32_Plain.toHexStr(), '')
 
200
      # ...to same self.passphrase'
 
201
      origKdfKey = self.wlt.kdfKey
 
202
      self.wlt.changeWalletEncryption( securePassphrase=self.passphrase )
 
203
      self.assertEqual(origKdfKey, self.wlt.kdfKey)
 
204
   
 
205
      # (4)And now testing new self.passphrase...'
 
206
      self.wlt.changeWalletEncryption( securePassphrase=self.passphrase2 )
 
207
      self.assertNotEqual(origKdfKey, self.wlt.kdfKey)
 
208
      
 
209
      # (4) Re-reading wallet from file, compare the two wallets'
 
210
      wlt2 = PyBtcWallet().readWalletFile(self.wlt.getWalletPath())
 
211
      self.assertTrue(self.wlt.isEqualTo(wlt2))
 
212
   
 
213
      #############################################################################
 
214
      # (5)Testing changing KDF on encrypted wallet'
 
215
   
 
216
      self.wlt.unlock( securePassphrase=self.passphrase2 )
 
217
   
 
218
      MEMORY_REQT_BYTES = 1024
 
219
      NUM_ITER = 999
 
220
      SALT_ALL_0 ='00'*32
 
221
      self.wlt.changeKdfParams(MEMORY_REQT_BYTES, NUM_ITER, hex_to_binary(SALT_ALL_0), self.passphrase2)
 
222
      self.assertEqual(self.wlt.kdf.getMemoryReqtBytes(), MEMORY_REQT_BYTES)
 
223
      self.assertEqual(self.wlt.kdf.getNumIterations(), NUM_ITER)
 
224
      self.assertEqual(self.wlt.kdf.getSalt().toHexStr(),  SALT_ALL_0)
 
225
   
 
226
      self.wlt.changeWalletEncryption( securePassphrase=self.passphrase2 )
 
227
      # I don't know why this shouldn't be ''
 
228
      # Commenting out because it's a broken assertion
 
229
      # self.assertNotEqual(origKdfKey.toHexStr(), '')
 
230
   
 
231
      # (5) Get new address from locked wallet'
 
232
      # Locking wallet'
 
233
      self.wlt.lock()
 
234
      for i in range(10):
 
235
         self.wlt.getNextUnusedAddress()
 
236
      self.assertEqual(len(self.wlt.addrMap), originalLength+13)
 
237
      
 
238
      # (5) Re-reading wallet from file, compare the two wallets'
 
239
      wlt2 = PyBtcWallet().readWalletFile(self.wlt.getWalletPath())
 
240
      self.assertTrue(self.wlt.isEqualTo(wlt2))
 
241
   
 
242
      #############################################################################
 
243
      # !!!  #forkOnlineWallet()
 
244
      # (6)Testing forking encrypted wallet for online mode'
 
245
      self.wlt.forkOnlineWallet('OnlineVersionOfEncryptedWallet.bin')
 
246
      wlt2.readWalletFile('OnlineVersionOfEncryptedWallet.bin')
 
247
      for key in wlt2.addrMap:
 
248
         self.assertTrue(self.wlt.addrMap[key].isLocked)
 
249
         self.assertEqual(self.wlt.addrMap[key].binPrivKey32_Plain.toHexStr(), '')
 
250
      # (6)Getting a new addresses from both wallets'
 
251
      for i in range(self.wlt.addrPoolSize*2):
 
252
         self.wlt.getNextUnusedAddress()
 
253
         wlt2.getNextUnusedAddress()
 
254
   
 
255
      newaddr1 = self.wlt.getNextUnusedAddress()
 
256
      newaddr2 = wlt2.getNextUnusedAddress()   
 
257
      self.assertTrue(newaddr1.getAddr160() == newaddr2.getAddr160())
 
258
      self.assertEqual(len(wlt2.addrMap), 3*originalLength+14)
 
259
   
 
260
      # (6) Re-reading wallet from file, compare the two wallets
 
261
      wlt3 = PyBtcWallet().readWalletFile('OnlineVersionOfEncryptedWallet.bin')
 
262
      self.assertTrue(wlt3.isEqualTo(wlt2))
 
263
      #############################################################################
 
264
      # (7)Testing removing wallet encryption'
 
265
      # Wallet is locked?  ', self.wlt.isLocked
 
266
      self.wlt.unlock(securePassphrase=self.passphrase2)
 
267
      self.wlt.changeWalletEncryption( None )
 
268
      for key in self.wlt.addrMap:
 
269
         self.assertFalse(self.wlt.addrMap[key].isLocked)
 
270
         self.assertNotEqual(self.wlt.addrMap[key].binPrivKey32_Plain.toHexStr(), '')
 
271
   
 
272
      # (7) Re-reading wallet from file, compare the two wallets'
 
273
      wlt2 = PyBtcWallet().readWalletFile(self.wlt.getWalletPath())
 
274
      self.assertTrue(self.wlt.isEqualTo(wlt2))
 
275
   
 
276
      #############################################################################
 
277
      # \n'
 
278
      # *********************************************************************'
 
279
      # (8)Doing interrupt tests to test wallet-file-update recovery'
 
280
      def hashfile(fn):
 
281
         f = open(fn,'r')
 
282
         d = hash256(f.read())
 
283
         f.close()
 
284
         return binary_to_hex(d[:8])
 
285
 
 
286
      def verifyFileStatus(fileAExists = True, fileBExists = True, \
 
287
                           fileAupdExists = True, fileBupdExists = True):
 
288
         self.assertEqual(os.path.exists(self.fileA), fileAExists)
 
289
         self.assertEqual(os.path.exists(self.fileB), fileBExists)
 
290
         self.assertEqual(os.path.exists(self.fileAupd), fileAupdExists)
 
291
         self.assertEqual(os.path.exists(self.fileBupd), fileBupdExists)
 
292
 
 
293
      correctMainHash = hashfile(self.fileA)
 
294
      try:
 
295
         self.wlt.interruptTest1 = True
 
296
         self.wlt.getNextUnusedAddress()
 
297
      except InterruptTestError:
 
298
         # Interrupted!'
 
299
         pass
 
300
      self.wlt.interruptTest1 = False
 
301
   
 
302
      # (8a)Interrupted getNextUnusedAddress on primary file update'
 
303
      verifyFileStatus(True, True, False, True)
 
304
      # (8a)Do consistency check on the wallet'
 
305
      self.wlt.doWalletFileConsistencyCheck()
 
306
      verifyFileStatus(True, True, False, False)
 
307
      self.assertEqual(correctMainHash, hashfile(self.fileA))
 
308
 
 
309
      try:
 
310
         self.wlt.interruptTest2 = True
 
311
         self.wlt.getNextUnusedAddress()
 
312
      except InterruptTestError:
 
313
         # Interrupted!'
 
314
         pass
 
315
      self.wlt.interruptTest2 = False
 
316
   
 
317
      # (8b)Interrupted getNextUnusedAddress on between primary/backup update'
 
318
      verifyFileStatus(True, True, True, True)
 
319
      # (8b)Do consistency check on the wallet'
 
320
      self.wlt.doWalletFileConsistencyCheck()
 
321
      verifyFileStatus(True, True, False, False)
 
322
      self.assertEqual(hashfile(self.fileA), hashfile(self.fileB))
 
323
      # (8c) Try interrupting at state 3'
 
324
      verifyFileStatus(True, True, False, False)
 
325
   
 
326
      try:
 
327
         self.wlt.interruptTest3 = True
 
328
         self.wlt.getNextUnusedAddress()
 
329
      except InterruptTestError:
 
330
         # Interrupted!'
 
331
         pass
 
332
      self.wlt.interruptTest3 = False
 
333
   
 
334
      # (8c)Interrupted getNextUnusedAddress on backup file update'
 
335
      verifyFileStatus(True, True, True, False)
 
336
      # (8c)Do consistency check on the wallet'
 
337
      self.wlt.doWalletFileConsistencyCheck()
 
338
      verifyFileStatus(True, True, False, False)
 
339
      self.assertEqual(hashfile(self.fileA), hashfile(self.fileB))
 
340
   
 
341
      #############################################################################
 
342
      # \n'
 
343
      # *********************************************************************'
 
344
      # (9)Checksum-based byte-error correction tests!'
 
345
      # (9)Start with a good primary and backup file...'
 
346
   
 
347
      # (9a)Open primary wallet, change second byte in KDF'
 
348
      wltfile = open(self.wlt.walletPath,'r+b')
 
349
      wltfile.seek(326)
 
350
      wltfile.write('\xff')
 
351
      wltfile.close()
 
352
      # (9a)Byte changed, file hashes:'
 
353
      verifyFileStatus(True, True, False, False)
 
354
   
 
355
      # (9a)Try to read wallet from file, should correct KDF error, write fix'
 
356
      wlt2 = PyBtcWallet().readWalletFile(self.wlt.walletPath)
 
357
      verifyFileStatus(True, True, False, False)
 
358
      self.assertNotEqual(hashfile(self.fileA), hashfile(self.fileB))
 
359
   
 
360
      # \n'
 
361
      # *********************************************************************'
 
362
      # (9b)Change a byte in each checksummed field in root addr'
 
363
      wltfile = open(self.wlt.walletPath,'r+b')
 
364
      wltfile.seek(838);  wltfile.write('\xff')
 
365
      wltfile.seek(885);  wltfile.write('\xff')
 
366
      wltfile.seek(929);  wltfile.write('\xff')
 
367
      wltfile.seek(954);  wltfile.write('\xff')
 
368
      wltfile.seek(1000);  wltfile.write('\xff')
 
369
      wltfile.close()
 
370
      # (9b) New file hashes...'
 
371
      verifyFileStatus(True, True, False, False)
 
372
   
 
373
      # (9b)Try to read wallet from file, should correct address errors'
 
374
      wlt2 = PyBtcWallet().readWalletFile(self.wlt.walletPath)
 
375
      verifyFileStatus(True, True, False, False)
 
376
      self.assertNotEqual(hashfile(self.fileA), hashfile(self.fileB))
 
377
      
 
378
      # \n'
 
379
      # *********************************************************************'
 
380
      # (9c)Change a byte in each checksummed field, of first non-root addr'
 
381
      wltfile = open(self.wlt.walletPath,'r+b')
 
382
      wltfile.seek(1261+21+838);  wltfile.write('\xff')
 
383
      wltfile.seek(1261+21+885);  wltfile.write('\xff')
 
384
      wltfile.seek(1261+21+929);  wltfile.write('\xff')
 
385
      wltfile.seek(1261+21+954);  wltfile.write('\xff')
 
386
      wltfile.seek(1261+21+1000);  wltfile.write('\xff')
 
387
      wltfile.close()
 
388
      # (9c) New file hashes...'
 
389
      verifyFileStatus(True, True, False, False)
 
390
   
 
391
      # (9c)Try to read wallet from file, should correct address errors'
 
392
      wlt2 = PyBtcWallet().readWalletFile(self.wlt.walletPath)
 
393
      verifyFileStatus(True, True, False, False)
 
394
      self.assertNotEqual(hashfile(self.fileA), hashfile(self.fileB))
 
395
   
 
396
      # \n'
 
397
      # *********************************************************************'
 
398
      # (9d)Now butcher the CHECKSUM, see if correction works'
 
399
      wltfile = open(self.wlt.walletPath,'r+b')
 
400
      wltfile.seek(977); wltfile.write('\xff')
 
401
      wltfile.close()
 
402
      # (9d) New file hashes...'
 
403
      verifyFileStatus(True, True, False, False)
 
404
   
 
405
      # (9d)Try to read wallet from file, should correct address errors'
 
406
      wlt2 = PyBtcWallet().readWalletFile(self.wlt.walletPath)
 
407
      verifyFileStatus(True, True, False, False)
 
408
      self.assertNotEqual(hashfile(self.fileA), hashfile(self.fileB))
 
409
   
 
410
   
 
411
      # *******'
 
412
      # (9z) Test comment I/O'
 
413
      comment1 = 'This is my normal unit-testing address.'
 
414
      comment2 = 'This is fake tx... no tx has this hash.'
 
415
      comment3 = comment1 + '  Corrected!'
 
416
      hash1 = '\x1f'*20  # address160
 
417
      hash2 = '\x2f'*32  # tx hash
 
418
      self.wlt.setComment(hash1, comment1)
 
419
      self.wlt.setComment(hash2, comment2)
 
420
      self.wlt.setComment(hash1, comment3)
 
421
   
 
422
      wlt2 = PyBtcWallet().readWalletFile(self.wlt.walletPath)
 
423
      c3 = wlt2.getComment(hash1)
 
424
      c2 = wlt2.getComment(hash2)
 
425
      self.assertEqual(c3, comment3)
 
426
      self.assertEqual(c2, comment2)
 
427
 
 
428
# Running tests with "python <module name>" will NOT work for any Armory tests
 
429
# You must run tests with "python -m unittest <module name>" or run all tests with "python -m unittest discover"
 
430
# if __name__ == "__main__":
 
431
#    unittest.main()
 
 
b'\\ No newline at end of file'