5. Распределенное, параллельное выполнение скриптов. Multitestcase

Назад Содержание Дальше

В силу различных обстоятельств может возникнуть необходимость выполнять некоторые действия параллельно, возможно даже на нескольких машинах или просто для запуска скриптов на удаленной машине. Организация SilkTest-а позволяет это реализовать. Дело в том, что у SilkTest-а есть разделения на т.н. Host machine (НМ) и target machine (ТМ). С НМ запускаются скрипты, активируются базовые настройки. Фактически это машина, на которой открыт SilkTest из которого производится запуск. На ТМ эти скрипты выполняются. На ТМ работает Агент, который принимает инструкции с НМ и выполняет их на той машине, на которой Агент запущен. То есть, как минимум, на ТМ должен быть установлен Агент. Далее мы рассмотрим различные варианты использования такой конфигурации.

5.1. Запуск скрипта на удаленной машине

Это простейший вариант. Запуск на удаленной машине удобно производить в различных случаях, из которых можно выделить следующие:

  • Наличие тестируемого продукта на некоторой удаленной машине при отсутствии SilkTest-а на ней (установлен только Агент).
  • Тестируемый продукт работает лучше на удаленной машине или требуется протестировать именно на той машине. При этом на ней установлен только Агент или SilkTest работает нестабильно.
  • На НМ работает человек, которому помимо запусков скрипта нужно выполнять еще какие-то важные задачи. Поэтому его машина не должна быть полностью занятой во время тестирования.
  • Просто шутки ради. Например сидит за некоторой машиной человек, который занимается всем чем угодно, но не работой и SilkTest с Агентом открыт только для отвода глаз (вроде как он с ними работает). В этом случае можно написать простенький скрипт, который, например, открывает Блокнот и пишет в нем что-то наподобие “На работе бездельничать нехорошо”. Подобный “холодный душ” подействует весьма отрезвляюще на данного работника, а вы получите массу удовольствия и воспитательный момент присутствует.

Это основные случаи, когда нужно запускать скрипты удаленно. Теперь рассмотрим условия, которые нужно обеспечить для такого запуска.

Во-первых, нужная машина должна находиться в сети, иначе вы никак не сможете передавать сигналы на нее.

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

В-третьих, нужно настроить Агента так, чтобы он мог получать команды с другой машины. Для этого нужно выполнить следующие действия:

  1. Активировать окно Агента
  2. Кликнуть правой кнопкой мыши на заголовке и выбрать опцию Network.
  3. В появившемся диалоге нужно выбрать используемый сетевой протокол ( в Network: поле ) и ввести номер порта – комбинацию из 4-х цифр. Сетевой протокол может быть TCP/IP или NetBIOS.
  4. Нажмите ОК.

Агент настроен.

 

В-четвертых, нужно установить соединение между НМ и ТМ. Для этого на НМ в SilkTest-е выбираем меню Options > Runtime В появившемся диалоге ввыбираем в поле Network используемый сетевой протокол. После этого, в поле Agent Name вводим имя машины, которая будет выполнять роль ТМ. Имя вводится в формате <имя_машины>:<номер_порта>. Номер порта должен совпадать с номером порта, который был установлен в Агенте на ТМ. После этого можно закрывать диалог нажатием ОК.

Теперь, если вы запустите скрипт на НМ, то выполнится он на той ТМ, на которую вы настроили. Если вы в скриптах испоьзуете расширения, то нужно позаботиться о том, чтобы на ТМ эти расширения тоже были подключены. Подключение расширений рассматриваются в главе Использование расширений (ActiveX, Java, .NET, Explorer extensions). Если вы работаете с веб-приложениями, то нужно позаботиться еще и о том, чтобы настройки IE DOM расширений ( например уровень распознавания таблиц с нулевой границей, распознавание форм, текста и т.д.) имели необходимые значения.

