TestComplete: ожидание окончания активности приложения без использования TestComplete

В этой статье я рассмотрю случай, когда стандартные методы TestComplete WaitProcess и WaitProperty не подходят из-за особенностей архитектуры приложения и необходимо найти другой способ определения, когда тестируемое приложение запустилось и готово к работе.

Наше тестируемое приложение запускается немного специфично: окно приложения становится доступным пользователю, однако после этого в течение ещё некоторого времени оно подгружает данные с сервера, при этом обновляя значения элементов управления. В зависимости от текущей скорости сети и нагрузки на сервер базы данных, такое подгружение данных может занимать от 10 секунд до нескольких минут. Если в это время пытаться получать значения свойств элементов с помощью TestComplete-овских стандартных методов типа WaitProperty, иногда это приводит к тому, что тестируемое приложение попросту крешится. Как же дожидаться, пока приложение выполнит все фоновые операции и будет готово к работе?

Самый простой способ — дождаться, пока потребляемое процессом процессорное время не снизится до нуля. Для этого мы можем в цикле проверять значение свойства CPUUsage нужного нам процесса. Однако даже простое обращение к объекту процесса (Sys.Process(…)) иногда приводит к крешу приложения, поэтому необходимо использовать другой подход: проверять CPUUsage извне TestComplete, т.е. используя возможности операционной системы.

Для этого можно воспользоваться объектом WMI (Windows Management Instrumentation) — технологии для централизованного управления и слежения за работой различных частей компьютерной инфраструктуры под управлением Windows. Мы напишем 2 функции: первая будет ожидать появления процесса с помощью WMI-объекта Win32_Process, вторая с помощью объекта Win32_PerfRawData_PerfProc_Process будет дожидаться, пока используемое процессом процессорное время не снизится до нуля. Причем для второй функции нам недостаточно просто дождаться, пока CPU Usage станет равным нулю, нужно чтобы процесс бездействовал в течение некоторого времени.

Вот как будет выглядеть первая функция, ожидающая появления процесса:

function waitProcessStart(procName, maxWait){
  maxWait = maxWait || 10000;
  var wmiObj, dataset;
  var timer = HISUtils.StopWatch;
  timer.Start(); 
 
  var message = 'Waiting ' + maxWait/1000 + ' seconds for process ' + procName + ' to appear';
  Log.Message(message);
  
  do {
    wmiObj = GetObject("WinMgmts:{impersonationLevel=impersonate}!\\\\localhost\\root\\cimv2");
    dataset = wmiObj.ExecQuery("Select * from Win32_Process WHERE Name='" + procName + ".exe'");
 
    if(timer.Split()> maxWait) {
      Log.Warning("Maximum timeout reached, process didn't start");
      timer.Stop();
      return false;
    }
    
    if(dataset.Count> 1) {
      Log.Message('Only 1 existing process is supported, found: ' + dataset.Count);
      timer.Stop();
      return false;
    }
    else if(dataset.Count == 1) {
      Log.Message('Process ' + procName + ' appeared');
      break;      
    }
    else { /* no process found */
      var secondsLeft = maxWait/1000 - Math.round(timer.Split()/1000);
      Delay(1000, message + ' (' + secondsLeft + ' seconds left)');
    }
  }
  while(true);
  
  timer.Stop();
  return true;
}

Здесь мы передаём в качестве параметра имя процесса (без расширения) и максимальный таймаут ожидания. Для удобства в Индикаторе отображается оставшееся время до окончания ожидания:

Теперь напишем функцию, ожидающую, пока процесс прекратит любые действия:

function waitProcessIdle(procName, maxWait, numberOfRetries){
  maxWait = maxWait || 10000;
  numberOfRetries = numberOfRetries || 3;
  var tries = numberOfRetries; 
  var cpuUsage, wmiObj, dataset;
  var timer = HISUtils.StopWatch;
  timer.Start(); 
 
  var message = 'Waiting for process ' + procName + ' to idle';
  Log.Message(message);

  do {
    if(tries == 0) {
      timer.Stop();
      return true;
    }
  
    wmiObj = GetObject("WinMgmts:{impersonationLevel=impersonate}!\\\\localhost\\root\\cimv2");
    dataset = wmiObj.ExecQuery("SELECT * FROM Win32_PerfRawData_PerfProc_Process WHERE Name='" + procName + "'");
    var procs = new Enumerator(dataset);
    procs.moveFirst();
    var item = procs.item();
    var p1 = procs.item().PercentProcessorTime;
    var n1 = procs.item().Timestamp_Sys100NS;
    
    var secondsLeft = maxWait/1000 - Math.round(timer.Split()/1000);
    Delay(1000, message + ' (' + tries + ' retries and ' + secondsLeft + ' seconds left)');
    
    dataset = wmiObj.ExecQuery("SELECT * FROM Win32_PerfRawData_PerfProc_Process WHERE Name='" + procName + "'");
    procs = new Enumerator(dataset);
    procs.moveFirst();
    
    item = procs.item();
    var p2 = procs.item().PercentProcessorTime;
    var n2 = procs.item().Timestamp_Sys100NS;
    var p = p2 - p1;
    var n = n2 - n1;
    
    cpuUsage = 100*p/n.toFixed(2);
    
    if(cpuUsage == 0) {
      tries--;
      continue;
    }
    if(cpuUsage> 0) {
      tries = numberOfRetries;
    }
    if(timer.Split()> maxWait) {
      Log.Warning('Maximum timeout reached');
      timer.Stop();
      return false;
    }
  }
  while(true);
}

В этой функции есть дополнительный параметр numberOfRetries — количество попыток проверки бездействия процесса, которые должны успешно завершиться подряд. Для удобства эта функция выводит в Индикатор оставшееся время и количество оставшихся попыток:

Чтобы проверить работоспособность кода, достаточно запустить вот такую функцию:

function Test1(){
  Log.Message(waitProcessStart('notepad'));
  Log.Message(waitProcessIdle('notepad'));
}

После её запуска откройте Блокнот и начните набирать в нём любой текст. Пока вы это делаете, TestComplete будет выводить в Индикатор информацию о состоянии ожидания. Если вы закончите ввод, то примерно через 3 секунды функция остановится.

Ссылки по теме:

http://www.tech-archive.net/Archive/Development/microsoft.public.win32.programmer.wmi/2005-09/msg00156.html
http://techsupt.winbatch.com/webcgi/webbatch.exe?techsupt/nftechsupt.web+WinBatch/WMI/User~Sample~Code+Get~CPU~Usage~of~a~Process.txt
https://www.petri.com/powershell-problem-solver-process-cpu-utilization
https://docs.microsoft.com/en-us/scripting/javascript/reference/enumerator-object-javascript