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.
Комментариев нет:
Отправить комментарий