суббота, 9 ноября 2013 г.

GUI-тестирование "по-русски". Работа с "контролом в фокусе" и определение его класса

Отвлечёмся немного от "хардкорного программирования" и устройства скриптовой машины.

Вот тут - http://18delphi.blogspot.ru/2013/11/gui.html - я писал как искать форму и контрол по их имени

А вот тут - http://18delphi.blogspot.ru/2013/11/gui_4.html - я писал - как в TEdit ввести текст.

А вот тут - http://18delphi.blogspot.ru/2013/11/gui-rtti.html - я писал как сделать "то же самое" средствами "классического" RTTI.

Напомню, что у нас в итоге получилось:

PROCEDURE "Присвоить текст в текущий контрол" STRING IN aText
 OBJECT VAR l_Control
 FocusedControl =: l_Control
 aText l_Control EditSetText
END //"Присвоить текст в текущий контрол"

Теперь пусть мы опять же хотим поработать с контролом в фокусе, но при этом мы хотим проконтролировать, что контрол в фокусе - НУЖНОГО нам класса - TEdit или TCustomEdit, ну или наследник он него.

Ну скажем - логика теста зависит от этого.

Как это сделать?

Сейчас расскажу.

Напишем для начала два "вспомогательных" слова. 

Они - в ПРИНЦИПЕ полезны для GUI-Тестирования.

Но и ещё они помогут нам расширить наши примеры.

Вот эти слова:

OBJECT FUNCTION "Контрол в фокусе"
 FocusedControl =: Result
END // "Контрол в фокусе"

WORDWORKER "Присвоить текст в" STRING IN aText
 OBJECT VAR l_Control
 WordToWork DO =: l_Control
 aText l_Control EditSetText
END // "Присвоить текст в"

Про слово WORDWORKER я писал тут - http://18delphi.blogspot.ru/2013/11/gui_9015.html и тут - http://18delphi.blogspot.ru/2013/11/gui-2_7.html

Тогда наш пример переписывается так:

PROCEDURE "Присвоить текст в текущий контрол" STRING IN aText
 "Присвоить текст  {(aText)} в" "Контрол в фокусе"
END //"Присвоить текст в текущий контрол"