Подключение НМ к ТМ можно реализовать и програмно. Для этого нужно запустить Агент на ТМ, сделать ему необходимые настройки (см. выше). Допустим наша ТМ называется TEST, а порт Агента установлен в значение 1111. Тогда подключаться к ТМ можно следующим кодом:

Code

	[ ] HMACHINE tm
	[ ] HMACHINE host = GetMachine()
	[ ] 
	[ ] tm = Connect("TEST:1111")
	[ ] SetMachine(tm)
	[ ] 
	[ ] // Enter your code
	[ ] 
	[ ] SetMachine(host)
	[ ] Disconnect(tm)

Рассмотрим этот пример детально. Во-первых, используются переменные типа HMACHINE. Величины данного типа являются дискрипторами. Эти дискрипторы используются в функциях для распределенной работы, а также для извлечения информации о той или иной машине. Диксриптор текущей машины можно получить с помощью функции GetMachine(). В данном примере мы задействовали 2 переменные-дискриптора. И одна из них, host, как раз содержит дискриптор главной машины. Для подсоединения к удаленной машине используется функция Connect, которая принимает параметром строку в формате <имя_машины>:<номер_порта>. Возвращаемое значение этой функции – дискриптор машины, к которой произвелось подключение. Для того, чтобы переключить выполнение скрипта на Агент на удаленной машине, используется функция SetMachine. Её вызов приводит к тому, что дальнейший код будет выполнен на машине, дискриптор которой передан параметром. В данном случае это дискриптор удаленной машины. То есть, дальнейший код, который должен находиться на месте закомментированного текста будет выполнен на машине, дискриптор которой хранится в переменной tm. В данном случае это машина TEST. На ней начнет выполнять инструкции Агент, у которого номер порта 1111. Для разрыва соединения используется функция Disconnect, принимающая также дискриптор машины, от которой нужно отсоединиться.

 

Вот таким образом можно запустить отдельный скрипт на удаленной машине.

5.2. Параллельное выполнение скриптов

Часто могут возникнуть случаи, когда некоторые фрагменты кода должны выполняться параллельно, отдельными потоками. Необходимость в этом может возникнуть в разных ситуациях, например, нужно выполнять какие-то действия до тех пор, пока не выполнятся какие-то внешние условия, которые нужно проверять отдельно. То есть имеется 2 потока, один из которых выполняет требуемые действия, а другой в это время проверяет на выполнение некоторого условия. И если это условие выполнено, то второй поток подает сигнал первому, что надо завершать выполнение.

 

Каждый такой поток в SilkTest-е может быть реализован в spawn-блоке. Этот блок содержит в себе код, который будет выполнен данным потоком. Выглядит генерация нескольких потоков так:

Code

	[+] spawn
		[ ] // Actions for the first thread
	[+] spawn
		[ ] // Actions for the second thread
	[+] spawn
		[ ] // Actions for the third thread

В данном примере код находящийся внутри каждого из spawn-блоков будет выполнен одновременно. Если быть точным, то эти блоки начнут одновременно выполняться. Если же нужно, чтобы код, следующий за данными блоками выполнился только по завершении выполнения всех потоков, то для этих целей используется оператор rendezvous. Соответственно, в коде вида:

Code

	[+] spawn
		[ ] // Actions for the first thread
	[+] spawn
		[ ] // Actions for the second thread
	[+] spawn
		[ ] // Actions for the third thread
	[ ] rendezvous
	[ ] // Other actions

участок кода, на месте которого находится комментарий // Other actions, выполнится только после завершения последнего из вышесгенерированных потоков.

 

Теперь допустим, что у нас есть несколько потоков, каждый из которых заключается в работе некоторой функции. То есть весь код некоторого потока заключается в вызове некоторой функции. В этом случае более удобным и компактным будет использование оператора parallel. Допустим, у нас есть функции MyFunction() и AnotherFunction(), которые нужно выполнять параллельно. Реализуется это так:

Code

	[+] parallel
		[ ] MyFunction()
		[ ] AnotherFunction()

