// Copyright (c) 1998-2009 Nokia Corporation and/or its subsidiary(-ies).
// All rights reserved.
// This component and the accompanying materials are made available
// under the terms of "Eclipse Public License v1.0"
// which accompanies this distribution, and is available
// at the URL "http://www.eclipse.org/legal/epl-v10.html".
//
// Initial Contributors:
// Nokia Corporation - initial contribution.
//
// Contributors:
//
// Description:
//

#include "t_dbstress.h"

//#define __DUMP_STATE
#if defined(__WINS__)
//#define _INSTALL_FILE_SYSTEM
#endif

GLDEF_D RTest test(_L("t_dbstress: Stress testing DBMS"));

GLDEF_D TPtrC KTestDir=_S("\\DBMS-TST\\");
GLDEF_D TPtrC KLogFile=_L("T_STRESS.LOG");
GLDEF_D TPtrC KTestDatabase=_S("T_STRESS.DB");
GLDEF_D TInt NewCount,OldCount;
GLDEF_D TInt TransId;
GLDEF_D Timer RunTimer;


LOCAL_D RFs TheFs;
LOCAL_D RThread TheThread;
LOCAL_D TRequestStatus TheStatus;
LOCAL_D RDbStoreDatabase TheDatabase;
LOCAL_D RDbView Accs;
LOCAL_D RDbView Trans;
LOCAL_D RDbTable TheTable;
LOCAL_D TInt Shot,ShotDuringCommit;
LOCAL_D TInt64 RunningTime(1);
LOCAL_D Timer Stopwatch;

#ifdef __DUMP_STATE
const TPtrC KDumpFile=_S("T_STRESS.DMP");
#endif
const TInt KTestCleanupStack=0x20;

void Timer::Start()
	{
	iTime.UniversalTime();
	}

TInt64 Timer::Stop()
	{
	TTime t;
	t.UniversalTime();
	return ((t.MicroSecondsFrom(iTime).Int64()) + 500)/1000;
	}

void Timer::Print()
	{
	TInt64 milli=Stop();
	test.Printf(_L("  %u milliseconds\n"), I64LOW(milli) );
	}

class Set
	{
public:
	struct SColDef
		{
		const TDesC* iName;
		TDbColType iType;
		TInt iAttributes;
		};
public:
	static CDbColSet* CreateL(const SColDef* aDef);
	};
CDbColSet* Set::CreateL(const SColDef* aDef)
	{
	CDbColSet *set=CDbColSet::NewLC();
	for (;aDef->iName!=NULL;++aDef)
		{
		TDbCol col(*aDef->iName,aDef->iType);
		col.iAttributes=aDef->iAttributes;
		set->AddL(col);
		}
	CleanupStack::Pop();
	return set;
	}

// Accounts table
const TPtrC KAccounts=_S("ACCOUNTS");
const TPtrC KAccountsID=_S("ID");
const TPtrC KAccountsBalance=_S("BALANCE");
const TPtrC KAccountsStatement=_S("STATEMENT_BALANCE");
Set::SColDef const AccountsDef[]=
	{
	{&KAccountsID,EDbColInt32,TDbCol::ENotNull},
	{&KAccountsBalance,EDbColInt32,TDbCol::ENotNull},
	{&KAccountsStatement,EDbColInt32,TDbCol::ENotNull},
	{0}
	};
const TInt KInitialCash=100000;
const TInt KInitialBalance=1000;

// Transaction table
const TPtrC KTransactions=_S("TRANSACTIONS");
const TPtrC KTransactionDate=_S("T_DATE");
const TPtrC KTransactionFrom=_S("FROM_ID");
const TPtrC KTransactionTo=_S("TO_ID");
const TPtrC KTransactionAmount=_S("AMOUNT");
Set::SColDef const TransactionsDef[]=
	{
//	{&KTransactionDate,EDbColDateTime,TDbCol::ENotNull},
	{&KTransactionDate,EDbColInt32,TDbCol::ENotNull},
	{&KTransactionFrom,EDbColInt32,TDbCol::ENotNull},
	{&KTransactionTo,EDbColInt32,TDbCol::ENotNull},
	{&KTransactionAmount,EDbColInt32,TDbCol::ENotNull},
	{0}
	};

