Added ==smoke-test support and ciftest command.
authorTom Sutcliffe <thomas.sutcliffe@accenture.com>
Wed, 15 Sep 2010 00:44:34 +0100
changeset 70 b06038904ef8
parent 69 5b2eab065888
child 71 264162c6ed91
Added ==smoke-test support and ciftest command. Commands can now define a "==smoke-test" section in their CIF files, which defines a snippet of fshell script that will be run as part of "fshell smoketest" or by invoking ciftest directly. See the ciftest documentation for more details. Added ==smoke-test sections to a few commands, the ones that were easy to test!
commands/clipboard/clipboard.cif
commands/drvinfo/drvinfo.cif
commands/hal/hal.cif
commands/kerninfo/kerninfo.cif
commands/rcomm/rcomm.cif
commands/rconn/rconn.cif
commands/sysinfo/sysinfo.cif
commands/uidinfo/uidinfo.cif
core/builtins/ciftest.cif
core/builtins/ciftest.cpp
core/builtins/ciftest.h
core/group/bld.inf
core/group/fshell_core.iby
core/src/command_factory.cpp
core/src/fshell.mmp
core/src/parser.cpp
core/src/parser.h
core/tsrc/smoketest.script
documentation/change_history.pod
libraries/iosrv/bwins/iocliu.def
libraries/iosrv/client/command_info_file.cpp
libraries/iosrv/eabi/iocliu.def
libraries/iosrv/inc/ioutils.h
--- a/commands/clipboard/clipboard.cif	Thu Sep 09 19:21:34 2010 +0100
+++ b/commands/clipboard/clipboard.cif	Wed Sep 15 00:44:34 2010 +0100
@@ -24,7 +24,16 @@
 
 Read the text from C<stdin> instead of from the command line.
 
+==smoke-test
+
+clipboard "Test data"
+clipboard | export -s RESULT
+var RESULT == "Test data" || $Error
+
+echo "$RESULT" | clipboard --stdin
+clipboard | export -s RES2
+var RES2 == "Test data^r^n" || $Error
+
 ==copyright
 
 Copyright (c) 2008-2010 Accenture. All rights reserved.
-
--- a/commands/drvinfo/drvinfo.cif	Thu Sep 09 19:21:34 2010 +0100
+++ b/commands/drvinfo/drvinfo.cif	Wed Sep 15 00:44:34 2010 +0100
@@ -32,3 +32,6 @@
 
 Copyright (c) 2007-2010 Accenture. All rights reserved.
 
+==smoke-test
+
+drvinfo $Quiet
--- a/commands/hal/hal.cif	Thu Sep 09 19:21:34 2010 +0100
+++ b/commands/hal/hal.cif	Wed Sep 15 00:44:34 2010 +0100
@@ -60,3 +60,6 @@
 
 Copyright (c) 2009-2010 Accenture. All rights reserved.
 
+==smoke-test
+
+hal $Quiet
--- a/commands/kerninfo/kerninfo.cif	Thu Sep 09 19:21:34 2010 +0100
+++ b/commands/kerninfo/kerninfo.cif	Wed Sep 15 00:44:34 2010 +0100
@@ -86,3 +86,17 @@
 
 Copyright (c) 2008-2010 Accenture. All rights reserved.
 
+==smoke-test
+
+kerninfo process $Quiet
+kerninfo thread $Quiet
+kerninfo chunk $Quiet
+kerninfo server $Quiet
+kerninfo codeseg $Quiet
+kerninfo hal $Quiet
+# Don't test windowgroup, mimetype - we may be on tshell
+kerninfo openfile $Quiet
+kerninfo msgq $Quiet
+kerninfo mutex $Quiet
+kerninfo semaphore $Quiet
+kerninfo timer $Quiet
--- a/commands/rcomm/rcomm.cif	Thu Sep 09 19:21:34 2010 +0100
+++ b/commands/rcomm/rcomm.cif	Wed Sep 15 00:44:34 2010 +0100
@@ -74,3 +74,6 @@
 
 Copyright (c) 2007-2010 Accenture. All rights reserved.
 