Этот код полностью идентичен следующему:

Code

	[+] spawn
		[ ] MyFunction()
	[+] spawn
		[ ] AnotherFunction()
	[ ] rendezvous

Как видно из 2-х последних примеров, использование parallel делает код более компактным.

 

А теперь представим ситуацию, когда есть несколько потоков и один из них является управляющим. Пока все потоки выполняют определенные действия, управляющий поток следит за системой и координирует действия других потоков. Соответственно, если данный поток вдруг должен быть остановлен, то другие потоки тоже должны остановить выполнение независимо от того, на какой стадии выполнения они находились. Исключение генерировать в данном случае нежелательно, так как это может иметь непредсказуемые последствия. В этом случае такой управляющий поток реализуется в critical-блоке. Пример реализации:

Code

	[+] spawn
		[ ] MyFunction()
	[+] critical
		[ ] AnotherFunction()
	[ ] rendezvous

Следует отметить, что в любой момент выполнения скрипта должно выполняться не более одного critical-блока.

 

Теперь следует обратить внимание на работу с переменными при параллельном выполнении потоков. В следующем примере

Code

	[ ] INTEGER iValue
	[ ]
	[+] spawn
		[ ] iValue = 5
	[+] spawn
		[ ] iValue = 3
	[ ] rendezvous

локальная переменная iValue будет иметь значение 5 в одном потоке и значение 3 в другом потоке. При этом она будет иметь разные значения одновременно. По-крайней мере это можно воспринимать именно так, поскольку изменение данной переменной в одном потоке никак не сказывается на значение этой переменной в другом потоке. Например, такой код:

Code

	[ ] INTEGER iValue=2
	[ ] 
	[+] spawn
		[ ] iValue = 5
	[+] spawn
		[ ] iValue++
	[ ] Print( iValue )
	[ ] rendezvous

В файл результата все-равно введется значение 2, хотя казалось бы оно менялось в spawn-блоках. А что если нужно в каждом потоке обращаться к некоторой переменной, значение которой может быть доступно другому потоку? Или, что делать, если нужно внутри каждого потока занести некоторые результаты в переменную ( список, например ), чтобы потом можно было получить их значения вне потоков? То есть нужна переменная, значение которой было бы доступно после выполнения потоков. Обычная глобальная переменная в этом не поможет, так как может возникнуть ситуация, когда несколько потоков одновременно могут попытаться изменить значение этой переменной. Поэтому нужна переменная, которая бы требовала дополнительно доступ к своему содержимому. Для таких целей служит ключевое слово share. Оно применяется к внешним переменным. Особенностью переменных с таким модификатором является то, что к ним нужно открывать доступ с помощью ключевого слова access. Ключевое слово access формирует блок, внутри которого возможен доступ к переменной. Пример использования:

Code

[ ] share INTEGER iValue=2
[ ] 
[+] main()
	[ ] 
	[ ] 
	[+] spawn
		[+] access iValue
			[ ] iValue = 5
	[+] spawn
		[+] access iValue
			[ ] iValue++
	[ ] rendezvous
	[+] access iValue
		[ ] Print( iValue )

В результате работы данного скрипта в файл результатов запишется значение 6. То есть переменная iValue вначале была изменена первым потоком, а затем вторым. То есть каждый из потоков смог изменить значение внешней переменной. Обратите внимание, что если access-блок содержит в себе spawn-блоки, то внутри последних доступа к переменной уже не будет. Вот в этом примере:

Code

[ ] share INTEGER iValue=2
[ ] 
[+] main()
	[+] access iValue
		[+] spawn
			[ ] iValue = 5
		[+] spawn
			[ ] iValue++
		[ ] rendezvous
		[ ] Print( iValue )
	[ ] 
	[ ] return

переменная iValue после завершения работы потоков все равно останется равной 2.

5.3. Параллельное выполнение скриптов на нескольких машинах

