понедельник, 16 марта 2015 г.

GUI-testing 13. GUI-testing “in spoken style". Back to the basics. Example of pressing the button of the form through the script

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:

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.

Комментариев нет:

Отправить комментарий