LOCAL_D TInt32 TotalMonies;
LOCAL_D TBuf<100> Buf;

GLDEF_C TInt Random(TInt aRange)
	{
	return (Math::Random()>>11)%aRange;
	}

#undef test
LOCAL_C void Test(TInt aValue,TInt anExpected,TInt aLine)
	{
	if (aValue==anExpected)
		return;
	test.Printf(_L("** Expected %d, was %d\n"),anExpected,aValue);
	test(EFalse,aLine);
	}
#define test1(aTest) test(aTest,__LINE__)
#define test2(aValue,anExpected) Test(aValue,anExpected,__LINE__)

LOCAL_C void CreateIndexL(RDbDatabase& aDatabase,const TDesC& aTable,const TDesC& aColumn,TBool aUnique)
	{
	CDbKey* key=CDbKey::NewLC();
	key->AddL(aColumn);
	if (aUnique)
		key->MakeUnique();
	test2(aDatabase.CreateIndex(aColumn,aTable,*key),KErrNone);
	CleanupStack::PopAndDestroy();
	}

//
// Create the database
//
LOCAL_C void CreateDatabaseL()
	{
	CFileStore* store=CPermanentFileStore::ReplaceLC(TheFs,KTestDatabase,EFileRead|EFileWrite);
	store->SetTypeL(KPermanentFileStoreLayoutUid);
	store->SetRootL(TheDatabase.CreateL(store));
//	create the tables
	TheDatabase.Begin();
	CDbColSet* set=Set::CreateL(AccountsDef);
	test2(TheDatabase.CreateTable(KAccounts,*set),KErrNone);
	delete set;
	CreateIndexL(TheDatabase,KAccounts,KAccountsID,ETrue);
	CreateIndexL(TheDatabase,KAccounts,KAccountsBalance,EFalse);
	set=Set::CreateL(TransactionsDef);
	test2(TheDatabase.CreateTable(KTransactions,*set),KErrNone);
	delete set;
	CreateIndexL(TheDatabase,KTransactions,KTransactionDate,EFalse);
	test2(TheDatabase.Commit(),KErrNone);
	OldCount=NewCount=0;
// prepare Accs table
	TheDatabase.Begin();
	test2(Accs.Prepare(TheDatabase,_L("select * from accounts"),Accs.EInsertOnly),KErrNone);
	Accs.InsertL();
	Accs.SetColL(1,TInt32(ECash));
	Accs.SetColL(2,KInitialCash);
	Accs.SetColL(3,KInitialCash);
	Accs.PutL();
	TotalMonies=KInitialCash;
	for (TInt ii=EJohn;ii<=EPenny;++ii)
		{
		Accs.InsertL();
		Accs.SetColL(1,ii);
		Accs.SetColL(2,KInitialBalance);
		Accs.SetColL(3,KInitialBalance);
		Accs.PutL();
		TotalMonies+=KInitialBalance;
		}
	test2(TheDatabase.Commit(),KErrNone);
	Accs.Close();
	TheDatabase.Close();
	CleanupStack::PopAndDestroy();	// store
	}


#ifdef __DUMP_STATE
LOCAL_C void DumpStateL()
	{
	RFile file;
	CleanupClosePushL(file);
	User::LeaveIfError(file.Replace(TheFs,KLogFile,EFileWrite|EFileStreamText));
	RDbRowSet::RConstraint match;
	CleanupClosePushL(match);
	for (TInt id=ECash;id<=EPenny;++id)
		{
		Buf.Format(_L("id=%d"),id);
		Accs.FirstL();
		test1(Accs.FindL(Accs.EForwards,Buf)>=0);
		Accs.GetL();
		TInt balance=Accs.ColInt(2);
		Buf.Format(_L("\nStatement for account %d: Previous balance %d\n"),id,balance);
		User::LeaveIfError(file.Write(Buf));
		Buf.Format(_L("from_id=%d or to_id=%d"),id,id);
		User::LeaveIfError(match.Open(Trans,Buf));
		for (Trans.BeginningL();Trans.NextL();)
			{
			if (Trans.MatchL(match))
				{
				Trans.GetL();
				TInt from=Trans.ColInt(2);
				TInt amount=Trans.ColInt(4);
				Buf.Format(_L("%04d: %6s %5d\n"),Trans.ColInt(1),from==id?_S("debit"):_S("credit"),amount);
				User::LeaveIfError(file.Write(Buf));
				if (from==id)
					balance-=amount;
				else
					balance+=amount;
				}
			}
		Buf.Format(_L("Closing balance %d\n"),balance);
		User::LeaveIfError(file.Write(Buf));
		Buf.Format(_L("Account balance %d\n"),Accs.ColInt(3));
		User::LeaveIfError(file.Write(Buf));
		}
	CleanupStack::PopAndDestroy(2);
	test1(0);
	}