+==smoke-test
+
+rcomm $Quiet
--- a/commands/rconn/rconn.cif	Thu Sep 09 19:21:34 2010 +0100
+++ b/commands/rconn/rconn.cif	Wed Sep 15 00:44:34 2010 +0100
@@ -48,3 +48,6 @@
 
 Copyright (c) 2009-2010 Accenture. All rights reserved.
 
+==smoke-test
+
+rconn list $Quiet
--- a/commands/sysinfo/sysinfo.cif	Thu Sep 09 19:21:34 2010 +0100
+++ b/commands/sysinfo/sysinfo.cif	Wed Sep 15 00:44:34 2010 +0100
@@ -60,3 +60,6 @@
 
 Copyright (c) 2008-2010 Accenture. All rights reserved.
 
+==smoke-test
+
+sysinfo $Silent # Warnings about not being able to open TSY etc are acceptable so use $Silent
--- a/commands/uidinfo/uidinfo.cif	Thu Sep 09 19:21:34 2010 +0100
+++ b/commands/uidinfo/uidinfo.cif	Wed Sep 15 00:44:34 2010 +0100
@@ -32,3 +32,7 @@
 
 Copyright (c) 2009-2010 Accenture. All rights reserved.
 
+==smoke-test
+
+uidinfo 0x100041af | export -s RESULT
+var RESULT == "0x100041af EKern.exe^r^n" || $Error
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/core/builtins/ciftest.cif	Wed Sep 15 00:44:34 2010 +0100
@@ -0,0 +1,94 @@
+# ciftest.cif
+# 
+# Copyright (c) 2010 Accenture. All rights reserved.
+# This component and the accompanying materials are made available
+# under the terms of the "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:
+# Accenture - Initial contribution
+#
+
+==name ciftest
+
+==short-description
+
+Run fshell command smoke tests.
+
+==long-description
+
+This command runs smoke-tests for any or all commands that define a C<==smoke-test> section in their CIF file. A C<==smoke-test> section defines a short snippet of fshell script which tests the basic functionality offered by the command. It can be as simple as running the command with no arguments to make sure nothing catastrophic is wrong, or it can be a more in-depth test of all the command's functionality, or anything in between.
+
+Example CIF file that supports ciftest:
+
+    ==name mycmd
+    
+    [...]
+    
+    ==smoke-test
+    
+    mycmd | export -s RESULT
+    var RESULT == "Expected results of running mycmd" || $Error
+
+The following environment variables are defined for convenience when ciftest runs a smoke-test section:
+
+=over 5
+
+=item * Error
+
+Expands to a string that will cause a test to fail. Additionally it prints the current environment, hence is useful to use when C<var> commands fail, as in the above example. Equivalent to something like C<env && error>.
+
+=item * SCRIPT_NAME
+
+The script name is appended with ":smoke-test", eg "cifname.cif:smoke-test".
+
+=item * SCRIPT_PATH, 0
+
+Set as in any other script.
+
+=item * SCRIPT_LINE
+
+Set as in any other script. Line numbers are relative to the start of the CIF file, not the first line of the smoke-test section.
+
+=item * Quiet
+
+Used to supress stdout from a command, for when you don't want it to appear in the smoketest results. Usage:
+
+    mynoisycommand $Quiet
+
+Equivalent to putting C<E<gt>/dev/null> on the end of the command.
+
+=item * Silent
+
+Supresses both stdout and stderr. Useful when an operation is expected to fail. Usage:
+
+    mycommand expectfailure $Silent && $Error
+
+Note how $Silent is combined with C<&& $Error> such that if the command actually succeeded where it was expected to fail, the $Error case would cause the script to abort.
+
+=item * Verbose
+
+Defined if the C<--verbose> option was given to ciftest. Example usage:
+
+    var Verbose defined && echo "About to test something-or-other"
+
+=back
+
+The environment used for running the smoke-test snippets is not shared between commands, so do not set things in one smoketest script and expect to be able to see them in another. (Ie the snippets are run as if with "fshell" not "source").
+
+==argument string command optional
+
+If specified, run the tests associated with the specified command. If not specified, run tests for all commands.
+
+==option bool v verbose
+
+Print information about every test even when they succeed. By default only failures are printed. Also causes a summary to be printed at the end. Scripts can also print extra information themselves if this flag is set, by checking for the C<Verbose> environment variable.
+
+==option bool k keep-going
+
+Rather than stop on the first failure, attempt to run all tests even if some of them fail. Only relevant if no command argument is given.
+
+==copyright
+
+Copyright (c) 2010 Accenture. All rights reserved.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/core/builtins/ciftest.cpp	Wed Sep 15 00:44:34 2010 +0100
@@ -0,0 +1,190 @@
+// ciftest.cpp
+//
+// Copyright (c) 2010 Accenture. All rights reserved.
+// This component and the accompanying materials are made available
+// under the terms of the "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:
+// Accenture - Initial contribution
+//
+
+#include "ciftest.h"
+#include "fshell.h"
+#include "command_factory.h"
+
+CCommandBase* CCmdCifTest::NewLC()
+	{
+	CCmdCifTest* self = new(ELeave) CCmdCifTest();
+	CleanupStack::PushL(self);
+	self->BaseConstructL();
+	return self;
+	}
+
+CCmdCifTest::~CCmdCifTest()
+	{
+	delete iCmd;
+	delete iParser;
+	delete iEnvForScript;
+	delete iCurrentCif;
+	iCifFiles.ResetAndDestroy();
+	}
+
+CCmdCifTest::CCmdCifTest()
+	: CCommandBase(EManualComplete | EReportAllErrors)
+	{
+	}
+
+const TDesC& CCmdCifTest::Name() const
+	{
+	_LIT(KName, "ciftest");	
+	return KName;
+	}
+
+void CCmdCifTest::ArgumentsL(RCommandArgumentList& aArguments)
+	{
+	aArguments.AppendStringL(iCmd, _L("command"));
+	}
+
+void CCmdCifTest::OptionsL(RCommandOptionList& aOptions)
+	{
+	aOptions.AppendBoolL(iVerbose, _L("verbose"));
+	aOptions.AppendBoolL(iKeepGoing, _L("keep-going"));
+	}
+
+void CCmdCifTest::DoRunL()
+	{
+	if (iCmd)
+		{
+		CCommandInfoFile* cif = CCommandInfoFile::NewL(FsL(), Env(), *iCmd);
+		TestCifL(cif); // Takes ownership
+		}
+	else
+		{
+		_LIT(KCifDir, "y:\\resource\\cif\\fshell\\");
+		TFindFile find(FsL());
+		CDir* dir = NULL;
+		TInt found = find.FindWildByDir(_L("*.cif"), KCifDir, dir);
+		while (found == KErrNone)
+			{
+			for (TInt i = 0; i < dir->Count(); i++)
+				{
+				iFileName.Copy(TParsePtrC(find.File()).DriveAndPath()); // The docs for TFindFile state you shouldn't need the extra TParsePtrC::DriveAndPath(). Sigh.
+				iFileName.Append((*dir)[i].iName);
+				iCifFiles.AppendL(iFileName.AllocLC());
+				CleanupStack::Pop();
+				}
+			delete dir;
+			dir = NULL;
+			found = find.FindWild(dir);
+			}
+		NextCif();
+		}
+	}
+
+void CCmdCifTest::NextCif()
+	{
+	if (iNextCif == iCifFiles.Count())
+		{
+		if (iVerbose)
+			{
+			Printf(_L("%d tests run, %d passes %d failures. %d commands have no tests defined.\r\n"), iPasses + iFailures, iPasses, iFailures, iCifFiles.Count() - iPasses - iFailures);
+			}
+		Complete(KErrNone);
+		}
+	else
+		{
+		CCommandInfoFile* cif = NULL;
+		TRAPD(err, cif = CCommandInfoFile::NewL(FsL(), *iCifFiles[iNextCif]));
+		if (!err)
+			{
+			TRAP(err, TestCifL(cif));
+			if (err) PrintError(err, _L("Error setting up test for CIF %S"), iCifFiles[iNextCif]);
+			}
+		iNextCif++;
+		
+		if (err)
+			{
+			iFailures++;
+			TestCompleted(err);
+			}
+		}
+	}
+
+void CCmdCifTest::TestCifL(CCommandInfoFile* aCif)
+	{
+	iCurrentCif = aCif;
+	if (iVerbose) Printf(_L("Checking %S\r\n"), &aCif->CifFileName());
+
+	const TDesC& scriptData = aCif->SmokeTest();
+	if (scriptData.Length() == 0)
+		{
+		if (iVerbose) Printf(_L("Cif has no smoketest section\r\n"));
+		TestCompleted(KErrNone);
+		return;
+		}
+
+	iEnvForScript = CEnvironment::NewL(Env());
+	iEnvForScript->SetL(_L("Error"), _L("fshell -e 'echo \"Test failed, env is:\" && env && error'"));
+	iEnvForScript->SetL(_L("Quiet"), _L(">/dev/null"));
+	iEnvForScript->SetL(_L("Silent"), _L("2>&1 >/dev/null"));
+	iEnvForScript->Remove(_L("Verbose")); // In case it's ended up in our parent env
+	if (iVerbose) iEnvForScript->SetL(_L("Verbose"), 1);
+	iFileName.Copy(aCif->CifFileName());
+	iFileName.Append(_L(":smoke-test"));
+	TParsePtrC parse(iFileName);
+	iEnvForScript->SetL(KScriptName, parse.NameAndExt());
+	iEnvForScript->SetL(KScriptPath, parse.DriveAndPath());
+	iEnvForScript->SetL(_L("0"), iFileName);
+
+	iParser = CParser::NewL(CParser::EExportLineNumbers, scriptData, IoSession(), Stdin(), Stdout(), Stderr(), *iEnvForScript, gShell->CommandFactory(), this, aCif->GetSmokeTestStartingLineNumber());
+	iParser->Start();
+	}
+
+void CCmdCifTest::HandleParserComplete(CParser& /*aParser*/, const TError& aError)
+	{
+	TInt err = aError.Error();
+	if (err)
+		{
+		iFailures++;
+		PrintError(err, _L("%S failed at line %d"), &aError.ScriptFileName(), aError.ScriptLineNumber());
+		}
+	else
+		{
+		if (iVerbose)
+			{
+			Printf(_L("Smoketest for %S completed ok.\r\n"), &iCurrentCif->Name());
+			}
+		iPasses++;
+		}
+	TestCompleted(err);
+	}
+
+void CCmdCifTest::TestCompleted(TInt aError)
+	{
+	// Delete interim data
+	delete iEnvForScript;
+	iEnvForScript = NULL;
+	delete iParser;
+	iParser = NULL;
+	delete iCurrentCif;
+	iCurrentCif = NULL;
+
+	if (aError == KErrNone || iKeepGoing)
+		{
+		// Async call NextCif()
+		TRequestStatus* stat = &iStatus;
+		User::RequestComplete(stat, KErrNone);
+		SetActive();
+		}
+	else
+		{
+		Complete(aError);
+		}
+	}
+
+void CCmdCifTest::RunL()
+	{
+	NextCif();
+	}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/core/builtins/ciftest.h	Wed Sep 15 00:44:34 2010 +0100
@@ -0,0 +1,56 @@
+// ciftest.h
+//
+// Copyright (c) 2010 Accenture. All rights reserved.
+// This component and the accompanying materials are made available
+// under the terms of the "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:
+// Accenture - Initial contribution
+//
+
+#ifndef CIFTEST_H
+#define CIFTEST_H
+
+#include <fshell/ioutils.h>
+#include "parser.h"
+
+using namespace IoUtils;
+
+class CCmdCifTest : public CCommandBase, public MParserObserver
+	{
+public:
+	static CCommandBase* NewLC();
+	~CCmdCifTest();
+private:
+	CCmdCifTest();
+	void TestCifL(CCommandInfoFile* aCif);
+	void NextCif();
+	void TestCompleted(TInt aError);
+private: // From CCommandBase.
+	void RunL();
+	virtual const TDesC& Name() const;
+	virtual void DoRunL();
+	virtual void ArgumentsL(RCommandArgumentList& aArguments);
+	virtual void OptionsL(RCommandOptionList& aOptions);
+private: // From MParserObserver.
+	virtual void HandleParserComplete(CParser& aParser, const TError& aError);
+
+private:
+	HBufC* iCmd;
+	TBool iVerbose;
+	TBool iKeepGoing;
+
+	TFileName iFileName;
+	CCommandInfoFile* iCurrentCif;
+	CParser* iParser;
+	CEnvironment* iEnvForScript;
+	RPointerArray<HBufC> iCifFiles;
+
+	TInt iPasses;
+	TInt iFailures;
+	TInt iNextCif;
+	};
+
+#endif
--- a/core/group/bld.inf	Thu Sep 09 19:21:34 2010 +0100
+++ b/core/group/bld.inf	Wed Sep 15 00:44:34 2010 +0100
@@ -81,6 +81,7 @@
 ..\builtins\ymodem.cif            z:\resource\cif\fshell\ymodem.cif
 ..\builtins\version.cif           z:\resource\cif\fshell\version.cif
 ..\builtins\undertaker.cif        z:\resource\cif\fshell\undertaker.cif
