Original in Russian: http://18delphi.blogspot.ru/2013/11/gui-back-to-basics_22.html
GUI-testing. Table of contents
They wrote to me that I “write in lecture and it would be better in seminar”.
I’ll try the seminar.
So.
The main points:
1. Test has to be benchmark, anchor test
2. looking like test-case
3. readable for a human being
4. making use of domain-specific terms
(we’ll get back to these points again)
Parlty, these points are covered here - http://18delphi.blogspot.ru/2013/11/blog-post_19.html.
I’ll try to make a finished example in “classic RAD-style”.
And in some way “in XP style” - hhttp://18delphi.blogspot.com/2015/02/about-rs-time-pressure-and-forgetting.html.
If it is not clear what to do I will write Asserts.
The example is available here - https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/GUITests/Chapter0/
Let’s create a form TForm1 with buttons Button1, Button2, Button3 and an input stream Edit1.
Thus, we add “publicity” and “novelty of the approach” on FM.
The code of the form is given here - http://18delphi.blogspot.com/2015/03/gui-testing-12-gui-testing-in-spoken.html.
I see no reason to repeat it.
The whole code of the example is available here - https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/GUITests/Chapter1/
Also, we’ll do our utmost to use generics and interfaces as well as TInterfacedObject.
I have my containers here - About containers. Table of contents.
I also have my vision of “reference counting” – here - http://18delphi.blogspot.com/2015/02/containers-2-my-own-implementation-of.html and here - http://18delphi.blogspot.ru/2013/09/arc.html.
But I’ll leave these points out of scope of this article so that to stay on message. Curious readers can draw a conclusion of the references given above – on their own.
Again, I will try to make do with standard programming terms and libraries.
Now, let’s “learn” to implement pressing the buttons on the form.
Not just pressing but using “script engine”.
Let’s introduce concepts:
1. Test script (script) – a code on “pseudo language” to describe actions that are “most closely approached to user actions”, such as “pressing the button”, “keyboard input” and “mouse controlling”.
2. Script engine (TscriptEngine) - a program entity (class) that can run scripts.
3. Execution context (TscriptContext) - a program entity, that provides the context (again, tautology) of scripts execution and theis components. Some analog of values stack.
4. Script word (IscriptWord) – a “little brick” to make the script body. It possesses one of the most important methods - DoIt. Actually, this method executes the word code. (There are words “of compile time” and “of runtime”. I’m getting ahead. If you’re interested, read here - https://en.wikipedia.org/wiki/Forth_(programming_language))
5. Script code (TscriptCode) – a compiled script code consisting of IscriptWord that calls methods IscriptWord.DoIt one after another.
6. Script engine dictionary (TscriptDictionary) - a program entity, in which the script words are registered (IscriptWord). It is an important element when compiling the script. All “tokens” of the input stream (described here - http://18delphi.blogspot.com/2015/02/containers-6-abstract-containers.html) are compared to the word in dictionary. (We’ll create this class on the “native” generic class TDictionary of Embarcadero, although I have questions and the “true” script engine is based on own-made abstract containers).
7. Script engine axiomatiсs (Tscript) – a kind of script engine dictionary (TscriptDictionary). It is a “base dictionary” defining “base terms” of script engine (axiomatiсs) in Delphi. Ir you’re interested – axiomatics is presented by singleton (https://en.wikipedia.org/wiki/Singleton_pattern). The rest of dictionaries are nested and built as script code is compiled (but I’m getting ahead again).
8. Execution log (IscriptLog) – “something”, say, “console” in which script engine and words of script engine can output all their “thought” about compilation and execution process. Execution log serves for debugging.
9. Actually, there are two logs - IscriptCompileLog and IscriptRunLog, the compilation log and the execution log. We’ll see them both “as it goes along”.
10. Input stream parser (TscriptParser) – an engine to parse the input stream of the script in order to parcel the stream into tokens. It was briefly described here - http://18delphi.blogspot.com/2015/03/gui-testing-6-thinking-of-testing-5.html.
11. Values stack - the major mechanism to pass values in our script engine. It includes the values TscriptValue and is related to compile and execution contexts. At any moment, script engine word can put some value into the values stack and read it from the top of the stack.
I’ll just note that our script engine works as a “compiler”. First it creates the script code, checks it for validity, only after – executes it.
The once compiled code – can be executed any number of times.
TO BEGIN WITH, let’s describe fake script engine that compiles the code of input stream tokens directly and this code will print the names of these tokens into log.
So, the code of the introduced concepts:
First, TscriptCode.
It is MORE THAN simple since it has generics:
Next:
The code of the script engine:
The code of base word:
The code of the “fake” word:
Presto! We’ve got the code of the script that is compiled and executed. Each token prints its name into the log.
Next, we’ll go on by “pressing the buttons”.
(As I wrote this all I’ve begun to think – do I have to write about dependency injection (https://en.wikipedia.org/wiki/Dependency_injection) and interface factories (https://en.wikipedia.org/wiki/Abstract_factory_pattern) right now? This techniques can already be applied in several areas. Or, should I leave it for the “next articles”? I will think it over more while writing.)
Now, let’s separate code compilation from its launch.
Let’s also separate compilation code from launch code:
We introduce new interfaces - IscriptCompileLog and IscriptRunLog:
We introduce new class - TscriptRunContext:
We also modify the script engine:
It is clear that the compilation process is “separate” from execution process.
At the same time, the compilation is logged separately from logging the execution.
In this way:
- here you can see that form TForm1 began to implement two interfaces - IscriptCompileLog and IscriptRunLog.
Moreover – through various methods.
The compilation log is printed into component CompileLog, and the execution log is printed into component RunLog.
Now, let’s really separate compilation from launch.
Let’s extract method CompileScript from method RunScript that will return the compiled code and the interface IscriptCode, which actually is the compiled code.
We’ll also introduce interface IscriptCompiler – the script code compiler.
(One more note - all interfaces I’ve described do not have GUID. This is done on pupose. The reasons are here - http://18delphi.blogspot.ru/2013/11/supports.html and here - http://18delphi.blogspot.ru/2013/11/queryinterface-supports.html and here - http://18delphi.blogspot.ru/2013/10/supports.html .
When GUIDs will really be necessary we’ll sure introduce them).
That’s what we get:
(The most curious readers can look at the commits log in SVN. It deserves special consideration. USUALLY I look into other’s logs so that to “catch on the author’s train of thought”.)
The interface of IscriptCode and IscriptCompiler is here:
The implementation of interfaces IscriptCode and IscriptCompiler is here:
(my hands itch to tell about dependency injections...)
The use of interfaces IscriptCode and IscriptCompiler and, again, the modified script engine:
It is so somehow.
(The main thing is – we absolutely forgot about tests described here - http://18delphi.blogspot.com/2013/11/5.html. They ALWAYS succeed. TDD (https://en.wikipedia.org/wiki/Test-driven_development) - is cool)
(Does the seminar come out not very long? Not for “a pair double classes”?)
Now, let’s separately compile tokens (script_ttToken) and strings (script_ttString).
A new class appears - TscriptUnknownToken. It looks like TscriptStringWord, but I do not generalize their codes on purpose:
Here is how method TscriptEngine.CompileScript is modified:
Let’s think – “exactly what tokens are not Unknown?
Let’s analyze registration of words in the dictionary.
Let’s look at the dictionary class implementation - TscriptDictionary. Again, it is more than simple.
Due to the same generis:
Now, let’s look at “axiomatics” - TscriptAxiomatiсs.
For now, we inherit this class from TscriptDictionary and make it singleton (https://en.wikipedia.org/wiki/Singleton_pattern).
Here is our new class:
The implementation is not perfect and does not provide thread safety, but telling about how to make singletons right is not the purpose of this article.
The code of script engine modifies like this:
Now, we use this class (TscriptAxiomatics) to register at least one word of axiomatics:
Actually, we’ve registered two words DoNothing and DoNothing2, having connected them with classes TscriptWordExample1 and TscriptWordExample2 respectively.
We launch our example and see that printing into log has changed.
Let's take our mind off “growing the functional” and go in for refactoring a bit.
We’ll extract a factory method (http://18delphi.blogspot.ru/2013/04/blog-post_7483.html .) TscriptEngine.CompileToken from method TscriptEngine.CompileScript
First, we’ll introduce the interface IscriptParser and the factory method TscriptParser.Make. We’ll also connect compilation context TscriptCompileContext to the parser IscriptParser and the compiler IscriptCompiler.
Here’s what we get.
We introduce the interface of parser:
We connect the parser and the compiler to the context:
And here comes the implementation of TscriptParser.Make:
In order to implement the interface IscriptParser – we did not “break inheritance” and change the structure of the class TscriptParser. Instead, we’ve introduced an additional hidden class TscriptParserContainer that aggregates TscriptParser and implements interface IscriptParser using the functional of aggregated class TscriptParser. This technique is like the pattern adapter (https://en.wikipedia.org/wiki/Adapter_pattern) or the pattern facade (http://18delphi.blogspot.com/2015/02/once-again-about-testing-levels.html) .
So, class TscriptEngine is modified again:
Now, let’s get back to “why this all is done”. We’ll learn to “press the buttons through software”.
Here we’ve got to the values stack.
Let’s see how it looks like:
- here, again, we’ve used generics from standard library.
Now, basing on the knowledge acquired we’ll introduce two words of axiomatics - TkwFindComponent and TkwButtonClick.
The first word searches the component with the specified name on the current application form and puts it into values stack.
The second word selects the object value from the stack, interprets it as a button and tries to press the specified button..
Here are the words:
Note. The type TControlAccess – allows access to the method Click of TControl class. This is a well-know “trick”. Even Borland and Embarcadero use it now and then. Later, we’ll consider the “new” RTTI. There we will not need such “tricks”.
So.
We are now ready to press the button through script.
The code of the script:
or this like:
Try them both. May be you will like it.
Conclusion.
So. Using rather simply example of “pressing the button on the form” we have looked into not the simple things.
1. We have described the script engine.
2. We have described the process of script compilation.
3. We have described the registration of words un the axiomatics dictionary.
4. We have analysed the process of scripts launch.
5. We have described the values stack and looked into the examples of using it.
Well, and “on the surface” we’ve achieved our goal – learned to “press the buttons through scripts”.
I hope one of the main ideas is clear – the described script engine may extend by registration of new layers in axiomatics in Delphi.
This finishes my article.
If the readers are interested, I’ll continue to write about script engine and “how it is organized”.
Generally speaking, much can be discussed about it:
1. Conditional operators.
2. Loops.
3. Variables.
4. User defining of new words through script code.
5. Introducing of the script from other files to the code.
6. And many others.
P.S. In LONG time I have not typed so much code MANUALLY.
It is long time since I started “drawing squares on the model”. This is FAR MORE quick…
P.P.S. I’ll repeat. We used Delphi to write “this”. But “this” may be written on any language – either Objective-C, or Python, or C++, or classic C, or classic Pascal (in Turbo Professional). The difference is in “commas”.
The most important is the essence of this APPROACH.
P.P.P.S. One more “wish” is to make GUITestRunner for DUnit to develope on FM. I’ll try to implement it.
Update:
We did it - http://programmingmindstream.blogspot.ru/2014/11/firemonkey-dunit.html
P.P.P.P.S. The announcement. Here is the new version of the example - https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/GUITests/Chapter2/ The example of VCL-project is also available there, as well as the example for mobile devices, but for now I have launched it only with emulator.
GUI-testing. Table of contents
They wrote to me that I “write in lecture and it would be better in seminar”.
I’ll try the seminar.
So.
The main points:
1. Test has to be benchmark, anchor test
2. looking like test-case
3. readable for a human being
4. making use of domain-specific terms
(we’ll get back to these points again)
Parlty, these points are covered here - http://18delphi.blogspot.ru/2013/11/blog-post_19.html.
I’ll try to make a finished example in “classic RAD-style”.
And in some way “in XP style” - hhttp://18delphi.blogspot.com/2015/02/about-rs-time-pressure-and-forgetting.html.
If it is not clear what to do I will write Asserts.
The example is available here - https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/GUITests/Chapter0/
Let’s create a form TForm1 with buttons Button1, Button2, Button3 and an input stream Edit1.
Thus, we add “publicity” and “novelty of the approach” on FM.
The code of the form is given here - http://18delphi.blogspot.com/2015/03/gui-testing-12-gui-testing-in-spoken.html.
I see no reason to repeat it.
The whole code of the example is available here - https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/GUITests/Chapter1/
Also, we’ll do our utmost to use generics and interfaces as well as TInterfacedObject.
I have my containers here - About containers. Table of contents.
I also have my vision of “reference counting” – here - http://18delphi.blogspot.com/2015/02/containers-2-my-own-implementation-of.html and here - http://18delphi.blogspot.ru/2013/09/arc.html.
But I’ll leave these points out of scope of this article so that to stay on message. Curious readers can draw a conclusion of the references given above – on their own.
Again, I will try to make do with standard programming terms and libraries.
Now, let’s “learn” to implement pressing the buttons on the form.
Not just pressing but using “script engine”.
Let’s introduce concepts:
1. Test script (script) – a code on “pseudo language” to describe actions that are “most closely approached to user actions”, such as “pressing the button”, “keyboard input” and “mouse controlling”.
2. Script engine (TscriptEngine) - a program entity (class) that can run scripts.
3. Execution context (TscriptContext) - a program entity, that provides the context (again, tautology) of scripts execution and theis components. Some analog of values stack.
4. Script word (IscriptWord) – a “little brick” to make the script body. It possesses one of the most important methods - DoIt. Actually, this method executes the word code. (There are words “of compile time” and “of runtime”. I’m getting ahead. If you’re interested, read here - https://en.wikipedia.org/wiki/Forth_(programming_language))
5. Script code (TscriptCode) – a compiled script code consisting of IscriptWord that calls methods IscriptWord.DoIt one after another.
6. Script engine dictionary (TscriptDictionary) - a program entity, in which the script words are registered (IscriptWord). It is an important element when compiling the script. All “tokens” of the input stream (described here - http://18delphi.blogspot.com/2015/02/containers-6-abstract-containers.html) are compared to the word in dictionary. (We’ll create this class on the “native” generic class TDictionary of Embarcadero, although I have questions and the “true” script engine is based on own-made abstract containers).
7. Script engine axiomatiсs (Tscript) – a kind of script engine dictionary (TscriptDictionary). It is a “base dictionary” defining “base terms” of script engine (axiomatiсs) in Delphi. Ir you’re interested – axiomatics is presented by singleton (https://en.wikipedia.org/wiki/Singleton_pattern). The rest of dictionaries are nested and built as script code is compiled (but I’m getting ahead again).
8. Execution log (IscriptLog) – “something”, say, “console” in which script engine and words of script engine can output all their “thought” about compilation and execution process. Execution log serves for debugging.
9. Actually, there are two logs - IscriptCompileLog and IscriptRunLog, the compilation log and the execution log. We’ll see them both “as it goes along”.
10. Input stream parser (TscriptParser) – an engine to parse the input stream of the script in order to parcel the stream into tokens. It was briefly described here - http://18delphi.blogspot.com/2015/03/gui-testing-6-thinking-of-testing-5.html.
11. Values stack - the major mechanism to pass values in our script engine. It includes the values TscriptValue and is related to compile and execution contexts. At any moment, script engine word can put some value into the values stack and read it from the top of the stack.
I’ll just note that our script engine works as a “compiler”. First it creates the script code, checks it for validity, only after – executes it.
The once compiled code – can be executed any number of times.
TO BEGIN WITH, let’s describe fake script engine that compiles the code of input stream tokens directly and this code will print the names of these tokens into log.
So, the code of the introduced concepts:
First, TscriptCode.
It is MORE THAN simple since it has generics:
unit Script.Code;
interface
uses
System.Generics.Collections,
Script.WordsInterfaces
;
type
TscriptCode = class(TList<iscriptword>)
public
procedure Run(aContext : TscriptContext);
{* - executes the compiled code. }
procedure CompileWord(const aWord: IscriptWord);
{* - compiles the specified word to the code. }
end;//TscriptCode
implementation
procedure TscriptCode.Run(aContext : TscriptContext);
var
l_Word : IscriptWord;
begin
for l_Word in Self do
l_Word.DoIt(aContext);
end;
procedure TscriptCode.CompileWord(const aWord: IscriptWord);
{* - compiles the specified word to the code. }
begin
Self.Add(aWord);
end;
end.
Next:
The code of the script engine:
unit Script.Engine;
interface
uses
Script.Interfaces
;
type
TscriptEngine = class
public
class procedure RunScript(const aFileName: String; const aLog: IscriptLog);
end;//TscriptEngine
implementation
uses
System.SysUtils,
Script.Parser,
Testing.Engine,
Script.Code,
Script.WordsInterfaces,
Script.StringWord
;
class procedure TscriptEngine.RunScript(const aFileName: String; const aLog: IscriptLog);
var
l_Parser : TscriptParser;
l_Context : TscriptCompileContext;
l_Code : TscriptCode;
l_StringWord : IscriptWord;
begin
TtestEngine.StartTest(aFileName);
try
l_Code := TscriptCode.Create;
try
l_Context := TscriptCompileContext.Create(aLog);
try
l_Parser := TscriptParser.Create(aFileName);
try
while not l_Parser.EOF do
begin
l_Parser.NextToken;
// if (aLog <> nil) then
// aLog.Log(l_Parser.TokenString);
l_StringWord := TscriptStringWord.Make(l_Parser.TokenString);
try
l_Code.CompileWord(l_StringWord);
finally
l_StringWord := nil;
end;//try..finally
end;//while not l_Parser.EOF
finally
FreeAndNil(l_Parser);
end;//try..finally
l_Code.Run(l_Context);
// - we execute the compiled code
finally
FreeAndNil(l_Context);
end;//try..finally
finally
FreeAndNil(l_Code);
end;//try..finally
finally
TtestEngine.StopTest;
end;//try..finally
end;
end.
The code of base word:
unit Script.Word;
interface
uses
Script.WordsInterfaces
;
type
TscriptWord = class(TinterfacedObject, IscriptWord)
protected
procedure DoIt(aContext: TscriptContext); virtual; abstract;
{* - the procedure for executing of the word from the dictionary. }
protected
procedure Cleanup; virtual;
public
class function Make: IscriptWord;
{* - factory }
destructor Destroy; override;
end;//TscriptWord
RscriptWord = class of TscriptWord;
implementation
class function TscriptWord.Make: IscriptWord;
{* - factory }
begin
Result := Create;
end;
destructor TscriptWord.Destroy;
begin
Cleanup;
inherited;
end;
procedure TscriptWord.Cleanup;
begin
// - here we do nothing, the descendants will do everything
end;
end.
The code of the “fake” word:
unit Script.StringWord; interface uses Script.WordsInterfaces, Script.Word ; type TscriptStringWord = class(TscriptWord) private f_String : String; protected procedure DoIt(aContext: TscriptContext); override; public constructor Create(const aString: String); class function Make(const aString: String): IscriptWord; end;//TscriptStringWord implementation constructor TscriptStringWord.Create(const aString: String); begin inherited Create; f_String := aString; end; class function TscriptStringWord.Make(const aString: String): IscriptWord; begin Result := Create(aString); end; procedure TscriptStringWord.DoIt(aContext: TscriptContext); begin aContext.Log(Self.f_String); end; end.
Presto! We’ve got the code of the script that is compiled and executed. Each token prints its name into the log.
Next, we’ll go on by “pressing the buttons”.
(As I wrote this all I’ve begun to think – do I have to write about dependency injection (https://en.wikipedia.org/wiki/Dependency_injection) and interface factories (https://en.wikipedia.org/wiki/Abstract_factory_pattern) right now? This techniques can already be applied in several areas. Or, should I leave it for the “next articles”? I will think it over more while writing.)
Now, let’s separate code compilation from its launch.
Let’s also separate compilation code from launch code:
We introduce new interfaces - IscriptCompileLog and IscriptRunLog:
unit Script.Interfaces; interface type IscriptLog = interface procedure Log(const aString: String); end;//IscriptLog IscriptCompileLog = interface(IscriptLog) end;//IscriptCompileLog IscriptRunLog = interface(IscriptLog) end;//IscriptRunLog implementation end.
We introduce new class - TscriptRunContext:
unit Script.WordsInterfaces;
interface
uses
Core.Obj,
Script.Interfaces
;
type
TscriptContext = class(TCoreObject)
private
f_Log : IscriptLog;
protected
procedure Cleanup; override;
public
constructor Create(const aLog: IscriptLog);
procedure Log(const aString: String);
{* - Prints message into log. }
end;//TscriptContext
TscriptCompileContext = class(TscriptContext)
public
constructor Create(const aLog: IscriptCompileLog);
end;//TscriptCompileContext
TscriptRunContext = class(TscriptContext)
public
constructor Create(const aLog: IscriptRunLog);
end;//TscriptRunContext
IscriptWord = interface
procedure DoIt(aContext: TscriptContext);
{* - the procedure for execution of the word from dictionary. }
end;//IscriptWord
implementation
// TscriptContext
constructor TscriptContext.Create(const aLog: IscriptLog);
begin
inherited Create;
f_Log := aLog;
end;
procedure TscriptContext.Log(const aString: String);
{* - Prints message into log. }
begin
if (f_Log <> nil) then
f_Log.Log(aString);
end;
procedure TscriptContext.Cleanup;
begin
f_Log := nil;
inherited;
end;
// TscriptCompileContext
constructor TscriptCompileContext.Create(const aLog: IscriptCompileLog);
begin
inherited Create(aLog);
end;
// TscriptRunContext
constructor TscriptRunContext.Create(const aLog: IscriptRunLog);
begin
inherited Create(aLog);
end;
end.
We also modify the script engine:
unit Script.Engine;
interface
uses
Script.Interfaces
;
type
TscriptEngine = class
public
class procedure RunScript(const aFileName: String;
const aCompileLog: IscriptCompileLog;
const aRunLog : IscriptRunLog);
end;//TscriptEngine
implementation
uses
System.SysUtils,
Script.Parser,
Testing.Engine,
Script.Code,
Script.WordsInterfaces,
Script.StringWord
;
class procedure TscriptEngine.RunScript(const aFileName: String;
const aCompileLog: IscriptCompileLog;
const aRunLog : IscriptRunLog);
var
l_Parser : TscriptParser;
l_CompileContext : TscriptCompileContext;
l_RunContext : TscriptRunContext;
l_Code : TscriptCode;
l_StringWord : IscriptWord;
begin
TtestEngine.StartTest(aFileName);
try
l_Code := TscriptCode.Create;
try
l_CompileContext := TscriptCompileContext.Create(aCompileLog);
try
l_Parser := TscriptParser.Create(aFileName);
try
while not l_Parser.EOF do
begin
l_Parser.NextToken;
if (aCompileLog <> nil) then
aCompileLog.Log(l_Parser.TokenString);
l_StringWord := TscriptStringWord.Make(l_Parser.TokenString);
try
l_Code.CompileWord(l_StringWord);
finally
l_StringWord := nil;
end;//try..finally
end;//while not l_Parser.EOF
finally
FreeAndNil(l_Parser);
end;//try..finally
finally
FreeAndNil(l_CompileContext);
end;//try..finally
l_RunContext := TscriptRunContext.Create(aRunLog);
try
l_Code.Run(l_RunContext);
// - we execute the compiled code
finally
FreeAndNil(l_RunContext);
end;//try..finally
finally
FreeAndNil(l_Code);
end;//try..finally
finally
TtestEngine.StopTest;
end;//try..finally
end;
end.
It is clear that the compilation process is “separate” from execution process.
At the same time, the compilation is logged separately from logging the execution.
In this way:
unit Unit1;
interface
uses
System.SysUtils, System.Types, System.UITypes, System.Rtti, System.Classes,
System.Variants, FMX.Types, FMX.Controls, FMX.Forms, FMX.Dialogs,
FMX.StdCtrls, FMX.Edit, FMX.Layouts, FMX.Memo,
Script.Interfaces
;
type
TForm1 = class(TForm, IscriptCompileLog, IscriptRunLog)
Button1: TButton;
Button2: TButton;
Button3: TButton;
Edit1: TEdit;
Run: TButton;
CompileLog: TMemo;
RunLog: TMemo;
procedure Button1Click(Sender: TObject);
procedure RunClick(Sender: TObject);
private
{ Private declarations }
procedure IscriptCompileLog_Log(const aString: String);
procedure IscriptCompileLog.Log = IscriptCompileLog_Log;
procedure IscriptRunLog_Log(const aString: String);
procedure IscriptRunLog.Log = IscriptRunLog_Log;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
uses
Script.Engine
;
{$R *.fmx}
procedure TForm1.IscriptCompileLog_Log(const aString: String);
begin
CompileLog.Lines.Add(aString);
end;
procedure TForm1.IscriptRunLog_Log(const aString: String);
begin
RunLog.Lines.Add(aString);
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
Edit1.Text := (Sender As TButton).Text;
end;
procedure TForm1.RunClick(Sender: TObject);
begin
CompileLog.Lines.Clear;
RunLog.Lines.Clear;
TScriptEngine.RunScript('FirstScript.script', Self, Self);
end;
end.
- here you can see that form TForm1 began to implement two interfaces - IscriptCompileLog and IscriptRunLog.
Moreover – through various methods.
The compilation log is printed into component CompileLog, and the execution log is printed into component RunLog.
Now, let’s really separate compilation from launch.
Let’s extract method CompileScript from method RunScript that will return the compiled code and the interface IscriptCode, which actually is the compiled code.
We’ll also introduce interface IscriptCompiler – the script code compiler.
(One more note - all interfaces I’ve described do not have GUID. This is done on pupose. The reasons are here - http://18delphi.blogspot.ru/2013/11/supports.html and here - http://18delphi.blogspot.ru/2013/11/queryinterface-supports.html and here - http://18delphi.blogspot.ru/2013/10/supports.html .
When GUIDs will really be necessary we’ll sure introduce them).
That’s what we get:
(The most curious readers can look at the commits log in SVN. It deserves special consideration. USUALLY I look into other’s logs so that to “catch on the author’s train of thought”.)
The interface of IscriptCode and IscriptCompiler is here:
unit Script.WordsInterfaces;
interface
uses
Core.Obj,
Script.Interfaces
;
type
TscriptContext = class(TCoreObject)
private
f_Log : IscriptLog;
protected
procedure Cleanup; override;
public
constructor Create(const aLog: IscriptLog);
procedure Log(const aString: String);
{* - Prints message into log. }
end;//TscriptContext
TscriptCompileContext = class(TscriptContext)
public
constructor Create(const aLog: IscriptCompileLog);
end;//TscriptCompileContext
TscriptRunContext = class(TscriptContext)
public
constructor Create(const aLog: IscriptRunLog);
end;//TscriptRunContext
IscriptWord = interface
{* - script engine word. }
procedure DoIt(aContext: TscriptContext);
{* - the procedure for execution of the word from dictionary. }
end;//IscriptWord
IscriptCode = interface
{* - the compiled code for script engine. }
procedure Run(aContext : TscriptRunContext);
{* - executes the compiled code. }
end;//IscriptCode
IscriptCompiler = interface
{* - the compiler of script engine code. }
procedure CompileWord(const aWord: IscriptWord);
{* - compiles the specified word to code. }
function CompiledCode: IscriptCode;
{* - the compiled code }
end;//IscriptCompiler
implementation
// TscriptContext
constructor TscriptContext.Create(const aLog: IscriptLog);
begin
inherited Create;
f_Log := aLog;
end;
procedure TscriptContext.Log(const aString: String);
{* - Prints message into log. }
begin
if (f_Log <> nil) then
f_Log.Log(aString);
end;
procedure TscriptContext.Cleanup;
begin
f_Log := nil;
inherited;
end;
// TscriptCompileContext
constructor TscriptCompileContext.Create(const aLog: IscriptCompileLog);
begin
inherited Create(aLog);
end;
// TscriptRunContext
constructor TscriptRunContext.Create(const aLog: IscriptRunLog);
begin
inherited Create(aLog);
end;
end.
(One more note – “mixing objects with interfaces” – does not have to scare you. This is “not scary”. The “interfaces” internal and do not cross the areas boundaries. They make nothing more than reference counting. It could be done in different ways, for example - http://18delphi.blogspot.com/2015/02/containers-2-my-own-implementation-of.html or - http://18delphi.blogspot.com/2015/02/containers-1-implementation-of.html. . If a curious reader wishes to argue about “objects and interfaces” and the “pure Aryan race”, he should look into the given references and, only after, “write letters”. I had a good teacher – Oleg Evseev, he used to tell: “A good question. You, Lyulin, stay after the seminar and we’ll discuss it”.)The implementation of interfaces IscriptCode and IscriptCompiler is here:
unit Script.Code;
interface
uses
Core.Obj,
System.Generics.Collections,
Script.WordsInterfaces
;
type
TscriptCodeContainer = class(TList<iscriptword>)
end;//TscriptCodeContainer
TscriptCode = class(TCoreInterfacedObject, IscriptCode, IscriptCompiler)
private
f_Code : TscriptCodeContainer;
protected
// interfaces methods
procedure Run(aContext : TscriptRunContext);
{* - executes the compiled code. }
procedure CompileWord(const aWord: IscriptWord);
{* - compiles the specified word to code. }
function CompiledCode: IscriptCode;
{* - the compiled code }
protected
procedure Cleanup; override;
public
class function Make: IscriptCompiler;
{* - factory. }
end;//TscriptCode
TscriptCompiler = TscriptCode;
implementation
uses
System.SysUtils
;
// TscriptCode
class function TscriptCode.Make: IscriptCompiler;
{* - factory. }
begin
Result := Create;
end;
procedure TscriptCode.Cleanup;
begin
FreeAndNil(f_Code);
inherited;
end;
procedure TscriptCode.Run(aContext : TscriptRunContext);
var
l_Word : IscriptWord;
begin
if (Self.f_Code <> nil) then
for l_Word in Self.f_Code do
l_Word.DoIt(aContext);
end;
procedure TscriptCode.CompileWord(const aWord: IscriptWord);
{* - compiles the specified word to code. }
begin
if (Self.f_Code = nil) then
Self.f_Code := TscriptCodeContainer.Create;
Self.f_Code.Add(aWord);
end;
function TscriptCode.CompiledCode: IscriptCode;
{* - the compiled code }
begin
Result := Self;
end;
end.
(my hands itch to tell about dependency injections...)
The use of interfaces IscriptCode and IscriptCompiler and, again, the modified script engine:
unit Script.Engine;
interface
uses
Script.Interfaces,
Script.WordsInterfaces
;
type
TscriptEngine = class
public
class function CompileScript(const aFileName: String;
const aCompileLog: IscriptCompileLog): IscriptCode;
class procedure RunScript(const aFileName: String;
const aCompileLog: IscriptCompileLog;
const aRunLog : IscriptRunLog);
end;//TscriptEngine
implementation
uses
System.SysUtils,
Script.Parser,
Testing.Engine,
Script.Code,
Script.StringWord
;
class function TscriptEngine.CompileScript(const aFileName: String;
const aCompileLog: IscriptCompileLog): IscriptCode;
var
l_CodeCompiler : IscriptCompiler;
l_CompileContext : TscriptCompileContext;
l_Parser : TscriptParser;
l_StringWord : IscriptWord;
begin
TtestEngine.StartTest(aFileName);
try
l_CodeCompiler := TscriptCompiler.Make;
try
l_CompileContext := TscriptCompileContext.Create(aCompileLog);
try
l_Parser := TscriptParser.Create(aFileName);
try
while not l_Parser.EOF do
begin
l_Parser.NextToken;
if (aCompileLog <> nil) then
aCompileLog.Log(l_Parser.TokenString);
l_StringWord := TscriptStringWord.Make(l_Parser.TokenString);
try
l_CodeCompiler.CompileWord(l_StringWord);
finally
l_StringWord := nil;
end;//try..finally
end;//while not l_Parser.EOF
finally
FreeAndNil(l_Parser);
end;//try..finally
finally
FreeAndNil(l_CompileContext);
end;//try..finally
Result := l_CodeCompiler.CompiledCode;
finally
l_CodeCompiler := nil;
end;//try..finally
finally
TtestEngine.StopTest;
end;//try..finally
end;
class procedure TscriptEngine.RunScript(const aFileName: String;
const aCompileLog: IscriptCompileLog;
const aRunLog : IscriptRunLog);
var
l_RunContext : TscriptRunContext;
l_Code : IscriptCode;
begin
l_Code := Self.CompileScript(aFileName, aCompileLog);
try
l_RunContext := TscriptRunContext.Create(aRunLog);
try
l_Code.Run(l_RunContext);
// - we execute the compiled code
finally
FreeAndNil(l_RunContext);
end;//try..finally
finally
l_Code := nil;
end;//try..finally
end;
end.
It is so somehow.
(The main thing is – we absolutely forgot about tests described here - http://18delphi.blogspot.com/2013/11/5.html. They ALWAYS succeed. TDD (https://en.wikipedia.org/wiki/Test-driven_development) - is cool)
(Does the seminar come out not very long? Not for “a pair double classes”?)
Now, let’s separately compile tokens (script_ttToken) and strings (script_ttString).
A new class appears - TscriptUnknownToken. It looks like TscriptStringWord, but I do not generalize their codes on purpose:
unit Script.UnknownToken;
interface
uses
Script.WordsInterfaces,
Script.Word
;
type
TscriptUnknownToken = class(TscriptWord)
private
f_String : String;
protected
procedure DoIt(aContext: TscriptContext); override;
public
constructor Create(const aString: String);
class function Make(const aString: String): IscriptWord;
end;//TscriptUnknownToken
implementation
constructor TscriptUnknownToken.Create(const aString: String);
begin
inherited Create;
f_String := aString;
end;
class function TscriptUnknownToken.Make(const aString: String): IscriptWord;
begin
Result := Create(aString);
end;
procedure TscriptUnknownToken.DoIt(aContext: TscriptContext);
begin
aContext.Log('Unknown token: ' + Self.f_String);
end;
end.
Here is how method TscriptEngine.CompileScript is modified:
class function TscriptEngine.CompileScript(const aFileName: String;
const aCompileLog: IscriptCompileLog): IscriptCode;
var
l_CodeCompiler : IscriptCompiler;
l_CompileContext : TscriptCompileContext;
l_Parser : TscriptParser;
begin
TtestEngine.StartTest(aFileName);
try
l_CodeCompiler := TscriptCompiler.Make;
try
l_CompileContext := TscriptCompileContext.Create(aCompileLog);
try
l_Parser := TscriptParser.Create(aFileName);
try
while not l_Parser.EOF do
begin
l_Parser.NextToken;
if (aCompileLog <> nil) then
aCompileLog.Log(l_Parser.TokenString);
Case l_Parser.TokenType of
script_ttEOF:
break;
script_ttToken:
l_CodeCompiler.CompileWord(TscriptUnknownToken.Make(l_Parser.TokenString));
script_ttString:
l_CodeCompiler.CompileWord(TscriptStringWord.Make(l_Parser.TokenString));
else
Assert(false, 'Unknown token type: ' + GetEnumName(TypeInfo(TscriptTokenType), Ord(l_Parser.TokenType)));
end;//Case l_Parser.TokenType
end;//while not l_Parser.EOF
finally
FreeAndNil(l_Parser);
end;//try..finally
finally
FreeAndNil(l_CompileContext);
end;//try..finally
Result := l_CodeCompiler.CompiledCode;
finally
l_CodeCompiler := nil;
end;//try..finally
finally
TtestEngine.StopTest;
end;//try..finally
end;
Let’s think – “exactly what tokens are not Unknown?
Let’s analyze registration of words in the dictionary.
Let’s look at the dictionary class implementation - TscriptDictionary. Again, it is more than simple.
Due to the same generis:
unit Script.Dictionary; interface uses System.Generics.Collections, Script.WordsInterfaces, Script.Word ; type TscriptDictionary = class(TDictionary<string, IscriptWord>) public procedure AddWord(const aKey: String; aWordClass : RscriptWord); end;//TscriptDictionary implementation procedure TscriptDictionary.AddWord(const aKey: String; aWordClass : RscriptWord); var l_Word : IscriptWord; begin l_Word := aWordClass.Make; try Self.Add(aKey, l_Word); finally l_Word := nil end;//try..finally end; end.
Now, let’s look at “axiomatics” - TscriptAxiomatiсs.
For now, we inherit this class from TscriptDictionary and make it singleton (https://en.wikipedia.org/wiki/Singleton_pattern).
Here is our new class:
unit Script.Axiomatics; interface uses Script.Dictionary ; type TscriptAxiomatics = class(TscriptDictionary) private class var f_Instance : TscriptAxiomatics; public class function Instance: TscriptAxiomatics; end;//TscriptAxiomatics implementation uses System.SysUtils ; class function TscriptAxiomatics.Instance: TscriptAxiomatics; begin if (f_Instance = nil) then f_Instance := TscriptAxiomatics.Create; Result := f_Instance; end; initialization finalization FreeAndNil(TscriptAxiomatics.f_Instance); end.
The implementation is not perfect and does not provide thread safety, but telling about how to make singletons right is not the purpose of this article.
The code of script engine modifies like this:
class function TscriptEngine.CompileScript(const aFileName: String;
const aCompileLog: IscriptCompileLog): IscriptCode;
var
l_CodeCompiler : IscriptCompiler;
l_CompileContext : TscriptCompileContext;
l_Parser : TscriptParser;
l_FoundWord : IscriptWord;
begin
TtestEngine.StartTest(aFileName);
try
l_CodeCompiler := TscriptCompiler.Make;
try
l_CompileContext := TscriptCompileContext.Create(aCompileLog);
try
l_Parser := TscriptParser.Create(aFileName);
try
while not l_Parser.EOF do
begin
l_Parser.NextToken;
if (aCompileLog <> nil) then
aCompileLog.Log(l_Parser.TokenString);
Case l_Parser.TokenType of
script_ttEOF:
break;
script_ttToken:
begin
if TscriptAxiomatics.Instance.TryGetValue(l_Parser.TokenString, l_FoundWord) then
// - word has been registered in axiomatics
l_CodeCompiler.CompileWord(l_FoundWord)
// - we compile it
else
l_CodeCompiler.CompileWord(TscriptUnknownToken.Make(l_Parser.TokenString));
// - for now, we compile the stub
end;//script_ttToken
script_ttString:
l_CodeCompiler.CompileWord(TscriptStringWord.Make(l_Parser.TokenString));
else
Assert(false, 'Unknown token type: ' + GetEnumName(TypeInfo(TscriptTokenType), Ord(l_Parser.TokenType)));
end;//Case l_Parser.TokenType
end;//while not l_Parser.EOF
finally
FreeAndNil(l_Parser);
end;//try..finally
finally
FreeAndNil(l_CompileContext);
end;//try..finally
Result := l_CodeCompiler.CompiledCode;
finally
l_CodeCompiler := nil;
end;//try..finally
finally
TtestEngine.StopTest;
end;//try..finally
end;
Now, we use this class (TscriptAxiomatics) to register at least one word of axiomatics:
unit Script.Word.Examples;
interface
uses
Script.WordsInterfaces,
Script.Word
;
type
TscriptWordExample1 = class(TscriptWord)
protected
procedure DoIt(aContext: TscriptContext); override;
end;//TscriptWordExample1
TscriptWordExample2 = class(TscriptWord)
protected
procedure DoIt(aContext: TscriptContext); override;
end;//TscriptWordExample2
implementation
uses
Script.Engine
;
// TscriptWordExample1
procedure TscriptWordExample1.DoIt(aContext: TscriptContext);
begin
aContext.Log('Example 1');
end;
// TscriptWordExample2
procedure TscriptWordExample2.DoIt(aContext: TscriptContext);
begin
aContext.Log('Example 2');
end;
initialization
TscriptEngine.RegisterKeyWord('DoNothing', TscriptWordExample1);
TscriptEngine.RegisterKeyWord('DoNothing2', TscriptWordExample2);
end.
Actually, we’ve registered two words DoNothing and DoNothing2, having connected them with classes TscriptWordExample1 and TscriptWordExample2 respectively.
We launch our example and see that printing into log has changed.
Let's take our mind off “growing the functional” and go in for refactoring a bit.
We’ll extract a factory method (http://18delphi.blogspot.ru/2013/04/blog-post_7483.html .) TscriptEngine.CompileToken from method TscriptEngine.CompileScript
First, we’ll introduce the interface IscriptParser and the factory method TscriptParser.Make. We’ll also connect compilation context TscriptCompileContext to the parser IscriptParser and the compiler IscriptCompiler.
Here’s what we get.
We introduce the interface of parser:
unit Script.Interfaces;
interface
type
IscriptLog = interface
procedure Log(const aString: String);
end;//IscriptLog
TscriptTokenType = (script_ttUnknown, script_ttToken, script_ttString, script_ttEOF);
IscriptParser = interface
function Get_TokenType: TscriptTokenType;
function Get_TokenString: String;
function EOF: Boolean;
{* - The end of the input stream has been reached. }
procedure NextToken;
{* - Choose the next token from the input stream. }
property TokenType: TscriptTokenType
read Get_TokenType;
property TokenString: String
read Get_TokenString;
end;//IscriptParser
IscriptCompileLog = interface(IscriptLog)
end;//IscriptCompileLog
IscriptRunLog = interface(IscriptLog)
end;//IscriptRunLog
implementation
end.
We connect the parser and the compiler to the context:
unit Script.WordsInterfaces;
interface
uses
Core.Obj,
Script.Interfaces
;
type
TscriptContext = class(TCoreObject)
private
f_Log : IscriptLog;
protected
procedure Cleanup; override;
public
constructor Create(const aLog: IscriptLog);
procedure Log(const aString: String);
{* - Prints message into log. }
end;//TscriptContext
IscriptCompiler = interface;
TscriptCompileContext = class(TscriptContext)
private
f_Parser : IscriptParser;
f_Compiler : IscriptCompiler;
protected
procedure Cleanup; override;
public
constructor Create(const aLog : IscriptCompileLog;
const aParser : IscriptParser;
const aCompiler : IscriptCompiler);
property Parser: IscriptParser
read f_Parser;
property Compiler: IscriptCompiler
read f_Compiler;
end;//TscriptCompileContext
TscriptRunContext = class(TscriptContext)
public
constructor Create(const aLog: IscriptRunLog);
end;//TscriptRunContext
IscriptWord = interface
{* - the word of script engine. }
procedure DoIt(aContext: TscriptContext);
{* - the procedure for execution of the word from the dictionary. }
end;//IscriptWord
IscriptCode = interface
{* - the compiled script engine code. }
procedure Run(aContext : TscriptRunContext);
{* - executes the compiled code. }
end;//IscriptCode
IscriptCompiler = interface
{* - the compiler of script engine code. }
procedure CompileWord(const aWord: IscriptWord);
{* - compiles the specified word in the code. }
function CompiledCode: IscriptCode;
{* - the compiled code }
end;//IscriptCompiler
implementation
// TscriptContext
constructor TscriptContext.Create(const aLog: IscriptLog);
begin
inherited Create;
f_Log := aLog;
end;
procedure TscriptContext.Log(const aString: String);
{* - Prints the message to the log. }
begin
if (f_Log <> nil) then
f_Log.Log(aString);
end;
procedure TscriptContext.Cleanup;
begin
f_Log := nil;
inherited;
end;
// TscriptCompileContext
constructor TscriptCompileContext.Create(const aLog : IscriptCompileLog;
const aParser : IscriptParser;
const aCompiler : IscriptCompiler);
begin
Assert(aParser <> nil);
Assert(aCompiler <> nil);
inherited Create(aLog);
f_Parser := aParser;
f_Compiler := aCompiler;
end;
procedure TscriptCompileContext.Cleanup;
begin
f_Parser := nil;
f_Compiler := nil;
inherited;
end;
// TscriptRunContext
constructor TscriptRunContext.Create(const aLog: IscriptRunLog);
begin
inherited Create(aLog);
end;
end.
And here comes the implementation of TscriptParser.Make:
unit Script.Parser;
interface
uses
Classes,
Core.Obj,
Script.Interfaces
;
{$IfNDef NoTesting}
{$Define TestParser}
{$EndIf NoTesting}
type
TscriptParser = class(TCoreObject)
private
f_Stream : TStream;
f_EOF : Boolean;
f_CurrentLine : String;
f_PosInCurrentLine : Integer;
f_Token : String;
f_TokenType : TscriptTokenType;
protected
procedure Cleanup; override;
function ReadLn: String;
protected
function GetChar(out aChar: AnsiChar): Boolean;
public
constructor Create(const aStream : TStream); overload;
constructor Create(const aFileName : String); overload;
class function Make(const aFileName : String): IscriptParser;
{* - Factory of interface IscriptParser. }
function EOF: Boolean;
{* - The end of the input stream has been reached. }
procedure NextToken;
{* - Choose the next token from the input stream. }
public
property TokenString: String
read f_Token;
{* - current token. }
property TokenType: TscriptTokenType
read f_TokenType;
{* - type of the current token. }
end;//TscriptParser
implementation
uses
System.SysUtils
{$IfDef TestParser}
,
Testing.Engine
{$EndIf TestParser}
;
type
TscriptParserContainer = class(TCoreInterfacedObject, IscriptParser)
private
f_Parser : TscriptParser;
private
function Get_TokenType: TscriptTokenType;
function Get_TokenString: String;
function EOF: Boolean;
{* - The end of the input stream has been reached. }
procedure NextToken;
{* - Choose the next token from the input stream. }
protected
procedure Cleanup; override;
public
constructor Create(aParser: TscriptParser);
class function Make(aParser: TscriptParser): IscriptParser;
end;//TscriptParserContainer
constructor TscriptParserContainer.Create(aParser: TscriptParser);
begin
Assert(aParser <> nil);
inherited Create;
f_Parser := aParser;
end;
class function TscriptParserContainer.Make(aParser: TscriptParser): IscriptParser;
begin
Result := TscriptParserContainer.Create(aParser);
end;
procedure TscriptParserContainer.Cleanup;
begin
FreeAndNil(f_Parser);
inherited;
end;
function TscriptParserContainer.Get_TokenType: TscriptTokenType;
begin
Result := f_Parser.TokenType;
end;
function TscriptParserContainer.Get_TokenString: String;
begin
Result := f_Parser.TokenString;
end;
function TscriptParserContainer.EOF: Boolean;
{* - The end of the input stream has been reached. }
begin
Result := f_Parser.EOF;
end;
procedure TscriptParserContainer.NextToken;
{* - Choose the next token from the input stream. }
begin
f_Parser.NextToken;
end;
// TscriptParser
constructor TscriptParser.Create(const aStream : TStream);
begin
inherited Create;
f_PosInCurrentLine := 1;
f_EOF := false;
f_Stream := aStream;
end;
constructor TscriptParser.Create(const aFileName : String);
var
l_FileName : String;
begin
l_FileName := ExtractFilePath(ParamStr(0)) + '\' + aFileName;
Create(TFileStream.Create(l_FileName, fmOpenRead));
end;
class function TscriptParser.Make(const aFileName : String): IscriptParser;
{* - Factory of interface IscriptParser. }
begin
Result := TscriptParserContainer.Make(Self.Create(aFileName));
end;
...
end.
In order to implement the interface IscriptParser – we did not “break inheritance” and change the structure of the class TscriptParser. Instead, we’ve introduced an additional hidden class TscriptParserContainer that aggregates TscriptParser and implements interface IscriptParser using the functional of aggregated class TscriptParser. This technique is like the pattern adapter (https://en.wikipedia.org/wiki/Adapter_pattern) or the pattern facade (http://18delphi.blogspot.com/2015/02/once-again-about-testing-levels.html) .
So, class TscriptEngine is modified again:
Script.Engine;
interface
uses
Script.Interfaces,
Script.WordsInterfaces,
Script.Word
;
type
TscriptEngine = class
protected
class function CompileToken(aContext : TscriptCompileContext): Boolean;
public
class function CompileScript(const aFileName: String;
const aCompileLog: IscriptCompileLog): IscriptCode;
class procedure RunScript(const aFileName: String;
const aCompileLog: IscriptCompileLog;
const aRunLog : IscriptRunLog);
class procedure RegisterKeyWord(const aKeyWord: String; aWordClass: RscriptWord);
end;//TscriptEngine
implementation
uses
TypInfo,
System.SysUtils,
Script.Parser,
Testing.Engine,
Script.Code,
Script.StringWord,
Script.UnknownToken,
Script.Axiomatics
;
class function TscriptEngine.CompileToken(aContext : TscriptCompileContext): Boolean;
var
l_FoundWord : IscriptWord;
begin
Result := true;
aContext.Parser.NextToken;
aContext.Log(aContext.Parser.TokenString);
Case aContext.Parser.TokenType of
script_ttEOF:
Result := false;
script_ttToken:
begin
if TscriptAxiomatics.Instance.TryGetValue(aContext.Parser.TokenString, l_FoundWord) then
// - the word has been registered in axiomatics
aContext.Compiler.CompileWord(l_FoundWord)
// - we compile it
else
aContext.Compiler.CompileWord(TscriptUnknownToken.Make(aContext.Parser.TokenString));
// - for now, we compile the stub
end;//script_ttToken
script_ttString:
aContext.Compiler.CompileWord(TscriptStringWord.Make(aContext.Parser.TokenString));
else
Assert(false, 'Unknown token type: ' + GetEnumName(TypeInfo(TscriptTokenType), Ord(aContext.Parser.TokenType)));
end;//Case l_CompileContext.Parser.TokenType
end;
class function TscriptEngine.CompileScript(const aFileName: String;
const aCompileLog: IscriptCompileLog): IscriptCode;
var
l_CompileContext : TscriptCompileContext;
l_FoundWord : IscriptWord;
begin
l_CompileContext := TscriptCompileContext.Create(aCompileLog,
TscriptParser.Make(aFileName),
TscriptCompiler.Make);
try
while CompileToken(l_CompileContext) do
;
Result := l_CompileContext.Compiler.CompiledCode;
finally
FreeAndNil(l_CompileContext);
end;//try..finally
end;
class procedure TscriptEngine.RunScript(const aFileName: String;
const aCompileLog: IscriptCompileLog;
const aRunLog : IscriptRunLog);
var
l_RunContext : TscriptRunContext;
l_Code : IscriptCode;
begin
l_Code := Self.CompileScript(aFileName, aCompileLog);
try
l_RunContext := TscriptRunContext.Create(aRunLog);
try
l_Code.Run(l_RunContext);
// - we execute the compiled code
finally
FreeAndNil(l_RunContext);
end;//try..finally
finally
l_Code := nil;
end;//try..finally
end;
class procedure TscriptEngine.RegisterKeyWord(const aKeyWord: String; aWordClass: RscriptWord);
begin
TscriptAxiomatics.Instance.AddWord(aKeyWord, aWordClass);
end;
end.
Now, let’s get back to “why this all is done”. We’ll learn to “press the buttons through software”.
Here we’ve got to the values stack.
Let’s see how it looks like:
unit Script.WordsInterfaces;
interface
uses
System.Generics.Collections,
Core.Obj,
Script.Interfaces
;
type
TscriptValueType = (script_vtUnknown, script_vtString, script_vtObject);
TscriptValue = record
public
rValueType : TscriptValueType;
private
rAsString : String;
rAsObject : TObject;
public
constructor Create(const aString: String); overload;
constructor Create(anObject: TObject); overload;
function AsString: String;
function AsObject: TObject;
end;//TscriptValue
TscriptValuesStack = TList<tscriptvalue>;
TscriptContext = class(TCoreObject)
private
f_Log : IscriptLog;
f_Stack : TscriptValuesStack;
protected
procedure Cleanup; override;
public
constructor Create(const aLog: IscriptLog);
procedure Log(const aString: String);
{* - Prints the message into log. }
function PopString: String;
procedure PushString(const aString: String);
function PopObject: TObject;
procedure PushObject(const anObject: TObject);
end;//TscriptContext
IscriptCompiler = interface;
TscriptCompileContext = class(TscriptContext)
private
f_Parser : IscriptParser;
f_Compiler : IscriptCompiler;
protected
procedure Cleanup; override;
public
constructor Create(const aLog : IscriptCompileLog;
const aParser : IscriptParser;
const aCompiler : IscriptCompiler);
property Parser: IscriptParser
read f_Parser;
property Compiler: IscriptCompiler
read f_Compiler;
end;//TscriptCompileContext
TscriptRunContext = class(TscriptContext)
public
constructor Create(const aLog: IscriptRunLog);
end;//TscriptRunContext
IscriptWord = interface
{* - word of script engine }
procedure DoIt(aContext: TscriptContext);
{* - the procedure for execution of the word from dictionary. }
end;//IscriptWord
IscriptCode = interface
{* - the compiled code of the script engine. }
procedure Run(aContext : TscriptRunContext);
{* - executes the compiled code. }
end;//IscriptCode
IscriptCompiler = interface
{* - the compiler of script engine code. }
procedure CompileWord(const aWord: IscriptWord);
{* - compiles the specified word into code. }
function CompiledCode: IscriptCode;
{* - the compiled code }
end;//IscriptCompiler
implementation
uses
System.SysUtils
;
// TscriptValue
constructor TscriptValue.Create(const aString: String);
begin
inherited;
rValueType := script_vtString;
rAsString := aString;
end;
constructor TscriptValue.Create(anObject: TObject);
begin
inherited;
rValueType := script_vtObject;
rAsObject := anObject;
end;
function TscriptValue.AsString: String;
begin
Assert(rValueType = script_vtString);
Result := rAsString;
end;
function TscriptValue.AsObject: TObject;
begin
Assert(rValueType = script_vtObject);
Result := rAsObject;
end;
// TscriptContext
constructor TscriptContext.Create(const aLog: IscriptLog);
begin
inherited Create;
f_Log := aLog;
f_Stack := TscriptValuesStack.Create;
end;
procedure TscriptContext.Log(const aString: String);
{* - Prints the message to log. }
begin
if (f_Log <> nil) then
f_Log.Log(aString);
end;
function TscriptContext.PopString: String;
begin
Assert(f_Stack.Count > 0);
Result := f_Stack.Last.AsString;
f_Stack.Delete(f_Stack.Count - 1);
end;
procedure TscriptContext.PushString(const aString: String);
begin
f_Stack.Add(TscriptValue.Create(aString));
end;
function TscriptContext.PopObject: TObject;
begin
Assert(f_Stack.Count > 0);
Result := f_Stack.Last.AsObject;
f_Stack.Delete(f_Stack.Count - 1);
end;
procedure TscriptContext.PushObject(const anObject: TObject);
begin
f_Stack.Add(TscriptValue.Create(anObject));
end;
procedure TscriptContext.Cleanup;
begin
f_Log := nil;
FreeAndNil(f_Stack);
inherited;
end;
// TscriptCompileContext
constructor TscriptCompileContext.Create(const aLog : IscriptCompileLog;
const aParser : IscriptParser;
const aCompiler : IscriptCompiler);
begin
Assert(aParser <> nil);
Assert(aCompiler <> nil);
inherited Create(aLog);
f_Parser := aParser;
f_Compiler := aCompiler;
end;
procedure TscriptCompileContext.Cleanup;
begin
f_Parser := nil;
f_Compiler := nil;
inherited;
end;
// TscriptRunContext
constructor TscriptRunContext.Create(const aLog: IscriptRunLog);
begin
inherited Create(aLog);
end;
end.
- here, again, we’ve used generics from standard library.
Now, basing on the knowledge acquired we’ll introduce two words of axiomatics - TkwFindComponent and TkwButtonClick.
The first word searches the component with the specified name on the current application form and puts it into values stack.
The second word selects the object value from the stack, interprets it as a button and tries to press the specified button..
Here are the words:
unit Script.Word.Buttons;
interface
uses
Script.WordsInterfaces,
Script.Word
;
type
TkwFindComponent = class(TscriptWord)
protected
procedure DoIt(aContext: TscriptContext); override;
end;//TkwFindComponent
TkwButtonClick = class(TscriptWord)
protected
procedure DoIt(aContext: TscriptContext); override;
end;//TkwButtonClick
implementation
uses
System.Classes,
Script.Engine,
FMX.Controls,
FMX.StdCtrls,
FMX.Forms
;
// TkwFindComponent
procedure TkwFindComponent.DoIt(aContext: TscriptContext);
var
l_Name : String;
l_Component : TComponent;
begin
aContext.Log(ClassName);
l_Name := aContext.PopString;
Assert(l_Name <> '');
l_Component := Screen.ActiveForm.FindComponent(l_Name);
Assert(l_Component <> nil);
aContext.PushObject(l_Component);
end;
// TkwButtonClick
type
TControlAccess = class(TControl)
end;//TControlAccess
procedure TkwButtonClick.DoIt(aContext: TscriptContext);
var
l_Component : TComponent;
begin
aContext.Log(ClassName);
l_Component := aContext.PopObject As TComponent;
Assert(l_Component Is TButton);
TControlAccess(l_Component).Click;
end;
initialization
TscriptEngine.RegisterKeyWord('FindComponent', TkwFindComponent);
TscriptEngine.RegisterKeyWord('ButtonClick', TkwButtonClick);
end.
Note. The type TControlAccess – allows access to the method Click of TControl class. This is a well-know “trick”. Even Borland and Embarcadero use it now and then. Later, we’ll consider the “new” RTTI. There we will not need such “tricks”.
So.
We are now ready to press the button through script.
The code of the script:
'Button1' FindComponent ButtonClick // - we press the button
or this like:
'Button2' FindComponent ButtonClick // - we press the button
Try them both. May be you will like it.
Conclusion.
So. Using rather simply example of “pressing the button on the form” we have looked into not the simple things.
1. We have described the script engine.
2. We have described the process of script compilation.
3. We have described the registration of words un the axiomatics dictionary.
4. We have analysed the process of scripts launch.
5. We have described the values stack and looked into the examples of using it.
Well, and “on the surface” we’ve achieved our goal – learned to “press the buttons through scripts”.
I hope one of the main ideas is clear – the described script engine may extend by registration of new layers in axiomatics in Delphi.
This finishes my article.
If the readers are interested, I’ll continue to write about script engine and “how it is organized”.
Generally speaking, much can be discussed about it:
1. Conditional operators.
2. Loops.
3. Variables.
4. User defining of new words through script code.
5. Introducing of the script from other files to the code.
6. And many others.
P.S. In LONG time I have not typed so much code MANUALLY.
It is long time since I started “drawing squares on the model”. This is FAR MORE quick…
P.P.S. I’ll repeat. We used Delphi to write “this”. But “this” may be written on any language – either Objective-C, or Python, or C++, or classic C, or classic Pascal (in Turbo Professional). The difference is in “commas”.
The most important is the essence of this APPROACH.
P.P.P.S. One more “wish” is to make GUITestRunner for DUnit to develope on FM. I’ll try to implement it.
Update:
We did it - http://programmingmindstream.blogspot.ru/2014/11/firemonkey-dunit.html
P.P.P.P.S. The announcement. Here is the new version of the example - https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/GUITests/Chapter2/ The example of VCL-project is also available there, as well as the example for mobile devices, but for now I have launched it only with emulator.
Комментариев нет:
Отправить комментарий