|
1 // Copyright (c) 2005-2009 Nokia Corporation and/or its subsidiary(-ies). |
|
2 // All rights reserved. |
|
3 // This component and the accompanying materials are made available |
|
4 // under the terms of the License "Eclipse Public License v1.0" |
|
5 // which accompanies this distribution, and is available |
|
6 // at the URL "http://www.eclipse.org/legal/epl-v10.html". |
|
7 // |
|
8 // Initial Contributors: |
|
9 // Nokia Corporation - initial contribution. |
|
10 // |
|
11 // Contributors: |
|
12 // |
|
13 // Description: |
|
14 // e32test\mmu\t_wdpsoak.cpp |
|
15 // |
|
16 // |
|
17 |
|
18 #define __E32TEST_EXTENSION__ |
|
19 #include <e32test.h> |
|
20 #include <dptest.h> |
|
21 #include <hal.h> |
|
22 #include "../mmu/mmudetect.h" |
|
23 #include "../mmu/freeram.h" |
|
24 |
|
25 #define MAX_CHUNKS 10 |
|
26 #define PRINT(string) if (!gQuiet) test.Printf(string) |
|
27 #define PRINT1(string,param) if (!gQuiet) test.Printf(string,param) |
|
28 #define TESTNEXT(string) if (!gQuiet) test.Next(string) |
|
29 |
|
30 //------------globals--------------------- |
|
31 LOCAL_D RTest test(_L("T_WDPSOAK")); |
|
32 LOCAL_D TInt gPageSize = 0; |
|
33 LOCAL_D TUint gChunkSize = 0; // default chunk size |
|
34 LOCAL_D RChunk gChunk[MAX_CHUNKS]; |
|
35 LOCAL_D TUint gNextChunk = 0; |
|
36 LOCAL_D TBool gQuiet = EFalse; |
|
37 LOCAL_D TUint gPeriod = 0; |
|
38 LOCAL_D TUint gMin = 0; |
|
39 LOCAL_D TUint gMax = 0; |
|
40 LOCAL_D TUint gMemScheme = 0; |
|
41 |
|
42 const TUint32 KFlushQuietLimit = 100000; |
|
43 |
|
44 TUint64 SwapFree() |
|
45 { |
|
46 SVMSwapInfo swapInfo; |
|
47 test_KErrNone(UserSvr::HalFunction(EHalGroupVM, EVMHalGetSwapInfo, &swapInfo, 0)); |
|
48 |
|
49 return swapInfo.iSwapFree; |
|
50 } |
|
51 |
|
52 TUint64 SwapSize() |
|
53 { |
|
54 SVMSwapInfo swapInfo; |
|
55 test_KErrNone(UserSvr::HalFunction(EHalGroupVM, EVMHalGetSwapInfo, &swapInfo, 0)); |
|
56 |
|
57 return swapInfo.iSwapSize; |
|
58 } |
|
59 |
|
60 void CacheSize(TUint aMin, TUint aMax) |
|
61 { |
|
62 SVMCacheInfo info; |
|
63 if (UserSvr::HalFunction(EHalGroupVM,EVMHalGetCacheSize,&info,0) != KErrNone) |
|
64 { |
|
65 return; |
|
66 } |
|
67 |
|
68 if (aMin > 0 || aMax > 0) |
|
69 { |
|
70 if (aMin > 0) |
|
71 { |
|
72 info.iMinSize = aMin; |
|
73 } |
|
74 if (aMax > 0) |
|
75 { |
|
76 info.iMaxSize = aMax; |
|
77 } |
|
78 UserSvr::HalFunction(EHalGroupVM,EVMHalSetCacheSize,(TAny*)info.iMinSize,(TAny*)info.iMaxSize); |
|
79 if (UserSvr::HalFunction(EHalGroupVM,EVMHalGetCacheSize,&info,0) != KErrNone) |
|
80 { |
|
81 return; |
|
82 } |
|
83 } |
|
84 |
|
85 PRINT1(_L("Paging Cache min size %d"),info.iMinSize); |
|
86 PRINT1(_L(" max size %d"),info.iMaxSize); |
|
87 PRINT1(_L(" current size %d\n"),info.iCurrentSize); |
|
88 } |
|
89 |
|
90 void ShowMemoryUse() |
|
91 { |
|
92 PRINT1(_L("RAM free 0x%08X bytes"),FreeRam()); |
|
93 PRINT1(_L(" Swap free 0x%08X bytes\n"),SwapFree()); |
|
94 |
|
95 TPckgBuf<DPTest::TEventInfo> infoBuf; |
|
96 TInt r = UserSvr::HalFunction(EHalGroupVM,EVMHalGetEventInfo,&infoBuf,0); |
|
97 if (r!=KErrNone) |
|
98 { |
|
99 return; |
|
100 } |
|
101 PRINT1(_L("Page fault count %d"),infoBuf().iPageFaultCount); |
|
102 PRINT1(_L(" Page IN count %d\n"),infoBuf().iPageInReadCount); |
|
103 |
|
104 return; |
|
105 } |
|
106 |
|
107 void ShowHelp() |
|
108 { |
|
109 PRINT(_L("***************************************\n")); |
|
110 PRINT(_L("The following are immediate commands\n")); |
|
111 PRINT(_L("F flush the paging cache\n")); |
|
112 PRINT(_L("I show memory information\n")); |
|
113 PRINT(_L("? show this help\n")); |
|
114 PRINT(_L("Rn read all pages of chunk n\n")); |
|
115 PRINT(_L("Wn write all pages of chunk n\n")); |
|
116 PRINT(_L("Mn periodic memory scheme n\n")); |
|
117 PRINT(_L("The following require a <CR> termination\n")); |
|
118 PRINT(_L("C=nnnn create a chunnk of size nnnn\n")); |
|
119 PRINT(_L("L=nnnn paging cache min size nnnn\n")); |
|
120 PRINT(_L("H=nnnn paging cache max size nnnn\n")); |
|
121 PRINT(_L("P=nnnn periodic flush/memory scheme nnnn microseconds\n")); |
|
122 PRINT(_L("Esc to exit\n")); |
|
123 PRINT(_L("***************************************\n")); |
|
124 } |
|
125 |
|
126 void CreateChunk(RChunk * aChunk, TUint aSize) |
|
127 { |
|
128 TESTNEXT(_L("Creating a paged chunk")); |
|
129 TChunkCreateInfo createInfo; |
|
130 PRINT1(_L("Creating chunk size 0x%08X bytes "),aSize); |
|
131 PRINT1(_L("at index %d\n"),gNextChunk); |
|
132 createInfo.SetPaging(TChunkCreateInfo::EPaged); |
|
133 createInfo.SetNormal(aSize,aSize); |
|
134 test_KErrNone(aChunk->Create(createInfo)); |
|
135 } |
|
136 |
|
137 void ReadChunk(RChunk * aChunk) |
|
138 { |
|
139 TESTNEXT(_L("Reading from each page of chunk")); |
|
140 TUint8* chunkBase = aChunk->Base(); |
|
141 |
|
142 TUint8 chunkVal = 0; |
|
143 for (TInt i = 0; i < aChunk->Size(); i += gPageSize) |
|
144 { |
|
145 chunkVal = * (chunkBase + i); |
|
146 } |
|
147 |
|
148 // only needed to remove compiler warning on unused variable |
|
149 if (chunkVal) |
|
150 chunkVal = 0; |
|
151 |
|
152 return; |
|
153 } |
|
154 |
|
155 void WriteChunk(RChunk * aChunk, TUint8 aValue = 0) |
|
156 { |
|
157 static TUint8 lastWriteValue = 1; |
|
158 TESTNEXT(_L("Writing to each page of chunk")); |
|
159 TUint8* chunkBase = aChunk->Base(); |
|
160 |
|
161 lastWriteValue = (TUint8)(aValue == 0 ? lastWriteValue + 1 : aValue); |
|
162 for (TInt i = 0; i < aChunk->Size(); i += gPageSize) |
|
163 { |
|
164 * (chunkBase + i) = lastWriteValue; |
|
165 } |
|
166 |
|
167 return; |
|
168 } |
|
169 |
|
170 void ParseCommandLine () |
|
171 { |
|
172 TBuf<64> c; |
|
173 |
|
174 User::CommandLine(c); |
|
175 c.LowerCase(); |
|
176 |
|
177 if (c != KNullDesC) |
|
178 { |
|
179 TLex lex(c); |
|
180 TPtrC token; |
|
181 |
|
182 while (token.Set(lex.NextToken()), token != KNullDesC) |
|
183 { |
|
184 if (token.Mid(0) == _L("quiet")) |
|
185 { |
|
186 gQuiet = ETrue; |
|
187 continue; |
|
188 } |
|
189 |
|
190 if (token.Mid(0) == _L("verbose")) |
|
191 { |
|
192 gQuiet = EFalse; |
|
193 continue; |
|
194 } |
|
195 |
|
196 if (token.Left(5) == _L("chunk")) |
|
197 { |
|
198 TInt equalPos; |
|
199 equalPos = token.Locate('='); |
|
200 if (equalPos > 0 && (equalPos+1) < token.Length()) |
|
201 { |
|
202 TLex lexNum(token.Mid(equalPos+1)); |
|
203 lexNum.Val(gChunkSize,EDecimal); |
|
204 } |
|
205 continue; |
|
206 } |
|
207 |
|
208 if (token.Left(3) == _L("low")) |
|
209 { |
|
210 TInt equalPos; |
|
211 equalPos = token.Locate('='); |
|
212 if (equalPos > 0 && (equalPos+1) < token.Length()) |
|
213 { |
|
214 TLex lexNum(token.Mid(equalPos+1)); |
|
215 lexNum.Val(gMin,EDecimal); |
|
216 } |
|
217 continue; |
|
218 } |
|
219 |
|
220 if (token.Left(5) == _L("high")) |
|
221 { |
|
222 TInt equalPos; |
|
223 equalPos = token.Locate('='); |
|
224 if (equalPos > 0 && (equalPos+1) < token.Length()) |
|
225 { |
|
226 TLex lexNum(token.Mid(equalPos+1)); |
|
227 lexNum.Val(gMax,EDecimal); |
|
228 } |
|
229 continue; |
|
230 } |
|
231 |
|
232 if (token.Left(6) == _L("period")) |
|
233 { |
|
234 TInt equalPos; |
|
235 equalPos = token.Locate('='); |
|
236 if (equalPos > 0 && (equalPos+1) < token.Length()) |
|
237 { |
|
238 TLex lexNum(token.Mid(equalPos+1)); |
|
239 lexNum.Val(gPeriod,EDecimal); |
|
240 } |
|
241 continue; |
|
242 } |
|
243 |
|
244 if (token.Left(3) == _L("mem")) |
|
245 { |
|
246 TInt equalPos; |
|
247 equalPos = token.Locate('='); |
|
248 if (equalPos > 0 && (equalPos+1) < token.Length()) |
|
249 { |
|
250 TLex lexNum(token.Mid(equalPos+1)); |
|
251 lexNum.Val(gMemScheme,EDecimal); |
|
252 } |
|
253 continue; |
|
254 } |
|
255 |
|
256 } |
|
257 } |
|
258 } |
|
259 |
|
260 enum TimerActions |
|
261 { |
|
262 ENoaction = 0, |
|
263 EFlush = 1, |
|
264 EFlushQuiet = 2, |
|
265 EMemScheme1 = 1 << 4, |
|
266 EMemScheme2 = 2 << 4, |
|
267 EMemScheme3 = 3 << 4, |
|
268 EMemScheme4 = 4 << 4, |
|
269 }; |
|
270 |
|
271 // CActive class to monitor KeyStrokes from User |
|
272 class CActiveConsole : public CActive |
|
273 { |
|
274 public: |
|
275 CActiveConsole(); |
|
276 ~CActiveConsole(); |
|
277 void GetCharacter(); |
|
278 static TInt Callback(TAny* aCtrl); |
|
279 |
|
280 private: |
|
281 CPeriodic* iTimer; |
|
282 TChar iCmdGetValue; |
|
283 TBool iGetHexValue; |
|
284 TBool iPrompt; |
|
285 TChar iLastChar; |
|
286 TUint iValue; |
|
287 TUint16 iActions; |
|
288 TUint32 iPeriod; |
|
289 |
|
290 // Defined as pure virtual by CActive; |
|
291 // implementation provided by this class. |
|
292 virtual void DoCancel(); |
|
293 // Defined as pure virtual by CActive; |
|
294 // implementation provided by this class, |
|
295 virtual void RunL(); |
|
296 void ProcessKeyPressL(TChar aChar); |
|
297 void ProcessValue(); |
|
298 }; |
|
299 |
|
300 // Class CActiveConsole |
|
301 CActiveConsole::CActiveConsole() |
|
302 : CActive(EPriorityHigh) |
|
303 { |
|
304 CActiveScheduler::Add(this); |
|
305 |
|
306 iTimer = CPeriodic::NewL(EPriorityNormal); |
|
307 iActions = ENoaction; |
|
308 iPrompt = ETrue; |
|
309 iPeriod = 0; |
|
310 if (gPeriod > 0) |
|
311 { |
|
312 if (gMemScheme > 0) |
|
313 { |
|
314 iActions = (TUint16)(gMemScheme << 4); |
|
315 } |
|
316 else |
|
317 { |
|
318 iActions = (TUint16)(gPeriod < KFlushQuietLimit ? EFlushQuiet : EFlush); |
|
319 } |
|
320 iPeriod = gPeriod; |
|
321 iTimer->Start(0,gPeriod,TCallBack(Callback,this)); |
|
322 } |
|
323 } |
|
324 |
|
325 CActiveConsole::~CActiveConsole() |
|
326 { |
|
327 iTimer->Cancel(); |
|
328 delete iTimer; |
|
329 |
|
330 Cancel(); |
|
331 } |
|
332 |
|
333 // Callback function for timer expiry |
|
334 TInt CActiveConsole::Callback(TAny* aControl) |
|
335 { |
|
336 switch (((CActiveConsole*)aControl)->iActions & 0x0F) |
|
337 { |
|
338 case ENoaction : |
|
339 break; |
|
340 |
|
341 case EFlush : |
|
342 PRINT(_L("Flush\n")); |
|
343 // drop through to quiet |
|
344 |
|
345 case EFlushQuiet : |
|
346 test_KErrNone(DPTest::FlushCache()); |
|
347 break; |
|
348 |
|
349 default : |
|
350 break; |
|
351 } |
|
352 |
|
353 switch (((CActiveConsole*)aControl)->iActions & 0xF0) |
|
354 { |
|
355 TUint i; |
|
356 case EMemScheme1 : |
|
357 for (i = 0; i < gNextChunk; i++) |
|
358 ReadChunk (&gChunk[i]); |
|
359 break; |
|
360 |
|
361 case EMemScheme2 : |
|
362 for (i = 0; i < gNextChunk; i++) |
|
363 WriteChunk (&gChunk[i]); |
|
364 break; |
|
365 |
|
366 default : |
|
367 break; |
|
368 } |
|
369 |
|
370 return KErrNone; |
|
371 } |
|
372 |
|
373 void CActiveConsole::GetCharacter() |
|
374 { |
|
375 if (iPrompt) |
|
376 { |
|
377 PRINT(_L("***Command (F,I,Q,V,?,Rn,Wn,Mn,C=nnnnn,H=nnnn,L=nnnn,P=nnnn) or Esc to exit ***\n")); |
|
378 iPrompt = EFalse; |
|
379 } |
|
380 test.Console()->Read(iStatus); |
|
381 SetActive(); |
|
382 } |
|
383 |
|
384 void CActiveConsole::DoCancel() |
|
385 { |
|
386 PRINT(_L("CActiveConsole::DoCancel\n")); |
|
387 test.Console()->ReadCancel(); |
|
388 } |
|
389 |
|
390 void CActiveConsole::ProcessKeyPressL(TChar aChar) |
|
391 { |
|
392 if (aChar == EKeyEscape) |
|
393 { |
|
394 PRINT(_L("CActiveConsole: ESC key pressed -> stopping active scheduler...\n")); |
|
395 CActiveScheduler::Stop(); |
|
396 return; |
|
397 } |
|
398 aChar.UpperCase(); |
|
399 if (iCmdGetValue != 0 && aChar == '\r') |
|
400 { |
|
401 if (iLastChar == 'K') |
|
402 { |
|
403 iValue *= iGetHexValue ? 0x400 : 1000; |
|
404 } |
|
405 if (iLastChar == 'M') |
|
406 { |
|
407 iValue *= iGetHexValue ? 0x10000 : 1000000; |
|
408 } |
|
409 PRINT1(_L("CActiveConsole: Value %d\n"),iValue); |
|
410 ProcessValue(); |
|
411 } |
|
412 if (iCmdGetValue != 0 ) |
|
413 { |
|
414 if (iGetHexValue) |
|
415 { |
|
416 if (aChar.IsDigit()) |
|
417 { |
|
418 iValue = iValue * 16 + aChar.GetNumericValue(); |
|
419 } |
|
420 else |
|
421 { |
|
422 if (aChar.IsHexDigit()) |
|
423 { |
|
424 iValue = iValue * 16 + (TUint)aChar - 'A' + 10; |
|
425 } |
|
426 else |
|
427 { |
|
428 if (aChar != 'K' && aChar != 'M') |
|
429 { |
|
430 PRINT(_L("Illegal hexadecimal character - Enter command\n")); |
|
431 iCmdGetValue = 0; |
|
432 } |
|
433 } |
|
434 } |
|
435 } |
|
436 else |
|
437 { |
|
438 if (aChar.IsDigit()) |
|
439 { |
|
440 iValue = iValue * 10 + aChar.GetNumericValue(); |
|
441 } |
|
442 else |
|
443 { |
|
444 if ((aChar == 'X') && (iLastChar == '0') && (iValue == 0)) |
|
445 iGetHexValue = ETrue; |
|
446 else |
|
447 { |
|
448 if (aChar != 'K' && aChar != 'M') |
|
449 { |
|
450 test.Printf(_L("Illegal decimal character - Enter command\n")); |
|
451 iCmdGetValue = 0; |
|
452 } |
|
453 } |
|
454 } |
|
455 } |
|
456 } |
|
457 else |
|
458 { |
|
459 switch (aChar) |
|
460 { |
|
461 case 'F' : |
|
462 TESTNEXT(_L("Flushing Cache")); |
|
463 test_KErrNone(DPTest::FlushCache()); |
|
464 ShowMemoryUse(); |
|
465 iPrompt = ETrue; |
|
466 break; |
|
467 |
|
468 case 'I' : |
|
469 CacheSize(0,0); |
|
470 ShowMemoryUse(); |
|
471 iPrompt = ETrue; |
|
472 break; |
|
473 |
|
474 case 'Q' : |
|
475 gQuiet = ETrue; |
|
476 iPrompt = ETrue; |
|
477 break; |
|
478 |
|
479 case 'V' : |
|
480 gQuiet = EFalse; |
|
481 iPrompt = ETrue; |
|
482 break; |
|
483 |
|
484 case '?' : |
|
485 ShowHelp(); |
|
486 break; |
|
487 |
|
488 case '=' : |
|
489 iCmdGetValue = iLastChar; |
|
490 iGetHexValue = EFalse; |
|
491 iValue = 0; |
|
492 break; |
|
493 |
|
494 default : |
|
495 if (aChar.IsDigit()) |
|
496 { |
|
497 if (iLastChar == 'R') |
|
498 { |
|
499 if (aChar.GetNumericValue() < (TInt)gNextChunk) |
|
500 { |
|
501 ReadChunk (&gChunk[aChar.GetNumericValue()]); |
|
502 } |
|
503 else |
|
504 { |
|
505 for (TUint i = 0; i < gNextChunk; i++) |
|
506 ReadChunk (&gChunk[i]); |
|
507 } |
|
508 iPrompt = ETrue; |
|
509 } |
|
510 if (iLastChar == 'W') |
|
511 { |
|
512 if (aChar.GetNumericValue() < (TInt)gNextChunk) |
|
513 { |
|
514 WriteChunk (&gChunk[aChar.GetNumericValue()]); |
|
515 } |
|
516 else |
|
517 { |
|
518 for (TUint i = 0; i < gNextChunk; i++) |
|
519 WriteChunk (&gChunk[i]); |
|
520 } |
|
521 iPrompt = ETrue; |
|
522 } |
|
523 if (iLastChar == 'M') |
|
524 { |
|
525 if (aChar.GetNumericValue() == 0) |
|
526 { |
|
527 iActions = (TUint16)(iPeriod < KFlushQuietLimit ? EFlushQuiet : EFlush); |
|
528 } |
|
529 else |
|
530 { |
|
531 iActions = (TUint16)(aChar.GetNumericValue() << 4); |
|
532 } |
|
533 iPrompt = ETrue; |
|
534 } |
|
535 } |
|
536 break; |
|
537 } |
|
538 } |
|
539 iLastChar = aChar; |
|
540 GetCharacter(); |
|
541 return; |
|
542 } |
|
543 |
|
544 void CActiveConsole::ProcessValue() |
|
545 { |
|
546 switch (iCmdGetValue) |
|
547 { |
|
548 case 'C' : |
|
549 if (iValue > 0 && gNextChunk < MAX_CHUNKS) |
|
550 { |
|
551 CreateChunk (&gChunk[gNextChunk], iValue); |
|
552 ReadChunk (&gChunk[gNextChunk]); |
|
553 ShowMemoryUse(); |
|
554 gNextChunk++; |
|
555 } |
|
556 break; |
|
557 |
|
558 case 'H' : |
|
559 CacheSize (0,iValue); |
|
560 break; |
|
561 |
|
562 case 'L' : |
|
563 CacheSize (iValue,0); |
|
564 break; |
|
565 |
|
566 case 'P' : |
|
567 iPeriod = iValue; |
|
568 iActions = (TUint16)(iValue < KFlushQuietLimit ? EFlushQuiet : EFlush); |
|
569 iTimer->Cancel(); |
|
570 if (iValue > 0) |
|
571 { |
|
572 iTimer->Start(0,iValue,TCallBack(Callback,this)); |
|
573 } |
|
574 break; |
|
575 |
|
576 default : |
|
577 break; |
|
578 } |
|
579 iCmdGetValue = 0; |
|
580 iPrompt = ETrue; |
|
581 } |
|
582 |
|
583 void CActiveConsole::RunL() |
|
584 { |
|
585 ProcessKeyPressL(static_cast<TChar>(test.Console()->KeyCode())); |
|
586 } |
|
587 |
|
588 TInt E32Main() |
|
589 { |
|
590 test.Title(); |
|
591 test.Start(_L("Writable Data Paging Soak Test")); |
|
592 |
|
593 ParseCommandLine(); |
|
594 |
|
595 if (DPTest::Attributes() & DPTest::ERomPaging) |
|
596 test.Printf(_L("Rom paging supported\n")); |
|
597 if (DPTest::Attributes() & DPTest::ECodePaging) |
|
598 test.Printf(_L("Code paging supported\n")); |
|
599 if (DPTest::Attributes() & DPTest::EDataPaging) |
|
600 test.Printf(_L("Data paging supported\n")); |
|
601 |
|
602 TInt totalRamSize; |
|
603 HAL::Get(HAL::EMemoryRAM,totalRamSize); |
|
604 HAL::Get(HAL::EMemoryPageSize,gPageSize); |
|
605 test.Printf(_L("Total RAM size 0x%08X bytes"),totalRamSize); |
|
606 test.Printf(_L(" Swap size 0x%08X bytes"),SwapSize()); |
|
607 test.Printf(_L(" Page size 0x%08X bytes\n"),gPageSize); |
|
608 CacheSize(gMin,gMax); |
|
609 |
|
610 if ((DPTest::Attributes() & DPTest::EDataPaging) == 0) |
|
611 { |
|
612 test.Printf(_L("Writable Demand Paging not supported\n")); |
|
613 test.End(); |
|
614 return 0; |
|
615 } |
|
616 |
|
617 ShowMemoryUse(); |
|
618 |
|
619 //User::SetDebugMask(0x00000008); //KMMU |
|
620 //User::SetDebugMask(0x00000080); //KEXEC |
|
621 //User::SetDebugMask(0x90000000); //KPANIC KMMU2 |
|
622 //User::SetDebugMask(0x40000000, 1); //KPAGING |
|
623 |
|
624 if (gChunkSize) |
|
625 { |
|
626 CreateChunk (&gChunk[gNextChunk], gChunkSize); |
|
627 ReadChunk (&gChunk[gNextChunk]); |
|
628 ShowMemoryUse(); |
|
629 gNextChunk++; |
|
630 } |
|
631 |
|
632 CActiveScheduler* myScheduler = new (ELeave) CActiveScheduler(); |
|
633 CActiveScheduler::Install(myScheduler); |
|
634 |
|
635 CActiveConsole* myActiveConsole = new CActiveConsole(); |
|
636 myActiveConsole->GetCharacter(); |
|
637 |
|
638 CActiveScheduler::Start(); |
|
639 |
|
640 test.End(); |
|
641 |
|
642 return 0; |
|
643 } |