+..\builtins\ciftest.cif           z:\resource\cif\fshell\ciftest.cif
 
 #ifdef FSHELL_CORE_SUPPORT_CHUNKINFO
 ..\builtins\chunkinfo.cif         z:\resource\cif\fshell\chunkinfo.cif
--- a/core/group/fshell_core.iby	Thu Sep 09 19:21:34 2010 +0100
+++ b/core/group/fshell_core.iby	Wed Sep 15 00:44:34 2010 +0100
@@ -168,6 +168,7 @@
 #ifdef FSHELL_CORE_SUPPORT_BUILTIN_REBOOT
 FSHELL_COMMAND_INFO_FILE(fshell,reboot.cif)
 #endif
+FSHELL_COMMAND_INFO_FILE(fshell,ciftest.cif)
 
 #ifdef FSHELL_REPLACE_ECONS
 FSHELL_EXECUTABLE_AS_DATA(iocons.dll,iocons.dll)
--- a/core/src/command_factory.cpp	Thu Sep 09 19:21:34 2010 +0100
+++ b/core/src/command_factory.cpp	Wed Sep 15 00:44:34 2010 +0100
@@ -24,6 +24,7 @@
 #include "xmodem.h"
 #include "ymodem.h"
 #include "version.h"
+#include "ciftest.h"
 #include "worker_thread.h"
 
 //