#endif

//
// Check that the database structure is fully intact
//
LOCAL_C void VerifyDatabaseL(CPersistentStore& aStore)
	{
	TheDatabase.OpenL(&aStore,aStore.Root());
// check any indexes
	test2(TheTable.Open(TheDatabase,KAccounts,TheTable.EReadOnly),KErrNone);
	test2(TheTable.CountL(),KAccountIDs);
	TInt r=TheTable.SetIndex(KAccountsID);
	if (r!=KErrCorrupt)
		{
		test2(r,KErrNone);
		test2(TheTable.CountL(),KAccountIDs);
		for (TInt id=ECash;id<=EPenny;++id)
			{
			test1(TheTable.NextL());
			TheTable.GetL();
			test2(TheTable.ColInt(1),id);
			}
		test1(!TheTable.NextL());
		}
	r=TheTable.SetIndex(KAccountsBalance);
	if (r!=KErrCorrupt)
		{
		test2(r,KErrNone);
		test2(TheTable.CountL(),KAccountIDs);
		test1(TheTable.FirstL());
		TheTable.GetL();
		TInt last=TheTable.ColInt(2);
		for (TInt ii=1;ii<KAccountIDs;++ii)
			{
			test1(TheTable.NextL());
			TheTable.GetL();
			TInt bal=TheTable.ColInt(2);
			test1(bal>=last);
			last=bal;
			}
		test1(!TheTable.NextL());
		}
	TheTable.Close();
	test2(TheTable.Open(TheDatabase,KTransactions,TheTable.EReadOnly),KErrNone);
	TInt count=TheTable.CountL();
	r=TheTable.SetIndex(KTransactionDate);
	if (r!=KErrCorrupt)
		{
		test2(r,KErrNone);
		test2(TheTable.CountL(),count);
		if (count)
			{
			test1(TheTable.FirstL());
			TheTable.GetL();
			TInt last=TheTable.ColInt(1);
			while (--count!=0)
				{
				test1(TheTable.NextL());
				TheTable.GetL();
				TInt date=TheTable.ColInt(1);
				test1(date>last);
				last=date;
				}
			test1(!TheTable.NextL());
			}
		else
			test1(!TheTable.FirstL());
		}
	TheTable.Close();
// verify database integrity
	TInt balance[KAccountIDs];
	test2(Accs.Prepare(TheDatabase,_L("select id,statement_balance,balance from accounts"),Accs.EReadOnly),KErrNone);
	test2(Accs.CountL(),KAccountIDs);
	while (Accs.NextL())
		{
		Accs.GetL();
		TInt id=Accs.ColInt(1);
		balance[id]=Accs.ColInt(2);
		}
	test2(Trans.Prepare(TheDatabase,_L("select t_date,from_id,to_id,amount from Transactions"),Trans.EReadOnly),KErrNone);
	TInt transact=0;
	while (Trans.NextL())
		{
		++transact;
		Trans.GetL();
		TInt from=Trans.ColInt(2);
		TInt to=Trans.ColInt(3);
		TInt amount=Trans.ColInt(4);
		balance[from]-=amount;
		balance[to]+=amount;
		}
	test2(transact,Trans.CountL());
	if (NewCount!=-1 && transact!=NewCount)
		{
		test2(transact,OldCount);
		++ShotDuringCommit;
		}
	OldCount=NewCount=transact;
	TInt total=0;
	for (Accs.BeginningL();Accs.NextL();)
		{
		Accs.GetL();
		TInt id=Accs.ColInt(1);
#ifdef __DUMP_STATE
		if (balance[id]!=Accs.ColInt(3))
			DumpStateL();
#else
		test(balance[id]==Accs.ColInt(3));
#endif
		total+=balance[id];
		}
	test2(total,TotalMonies);
	Trans.Close();
	Accs.Close();
	TheDatabase.Close();
	}