А теперь рассмотрим комбинированный вариант, когда нужно запускать тесткейс на нескольких машинах, причем на каждой из машин он должен выполняться параллельно. В этом случае нужно, чтобы на всех машинах, на которых будет работать скрипт, был запущен и настроен Агент. Это естественно. Теперь разобъем условно весь скрипт на секции:

  • Подключение
  • Выполнение
  • Отключение

Этап подключения как правило имеет вид:

Code

	[+] LIST OF STRING lsMachines = {...}
		[ ] // Enum machine names
	[ ] STRING sMachine
	.................................
	[+] for each sMachine in lsMachines
		[+] do
			[+] if( !IsConnected(sMachine) )
				[ ] Connect( sMachine )
		[+] except
			[ ] ExceptPrint()
			[ ] // Add exception handling here

В данном примере мы для каждой машины из списка lsMachines проверяем наличие подключения ( функция IsConnected) и если его нет, то подключаемся. На этом этапе желательно перехватывать исключения, которые как правило сгенерируются в случае невозможности подключиться к той или иной машине. Такая ситуация может быть критичной, а может и нет. Например, если к машине не удалось установить подключение, то эту машину можно изъять из списка машин. Также на данном этапе можно установить подключение к некоторой главной машине. Это полезно в том случае, если скрипт ведет вывод промежуточных результатов или просто делает запись в некоторый файл. Чтобы не разбрасывать их по всем машинам, лучше эти файлы складывать в одном месте на одной машине. Поэтому нужно обеспечить доступ к главной машине отдельно.

 

Этап выполнения характеризуется наличием spawn-блоков. Если на всех машинах выполняется один и тот же сценарий (например для проведения стрессового тестирования) , то эти spawn-блоки можно реализовать в цикле. Более того, сам сценарий удобно реализовать отдельной функцией. Но это уже вопрос конкретной реализации. Итак, в каждом из spawn-блоков в первую очередь нужно переключиться на нужную машину при помощи функции SetMachine и только после этого выполнять действия. Функция SetMachine может принимать не только величину типа HMACHINE, но и STRING. Допустим в переменной sMachine хранится имя машины, на которой будет запущен очередной поток. Тогда код имеет вид:

Code

	[+] spawn
		[+] do
			[ ] SetMachine(sMachine)
			[ ] // Add code here
		[+] except
			[ ] ExceptPrint()
			[ ] // Add exception handling here

Это примерный каркасс, которого имеет смысл придерживаться.

 

Этап отключения представляет собой цикл, в котором происходит отсоединение от удаленных Агентов. Этот этап имеет вид:

Code

	[+] for each sMachine in lsMachines
		[+] do
			[+] if( IsConnected(sMachine) )
				[ ] Disconnect( sMachine )
		[+] except
			[ ] ExceptPrint()
			[ ] // Add exception handling here

или еще проще

Code

	[ ] DisconnectAll()

Это были основные моменты. Теперь рассмотрим более специфические случаи. Допустим нам нужно реализовать сценарий, по которому на разных машинах выполняются разные действия, причем начальные состояния тоже различаются. Реализацию можно построить так:

  1. Приведение системы в начальное состояние можно реализовать в appstate, который перебирает все машины и выполняет действия, необходимые на каждой из них. При этом подключение/отключение к машинам реализовать в этом appstate.
  2. Параллельные действия, выполняемые на каждой машине реализуются в отдельном spawn-блоке.

Это вполне приемлемо и будет работать, но вот такое использование appstate несколько неудобно, так как список машин нужно резервировать как внешнюю переменную. Более того, если приведение в начальное состояние на каждой из машин соответствует некоторому appstate ( то есть на каждой машине нужно запустить определенный appstate), то имеет смысл поискать более удобный способ, заключающийся просто в передаче имен appstate. Для такого сложного взаимодействия между тесткейсом и appstate при распределенном выполнении скриптов существует специальный вид тесткейсов, т.н. multitestcase.

 