@@ -325,6 +326,7 @@
 #ifdef FSHELL_CORE_SUPPORT_BUILTIN_REBOOT
 	AddThreadCommandL(CCmdReboot::NewLC);
 #endif
+	AddThreadCommandL(CCmdCifTest::NewLC);
 
 	// Add some DOS-style namings of common commands.
 	AddThreadCommandL(_L("del"), CCmdRm::NewLC, CCommandConstructorBase::EAttAlias);
--- a/core/src/fshell.mmp	Thu Sep 09 19:21:34 2010 +0100
+++ b/core/src/fshell.mmp	Wed Sep 15 00:44:34 2010 +0100
@@ -77,6 +77,7 @@
 source          xmodem.cpp
 source          ymodem.cpp
 source          version.cpp
+source          ciftest.cpp
 
 // There doesn't seem to be a nice way of turning the platform into a string, like you have $(PLATFORM) in extension makefiles, sigh.
 #if defined(WINSCW)
--- a/core/src/parser.cpp	Thu Sep 09 19:21:34 2010 +0100
+++ b/core/src/parser.cpp	Wed Sep 15 00:44:34 2010 +0100
@@ -52,9 +52,9 @@
 	{
 	}
 
-CParser* CParser::NewL(TUint aMode, const TDesC& aDes, RIoSession& aIoSession, RIoReadHandle& aStdin, RIoWriteHandle& aStdout, RIoWriteHandle& aStderr, IoUtils::CEnvironment& aEnv, CCommandFactory& aFactory, MParserObserver* aObserver)
+CParser* CParser::NewL(TUint aMode, const TDesC& aDes, RIoSession& aIoSession, RIoReadHandle& aStdin, RIoWriteHandle& aStdout, RIoWriteHandle& aStderr, IoUtils::CEnvironment& aEnv, CCommandFactory& aFactory, MParserObserver* aObserver, TInt aStartingLineNumber)
 	{
-	CParser* self = new(ELeave) CParser(aMode, aDes, aIoSession, aStdin, aStdout, aStderr, aEnv, aFactory, aObserver);
+	CParser* self = new(ELeave) CParser(aMode, aDes, aIoSession, aStdin, aStdout, aStderr, aEnv, aFactory, aObserver, aStartingLineNumber);
 	CleanupStack::PushL(self);
 	self->ConstructL();
 	CleanupStack::Pop();
@@ -78,8 +78,8 @@
 		}
 	}
 