Конкатенативный язык (http://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BD%D0%BA%D0%B0%D1%82%D0%B5%D0%BD%D0%B0%D1%82%D0%B8%D0%B2%D0%BD%D1%8B%D0%B9_%D1%8F%D0%B7%D1%8B%D0%BA_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F).

Не правда ли?

Теперь КАК определить, что "Контрол в фокусе" - НУЖНОГО нам класса?

А вот как:

На стороне Delphi определяем слово Inherits:

interface

type
 TkwInherits = class(TscriptKeyWord)
  protected
   procedure DoIt(aContext: TscriptContext); override;
 end;//TkwInherits

implementation

procedure TkwInherits.DoIt(aContext: TscriptContext);
var
 l_ClassName : String;

function localIsInherits(anObjectClass : TClass): Boolean;
begin
 if (anObjectClass = nil) then
  Result := false
 else
 if SameText(anObjectClass.ClassName, l_ClassName) then
  Result := true
 else
  Result := localIsInherits(anObjectClass.ClassParent);
end;
 
var
 l_Object : TObject;
begin
 l_Object := aContext.PopObject;
 Assert(l_Object <> nil);
 l_ClassName := aContext.PopString;
 aContext.PushBoolean(localIsInherits(l_Object.ClassType));
end;

initialization
 ScriptEngine.RegisterKeyWord(TkwInherits, 'Inherits');
 // - далее, если вызов RegisterKeyWord будет в виде RegisterKeyWord(TkwXXX, 'XXX'), 
 //   то я буду просто так и писать RegisterKeyWord(TkwXXX).
 //
 //   Тем более, что это у меня так и реализовано, через использование ClassName

Тогда можно написать такой код скрипта:

 'TCustomEdit' "Контрол в фокусе" Inherits IF (
  "Присвоить текст  {(aText)} в" "Контрол в фокусе"
 )

Ну или такой:

 'TCustomEdit' "Контрол в фокусе" Inherits ASSERT
 "Присвоить текст  {(aText)} в" "Контрол в фокусе"

Поработаем ещё немного.

Введём в словарь определение вот такого слова:

CONST "Строка ввода" 'TCustomEdit'

Тогда можно написать так:

 "Строка ввода" "Контрол в фокусе" Inherits IF (
  "Присвоить текст  {(aText)} в" "Контрол в фокусе"
 )

Ещё несколько штрихов.

Для начала определим слово ЯВЛЯЕТСЯ:

BOOLEAN WORDWORKERFUNCTION ЯВЛЯЕТСЯ OBJECT IN anObject
 WordToWork DO 
 // - вычисляем "параметр справа"
 anObject 
 // - anObject - "параметр слева"
 Inherits 
 // - зовём уже знакомое нам слово Inherits
 =: Result
 // - результат нашиз вычислений помещаем в результат функциии ЯВЛЯЕТСЯ
END // ЯВЛЯЕТСЯ

Тогда можно написать так:

 
 "Контрол в фокусе" ЯВЛЯЕТСЯ "Строка ввода" IF (
  "Присвоить текст  {(aText)} в" "Контрол в фокусе"
 )

Ещё определим синоним:

WordAlias "Строкой ввода" "Строка ввода"

Тогда можно написать так:

 
 "Контрол в фокусе" ЯВЛЯЕТСЯ "Строкой ввода" IF (
  "Присвоить текст  {(aText)} в" "Контрол в фокусе"
 )

Ещё немного поработаем над IF.

У нас помимо слова WORDWORKER - есть слово WORDWORKER2.

Которое умеет работать с двумя "параметрами справа".

Применим его.

Напишем следующее:

WORDWORKER2 если
 WordToWork1 DO
 // - вычислили значение "первого аргумента справа"
 IF (
  WordToWork2
  DO
  // - выполняем "второй аргумент справа"
 )
END // если

И тогда наш пример можно переписать так:

 если ( "Контрол в фокусе" ЯВЛЯЕТСЯ "Строкой ввода")
  ( "Присвоить текст  {(aText)} в" "Контрол в фокусе" )

По-русски? По-моему - по русски?

Теперь если определить слово "то":

Так:

WordAlias то NOP
// - NOP - это слово, которое ничего не делает, 
//   но может появляться там, где грамматика требует слово

Или так:

WORDWORKER то
 WordToWork DO
 // - просто вычисляем "аргумент справа"
 //   т.е. слово "то" в данном случае является proxy-словом
END // то

То мы можем переписать пример так:

если ( "Контрол в фокусе" ЯВЛЯЕТСЯ "Строкой ввода") то
  ( "Присвоить текст  {(aText)} в" "Контрол в фокусе" )

Ну и ещё введём синоним:

WordAlias Если если
// - у нас скриптовая машина - РЕГИСТРОЗАВИСИМАЯ, это - ОСОЗНАННО

Тогда можно написать так:

 Если ( "Контрол в фокусе" ЯВЛЯЕТСЯ "Строкой ввода") то
  ( "Присвоить текст  {(aText)} в" "Контрол в фокусе" )

Ну и "на закуску".

Ещё есть слово WORDWORKER2+.

Оно умеет работать с двумя фиксированными "параметрами справа".

И одним опциональным. Предварённым другим ключевым словом.

Вот пример:

WORDWORKER2+ 
 если 
  // - "если" - ОСНОВНОЕ ключевое слово. С которого начинается разбор грамматики
 иначе
 // - "иначе" - ДОПОЛНИТЕЛЬНОЕ ключевое слово. Которого может и не быть.
 WordToWork1 DO
 // - вычислили значение "первого аргумента справа"
 IF (
  WordToWork2
  DO
  // - выполняем "второй аргумент справа"
 )
 ELSE (
  WordToWork2+
  DO
  // - выполняем третий опциональный аргумент справа, если он передан"
  //   Если же он НЕ ПЕРЕДАН, то нам в аргументе WordToWork2+ - передадут уже знакомое слово NOP
 )
END // если

Тогда можно написать так:

 Если ( "Контрол в фокусе" ЯВЛЯЕТСЯ "Строкой ввода") то
  ( "Присвоить текст  {(aText)} в" "Контрол в фокусе" )
 иначе
  ( false ASSERT ) // - тут просто "упадём".

По-моему - "уже перебор информации".

Поэтому - закругляюсь.

Чуть позже я расскажу - как регистрировать РЕАЛЬНЫЕ классы объектов в тестовой машине, чтобы работать с их идентификаторами, а не со "строковыми именами".

И БОЛЕЕ ТОГО - вызывать их КОНСТРУКТОРЫ. Т.е. на самом деле - скриптовая машина - МОЖЕТ обладать возможностью СОЗДАВАТЬ объекты реального приложения.

Неплохо для "тестовой машины", а?

На самом деле - наверное самые пытливые читатели отметили, для себя, что эту скриптовую машину можно использовать не только для тестирования, но и для АВТОМАТИЗАЦИИ и написания макросов пользователя.

Такой вот VBA (http://ru.wikipedia.org/wiki/VBA) "на коленке".

Если компилировать "скриптовую обвязку" не только под IfDef Test, но и для конечного клиентского приложения.

И ДАТЬ пользователю возможность загружать и выполнять скрипты.

P.S. Я ещё вас не утомил? А то у меня план статей разросся уже до 30-ти штук.

Кому-то вообще интересно "сделать скриптовую машину своими руками" из "модели для сборки"?

Или пора закругляться?

Учитывая, что каждая новая статья приносит с собой ещё две статьи в "план для написания".

пятница, 8 ноября 2013 г.

Переменные мета-класса. Чужие ссылки

Вот тут - http://18delphi.blogspot.ru/2013/04/delphi.html я писал о "переменных мета-класса". И как их сделать "на коленке".

А вот Николай Зверев  прислал мне такие ссылки:
http://hallvards.blogspot.ru/2007/05/hack17-virtual-class-variables-part-i.html
http://hallvards.blogspot.ru/2007/05/hack17-virtual-class-variables-part-ii.html

Люди - думают о том же самом. Значит - не совсем - "велосипед изобретаю".

Виктор Морозов кстати - мне тоже присылал подобные ссылки. И я их уже где-то в блоге публиковал.

Не могу только найти где.

GUI-тестирование "по-русски". Продолжение про WordWorker'ы

Предыдущая серия была тут - http://18delphi.blogspot.com/2013/11/gui-3-wordworker.html

Теперь как же в процедуру WorkTheWord передаётся управление и откуда берётся aWordToTheRight?

Дело в том, что слово  TscriptWordWorker - помечено, как слово "непосредственного исполнения". Т.е. оно выполняется в процессе компиляции кода.

Как это выглядит?

А вот так:

interface

type
  TscriptWordWorker = class(TscriptKeyWord)
   protected
    function IsImmediate: Boolean; override;
    procedure DoIt(aContext: TscriptContext); override;
    procedure WorkTheWord(aWordToTheRight : TscriptKeyWord; aContext: TscriptContext); virtual; abstract;
  end;//TscriptWordWorker

implementation

function TscriptWordWorker.IsImmediate: Boolean;
begin
 Result := true;
end;

type
  TscriptCompiledWordWorker = class(TscriptKeyWord)
  // - вспомогательное слово, которое компилируется по месту вызова
   private
    f_Worker : TscriptWordWorker;
    f_WordToRight : TscriptKeyWord;
   protected
    procedure DoIt(aContext: TscriptContext); override;
    procedure Cleanup; override;
   public
    constructor Create(aWorker : TscriptWordWorker; aWordToRight: TscriptKeyWord);
  end;//TscriptCompiledWordWorker

constructor TscriptCompiledWordWorker.Create(aWorker : TscriptWordWorker; aWordToRight: TscriptKeyWord);
begin
 inherited Create;
 aWorker.SetReferenceTo(f_Worker);
 aWordToRight.SetReferenceTo(f_WordToRight);
end;

procedure TscriptCompiledWordWorker.Cleanup;
begin
 FreeAndNil(f_Worker);
 FreeAndNil(f_WordToRight);
 inherited;
end;

procedure TscriptCompiledWordWorker.DoIt(aContext: TscriptContext);
begin
 f_Worker.WorkTheWord(f_WordToRight);
end;

procedure TscriptWordWorker.DoIt(aContext: TscriptContext);
var
 l_CompileContext : TscriptCompileContext; 
 l_WordToTheRight : TscriptKeyWord;
 l_CompiledWordWorker : TscriptCompiledWordWorker;
begin
 l_CompileContext := (aContext As TscriptCompileContext);
 l_WordToTheRight := l_CompileContext.CompileWordToTheRight; // - Об устройстве этого метода поговорим чуть позже
 try
  l_CompiledWordWorker := TscriptCompiledWordWorker.Create(Self, l_WordToTheRight);
  try
   l_CompileContext.Code.CompileKeyWord(l_CompiledWordWorker);
  finally
   FreeAndNil(l_CompiledWordWorker);
  end;//try..finally
 finally
  FreeAndNil(l_WordToTheRight);
 end;//try..finally
end;

Устройство метода CompileWordToTheRight - разберём в следующих сериях.

GUI-тестировании "по-русски". Как всё это устроено №3. Как устроена работа с переменными, оператор равно и что такое WordWorker'ы.

Предыдущая серия была тут - http://18delphi.blogspot.com/2013/11/gui-2.html

Теперь хочется рассказать вот о чём.

Как устроена работа с переменными, оператор равно и что такое WordWorker'ы.

Рассмотрим пример:

VAR I
1 =: I

Что тут написано?
VAR I - определяет переменную с именем I в текущем контексте видимости (о контекстах позже поговорим отдельно).
1 - кладёт на стек значений целое число 1.
=: I - выбирает значение со стека и кладёт его в переменную I.

Как всё это устроено?

Сейчас попробую рассказать.

До сих пор нам встречались слова-наследники от TscriptKeyWord, которые умели выбирать значения из стека значений (TscriptContext) и работать с ними. Также они умели класть значения на стек. Пример тут - http://18delphi.blogspot.ru/2013/11/gui.html

Теперь хочется затронуть слова, которые не только умеют работать со значениями со стека ("параметры слева"), но и со словами, которые следуют в коде за ними ("параметры справа").

Одним из таких слов и является оператор равно - =:

Семантически он похож на оператор равно Паскаля, только "перевёрнут в другую сторону". Это "наследие" обратной польской записи, но это, поверьте мне, не так уж и важно.

Слова, которые умеют работать с "параметрами справа" я исторически называю WordWorker'ами.

И соответственно - наследуются они от класса TscriptWordWorker.

Разберём устройство этого класса:
interface

type
  TscriptWordWorker = class(TscriptKeyWord)
   protected
    procedure WorkTheWord(aWordToTheRight : TscriptKeyWord; aContext: TscriptContext); virtual; abstract;
  end;//TscriptWordWorker

Пока в дальнейшие детали этого слова вдаваться не будем.

Также есть ещё наследник от него TscriptVarWorker - слово, которое обрабатывает переменную.
Выглядит оно вот так:

interface

type
  IscriptVar = interface
   ['{39369047-E783-4A4B-9657-6C2450D737C3}']
   procedure PopFrom(aContext: TscriptContext);
   ...
  end;//IscriptVar

  TscriptVarWorker = class(TscriptWordWorker)
   protected
    procedure WorkTheWord(aWordToTheRight : TscriptKeyWord; aContext: TscriptContext); override;
    procedure WorkTheVar(aVarToTheRight : IscriptVar; aContext: TscriptContext); virtual; abstract;
  end;//TscriptVarWorker 

implementation

procedure TscriptVarWorker.WorkTheWord(aWordToTheRight : TscriptKeyWord; aContext: TscriptContext)
var
 l_Var : IscriptVar;
begin
 if Supports(aWordToTheRight, l_Var) then
  WorkTheVar(l_Var, aContext)
 else
  Assert(false);
end;

И тогда мы можем написать:

interface

type
  TkwPopToVar = class(TscriptVarWorker)
   protected
    procedure WorkTheVar(aVarToTheRight : IscriptVar; aContext: TscriptContext); override;
  end;//TkwPopToVar

implementation

procedure TkwPopToVar.WorkTheVar(aVarToTheRight : IscriptVar; aContext: TscriptContext);
begin
 aVarToTheRight.PopFrom(aContext);
end;

initialization
 ScriptEngine.RegisterKeyWord(TkwPopToVar, '=:');

Пока - всё.

Дальнейшие детали опишу в следующих сериях.

Ссылка. "Обучение архитекторов данных: проектирование и моделирование информационных систем с помощью ER/Studio"

Ссылка. Со многим бы поспорил, но не буду

GUI-Тестирование "по-русски". Как всё устроено №2.5. Про TscriptContext

Предыдущая серия была тут - http://18delphi.blogspot.com/2013/11/gui-2.html

Теперь хочется рассказать про устройство TscriptContext.

В целом это конечно - "стек значений".

Но не только... Но об этом - позже.

Пока - про стек..

Стек значений - это ОДИН из краеугольных камней устройства нашей тестовой машины.

Теорию можно почитать тут - http://ru.wikipedia.org/wiki/Forth

И кстати ещё одна забавная ссылка - http://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BD%D0%BA%D0%B0%D1%82%D0%B5%D0%BD%D0%B0%D1%82%D0%B8%D0%B2%D0%BD%D1%8B%D0%B9_%D1%8F%D0%B7%D1%8B%D0%BA_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F

А пока - займёмся практикой.

Как выглядит стек значений внутри.
Примерно вот так:
interface

type
  TscriptValueType = (script_vtVoid, script_vtInteger, script_vtBoolean, script_vtString, script_vtObject);

  TscriptValue = {$IfDef XE}record{$Else}object{$EndIf}
   private
    rType : TscriptValueType;
    rInteger : Integer;
    rBoolean : Boolean;
    rString : String;
    rObject : TObject;
   public
    function AsInteger: Integer;
    function AsBoolean: Boolean;
    function AsString: String;
    function AsObject: TObject;

    function EQ(anOther: TscriptValue): Boolean;
  end;//TscriptValue
 
  function TscriptValue_C(aValue: Integer): TscriptValue; overload;
  function TscriptValue_C(aValue: Boolean): TscriptValue; overload;
  function TscriptValue_C(aValue: String): TscriptValue; overload;
  function TscriptValue_C(aValue: TObject): TscriptValue; overload;

  // - преобразовать это в "вариантную запись" - оставляю самым пытливым читателям

  _ItemType_ = TscriptValue;
  _List_Parent_ = TRefcounted;

  {$Include _List_.imp.pas}
 
  TscriptContext = class(_List_)
   public
    function PopInteger: Integer;
    function PopBoolean: Boolean;
    function PopString: String;
    function PopObject: TObject;

    procedure PushInteger(aValue: Integer);
    procedure PushBoolean(aValue: Boolean);
    procedure PushString(aValue: String);
    procedure PushObject(aValue: TObject);
  end;//TscriptContext

implementation

function IsSame(const A: _ItemType_;
  const B: _ItemType_): Boolean;
begin
 Result := A.EQ(B);
end;//IsSame

procedure FreeItem(var thePlace: _ItemType_);
begin
 Finalize(thePlace);
end;//FreeItem
 
procedure FillItem(var thePlace: _ItemType_;
  const aFrom: _ItemType_);
begin
 thePlace := aFrom;
end;//FillItem

{$Include _List_.imp.pas}

function TscriptValue.AsInteger: Integer;
begin
 Assert(rType = script_vtInteger);
 Result := rInteger;
end;

function TscriptValue.AsBoolean: Boolean;
begin
 Assert(rType = script_vtBoolean);
 Result := rBoolean;
end;

function TscriptValue.AsString: String;
begin
 Assert(rType = script_vtString);
 Result := rInteger;
end;

function TscriptValue.AsObject: TObject;
begin
 Assert(rType = script_vtObject);
 Result := rObject;
end;

function TscriptValue.EQ(anOther: TscriptValue): Boolean;
begin
 Result := (rType = anOther.rType);
 if Result then
 begin
  Case rType of
   script_vtInteger:
    Result := Self.AsInteger = anOther.AsInteger;
   script_vtBoolean:
    Result := Self.AsBoolean = anOther.AsBoolean;
   script_vtString:
    Result := Self.AsString = anOther.AsString;
   script_vtObject:
    Result := Self.AsObject = anOther.AsObject;
   else
   begin
    Assert(false);
    Result := false;
   end;//else
  end;//Case rType
 end;//Result
end;

function TscriptValue_C(aValue: Integer): TscriptValue; 
begin
 Result.rType := script_vtInteger;
 Result.rInteger := aValue;
end;

function TscriptValue_C(aValue: Boolean): TscriptValue;
begin
 Result.rType := script_vtBoolean;
 Result.rBoolean := aValue;
end;

function TscriptValue_C(aValue: String): TscriptValue;
begin
 Result.rType := script_vtString;
 Result.rString := aValue;
end;
 
function TscriptValue_C(aValue: TObject): TscriptValue;
begin
 Result.rType := script_vtObject;
 Result.rObject := aValue;
end;

function TscriptContext.PopInteger: Integer;
begin
 Result := Self.Last.AsInteger;
 Delete(Pred(Count));
end;

function TscriptContext.PopBoolean: Boolean;
begin
 Result := Self.Last.AsBoolean;
 Delete(Pred(Count));
end;

function TscriptContext.PopString: String;
begin
 Result := Self.Last.AsInteger;
 Delete(Pred(Count));
end;

function TscriptContext.PopObject: TObject;
begin
 Result := Self.Last.AsObject;
 Delete(Pred(Count));
end;

procedure TscriptContext.PushInteger(aValue: Integer);
begin
 Self.Add(TscriptValue_C(aValue));
end;

procedure TscriptContext.PushBoolean(aValue: Boolean);
begin
 Self.Add(TscriptValue_C(aValue));
end;

procedure TscriptContext.PushString(aValue: String);
begin
 Self.Add(TscriptValue_C(aValue));
end;

procedure TscriptContext.PushObject(aValue: TObject);
begin
 Self.Add(TscriptValue_C(aValue));
end;

Вот как-то так пока...

Замечу лишь, что ещё есть наследник от TscriptContext - TscriptCompileContext:

  TscriptCompileContext = class(TscriptContext)
   property Parser: IscriptParser;
    {* - Текущий парсер. }
   property Code: TscriptCode;
    {* - Текущий компилируемый код. }
  end;//TscriptCompileContext

Он используется в процессе компиляции кода и служит для обеспечения возможности управления процессом разбора кода (парсинга) и собственно построения компилируемого кода.

О нём подробнее я напишу в следующих сериях.

четверг, 7 ноября 2013 г.

Вопрос про "GUI-тестирование "по-русски""

Поскольку тема - ОБШИРНАЯ.

И если она кому-нибудь интересна, то хочется узнать.

В ПЕРВУЮ очередь писать о чём?

О том "как это устроено внутри"? Или концептуально как это делать? Например "как писать тесты в терминах предметной области".

Мне-то как программисту конечно интереснее писать - "как это устроено внутри".

Товарищ написал. "К слову про синтаксический сахар"

К этому http://delphiway.ru/enumerators-%D0%B2-delphi-%D1%87%D0%B0%D1%81%D1%82%D1%8C-2/

 Еще можно объявить enumerator для for-in не как class, а как record с нужными методами и поэкономить "на спичках" за счет выделения память под этот enumerator (ибо компилятор генерит try..finally, в котором enumerator, объявленный как класс создается, с ним что-то делается, а в конце - ему зовется Destroy):

type
  TSomeListEnumerator = record
  private
   f_CurrentIndex: Integer;
   f_List: TSomeList;
   function GetCurrent: TSomeListItem;
  public
   constructor Create(AList: TSomeList);
   function MoveNext: Boolean;
   property Current: TSomeListItem;
  end;

К нему же можно добавить class operator Increment, class operator Decrement и class operator Equal / NotEqual и тогда можно так:

l_Enum := l_List.GetEnumerator;

//в сторону к концу
while (l_Enum <> l_List.EndEnum) do
begin
  ...
  Inc(l_Enum);
end;

//обратно
while (l_Enum <> l_List.StartEnum) do
begin
  ...
  Dec(l_Enum);
end;

Offtopic. Товарищ прислал. В тему в общем-то

"Даня написал клёвый пост. Если подставишь вместо "комиксист" - "программист", а вместо "комикс" - "программу", то, наверное, узнаешь себя: http://blog.dahr.ru/?p=5950 "

ОСТОРОЖНО - нецензурная лексика!