591 lines
13 KiB
C++
591 lines
13 KiB
C++
|
/***
|
||
|
*suite.cpp
|
||
|
*
|
||
|
* Copyright (C) 1992-93, Microsoft Corporation. All Rights Reserved.
|
||
|
* Information Contained Herein Is Proprietary and Confidential.
|
||
|
*
|
||
|
*Purpose:
|
||
|
* This module contains the test suite drivers.
|
||
|
*
|
||
|
*Revision History:
|
||
|
*
|
||
|
* [00] 29-Apr-93 bradlo: Added header.
|
||
|
*
|
||
|
*Implementation Notes:
|
||
|
*
|
||
|
*****************************************************************************/
|
||
|
|
||
|
#include <stdio.h>
|
||
|
|
||
|
#include "disptest.h"
|
||
|
#include "tstsuite.h"
|
||
|
|
||
|
ASSERTDATA
|
||
|
|
||
|
|
||
|
#if OE_MAC
|
||
|
typedef void* HWND;
|
||
|
#endif
|
||
|
|
||
|
#if !OE_MAC
|
||
|
extern HINSTANCE g_hinst;
|
||
|
#endif
|
||
|
|
||
|
struct TEST_INFO {
|
||
|
HRESULT hresult;
|
||
|
HWND hwnd;
|
||
|
BSTR bstr;
|
||
|
|
||
|
ITestSuite FAR * ptst;
|
||
|
|
||
|
TEST_INFO () {
|
||
|
hresult = NOERROR;
|
||
|
hwnd = NULL;
|
||
|
bstr = NULL;
|
||
|
ptst = NULL;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
extern int g_fLog;
|
||
|
|
||
|
STDAPI_(void)
|
||
|
PassFail(HRESULT hresult, OLECHAR FAR* szCaption, HWND hwnd)
|
||
|
{
|
||
|
SCODE sc;
|
||
|
TCHAR buf[80];
|
||
|
|
||
|
sc = GetScode(hresult);
|
||
|
// E_ABORT indicates the test was aborted by the user, so we do nothing.
|
||
|
if(sc == E_ABORT)
|
||
|
return;
|
||
|
SPRINTF(buf,
|
||
|
HC_MPW ? TSTR("%s : %s") : TSTR("%Fs : %Fs"),
|
||
|
(TCHAR FAR*)(FAILED(sc) ? TSTR("FAIL") : TSTR("PASS")),
|
||
|
DbSzOfScode(sc));
|
||
|
|
||
|
DbPrintf(
|
||
|
HC_MPW ? "DT : %s ==> %s\n" : "DT : %Fs ==> %Fs\n",
|
||
|
szCaption == NULL ? "" : (char FAR*) STRING(szCaption), (char FAR*)buf);
|
||
|
|
||
|
#if OE_MAC
|
||
|
UNUSED(hwnd);
|
||
|
#else
|
||
|
MessageBox(hwnd, buf, STRING(szCaption), MB_OK);
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
#if !OE_MAC /* { */
|
||
|
|
||
|
int g_nPercent = 0;
|
||
|
BOOL g_fCancel = FALSE;
|
||
|
|
||
|
void PASCAL
|
||
|
PaintTheGuage(HWND hwndDlg)
|
||
|
{
|
||
|
RECT rc;
|
||
|
HDC hdc;
|
||
|
int width;
|
||
|
HBRUSH hbrush;
|
||
|
HWND hwndGuage;
|
||
|
|
||
|
hwndGuage = GetDlgItem(hwndDlg, IDD_SUITE_GUAGE);
|
||
|
|
||
|
hdc = GetDC(hwndGuage);
|
||
|
|
||
|
GetClientRect(hwndGuage, &rc);
|
||
|
|
||
|
// draw the border
|
||
|
//
|
||
|
hbrush = CreateSolidBrush(RGB(255, 255, 255));
|
||
|
hbrush = (HBRUSH)SelectObject(hdc, hbrush);
|
||
|
Rectangle(hdc, rc.left, rc.top, rc.right, rc.bottom);
|
||
|
DeleteObject(SelectObject(hdc, hbrush));
|
||
|
|
||
|
// leave room for the border
|
||
|
//
|
||
|
rc.left++, rc.top++, rc.right--, rc.bottom--;
|
||
|
|
||
|
// compute the ammount of the guage rect to fill
|
||
|
//
|
||
|
width = ((rc.right - rc.left) * g_nPercent) / 100;
|
||
|
rc.right = rc.left + width;
|
||
|
|
||
|
// fill the guage
|
||
|
//
|
||
|
hbrush = CreateSolidBrush(RGB(0, 0, 255)); // Blue brush
|
||
|
FillRect(hdc, &rc, hbrush);
|
||
|
DeleteObject(hbrush);
|
||
|
|
||
|
ReleaseDC(hwndGuage, hdc);
|
||
|
}
|
||
|
|
||
|
|
||
|
extern "C" BOOL CALLBACK EXPORT
|
||
|
SuiteDlgProc(HWND hwndDlg, unsigned message, WORD wparam, LONG lparam)
|
||
|
{
|
||
|
switch(message){
|
||
|
case WM_INITDIALOG:
|
||
|
return TRUE;
|
||
|
|
||
|
case WM_COMMAND:
|
||
|
if(wparam == IDCANCEL){
|
||
|
g_fCancel = TRUE;
|
||
|
return TRUE;
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case WM_PAINT:
|
||
|
PaintTheGuage(hwndDlg);
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
return FALSE;
|
||
|
}
|
||
|
|
||
|
|
||
|
void PASCAL
|
||
|
ProgressYield(HWND hwnd)
|
||
|
{
|
||
|
MSG msg;
|
||
|
|
||
|
// empty the message loop...
|
||
|
while(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)){
|
||
|
if(!hwnd || !IsDialogMessage(hwnd, &msg)){
|
||
|
TranslateMessage(&msg);
|
||
|
DispatchMessage(&msg);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#endif /* } */
|
||
|
|
||
|
|
||
|
// Open a logfile for the given suite, in the directory that the
|
||
|
// test app lives.
|
||
|
//
|
||
|
|
||
|
#if defined(WIN32)
|
||
|
#define GETMODULEFILENAME GetModuleFileNameA
|
||
|
#else
|
||
|
#define GETMODULEFILENAME GetModuleFileName
|
||
|
#endif
|
||
|
|
||
|
HFILE
|
||
|
OpenLogFile(ITestSuite FAR* ptst)
|
||
|
{
|
||
|
BSTR bstr;
|
||
|
HFILE hfile;
|
||
|
char szModuleName[512], FAR* p, FAR* q;
|
||
|
char buf[256];
|
||
|
|
||
|
bstr = NULL;
|
||
|
hfile = HFILE_ERROR;
|
||
|
|
||
|
#if OE_WIN
|
||
|
int cb;
|
||
|
if((cb = GETMODULEFILENAME(g_hinst, szModuleName,sizeof(szModuleName)))==0)
|
||
|
goto LError0; /* couldn't get the module name */
|
||
|
#endif
|
||
|
|
||
|
// get the logfile name for this suite
|
||
|
if(ptst->GetNameOfLogfile(&bstr) != NOERROR)
|
||
|
goto LError0;
|
||
|
|
||
|
#if OE_WIN
|
||
|
// backup to the first path separator (ie, backup over the .exe name)
|
||
|
for(p = &szModuleName[cb]; p > szModuleName; --p){
|
||
|
if(*p == '\\'){
|
||
|
++p;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
#else
|
||
|
p = szModuleName;
|
||
|
#endif
|
||
|
|
||
|
// tack the logfile name onto the end of the path
|
||
|
#if OE_WIN32
|
||
|
for(q = ConvertStrWtoA(bstr, buf); *q != '\0';)
|
||
|
#else
|
||
|
for(q = bstr; *q != '\0';)
|
||
|
#endif
|
||
|
*p++ = *q++;
|
||
|
*p = '\0';
|
||
|
|
||
|
#if OE_WIN
|
||
|
OFSTRUCT of;
|
||
|
hfile = OpenFile(szModuleName, &of, OF_CREATE);
|
||
|
#else
|
||
|
hfile = fopen(szModuleName, "w");
|
||
|
#endif
|
||
|
|
||
|
LError0:;
|
||
|
SysFreeString(bstr);
|
||
|
|
||
|
return hfile;
|
||
|
}
|
||
|
|
||
|
/***
|
||
|
*HRESULT DispTestDoSuite(ITestSuite*, HWND)
|
||
|
*Purpose:
|
||
|
* Execute all tests in the given suite.
|
||
|
*
|
||
|
*Entry:
|
||
|
* ptst = the ITestSuite* to execute tests from
|
||
|
* hwnd = HWND of the parent window
|
||
|
*
|
||
|
*Exit:
|
||
|
* return value = HRESULT
|
||
|
*
|
||
|
***********************************************************************/
|
||
|
HRESULT
|
||
|
DispTestDoSuite(ITestSuite FAR* ptst, HWND hwnd)
|
||
|
{
|
||
|
BSTR bstr;
|
||
|
HRESULT hresult;
|
||
|
unsigned int i, cTests;
|
||
|
|
||
|
#if !OE_MAC
|
||
|
TCHAR buf[32];
|
||
|
HWND hwndSuiteDlg;
|
||
|
|
||
|
#if OE_WIN16
|
||
|
static DLGPROC pfnSuiteDlgProc;
|
||
|
#endif // OE_WIN16
|
||
|
#endif
|
||
|
|
||
|
#if OE_MAC
|
||
|
UNUSED(hwnd);
|
||
|
#endif
|
||
|
|
||
|
#if !OE_MAC
|
||
|
g_fCancel = FALSE;
|
||
|
#endif
|
||
|
|
||
|
if(g_fLog){
|
||
|
ASSERT(Pappdata()->m_hfLogfile == HFILE_ERROR);
|
||
|
if((Pappdata()->m_hfLogfile = OpenLogFile(ptst)) == HFILE_ERROR){
|
||
|
hresult = RESULT(E_FAIL);
|
||
|
goto LError0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
IfFailGo(ptst->GetNameOfSuite(&bstr), LError0);
|
||
|
IfFailGo(ptst->GetTestCount(&cTests), LError0);
|
||
|
PrintSuiteHeader(STRING(bstr));
|
||
|
|
||
|
#if OE_WIN
|
||
|
|
||
|
#if OE_WIN16
|
||
|
pfnSuiteDlgProc =
|
||
|
(DLGPROC)MakeProcInstance((DLGPROC)SuiteDlgProc, g_hinst);
|
||
|
|
||
|
hwndSuiteDlg = CreateDialog(g_hinst, TSTR("SuiteDlg"), hwnd, pfnSuiteDlgProc);
|
||
|
|
||
|
#else // !OE_WIN16
|
||
|
hwndSuiteDlg = CreateDialog(g_hinst, TSTR("SuiteDlg"), hwnd, (DLGPROC)SuiteDlgProc);
|
||
|
#endif // !OE_WIN16
|
||
|
|
||
|
SetDlgItemText(hwndSuiteDlg, IDD_SUITE_NAME, STRING(bstr));
|
||
|
ShowWindow(hwndSuiteDlg, SW_SHOW);
|
||
|
#endif
|
||
|
|
||
|
SysFreeString(bstr);
|
||
|
|
||
|
for(i = 0; i < cTests; ++i){
|
||
|
IfFailGo(ptst->GetNameOfTest(i, &bstr), LError0);
|
||
|
PrintTestHeader(STRING(bstr));
|
||
|
#if OE_WIN
|
||
|
SetDlgItemText(hwndSuiteDlg, IDD_SUITE_TESTNAME, STRING(bstr));
|
||
|
#endif
|
||
|
SysFreeString(bstr);
|
||
|
|
||
|
#if OE_WIN
|
||
|
g_nPercent = (i*100) / cTests;
|
||
|
SPRINTF(buf, TSTR("%d%%"), g_nPercent);
|
||
|
SetDlgItemText(hwndSuiteDlg, IDD_SUITE_PERCENT, buf);
|
||
|
PaintTheGuage(hwndSuiteDlg);
|
||
|
#endif
|
||
|
|
||
|
// ** Execute the Test **
|
||
|
//
|
||
|
IfFailGo(ptst->DoTest(i), LError0);
|
||
|
|
||
|
#if OE_WIN
|
||
|
ProgressYield(hwndSuiteDlg);
|
||
|
if(g_fCancel == TRUE){
|
||
|
hresult = RESULT(E_ABORT);
|
||
|
goto LError0;
|
||
|
}
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
#if OE_WIN
|
||
|
g_nPercent = 100;
|
||
|
PaintTheGuage(hwndSuiteDlg);
|
||
|
#endif
|
||
|
|
||
|
hresult = NOERROR;
|
||
|
|
||
|
LError0:;
|
||
|
#if OE_WIN
|
||
|
if(hwndSuiteDlg)
|
||
|
DestroyWindow(hwndSuiteDlg);
|
||
|
|
||
|
#if OE_WIN16
|
||
|
FreeProcInstance(pfnSuiteDlgProc);
|
||
|
#endif // OE_WIN16
|
||
|
#endif
|
||
|
|
||
|
if(Pappdata()->m_hfLogfile != HFILE_ERROR){
|
||
|
#if OE_MAC
|
||
|
fclose(Pappdata()->m_hfLogfile);
|
||
|
#else
|
||
|
_lclose(Pappdata()->m_hfLogfile);
|
||
|
#endif
|
||
|
Pappdata()->m_hfLogfile = HFILE_ERROR;
|
||
|
}
|
||
|
|
||
|
return hresult;
|
||
|
}
|
||
|
|
||
|
struct {
|
||
|
int idm;
|
||
|
ITestSuite FAR* (*create)(void);
|
||
|
} g_rgTestSuite[] = {
|
||
|
#if OE_MAC
|
||
|
{ IDM_SUITE_BSTR, CBstrSuite::Create }
|
||
|
, { IDM_SUITE_TIME, CTimeSuite::Create }
|
||
|
, { IDM_SUITE_DATECNV, CDateCoersionSuite::Create }
|
||
|
, { IDM_SUITE_VARIANT, CVariantSuite::Create }
|
||
|
, { IDM_SUITE_SAFEARRAY, CSafeArraySuite::Create }
|
||
|
, { IDM_SUITE_NLS, CNlsSuite::Create }
|
||
|
// # if HC_MPW
|
||
|
, { IDM_SUITE_BIND, CBindSuite::Create }
|
||
|
, { IDM_SUITE_INVOKE_BYVAL, CInvokeByValSuite::Create }
|
||
|
, { IDM_SUITE_INVOKE_BYREF, CInvokeByRefSuite::Create }
|
||
|
, { IDM_SUITE_INVOKE_SAFEARRAY, CInvokeSafeArraySuite::Create }
|
||
|
, { IDM_SUITE_INVOKE_EXCEPINFO, CInvokeExcepinfoSuite::Create }
|
||
|
, { IDM_SUITE_COLLECTION, CCollectionSuite::Create }
|
||
|
// # endif
|
||
|
#else
|
||
|
{ IDM_SUITE_BSTR, CBstrSuite::Create }
|
||
|
, { IDM_SUITE_TIME, CTimeSuite::Create }
|
||
|
, { IDM_SUITE_DATECNV, CDateCoersionSuite::Create }
|
||
|
, { IDM_SUITE_VARIANT, CVariantSuite::Create }
|
||
|
, { IDM_SUITE_SAFEARRAY, CSafeArraySuite::Create }
|
||
|
, { IDM_SUITE_NLS, CNlsSuite::Create }
|
||
|
, { IDM_SUITE_BIND, CBindSuite::Create }
|
||
|
, { IDM_SUITE_INVOKE_BYVAL, CInvokeByValSuite::Create }
|
||
|
, { IDM_SUITE_INVOKE_BYREF, CInvokeByRefSuite::Create }
|
||
|
, { IDM_SUITE_INVOKE_SAFEARRAY, CInvokeSafeArraySuite::Create }
|
||
|
, { IDM_SUITE_INVOKE_EXCEPINFO, CInvokeExcepinfoSuite::Create }
|
||
|
, { IDM_SUITE_COLLECTION, CCollectionSuite::Create }
|
||
|
, { IDM_SUITE_EARLY, CEarlySuite::Create }
|
||
|
#endif
|
||
|
};
|
||
|
|
||
|
|
||
|
/***
|
||
|
*PRIVATE ITestSuite *ITestSuiteFromIDM(int)
|
||
|
*Purpose:
|
||
|
* Create an ITestSuite* from the given message ID.
|
||
|
*
|
||
|
*Entry:
|
||
|
* idm = the message ID.
|
||
|
*
|
||
|
*Exit:
|
||
|
* return value = ITestSuite*, NULL if unable to create.
|
||
|
*
|
||
|
***********************************************************************/
|
||
|
HRESULT
|
||
|
ITestSuiteFromIDM(int idm, ITestSuite FAR* FAR* pptst)
|
||
|
{
|
||
|
int ix;
|
||
|
|
||
|
for(ix = 0; ix < DIM(g_rgTestSuite); ++ix){
|
||
|
if(g_rgTestSuite[ix].idm == idm){
|
||
|
return((*pptst = g_rgTestSuite[ix].create()) != NULL
|
||
|
? NOERROR : RESULT(E_OUTOFMEMORY));
|
||
|
}
|
||
|
}
|
||
|
return RESULT(E_FAIL); // test suite not found
|
||
|
}
|
||
|
|
||
|
|
||
|
#if OE_WIN32
|
||
|
|
||
|
/***
|
||
|
*DWORD ThreadDoSuite
|
||
|
*Purpose:
|
||
|
* Run a test inside it's own thread.
|
||
|
*
|
||
|
*Entry:
|
||
|
* Pointer to a TEST_INFO structure.
|
||
|
*
|
||
|
*Exit:
|
||
|
* returns 0.
|
||
|
*
|
||
|
***********************************************************************/
|
||
|
DWORD ThreadDoSuite(LPDWORD lpdwParam)
|
||
|
{
|
||
|
TEST_INFO FAR* ptinfo = (TEST_INFO FAR *)lpdwParam;
|
||
|
HRESULT hresult;
|
||
|
|
||
|
hresult = OleInitialize(NULL);
|
||
|
ASSERT(SUCCEEDED(hresult));
|
||
|
|
||
|
if (InitAppData()) {
|
||
|
ptinfo->hresult = DispTestDoSuite(ptinfo->ptst, NULL);
|
||
|
ReleaseAppData();
|
||
|
}
|
||
|
|
||
|
OleUninitialize();
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
#endif // OE_WIN32
|
||
|
|
||
|
|
||
|
/***
|
||
|
*HRESULT DispTestAll(HWND)
|
||
|
*Purpose:
|
||
|
* Execute All IDispatch tests in all suites.
|
||
|
*
|
||
|
*Entry:
|
||
|
* hwnd = window handle
|
||
|
* fShowDialog = TRUE if we should display a dialog on a failure.
|
||
|
* fMultiThread = TRUE if we should multithread the tests.
|
||
|
*
|
||
|
*Exit:
|
||
|
* return value = HRESULT
|
||
|
*
|
||
|
***********************************************************************/
|
||
|
STDAPI
|
||
|
DispTestAll(HWND hwnd, int fShowDialog, int fMultiThread)
|
||
|
{
|
||
|
int i;
|
||
|
HRESULT hresult = NOERROR;
|
||
|
|
||
|
TEST_INFO rgtinfo[DIM(g_rgTestSuite)];
|
||
|
|
||
|
#if OE_WIN32
|
||
|
HANDLE rghThread[DIM(g_rgTestSuite)];
|
||
|
DWORD rgThreadId[DIM(g_rgTestSuite)];
|
||
|
|
||
|
HRESULT hRet;
|
||
|
#endif // OE_WIN32
|
||
|
|
||
|
// Set up all of the tests.
|
||
|
for(i = 0; i < DIM(g_rgTestSuite); ++i) {
|
||
|
rgtinfo[i].hwnd = hwnd;
|
||
|
rgtinfo[i].ptst = g_rgTestSuite[i].create();
|
||
|
if(rgtinfo[i].ptst == NULL) {
|
||
|
hresult = RESULT(E_OUTOFMEMORY);
|
||
|
goto LError0;
|
||
|
}
|
||
|
|
||
|
IfFailGo(rgtinfo[i].ptst->GetNameOfSuite(&rgtinfo[i].bstr), LError0);
|
||
|
}
|
||
|
|
||
|
// Run the suites.
|
||
|
for(i = 0; i < DIM(g_rgTestSuite); ++i) {
|
||
|
#if OE_WIN32
|
||
|
if (fMultiThread) {
|
||
|
rghThread[i] = CreateThread(NULL,
|
||
|
0,
|
||
|
(LPTHREAD_START_ROUTINE)ThreadDoSuite,
|
||
|
&rgtinfo[i],
|
||
|
0,
|
||
|
&rgThreadId[i]);
|
||
|
|
||
|
if (rghThread[i] == NULL) {
|
||
|
hresult = ResultFromScode(E_OUTOFMEMORY);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
#endif // OE_WIN32
|
||
|
{
|
||
|
rgtinfo[i].hresult = DispTestDoSuite(rgtinfo[i].ptst, hwnd);
|
||
|
|
||
|
// E_ABORT means the test was aborted by the user.
|
||
|
//
|
||
|
if(GetScode(rgtinfo[i].hresult) == E_ABORT) {
|
||
|
hresult = ResultFromScode(E_ABORT);
|
||
|
goto LError0;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#if OE_WIN32
|
||
|
// Wait for all of the threads to finish.
|
||
|
if (fMultiThread) {
|
||
|
hRet = WaitForMultipleObjects(DIM(g_rgTestSuite),
|
||
|
rghThread,
|
||
|
TRUE,
|
||
|
INFINITE);
|
||
|
|
||
|
ASSERT(SUCCEEDED(hRet));
|
||
|
}
|
||
|
#endif // OE_WIN32
|
||
|
|
||
|
// Check the results.
|
||
|
for (i = 0; i < DIM(g_rgTestSuite) && fShowDialog; ++i) {
|
||
|
if (FAILED(rgtinfo[i].hresult)) {
|
||
|
PassFail(rgtinfo[i].hresult, rgtinfo[i].bstr, hwnd);
|
||
|
hresult = ResultFromScode(S_FALSE);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
LError0:;
|
||
|
for (i = 0; i < DIM(g_rgTestSuite) && rgtinfo[i].ptst; ++i) {
|
||
|
rgtinfo[i].ptst->Release();
|
||
|
SysFreeString(rgtinfo[i].bstr);
|
||
|
}
|
||
|
|
||
|
return hresult;
|
||
|
}
|
||
|
|
||
|
|
||
|
/***
|
||
|
*PRIVATE HRESULT DispTestOne(int)
|
||
|
*Purpose:
|
||
|
* Execute the test suite associated with the given message ID.
|
||
|
*
|
||
|
*Entry:
|
||
|
* idm = the message id
|
||
|
*
|
||
|
*Exit:
|
||
|
* return value = HRESULT
|
||
|
*
|
||
|
***********************************************************************/
|
||
|
STDAPI
|
||
|
DispTestOne(HWND hwnd, int idm)
|
||
|
{
|
||
|
BSTR bstr;
|
||
|
HRESULT hresult;
|
||
|
ITestSuite FAR* ptst;
|
||
|
|
||
|
ptst = NULL;
|
||
|
bstr = NULL;
|
||
|
|
||
|
IfFailGo(ITestSuiteFromIDM(idm, &ptst), LError0);
|
||
|
|
||
|
IfFailGo(ptst->GetNameOfSuite(&bstr), LError0);
|
||
|
|
||
|
hresult = DispTestDoSuite(ptst, hwnd);
|
||
|
|
||
|
LError0:;
|
||
|
PassFail(hresult, bstr, hwnd);
|
||
|
|
||
|
if(bstr != NULL)
|
||
|
SysFreeString(bstr);
|
||
|
|
||
|
if(ptst != NULL)
|
||
|
ptst->Release();
|
||
|
|
||
|
return hresult;
|
||
|
}
|
||
|
|