-CParser::CParser(TUint aMode, const TDesC& aDes, RIoSession& aIoSession, RIoReadHandle& aStdin, RIoWriteHandle& aStdout, RIoWriteHandle& aStderr, IoUtils::CEnvironment& aEnv, CCommandFactory& aFactory, MParserObserver* aObserver)
-	: iMode(aMode), iData(aDes), iIoSession(aIoSession), iStdin(aStdin), iStdout(aStdout), iStderr(aStderr), iEnv(aEnv), iFactory(aFactory), iObserver(aObserver), iCompletionError(aStderr, aEnv), iNextLineNumber(1)
+CParser::CParser(TUint aMode, const TDesC& aDes, RIoSession& aIoSession, RIoReadHandle& aStdin, RIoWriteHandle& aStdout, RIoWriteHandle& aStderr, IoUtils::CEnvironment& aEnv, CCommandFactory& aFactory, MParserObserver* aObserver, TInt aStartingLineNumber)
+	: iMode(aMode), iData(aDes), iIoSession(aIoSession), iStdin(aStdin), iStdout(aStdout), iStderr(aStderr), iEnv(aEnv), iFactory(aFactory), iObserver(aObserver), iCompletionError(aStderr, aEnv), iNextLineNumber(aStartingLineNumber)
 	{
 	}
 