Multitestcase – (в дальнейшем “мультитесткейс”) это модификатор функции, который определяет, что данная функция не возвращает значения и при этом принимает агрумент типа LIST OF STRING. По своему применению такие функции аналогичны testcase-функциям. Мультитесткейс тоже не может вызывать другой тесткейс и не может быть вызван тесткейсом, но может быть вызван обычной функцией. Но мультитесткейс отличается от тесткейса (помимо обязательного аргумента списка строк) тем, что к мультитесткейсу не привязывается appstate. Вполне логично, так как мультитесткейс соответствуем множеству в общем случае различных тесткейсов работающих распределенно и у каждого такого тесткейса свое начальное состояние. Но эта недостача компенсируется другими средствами. В частности, для привязки некоторого appstate к сценарию, который будет работать на конкретной машине, используется функция SetUpMachine. Эта функция подключается к некоторой ТМ и привязывает к ней appstate, который будет запущен на данной машине. Общий вид функции:
SetUpMachine( sMachine , wMainWin , STRING sAppstateName ) , где
sMachine – имя машины, с которой ассоциируется данный appstate;
wMainWin – главное окно приложения на данной машине
sAppstateName – имя appstate Но эта функция только ассоциирует appstate, но не запускает. Чтобы запустить параллельно эти все appstate на соответствующих машинах, нужно использовать функцию SetMultiAppStates(). Таким образом, каркасс мультитесткейса выглядит следующим образом:

Code

[+] multitestcase MyMultiTestCase( LIST OF STRING lsMachines )
	[ ] 
	[ ] SetupMachine(lsMachines[1],NULL,"apsFirstAppstate")
	[ ] SetupMachine(lsMachines[2],NULL,"apsSecondAppstate")
	.......................................................
	[ ] 
	[ ] SetMultiAppStates()
	[ ] 
	[+] spawn
		[ ] SetMachine(lsMachines[1])
		[ ] // Code to run on first machine
	[+] spawn
		[ ] SetMachine(lsMachines[2])
		[ ] // Code to run on second machine
	.......................................................
	[ ] rendezvous
	[ ] 
	[ ] DisconnectAll()

Это общий вид. Естественно apsFirstAppstate и apsSecondAppstate – это существующие appstate. Если на всех машинах работает один сценарий и appstate у всех один и тот же, то тогда каркасс имеет вид:

Code

[+] multitestcase MyMultiTestCase( LIST OF STRING lsMachines )
	[ ] STRING sMachine
	[ ] 
	[+] for each sMachine in lsMachines
		[ ] SetupMachine(sMachine,NULL,"apsAppstate")
	[ ] 
	[ ] SetMultiAppStates()
	[ ] 
	[+] for each sMachine in lsMachines
		[+] spawn
			[ ] SetMachine(sMachine)
			[ ] // Code to run
	[ ] rendezvous
	[ ] 
	[ ] DisconnectAll()

Вариаций может быть много, но это основные случаи.

5.4. Выводы

Таким образом:

  1. Запуск скриптов на удаленной машине может быть осуществлен путем настройки опций или програмным путем
  2. Програмно распараллеливание потоков осуществляется при помощи ключевых слов spawn, parallel.
  3. Ключевое слово rendezvous позволяет продолжить выполнение скрипта только после того, как завершатся все созданные потоки.
  4. При распараллеливании потоков переменные при завершении выполнения потоков принимают значение до начала выполнения потока. То есть поток только во время своей работы может использовать измененные значения, но эти значения не сохраняются при завершении работы потока.
  5. Ключевое слово share применяется для внешних переменных и позволяет им открывать доступ к своему значению (в том числе и для изменения) внутри потоков. Ключевое слово access непосредственно открывает доступ к значению переменной.
  6. multitestcase – разновидность тесткейса, предоставляющая интерфейс для параллельного запуска скрипта на нескольких машинах.

Назад Содержание Дальше