LOCAL_C TInt Verify(CPersistentStore& aStore)
	{
	TRAPD(r,VerifyDatabaseL(aStore));
	return r;
	}

LOCAL_C TInt Recover(CPersistentStore& aStore)
	{
	TRAPD(r,TheDatabase.OpenL(&aStore,aStore.Root()));
	if (r==KErrNone)
		{
		r=TheDatabase.Recover();
		TheDatabase.Close();
		}
	return r;
	}

LOCAL_C void CompactL(CStreamStore& aStore)
	{
	TInt t=aStore.ReclaimL();
	Stopwatch.Start();
	t-=aStore.CompactL();
	test.Printf(_L("  compacted %d byte(s) in"),t);
	Stopwatch.Print();
	aStore.CommitL();
	}

LOCAL_C TInt Compact(CStreamStore& aStore)
	{
	TRAPD(r,CompactL(aStore));
	return r;
	}

LOCAL_C TInt EndThread()
	{
	RunningTime+=RunTimer.Stop();
	if (TheStatus==KRequestPending)
		TheThread.Kill(1);
	User::WaitForRequest(TheStatus);
	TInt r;
	if (TheThread.ExitType()==EExitKill)
		r=TheThread.ExitReason();
	else
		r=TheStatus.Int();
	TheThread.Close();
	return r;
	}

//aTestExecutionTime - desired test execution time in minutes
LOCAL_C void RunTestL(TInt aTestExecutionTime = 0)
	{
	__ASSERT_ALWAYS(aTestExecutionTime >= 0, User::Invariant());

	RThread().SetPriority(EPriorityMore);
	test.Start(_L("Create the database"));
	CreateDatabaseL();

	TTimeIntervalMinutes timeInterval(aTestExecutionTime);

	TTime timeCurrent;
	timeCurrent.UniversalTime();
	TTime timeEnd(timeCurrent);
	timeEnd += timeInterval;

	for (TBool condition=ETrue; condition; condition = aTestExecutionTime > 0 ? (timeCurrent < timeEnd) : ETrue)
		{
		test.Next(_L("Main loop"));
		test.Start(_L("Kick off the thread"));
		test2 (StartThread(TheThread,TheStatus),KErrNone);
		// random delay
		for (;;)
			{
			User::After(95000);
			if (TheStatus!=KRequestPending)
				break;
			if (Random(1000)<30)
				break;
			}
		test.Next(_L("End the thread"));
		TInt exit=EndThread();
		if (exit!=1)
			test.Printf(_L("  thread failed with error %d\n"),exit);
//
		++Shot;
		CFileStore* store=NULL;
		for (TInt ii=0;;++ii)
			{
			test.Printf(_L("Opening %d\r"),ii);
			TRAPD(r,store=CFileStore::OpenL(TheFs,KTestDatabase,EFileRead|EFileWrite));
			if (r==KErrNone)
				break;
			test (r==KErrInUse);
			User::After(100000);
			}
		test.Next(_L("Verify & Recover"));
		test2 (Verify(*store),KErrNone);
		TInt64 tps(TransId);
		tps*=1000u;
		tps/=RunningTime;
		test.Printf(_L("    Iteration %d, TPS %d, during commit %d%%\n"),Shot,I64LOW(tps),(100*ShotDuringCommit)/Shot);
		TInt r=Recover(*store);
		if (r==KErrNoMemory || r==KErrDiskFull)
			{	// need to compact before completing recovery
			test.Next(_L("No space, compacting"));
			test2 (Compact(*store),KErrNone);
			test.Next(_L("Verify & Recover again"));
			test2 (Verify(*store),KErrNone);
			r=Recover(*store);
			}
		test2 (r,KErrNone);
		test.Next(_L("Verify & Compact"));
//		test2 (Verify(*store),KErrNone);
		test2 (Compact(*store),KErrNone);
		test.Next(_L("Verify"));
		test2 (Verify(*store),KErrNone);
//
		delete store;
		test.End();

		timeCurrent.UniversalTime();
		}
		test.End();
	}