--- a/core/src/parser.h	Thu Sep 09 19:21:34 2010 +0100
+++ b/core/src/parser.h	Wed Sep 15 00:44:34 2010 +0100
@@ -41,7 +41,7 @@
 		EExportLineNumbers	= 0x00000004
 		};
 public:
-	static CParser* NewL(TUint aMode, const TDesC& aDes, RIoSession& aIoSession, RIoReadHandle& aStdin, RIoWriteHandle& aStdout, RIoWriteHandle& aStderr, IoUtils::CEnvironment& aEnv, CCommandFactory& aFactory, MParserObserver* aObserver);
+	static CParser* NewL(TUint aMode, const TDesC& aDes, RIoSession& aIoSession, RIoReadHandle& aStdin, RIoWriteHandle& aStdout, RIoWriteHandle& aStderr, IoUtils::CEnvironment& aEnv, CCommandFactory& aFactory, MParserObserver* aObserver, TInt aStartingLineNumber = 1);
 	~CParser();
 	void Start();
 	void Start(TBool& aIsForeground);
@@ -62,7 +62,7 @@
 		EAndOr
 		};
 private:
-	CParser(TUint aMode, const TDesC& aDes, RIoSession& aIoSession, RIoReadHandle& aStdin, RIoWriteHandle& aStdout, RIoWriteHandle& aStderr, IoUtils::CEnvironment& aEnv, CCommandFactory& aFactory, MParserObserver* aObserver);
+	CParser(TUint aMode, const TDesC& aDes, RIoSession& aIoSession, RIoReadHandle& aStdin, RIoWriteHandle& aStdout, RIoWriteHandle& aStderr, IoUtils::CEnvironment& aEnv, CCommandFactory& aFactory, MParserObserver* aObserver, TInt aStartingLineNumber);
 	void ConstructL();
 	void CreateNextPipeLine(TBool* aIsForeground);
 	void CreateNextPipeLineL(TBool* aIsForeground);
--- a/core/tsrc/smoketest.script	Thu Sep 09 19:21:34 2010 +0100
+++ b/core/tsrc/smoketest.script	Wed Sep 15 00:44:34 2010 +0100
@@ -18,3 +18,4 @@
 fshell -k $SCRIPT_PATH\fshell-ccommandbase-test.script
 fshell -k $SCRIPT_PATH\fshell-unicode-test.script
 fshell -k $SCRIPT_PATH\fshell-scriptcif-test.script
+ciftest -k
--- a/documentation/change_history.pod	Thu Sep 09 19:21:34 2010 +0100
+++ b/documentation/change_history.pod	Wed Sep 15 00:44:34 2010 +0100
@@ -14,6 +14,20 @@
 
 =head1 FShell Change History
 
+=head2 FCL features not yet in MCL (move this section before committing to MCL)
+
+=over 5
+
+=item *
+
+Fshell now reuses threads for built-in commands that execute in quick succession. The thread pool takes into account the requirements of the command when assigning a thread (eg whether it needs to share a heap with its parent command) and creates a new one if necessary. Excess threads are cleaned up after a short idle period (currently 1 second).
+
+=item *
+
+Commands can now define a C<==smoke-test> section in their CIF files, which defines a snippet of fshell script that will be run as part of C<fshell smoketest> or by invoking L<ciftest|commands::ciftest> directly. See the ciftest documentation for more details.
+
+=back
+
 =head2 Release 001 [Not yet officially made]
 
 =over 5
--- a/libraries/iosrv/bwins/iocliu.def	Thu Sep 09 19:21:34 2010 +0100
+++ b/libraries/iosrv/bwins/iocliu.def	Wed Sep 15 00:44:34 2010 +0100
@@ -550,4 +550,7 @@
 	?KeyPressed@MCommandExtensionsV2@IoUtils@@UAEXII@Z @ 549 NONAME ; void IoUtils::MCommandExtensionsV2::KeyPressed(unsigned int, unsigned int)
 	?ReadKey@CCommandBase@IoUtils@@QAEIXZ @ 550 NONAME ; unsigned int IoUtils::CCommandBase::ReadKey(void)
 	?Normalize@TFileName2@IoUtils@@QAEXAAVRFs@@@Z @ 551 NONAME ; void IoUtils::TFileName2::Normalize(class RFs &)
+	?SmokeTest@CCommandInfoFile@IoUtils@@QBEABVTDesC16@@XZ @ 552 NONAME ; class TDesC16 const & IoUtils::CCommandInfoFile::SmokeTest(void) const
+	?CifFileName@CCommandInfoFile@IoUtils@@QBEABVTDesC16@@XZ @ 553 NONAME ; class TDesC16 const & IoUtils::CCommandInfoFile::CifFileName(void) const
+	?GetSmokeTestStartingLineNumber@CCommandInfoFile@IoUtils@@QBEHXZ @ 554 NONAME ; int IoUtils::CCommandInfoFile::GetSmokeTestStartingLineNumber(void) const
 
--- a/libraries/iosrv/client/command_info_file.cpp	Thu Sep 09 19:21:34 2010 +0100
+++ b/libraries/iosrv/client/command_info_file.cpp	Wed Sep 15 00:44:34 2010 +0100
@@ -27,6 +27,7 @@
 _LIT(KCmndLongDescription, "long-description");
 _LIT(KCmndSeeAlso, "see-also");
 _LIT(KCmndCopyright, "copyright");
+_LIT(KCmndSmokeTest, "smoke-test");
 _LIT(KCmndArgument, "argument");
 _LIT(KCmndOption, "option");
 _LIT(KCmndInclude, "include");
@@ -315,6 +316,23 @@
 			{
 			iCopyright.Set(TextToNextCommand(aLex));
 			}