/**
@SYMTestCaseID          SYSLIB-DBMS-CT-0636
@SYMTestCaseDesc        DBMS stess testing.
@SYMTestPriority        Medium
@SYMTestActions         Tests for verifying the database integrity.
@SYMTestExpectedResults Test must not fail
@SYMREQ                 REQ0000
*/
static void RunVerify()
	{
	test.Start(_L(" @SYMTestCaseID:SYSLIB-DBMS-CT-0636 Open store "));
	CFileStore* store=NULL;
	TRAPD(r,store=CFileStore::OpenL(TheFs,KTestDatabase,EFileRead|EFileWrite));
	test2 (r,KErrNone);
	test.Next(_L("Verify"));
	NewCount=-1;
	TotalMonies=KInitialCash + (EPenny-EJohn+1)*KInitialBalance;
	test2 (Verify(*store),KErrNone);
	test.Next(_L("Recover"));
	test2 (Recover(*store),KErrNone);
	test.Next(_L("Verify"));
	test2 (Verify(*store),KErrNone);
	delete store;
	test.End();
	}

//
// Prepare the test directory.
//
LOCAL_C void setupTestDirectory()
    {
	TInt r=TheFs.Connect();
	test2(r,KErrNone);
//
	r=TheFs.MkDir(KTestDir);
	test1(r==KErrNone || r==KErrAlreadyExists);
	r=TheFs.SetSessionPath(KTestDir);
	test2(r,KErrNone);
	}

LOCAL_C CTrapCleanup* setupCleanup()
//
// Initialise the cleanup stack.
//
    {
	CTrapCleanup* cleanup=CTrapCleanup::New();
	test1(cleanup!=NULL);
	TRAPD(r,\
		{\
		for (TInt i=KTestCleanupStack;i>0;i--)\
			CleanupStack::PushL((TAny*)0);\
		CleanupStack::Pop(KTestCleanupStack);\
		});
	test2(r,KErrNone);
	return cleanup;
	}

//
// entry point
//
// Parameters usage:
//	t_stress [-v]|[0]|[<positive number>]
// Where:
//	-v - a verification test will be run;
//	0  - a stress test will be run for indefinite time;
//	<positive number>  - a stress test will be run for <positive number> minutes;
// If the test is run without arguments, the test execution time will be 10 minutes
// (KDefaultTestExecutionTime constant bellow).
GLDEF_C TInt E32Main()
    {
	test.Title();
	setupTestDirectory();
	CTrapCleanup* cleanup=setupCleanup();
	__UHEAP_MARK;
//
	TBuf<100> cmd;
    User::CommandLine(cmd);
	TLex lex(cmd);
	TInt err = KErrNone;
	for(;;)
		{
		TPtrC arg(lex.NextToken());
		if(arg.Length() == 0)
			{
			const TInt KDefaultTestExecutionTime = 10;//default test execution time - minutes
			TRAP(err, RunTestL(KDefaultTestExecutionTime));
			break;
			}
		else if(arg.CompareF(_L("-v")) == 0)
			{
			RunVerify();
			break;
			}
		else
			{
			TInt32 testExecutionTime = 0;
			lex.Assign(arg);
			(void)lex.Val(testExecutionTime);
			TRAP(err, RunTestL(testExecutionTime));
			break;
			}
		}
	TInt err2 = TheFs.Delete(KTestDatabase);
	if(err2 != KErrNone)
		{
		RDebug::Print(_L("Error %d deleting \"%S\" file.\n"), err2, &KTestDatabase);
		}
	err2 = TheFs.Delete(KLogFile);
	if(err2 != KErrNone)
		{
		RDebug::Print(_L("Error %d deleting \"%S\" file.\n"), err2, &KLogFile);
		}
	test2(err, KErrNone);
//
	__UHEAP_MARKEND;
	delete cleanup;
	TheFs.Close();
	test.Close();
	return 0;
    }