+		else if (command == KCmndSmokeTest)
+			{
+			// Hmm no easy way to get the line number we're currently on
+			iSmokeTestLineNumber = 1;
+			TLex lex(aLex);
+			lex.Inc(-aLex.Offset()); // Only way to put a TLex back to the beginning!
+			TPtrC preceding = lex.Remainder().Left(aLex.Offset());
+			const TUint16* ptr = preceding.Ptr();
+			const TUint16* end = ptr + preceding.Length();
+			while (ptr != end)
+				{
+				if (*ptr++ == '\n') iSmokeTestLineNumber++;
+				}
+			// At this point iSmokeTestLineNumber points to the "==smoketest" line - add 2 to skip this line and the blank line below it
+			iSmokeTestLineNumber += 2;
+			iSmokeTest.Set(TextToNextCommand(aLex));
+			}
 		else if (command == KCmndArgument)
 			{
 			ReadArgumentL(aLex, aFileName);
@@ -813,6 +831,16 @@
 	return iCopyright;
 	}
 
+EXPORT_C const TDesC& CCommandInfoFile::SmokeTest() const
+	{
+	return iSmokeTest;
+	}
+
+EXPORT_C TInt CCommandInfoFile::GetSmokeTestStartingLineNumber() const
+	{
+	return iSmokeTestLineNumber;
+	}
+
 EXPORT_C const RCommandArgumentList& CCommandInfoFile::Arguments()
 	{
 	return iArguments;
@@ -891,3 +919,8 @@
 	: iFileName(aParent.iFileName), iParent(&aParent)
 	{
 	}
+
+EXPORT_C const TDesC& CCommandInfoFile::CifFileName() const
+	{
+	return iFileName;
+	}
--- a/libraries/iosrv/eabi/iocliu.def	Thu Sep 09 19:21:34 2010 +0100
+++ b/libraries/iosrv/eabi/iocliu.def	Wed Sep 15 00:44:34 2010 +0100
@@ -626,4 +626,7 @@
 	_ZTIN7IoUtils20MCommandExtensionsV2E @ 625 NONAME
 	_ZTVN7IoUtils20MCommandExtensionsV2E @ 626 NONAME
 	_ZN7IoUtils10TFileName29NormalizeER3RFs @ 627 NONAME
+	_ZNK7IoUtils16CCommandInfoFile9SmokeTestEv @ 628 NONAME
+	_ZNK7IoUtils16CCommandInfoFile11CifFileNameEv @ 629 NONAME
+	_ZNK7IoUtils16CCommandInfoFile30GetSmokeTestStartingLineNumberEv @ 630 NONAME
 
--- a/libraries/iosrv/inc/ioutils.h	Thu Sep 09 19:21:34 2010 +0100
+++ b/libraries/iosrv/inc/ioutils.h	Wed Sep 15 00:44:34 2010 +0100
@@ -579,11 +579,14 @@
 	IMPORT_C static CCommandInfoFile* NewL(RFs& aFs, const TDesC& aFileName);
 	IMPORT_C static CCommandInfoFile* NewL(RFs& aFs, const CEnvironment& aEnvironment, const TDesC& aCommandName);
 	IMPORT_C ~CCommandInfoFile();
+	IMPORT_C const TDesC& CifFileName() const;
 	IMPORT_C const TDesC& Name() const;
 	IMPORT_C const TDesC& ShortDescription() const;
 	IMPORT_C const TDesC& LongDescription() const;
 	IMPORT_C const TDesC& SeeAlso() const;
 	IMPORT_C const TDesC& Copyright() const;
+	IMPORT_C const TDesC& SmokeTest() const;
+	IMPORT_C TInt GetSmokeTestStartingLineNumber() const;
 	IMPORT_C const RCommandArgumentList& Arguments();
 	IMPORT_C const RCommandOptionList& Options() const;
 	IMPORT_C void AssignL(RCommandArgumentList& aArguments, RCommandOptionList& aOptions) const;
@@ -609,6 +612,8 @@
 	TPtrC iLongDescription;
 	TPtrC iSeeAlso;
 	TPtrC iCopyright;
+	TPtrC iSmokeTest;
+	TInt iSmokeTestLineNumber;
 	RCommandArgumentList iArguments;
 	RCommandOptionList iOptions;
 	RArray<RBuf> iBufs;