INTELLIGENT WORK FORUMS
FOR COMPUTER PROFESSIONALS

Log In

Come Join Us!

Are you a
Computer / IT professional?
Join Tek-Tips Forums!
  • Talk With Other Members
  • Be Notified Of Responses
    To Your Posts
  • Keyword Search
  • One-Click Access To Your
    Favorite Forums
  • Automated Signatures
    On Your Posts
  • Best Of All, It's Free!

*Tek-Tips's functionality depends on members receiving e-mail. By joining you are opting in to receive e-mail.

Posting Guidelines

Promoting, selling, recruiting, coursework and thesis posting is forbidden.

Jobs

Powershell test of applications on PC image

Powershell test of applications on PC image

(OP)
Synopsis
========
- test-apps.ps1 is used to verify applications are correctly installed on a refurbished PC with a new RPK image before the PC is deployed to the end customer
- tests cover network link, web browsers, notepad, wordpad, MS Office, Open Office, etc
- missing / inoperative drivers are detected
- laptop batteries are exercised and an estimate of the usable battery life is provided
- a detailed html logfile shows test results and supporting screenshots as the tests progress
- test results files are optionally copied to a central server
- there is a summary file to facilitate integration of results into inventory control systems

Notes
=====
- you need a copy of wasp.dll from: http://wasp.codeplex.com/releases/view/22118
- put wasp.dll in the same directory as the .ps1 file
- if you are deploying this script as part of a PC image, it is best to turn on
powershell scripts in the registry via the supplied .reg file
- if you are using the default windows user account control setting, then you will need to open the powershell window by right clicking and selecting the Run as Administrator option
- if the script does not have administrator privileges, some of the tests will fail, the error messages will flag the administrator privilege issue
- if you have changed the user account control setting to be more permissive, there is a .bat file to run the tests from windows explorer
- if you uncomment line 34 of the .bat file, it will also update the registry entry needed to enable powershell scripts
- for more details and options, in a powershell window, type: ./test-apps.ps1 -h

CODE --> powershell

# Written by John Brearley, Fall 2013
# email: brearley@bell.net
# email: jrbrearley4@gmail.com

# License: This script is free to use, modify and/or redistribute,
# however you MUST leave the author credit information above as is
# and intact.

# Support: Available on a best effort basis.

# Acknowlegement: Thanks to CompuCorps.org for the loan of a PC 
# with MS Office 2013 and for beta testing.


# For online help, in a powershell window, type: test-apps.ps1 -h


#==================== script run control file =====================
# This script supports an optional run control file called
# test-apps-rc.ps1. If this file is found, it will be executed so 
# that different settings can be used on selected PC as specified 
# in the run control file.
#
# For example, some PC may not have MS Office installed, and you
# want the MS Office tests to be skipped. You can also specify 
# different try counts, iteration counts, colors, etc in the run
# control file. Everything in the User Defined Variables section
# just below here is fair game. 
#
# NB: While you probably could manipulate other variables elsewhere
# in the script, please DONT!
#==================================================================


#==================== User Defined Variables ======================
# User may need to update these variables depending on local
# configuration & preferences. 
#==================================================================

# When the tests are completed, the results files will be copied to 
# the server directory defined below. Put your own value here or in
# the run control file discussed above, and uncomment the line.
# $script:server_results_dir = "\\192.168.0.100\test_results"

# WinXP & Linux servers will allow you to define shared R/W directories
# that are accessible by all users without any username/password
# challenge. If you are using a server that requires authentication
# before you can access the desired results directory, then put the
# required domain\user and password info here, or in the run control
# file discussed above, and uncomment these lines.
# $script:server_username = "user" # you main need to put: domain\username
# $script:server_password = "password"

# Number of times a test case will be tried before being considered
# a test case failure. Can be dynamically modified via command line
# option: -try N
$script:testcase_try_max = 3  # minimum 1

# Number of times to run all the test cases. Can be dynamically 
# modified via command line option: -iter M
$script:testsuite_interation_max = 1  # minimum 1

# The tests to be run (or not run) can be chosen by the groupings
# shown below. There are command line options to dynamically control
# all these options. The value should be $true or $false
$script:test_batt = $true           # Laptop battery test, user MUST unplug/replug AC power
$script:test_config = $true         # CPU, RAM, HDD, Windows activation, missing driver tests
$script:test_ms_office = $true      # MS Office tests
$script:test_ms_sec_cl = $true      # MS Security Client test
$script:test_network = $true        # ping, clock sync, web browsers (Chrome, Firefox, IE) tests
$script:test_open_office = $true    # Open Office tests
$script:test_other = $true          # notepad, wordpad, screenshot tests

# If you want to run only a specific list of test cases, put your own list
# here or in the run control file discussed above, and uncomment the line.
# $script:tc_list = "tc_4 tc_6 tc_7"

# Minimum amount of RAM (Random Access Memory), in GB, that must be installed.
$script:ram_minimum_gb = 2 # in GB 

# Minimum number of CPU (or threads) that must be installed. This is helpful
# in weeding out the really old low power CPU PC.
$script:cpu_minimum = 2

# Minimum amount of HDD (Hard Disk Drive), in GB, that must be installed.
# Multiple HDD will be totaled up to check the minimum requirement.
# NB: The value here should be the software view of a GB, namely 1024 * 1024 * 1024,
# not the hardware view of a GB, namely 1000 * 1000 * 1000.
# NB: A hardware 80 GB disk will be only 74 GB from software point of view.
$script:hdd_minimum_gb = 74 # in GB 

# Web site used for ping tests.
$script:ping_host = "google.ca"

# Number of pings that the ping test will do.
$script:ping_cnt = 5

# Percentage of pings that must succeed for the ping test to be considered a pass.
$script:ping_pass_percent = 60 # %, range 1 - 100

# WIFI network links may need several cycles of sacrificial pings to 
# initially warmup and get going properly under adverse RSSI conditions.
# In each warmup cycle, $script:ping_cnt pings will be done to see what
# shape the WIFI link is in. If the pings all fail, another warmup cycle
# will be done. These tests continue until the WIFI link is working 
# properly, or we hit the maximum number of warmup cycles, below. 
$script:wifi_warmup_cycles_max = 5

# Web site used for browser tests.
$script:web_host = "webcrawler.com"

# Pattern used to validate page contents for above web_host.
$script:web_page_pattern = "title.*webcrawler.*web.*searchtitle"

# To test MS Outlook, the preferred way is to have a dummy email server
# available with a valid email address & password that can be used
# by this script. For a dummy email server, I use the free hMailServer
# from: http://www.hmailserver.com/index.php?page=download
#
# The dummy email server is expected to become a permanent part
# of the place you are testing PC apps. You could run the hMailServer
# on your own desktop PC, or you could run it on a separate PC
# dedicated just for this task.
#
# After you run the setup.exe file, all you need to do is configure
# a Domain name and one email Account, as described below:
#
# 1) On the hMailServer administrator window, click on the Domains
#    icon. Now you can type in the desired Domain name & save it.
#    The suggested domain name is: xyz123abc.ca
#
# 2) Now click on the "+" sign beside the Domain icon to expand it.
#    Now click on the "+" sign for the domain name you just created
#    to expand it.
#
# 3) On the Accounts folder icon, right click and choose Add...
#    Now you can type in the new email address, password and save it.
#    The suggested email address is: webmaster
#    The suggested password is: 1234abcd
#
# 4) Thats it, you are done!
#
# This takes all of 5 minutes for a novice user to set up hMailServer.
#
# NB: If Windows Firewall is active, it will block incoming mail 
# connections from other PC, causing the test case to fail. So, you
# will need to tell Windows Firewall to allow traffic on ports 
# 25 & 110. If you are running other anti-virus software, you could
# just turn off Windows Firewall completely.
#
# NB: DONT use a real corporate email server for this test!!!
# If you do, then you need to worry about people finding the 
# username and password in this script (or the run control file)
# and possibly abusing this email account, which is definitely
# NOT a desired outcome! So use the dummy email server described
# above or dont run the Outlook test.
#
# Define info for setting up email account to use with dummy email server.
$script:email_display = "Web Master"
$script:email_address = "webmaster@xyz123abc.ca"
$script:email_password = "1234abcd"
$script:email_needs_authentication = $true
$script:email_server_name = "mail.xyz123abc.ca"
# $script:email_server_ip = "192.168.0.100"
#
# NB: The MS Outlook test case tc_15 will not run without the email
# server ip address information. Once you have setup the dummy
# email server, put your own valu here and uncomment the line above,
# or put a line in the run control file discussed above,
#
# NB: The above email server name & IP are used to create a
# mapping in the local PC hosts file so that it can find the
# dummy email server on your local area network. You dont 
# need to worry about your DNS server knowing how to find
# the dummy email server. The dummy email server PC networking
# software does not even need to be given any knowledge of the
# domain name being used for email purposes. 
#
# On occasion, the email server may be running slowly, and the
# test email that the script sends is NOT received the first
# time the script does "F9" to get the new email. The option
# below lets you specify how many times the script will do "F9"
# to get new email.
$script:email_get_mail_cnt = 1

# Define allowable age, in hours, for Microsoft Security Client
# virus & scans. The virus definition age & most recent scan age
# must be less or equal to this value for the test to pass.
$script:ms_sec_age = 24 # in hours

# Define the lower battery life threshold for running the battery discharge
# test. If the PC battery life is less than this value, the battery discharge
# test will not be run. Its possible that the battery may not be fully
# charged, or if it is fully charged, that the battery is old and lost a lot
# of its original capacity. In either case, we dont want to run the battery
# discharge test, as there may not be enough battery life to complete the
# test.
$script:battery_life_threshold = 30 # %, range 1 - 100

# Define how long the battery test must discharge the battery, in minutes,
# in order to get a credible estimate of the battery life.
$script:battery_test_discharge_min = 7 # minutes

# Define the expected minimum battery life, in minutes, for the battery
# discharge test to be considered a PASS.
$script:battery_life_pass_min = 45 # minutes

# Alternate directory to find wasp.dll, in case it is not in the
# current working directory.
$script:wasp_dir="c:\wasp"

# Preferred web browser for displaying test case results page.
# Chrome, Firefox & IE all work OK.
$script:browser_results = "firefox"

# If you dont want the test case results automatically displayed
# in the preferred web browser, then uncomment the line below, or 
# add a line in the run control file discussed above.
# $script:nobr = $true

# Colors to be used in test case results page.
$script:color_background = "white"    # page backgound color
$script:color_info = "blue"           # normal, basic text color
$script:color_bold = "cadetblue"      # bold text color
$script:color_warning = "gold"        # warning text color
$script:color_error = "magenta"       # error text color
$script:color_fatal = "purple"        # fatal error text color
$script:color_tc_pending = "orange"   # test case pending result text color
$script:color_tc_fail = "red"         # test case fail result text color
$script:color_tc_pass = "green"       # test case pass result text color
$script:color_tc_skip = "brown"       # test case skipped text color
$script:color_iteration = "lime"      # test iteration page marker text color

# Size for individual window screenshots on test results html page.
# This ensures the individual screenshots fit into the available
# viewing space on ther results page.
$script:screenshot_window_width =  400  # in pixels
$script:screenshot_window_height = 300  # in pixels

# Scale factor for full desktop screenshots on test results html page.
# This ensures the full desktop screenshots fit into the available
# viewing space on the results page.
$script:screenshot_full_scale_factor = 0.5   # range is 0 to 1

# Define windows to ignore.
# I often have the CBC Radio running in the background.
$script:ignore_list = "CBC.*RADIO"


#==================== End of User Options =========================
# Dont change anything below here!!!
# Unless you are a Powershell expert!!! Or are planning to be one!!!
#==================================================================


#==================== Powershell Notes ============================
# Major powershell learning points here!!! 
#==================================================================
# Powershell uses the backtick as an escape, not the backslash!
# You get parsing errors if you try \"

# When a function returns, any stdout messages are returned as part
# of the results array. Being able to have debug info sent to the
# terminal window is a fundamental debugging technique. To extract
# the result at the end of all the debug messages, use:
# $result = myfunc $p1 $p2 $p3
# $rc=$($result[$result.count-1])
# The other approach is to use script level variables to pass
# selected results from the function to the calling routine.
# This avoids having to filter out the debug messages.

# While powershell itself seems able to set the return status $?,
# the powershell user community does not seem to be able to 
# figure out how to do that from within a function or cmdlet.

# Watch out for the .count method. In many places, it will return
# null value when there is 0 or 1 item. It will correctly return
# correct count for 2 or more items. It will also count null items.
# In places where you need accurate count of non-null items, you
# need to do your own testing of data and counting.


#==================== Common Functions ============================
# Common routines used in this script.
#==================================================================


#==================== add_file_content ============================
# Adds value string to the specified path. Provides error handing
# and a backoff / retry algorithm when errors writing to a file occur.
#
# Under load with larger log files, you get occasionlly errors about
# object in use by another process, such as firefox. Backing off and
# trying again usually allows the situation to recover.
#
# Calling parameters: path, value
# There are 2 logfiles, so we do need to be able to specify the path
#
# Returns: null or exits script on unrecoverable error
# Sets variables: $script:add_content_issue, $script:add_content_max_delay
#==================================================================
function add_file_content ($path="", $value="") {

   # There can be cases where the logfile is not yet initialized, 
   # so we add these messages to the queue for logging later on.
   # "add_file_content path=$path value=$value"
   if ($path -eq "") {
      $script:msg_q += $value
      return
   }

   # Define delays to use in backoff / retry algorithm, in milli-seconds
   # First delay is 0, so that the normal case logging is not impacted.
   $delay_array = 0, 100, 200, 500, 1000, 2000, 5000, 10000, 15000, 30000, 60000, 120000, 180000, 240000, 300000, 300000, 300000, 300000, 300000, 300000, 300000, 300000, 300000, 300000 # milliseconds

   # Try repeatedly to add content to file.
   $error_list = "" # keep track of all errors encountered, used when we DONT recover.
   $last_error = "" # keep track of last error, used when we recover OK.
   foreach ($delay in $delay_array) {

      # Wait before adding content.
      # "add_file_content delay=$delay"
      start-sleep -m $delay

      # Try to add content to file.
      $err = ""
      $pm = ""
      $saved_rc = $False
      try {
         add-content -path "$path" -value "$value"
         $saved_rc = $?
      } catch {
         $x = $Error | select-object -first 1 # Never seem to get error text, even though it is available at command prompt
         $err = $x.tostring()
         $pm = $x.invocationinfo.positionmessage # Shows which line in script got the error!!!
         $err = "$err $pm"
         $saved_rc = $False
      }

      # Test code to cause retries.
      # NB: This will cause a message to show up repeatedly in the logfile.
      # NB: For retries during finalize_logfile, this can cause the tc stats
      #     table to show up repeatedly! This was a very interesting side effect!
      # if ($script:add_content_issue -eq 0 -and $delay -lt 200) {
      #     $saved_rc = $false
      #     $err = "faking error..."
      #     "`n`n++++++++ add_file_content faking error for path=$path delay=$delay err=$err value=$value`n`n"
      # }

      # If we succeeded, exit loop.
      if ($saved_rc) {
         break

      } else {
         # Accumulate all errors.
         $error_list = "${error_list}delay=$delay saved_rc=$saved_rc err=$err`n"
         # "delay=$delay err=$err"
         if ($err -ne "") {
            $last_error = "delay=$delay saved_rc=$saved_rc err=$err"
         }
      }
   }

   # Look for errors.
   # "add_file_content saved_rc=$saved_rc"
   if ($saved_rc) {
      # We succeeded, but did we have to retry?
      if ($delay -gt 0) {
         $script:add_content_issue++
         if ($delay -gt $script:add_content_max_delay) {
            $script:add_content_max_delay = $delay
         }
         # Since we have recovered from the transient error, it SHOULD
         # be safe to log a warning about what just happened.
         # We show only the last delay & error to avoid log clutter.
         log_debug "add_file_content succeeded on delay=$delay last_error: $last_error"
         return
      }

   } else {
      # All logging retries have failed. Its game over!
      # We can always put the errors on the terminal window.
      "`nFATAL ERROR: add_file_content path=$path `nvalue=$value `nerror_list:`n$error_list`n"
      exit 1
   }
}


#==================== add_horizontal_rule =========================
# Adds a horizontal rule and surrounding whitespace to the logfile.
#
# Calling parameters: none
# Returns: null
#==================================================================
function add_horizontal_rule {

   # Add horizontal rule tags to logfile.
   log_msg "`n`n`n<br><hr color=`"$script:color_info`">"
}


#==================== add_html_header =============================
# Adds starting html tags to the logfile.
#
# Calling parameters: filepath
# Returns: null
#==================================================================
function add_html_header ($filepath) {

   # Add HTML header tags to logfile.
   $msg = ""
   $saved_rc = $False
   try {
      # NB: We dont dont use the add_file_content routine here while
      # trying to initialize the file. If this fails, its game over.
      add-content -path "$filepath" -value "<!DOCTYPE html>"
      add-content -path "$filepath" -value "<HTML>"
      add-content -path "$filepath" -value "<HEAD>"
      add-content -path "$filepath" -value "   <TITLE>$script:log_suffix</TITLE>"
      add-content -path "$filepath" -value "</HEAD>"
      add-content -path "$filepath" -value " "
      add-content -path "$filepath" -value "<BODY bgcolor=`"$script:color_background`">"
      add-content -path "$filepath" -value " "
      $saved_rc = $?
   } catch {
      $msg = $Error | select-object -first 1
      $saved_rc = $False
   }
   # "saved_rc=$saved_rc"

   # Check for errors.
   if(!$saved_rc) {
      log_fatal_error "add_html_header got: $msg"
   }
}


#==================== add_html_trailer ============================
# Adds ending html tags to the logfile.
#
# Calling parameters: filepath
# Returns: null
#==================================================================
function add_html_trailer ($filepath) {

   # Add HTML trailer tags to the logfile.
   $msg = ""
   $saved_rc = $False
   try {
      # NB: We dont dont use the add_file_content routine here while
      # trying to initialize the file. If this fails, its game over.
      add-content -path "$filepath" -value ""
      add-content -path "$filepath" -value "</BODY>"
      add-content -path "$filepath" -value "</HTML>"
      $saved_rc = $?
   } catch {
      $msg = $Error | select-object -first 1
      $saved_rc = $False
   }
   # "saved_rc=$saved_rc"

   # Check for errors.
   if(!$saved_rc) {
      log_fatal_error "add_html_trailer got: $msg"
   }
}


#==================== check_email_routing =========================
# Checks local PC host file for route entry for dummy email server,
# adds entry if needed.
#
# Calling parameters: none
# Returns: null
#==================================================================
function check_email_routing {

   # Define path to local PC host file.
   $hosts_file = "$env:SystemRoot\system32\drivers\etc\hosts"
   # log_debug "check_email_routing hosts_file=$hosts_file"

   # Read hosts file.
   get_file_contents $hosts_file "" -nolog

   # Check if the hosts file already has email server name & IP.
   $patt = "${script:email_server_ip}\s+${script:email_server_name}"
   # "patt=$patt"
   foreach ($l in $script:file_contents) {
      # "l=$l"
      if ($l -match "$patt") {
         log_info "check_email_routing patt=$patt MATCH l=$l"
         return
      }
   }

   # Add routing entry for email server.
   log_bold "check_email_routing adding routing entry to: $hosts_file"
   $msg = ""
   $saved_rc = $false
   try {
      add-content -path "$hosts_file" -value "$script:email_server_ip   $script:email_server_name `n`n"
      $saved_rc = $?
   } catch {
      $saved_rc = $false
   }

   # Check for errors.
   # "saved_rc=$saved_rc"
   if (!$saved_rc) {
      $msg = $error | select-object -first 1
      throw_error "NB: You MUST run as administrator to update ${hosts_file}, msg=$msg"
   }
}


#==================== clear_nav_bar ===============================
# Looks for browser navigation bar control and clicks on it and
# clears the text.
#
# NB: Only IE has controls to access nav bar. 
#
# Calling parameters: hnd
# Returns: null
#==================================================================
function clear_nav_bar ($hnd="") {

   # Sanity check.
   if (!$hnd -or $hnd -eq "" -or $hnd -eq 0) {
      log_error "clear_nav_bar invalid hnd=$hnd"
      return
   }

   # Get controls for specified window.
   log_info "clear_nav_bar hnd=$hnd"
   get_window_obj $hnd
   get_controls $script:curr_obj
   
   # Look for exact match for the navigation bar control.
   # NB: We are trying to avoid the Live Search bar control.
   search_controls "edit" "^(about|file|http|www)" 1 -quiet
   # $script:ctl_hnd = "" # test code
   if (!$script:ctl_hnd) {

      # Exact match failed, try first loose match.
      # log_info "clear_nav_bar trying loose match (1) hnd=$hnd"
      search_controls "" "about|file|http|www" 1 -quiet
      # $script:ctl_hnd = "" # test code
      if (!$script:ctl_hnd) {

         # First loose match failed, try second loose match, with -nottitle option.
         # log_info "clear_nav_bar trying loose match (2) hnd=$hnd"
         search_controls "edit" "live" 1 -nottitle -warnonly # want control data if not found
         # $script:ctl_hnd = "" # test code
         if (!$script:ctl_hnd) {

            # Control not found, we are done.
            log_error "clear_nav_bar: (3) hnd=$hnd, no loose match for navigation bar control!"
            return
         }
      }
   }

   # Click on the control.
   send_click $script:ctl_hnd
         
   # Clear the text with Ctrl-A Backspace.
   start-sleep -m 500
   send_keys $script:ctl_hnd "^a{backspace}"

   # Again to make it more reliable.
   start-sleep -m 500
   send_keys $script:ctl_hnd "^a{backspace}"
}


#==================== close_child_windows ==========================
# Looks for any child window or related modal windows and if found,
# closes them all. If hnd is not specified, then we try to close all
# child or modal windows based on the specified app_name.
#
# Calling parameters: hnd app_name
# NB: If hnd is specified, app_name will be retrieved based on hnd.
#
# Returns: null
# Sets variables: $script:curr_obj
#==================================================================
function close_child_windows ($hnd="",$app_name="") {

   # NB: Unfortuneately, the Firefox "Default Browser" dialog box
   # is not detectable as a separate window, child window or
   # control. So we cant deal with it yet...

   # NB: IE really gets unhappy if you minimize the appl & modal
   # windows. So call close_child_windows before doing a restore_window.

   # Sanity check
   if (!$hnd -and !$app_name) {
      log_error "close_child_windows both parameters invalid: hnd=$hnd app_name=$app_name"
      return
   }

   # Find the current window object based on hnd, get app_name.
   $script:curr_obj = ""
   if ($hnd) {
      # This makes sure that hnd takes precedance and we use the
      # correct app_name for the specified handle.
      get_window_obj $hnd
      $app_name = $script:curr_obj.processname
      # log_info "close_child_windows hnd=$hnd curr_obj=$script:curr_obj"
   } else {
      # Use the specified app_name, as is.
   }
   # log_info "close_child_windows hnd=$hnd app_name=$app_name"

   # Both Firefox & IE allow one SaveAs window open per main window,
   # so be careful here.

   # New versions of IE sometimes pop up a modal window and other
   # times will pop up a child window. It seems to always be one
   # way on a specific PC, but can vary from PC to PC.
   # The fun part here is that you could have multiple IE windows
   # active, or other apps that do modal windows, each with its own
   # modal SaveAs window. They all have different pids, which could
   # be matched up using wmic process. For now, we match on the app
   # name for a slightly better, but still imperfect result.

   # In case of misbehaved apps, we try multiple times to close 
   # child or associated modal windows. Most of the time, there
   # is no child window or modal window.
   $total_closed = 0
   $type = ""
   for ($i = 1; $i -le $script:max_loop; $i++) {

      # Look for a child window or modal windows.
      $ch = ""
      $closed = 0 # counters for this iteration
      $found = 0
      if ($app_name -match "iexplore" -and $script:ie_major_ver -gt 8) {
         $ch = select-window -class *modal* # cant specify 2 search parameters at once!
         $cnt = $ch.count
         $type = "modal"
         # "modal: i=$i cnt=$cnt ch = $ch"
         foreach ($w in $ch) {
            # Decode window object
            $c = $w.Class
            $h = $w.Handle
            $n = $w.ProcessName
            $t = $w.Title
            # "close_child_windows i=$i modal c=$c h=$h n=$n t=$t"
            if (!$h) {
               continue
            }

            # Close the modal windows that match app_name
            if ($n -match $app_name) {
               # "MATCHED app_name=$app_name"
               $found++
               log_info "close_child_windows i=$i hnd=$hnd app_name=$app_name closing $type window, c=$c h=$h n=$n t=$t"
               remove_window $w -warnonly
               if ($script:remove_window_rc) {
                  $closed++
                  $total_closed++
               }

            } else {
               log_debug "close_child_windows i=$i hnd=$hnd app_name=$app_name ignoring $type window, c=$c h=$h n=$n t=$t"
            }
         }

         # If we closed everything relevant for this iteration, we are done.
         # NB: Dont break here unless we really closed a window! Otherwise
         # we wont check for child windows!
         # "modal: i=$i found=$found closed=$closed"
         if ($found -eq $closed -and $closed -gt 0) {
            # "modal: i=$i break"
            break
         }
      }

      # If no modal windows found, look for child windows.
      if (!$ch) {
         if ($hnd) {
            # Handle was specified, just look at this single window.
            $ch = select-childwindow -window $hnd
         } else {
            # No handle was specified, so look at all windows for this app_name.
            map_app_name $app_name
            $ch = select-window -title "*$script:app_title*" | select-childwindow
         }
         $cnt = $ch.count
         $type = "child"
         # "child: i=$i cnt=$cnt ch=$ch"
         foreach ($w in $ch) {
            # Decode window object.
            $c = $w.Class
            $h = $w.Handle
            $n = $w.ProcessName
            $t = $w.Title
            if (!$h) {
               continue
            }

            # Close the child window.
            $found++
            log_info "close_child_windows i=$i hnd=$hnd app_name=$app_name closing $type window, c=$c h=$h n=$n t=$t"
            remove_window $w -warnonly
            if ($script:remove_window_rc) {
               $closed++
               $total_closed++
            }
         }

         # If we closed everything relevant for this iteration, we are done.
         # "child: i=$i found=$found closed=$closed"
         if ($found -eq $closed) {
            # "child: i=$i break"
            break
         }
      }

      # Wait a bit
      if ($i -lt $script:max_loop) {
         # "i=$i waiting..."
         start-sleep -s 1
      }
   } 

   # Check what really happened.
   # log_info "close_child_windows i=$i type=$type closed=$closed found=$found total_closed=$total_closed"
   if ($closed -ne $found  -or $i -gt $script:max_loop) {
      log_error "close_child_windows hnd=$hnd app_name=$app_name tried $script:max_loop times to close $type window(s), FAILED, still open: $ch"

   } elseif ($i -eq 1 -and $found -eq 0) {
      # Nothing found on first try, which is the usual case.
      log_info "close_child_windows hnd=$hnd app_name=$app_name no $type windows found OK, Try#: $i"

   } elseif ($i -eq 1 -and $found -gt 0) {
      # Succeeded on first try.
      log_info "close_child_windows hnd=$hnd app_name=$app_name found $total_closed $type window(s), closed OK, Try#: $i"

   } else {
      log_warning "close_child_windows hnd=$hnd app_name=$app_name found $total_closed $type window(s), closed OK, Try#: $i"
   }
}


#==================== close_windows ===============================
# Looks for any windows with the specified app_name and if found,
# closes them all. 
#
# Calling parameters: app_name
# Returns: null
#==================================================================
function close_windows ($app_name="") {

   # Sanity check
   if (!$app_name) {
      log_error "close_windows invalid: app_name=$app_name"
      return
   }

   # Try multiple times to close app_name windows.
   $total_closed = 0
   for ($i = 1; $i -le $script:max_loop; $i++) {

      # Look for app_name windows.
      $ch = ""
      $closed = 0 # counters for this iteration
      $found = 0
      map_app_name $app_name
      $w_list = select-window -title *$script:app_title*
      $cnt = $w_list.count
      # "i=$i cnt=$cnt w_list=$w_list"
      foreach ($w in $w_list) {
         # Decode window object
         $c = $w.Class
         $h = $w.Handle
         $n = $w.ProcessName
         $t = $w.Title
         # "close_windows i=$i c=$c h=$h n=$n t=$t"
         if (!$h) {
            continue
         }

         # Close this window
         $found++
         remove_window $w -warnonly
         if ($script:remove_window_rc) {
            $closed++
            $total_closed++
         }
      }

      # If we closed everything relevant for this iteration, we are done.
      # "close_windows i=$i found=$found closed=$closed"
      if ($found -eq $closed) {
         # "i=$i break"
         break
      }
   }

   # Check what really happened.
   # log_info "close_windows i=$i closed=$closed found=$found total_closed=$total_closed"
   if ($closed -ne $found  -or $i -gt $script:max_loop) {
      log_error "close_windows app_name=$app_name tried $script:max_loop times to close window(s), FAILED, still open: $w_list"

   } elseif ($i -eq 1 -and $found -eq 0) {
      # Nothing found on first try, which is the usual case.
      log_info "close_windows app_name=$app_name no windows found OK, Try#: $i"

   } elseif ($i -eq 1 -and $found -gt 0) {
      # Succeeded on first try.
      log_info "close_windows app_name=$app_name found $total_closed window(s), closed OK, Try#: $i"

   } else {
      log_warning "close_windows app_name=$app_name found $total_closed window(s), closed OK, Try#: $i"
   }
}


#==================== copy_file ===================================
# Copies src file path to dest file path. Will not overwrite an
# existing file, except for the <hostname>_latest.html file.
#
# Calling parameters: src dest
# Returns: null
# Sets variables: $script:copy_file_rc
#==================================================================
function copy_file ($src="", $dest="") {

   # Does the destination file already exist? Dont overwrite!
   $script:copy_file_rc = $false
   "copy_file src=$src dest=$dest" # show on terminal window as progress indicator.
   if (!($dest -match $script:latest_fn) -and (test-path "$dest")) {
      log_error "copy_file dest=$dest already exists, will NOT overwrite!"
      return
   }

   # Try multiple times to copy the file.
   for ($i = 1; $i -le $script:max_loop; $i++) {
      $msg = ""
      $saved_rc = $false
      try {
         copy-item -path "$src" -destination "$dest"
         $saved_rc = $?
      } catch {
         $msg = $Error | select-object -first 1
         $saved_rc = $false
      }

      # Test code
      # if ($i -le 1) {
      #    $msg = "yadee"
      #    $saved_rc = $false
      # }

      # If we succeeded, we are done.
      if ($saved_rc) {
         $script:copy_file_rc = $true
         # "copy_file i=$i OK $src"
         return
      }

      # Wait a bit before trying again.
      # "saved_rc=$saved_rc"
      log_warning "copy_file i=$i src=$src got: $msg"
      if ($i -lt $script:max_loop) {
         # "copy_file i=$i waiting..."
         start-sleep -s 10
      }
   }

   # We failed!
   log_error "copy_file tried $script:max_loop times src=$src dest=$dest got: $msg"
}


#==================== copy_results_to_server ======================
# Copies all files created during tests to the specified server
# results subdirectory. 
#
# Calling parameters: none
# Returns: null
#==================================================================
function copy_results_to_server {

   # Are we supposed to copy the test results to a central server?
   if ($script:nocs -or !$script:test_network -or !$script:server_results_subdir -or $script:server_results_subdir -eq "") {
      log_warning "copy_results_to_server files NOT copied to central server, nocs=$script:nocs test_network=$script:test_network server_results_subdir=$script:server_results_subdir"
      return
   }

   # Expected result files are: screenshots + fullreport + summary + <hostname>_latest.html
   $result_file_cnt = $script:total_screenshot + 3

   # For multiple iterations, add message to logfile, which shows timestamp.
   if ($script:testsuite_interation -ne 1) {
      log_info "copy_results_to_server starting copy of $result_file_cnt files..."
   }

   # Get list of results files.
   # NB: The dir command gives us the full pathname even if we
   # didnt explicitly ask for it.
   $files = dir "${pwd}\${script:logname}*"
   # "files=$files"     

   # Copy files to central server, except for fullreport.
   $copied = 0
   $found = 0 
   $full_report = ""
   foreach ($src in $files) {

      # Extract just the filename for use in destination pathname.
      $f = [System.IO.Path]::GetFileName($src)
      # "src=$src f=$f"

      # We defer copying this file to the very end.
      if ($src -match "fullreport") {
         $full_report = $src
         # log_info "defer copying full_report=$full_report"
         continue
      }
      
      # Copy the file to the server results directory.
      $found++
      copy_file "$src" "$script:server_results_subdir\$f"
      if ($script:copy_file_rc) {
         $copied++
      }
   }

   # Now copy the <hostname>_latest.html file
   $found++
   copy_file "$pwd\$script:latest_fn" "$script:server_results_subdir\$script:latest_fn"
   if ($script:copy_file_rc) {
      $copied++
   }

   # Leaving the fullreport to the very end allows copy errors to
   # to be appended to the report. Admittedly the summart stats
   # tables will not include these errors. 
   $found++
   $f = [System.IO.Path]::GetFileName($full_report)
   copy_file "$full_report" "$script:server_results_subdir\$f"
   if ($script:copy_file_rc) {
      $copied++
   }

   # Did we find the correct number of files?
   log_info "copy_results_to_server result_file_cnt=$result_file_cnt found=$found copied=$copied server_results_dir=$script:server_results_dir"
   if ($result_file_cnt -ne $found) {
      log_error "copy_results_to_server result_file_cnt=$result_file_cnt NE found=$found server_results_dir=$script:server_results_dir"
   }

   # NB: If you are checking files on the server against the local PC,
   # there is one more file on the server besides the ones we just copied,
   # namely the .res file created to reserve this logname at the start
   # of the tests.
}


#==================== date_time_sync ==============================
# Syncs up the date/time from the network, stores the results for 
# use later by tc_2 when a logfile is available.
#
# Calling parameters: none
# Returns: null
# Sets variables: $script:date_time_sync_rc, $script:date_time_sync_txt
#                 $script:date_time_sync_jpg
#==================================================================
function date_time_sync {

   # If the network tests are not being run, then quietly return.
   if (!$script:test_network) {
      return
   }

   # If tc_2 is not in the list of tests to run, then quietly return.
   # This avoids extra delays when debugging other code. We DONT need
   # to waste time running unnecessary date/time syncs from the network
   # every time the script runs.
   # "date_time_sync tc_list=$script:tc_list"
   $found = $false
   foreach ($tc in $script:tc_list) {
      # "tc=$tc"
      if ($tc -eq "tc_2") {
         # "FOUND: $tc"
         $found = $true
         break
      }
   }
   if (!$found) {
      # "date_time_sync not running"
      return
   }

   # Initialization
   $script:date_time_sync_rc = ""
   $script:date_time_sync_txt = ""
   $script:date_time_sync_jpg = ""
   log_info "date_time_sync starting"

   # For WinXP, the Date/Time control panel does not show up with
   # its own window object or handle. So we use the command line
   # w32tm.exe to force a network time sync.
   if ($script:win_major_ver -le 5) {
      # Run the command. Command does not return output until its done.
      $script:date_time_sync_txt = w32tm.exe /resync

      # Parse the stdout
      if ($script:date_time_sync_txt -match "complete.*success") {
         $script:date_time_sync_rc = $true
      } else {
         $script:date_time_sync_rc = $false
      }

      # There is nothing to take a screenshot of here.
      $script:date_time_sync_jpg = ""
      log_info "date_time_sync done"
      return
   }

   # For WinVista, Win7 onwards we start the Windows Date & Time control panel.
   # We dont use start_app because this process shows up as rundll32 & title=date & time.
   $msg = ""
   $saved_rc = $False
   try {
      start-process -filepath "control.exe" -argumentlist "timedate.cpl" -erroraction silentlycontinue
      $saved_rc = $?
   } catch {
      $msg = $Error | select-object -first 1
      $saved_rc = $False
   }

   # Check for errors
   if (!$saved_rc) {
      log_error "date_time_sync got: $msg"
   }

   # Find handle, get window object, focus window.
   start-sleep -s 2
   resolve_window_handle "Date"
   $date_hnd = $script:curr_hnd
   restore_window $date_hnd
   # log_info "date_time_sync date_hnd=$date_hnd"
   get_window_obj $date_hnd
   $date_obj = $script:curr_obj

   # Use tabs to select control panel top tab area.
   # Number of tab keys to send depends on windows version.
   if ($script:win_major_ver -eq 6 -and $script:win_minor_ver -eq 0) {
      # WinVista
      $tab_cmd = "{tab}{tab}{tab}{tab}{tab}{tab}"
   } else {
      # Win7 onwards.
      $tab_cmd = "{tab}{tab}{tab}{tab}{tab}{tab}{tab}"
   }
   start-sleep -s 1
   send_keys $date_hnd $tab_cmd

   # Select the right most tab via ctrl-rightarrow.
   send_keys $date_hnd "^{right}^{right}"

   # Look for Change Settings button & click it.
   start-sleep -s 1
   get_controls $date_obj
   search_controls "button" "Change.*Setting" 1
   send_click $script:ctl_hnd

   # Look for Update Now button and click it.
   start-sleep -s 1
   get_controls $date_obj -childonly
   search_controls "button" "Update.*Now" 1
   send_click $script:ctl_hnd

   # Wait for successfull synchronization msg.
   # NB: We ask for -childonly controls because the main window
   # has the most recent update status, which we dont want!!!
   # We want the latest status, which will be on the child window!!!
   wait_for_control $date_obj 30 "success.*sync" "error.*occur" -childonly
   $script:date_time_sync_rc = $script:wait_for_control_rc
   $script:date_time_sync_txt = $script:ctl_title
   # "date_time_sync_rc=$script:date_time_sync_rc date_time_sync_txt=$script:date_time_sync_txt"

   # Take screenshot with child window still open and cache it for use later.
   # NB: The first time the screenshot is taken, the script:logname will be null,
   # so the screenshot filename will be just tc_2_1.jpg. Later on, tc_2 has code
   # to rename the file and properly link it into the results logfile.
   get_screenshot "tc_2" "fg" "Internet.*Time"
   $script:date_time_sync_jpg = $script:scr_fn
   # "date_time_sync_jpg=$script:date_time_sync_jpg"

   # Click OK on child window.
   start-sleep -s 1
   get_controls $date_obj -childonly
   search_controls "button" "OK" 1
   send_click $script:ctl_hnd
   start-sleep -s 1

   # Click OK on main window.
   get_controls $date_obj
   search_controls "button" "OK" 1
   send_click $script:ctl_hnd
   start-sleep -s 2
   log_info "date_time_sync done"

   # NB: tc_2 will make PASS/FAIL decisions.
}


#==================== delete_email_profile ========================
# Deletes the email profile added by setup_ms_outlook.
#
# Calling parameters: none
# Returns: null
#==================================================================
function delete_email_profile {

   # MS Outlook installs its own control panel for managing the
   # email profiles, called MLCFG32.CPL. The location of this 
   # custom control panel is found in the registry at:
   # HKEY_CURRENT_USER\Control Panel\MMCPL or 
   # HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\Current Version\Control Panel\Cpls

   # Right now this code is not implemented. My concern is that 
   # even if I put an option run/not run this routine, someone
   # will run this on their production PC and trash their real
   # Outlook profile. While the .PST file will not get deleted,
   # all the other email server settings will be gone.

}


#==================== estimate_battery_life =======================
# Estimate the battery life from the data in $script:batt_array.
#
# Calling parameters: null
# Returns: null
# Sets variables: $script:batt_est_time_min
#==================================================================
function estimate_battery_life {

   # NB: The battery discharge data may have more than one cycle of
   # online-offline-online. This could be accidental, or the user 
   # seeing if we can handle some extended test scenarios.

   # Dummy test data.
   # $script:batt_array = "1 online 100", "2 offline 100", "3 offline 92", "4 offline 90", "5 online 95",
   #   "6 offline 94", "7 offline 93", "8 offline 92"
   # $script:batt_array = "1 offline 90", "2 online 91"
   # $script:batt_array = "1 online 90", "2 offline 90.0001", "193 offline 87.0005"
   # $script:batt_array = "1 online 90", "2 offline 90.0", "193 offline 91"


   # Initialization for parsing data
   $script:batt_est_time_min = 0
   $array_cnt=$script:batt_array.count
   # "estimate_battery_life array_cnt=$array_cnt"
   $interval_cnt = 0
   $interval_start_life = 0
   $interval_start_sec = 0
   $interval_finish_life = 0
   $interval_finish_sec = 0
   $start_batt_life = -1
   $total_discharge_sec = 0
   $weighted_numerator = 0

   # Parse the collected data
   for ($i = 0; $i -lt $array_cnt; $i++) {

      # Get next sample and split out parameters
      $temp1 = $script:batt_array[$i]
      $temp2 = $temp1.split(" ")
      $b_sec = $temp2[0]
      $b_state = $temp2[1]
      $b_life = $temp2[2]
      # log_debug "estimate_battery_life i=$i temp1=$temp1"
      log_info "estimate_battery_life i=$i b_sec=$b_sec b_state=$b_state b_life=$b_life%"

      # Save initial battery life.
      if ($start_batt_life -eq -1) {
         $start_batt_life = $b_life
      }

      # Ignore initial online data.
      if ($b_state -match "online" -and $interval_start_sec -eq 0) {
         # log_debug "estimate_battery_life ignoring initial online data i=$i b_sec=$b_sec b_state=$b_state b_life=$b_life%"
         continue
      }

      # Look for start of a discharge iterval.
      if ($b_state -match "offline" -and $interval_start_sec -eq 0) {
         $interval_start_life = $b_life
         $interval_start_sec = $b_sec
         $interval_cnt++
         log_debug "estimate_battery_life starting interval i=$i interval_start_sec=$interval_start_sec interval_start_life=$interval_start_life%"
         continue
      }

      # Collect most recent intermediate sample. That way we have it stored
      # when we hit the end of the data or we find a sample where we go online.
      # This is NOT the start discharge interval sample!
      if ($b_state -match "offline" -and $interval_start_sec -ne 0) {
         $interval_finish_life = $b_life
         $interval_finish_sec = $b_sec
         log_debug "estimate_battery_life continuing interval i=$i interval_finish_sec=$interval_finish_sec interval_finish_life=$interval_finish_life%"
         continue
      }

      # When we find an online sample, this marks the end of the current discharge interval.
      if ($b_state -match "online" -and $interval_start_sec -ne 0) {
         # Do we have both start & finish data for the current discharge cycle?
         if ($interval_start_sec -ne 0 -and $interval_finish_sec -ne 0) {
            # Add the current interval discharge data to the running weighted totals.
            $delta_life = $interval_finish_life - $interval_start_life # gives negative number
            $delta_sec = $interval_finish_sec - $interval_start_sec
            log_info "estimate_battery_life end of interval i=$i b_state=$b_state delta_life=$delta_life% delta_sec=$delta_sec"
            $weighted_numerator = $weighted_numerator - ($delta_life * $delta_sec) # want a positive number
            $total_discharge_sec = $total_discharge_sec + $delta_sec
            log_debug "estimate_battery_life end interval i=$i weighted_numerator=$weighted_numerator total_discharge_sec=$total_discharge_sec"
         }

         # Reset parser state info.
         $interval_start_life = 0
         $interval_start_sec = 0
         $interval_finish_life = 0
         $interval_finish_sec = 0

         # The rest of the online sample data is of no interest.
         continue
      }

      # Anything else is an error.
      log_error "estimate_battery_life i=$i should NOT go here: $b_sec $b_state $b_life% interval_cnt=$interval_cnt interval_start_life=$interval_start_life% interval_start_sec=$interval_start_sec interval_finish_life=$interval_finish_life% interval_finish_sec=$interval_finish_sec"
   }

   # We have reached the end of the aray data. Do we have an interval 
   # stored up? If so, the end of data implies the end of the sample,
   # even if we are still offline.
   if ($interval_start_sec -ne 0 -and $interval_finish_sec -ne 0) {
      $delta_life = $interval_finish_life - $interval_start_life # gives negative number
      $delta_sec = $interval_finish_sec - $interval_start_sec
      log_info "estimate_battery_life end of data delta_life=$delta_life% delta_sec=$delta_sec"
      $weighted_numerator = $weighted_numerator - ($delta_life * $delta_sec) # want a positive number
      $total_discharge_sec = $total_discharge_sec + $delta_sec
      log_debug "estimate_battery_life end of data weighted_numerator=$weighted_numerator total_discharge_sec=$total_discharge_sec"
   }

   # Estimated battery consumption rate/min
   $min_test_sec = $script:battery_test_discharge_min * 60
   $max_life_min = 240 # Typical battery upper limit, in minutes
   log_info "estimate_battery_life weighted_numerator=$weighted_numerator total_discharge_sec=$total_discharge_sec"
   if ($total_discharge_sec -ge $min_test_sec -and $total_discharge_sec -gt 0) {
      $weighted_discharge = $weighted_numerator / $total_discharge_sec # Covers one or more discharge intervals
      $rate_sec = $weighted_discharge / $total_discharge_sec # battery discharge rate per second
      if ($rate_sec -lt 0) {
         log_error "estimate_battery_life invalid rate_sec=$rate_sec, set to 0"
         $rate_sec = 0
      }
      $rate_min = $rate_sec * 60 # battery discharge rate per minute
      log_info "estimate_battery_life weighted_discharge=$weighted_discharge rate_sec=$rate_sec rate_min=$rate_min start_batt_life=$start_batt_life%"
      if ($rate_min -ne 0) {
         $script:batt_est_time_min = $start_batt_life / $rate_min
         if ($script:batt_est_time_min -gt $max_life_min) {
            $script:batt_est_time_min = $max_life_min
         }

      } else {
         # Some batteries may not show any appreciable discharge in a short time interval,
         # so show the typical maximum battery life.
         $script:batt_est_time_min = $max_life_min
      }
      $temp = "{0:0}" -f $script:batt_est_time_min
      log_bold "estimate_battery_life batt_est_time_min=$temp minutes"

   } else {
      log_error "estimate_battery_life total battery discharge time was $total_discharge_sec seconds, need minimum $min_test_sec seconds, cant estimate battery life time."
   }
}


#==================== find_desktop ================================
# Searches thru the controls found on the desktop. Appears to
# include all other open windows as well.
#
# Calling parameters: class_patt title_patt
#
# Returns: none
# Sets variables: $script:hnd_array
#==================================================================
function find_desktop ($class_patt="", $title_patt="") {

   # Initialization
   $script:hnd_array = @()
   if ($class_patt -eq "") {
      $class_patt = ".*"
   }
   if ($title_patt -eq "") {
      $title_patt = ".*"
   }
   # log_debug "find_desktop class_patt=$class_patt title_patt=$title_patt"

   # Get current list of desktop controls.
   $ctl_list = select-control -window $script:desk_hnd -recurse
   $found = 0
   $result = ""
   $total = 0

   # Filter out controls by title or class
   foreach ($ctl in $ctl_list) {
      # get-variable ctl
      $total++
      $c=$ctl.Class
      $h=$ctl.Handle
      $t=$ctl.Title
      $l=$t.length
      if ($l -gt 20) {
         $l = 20
      }
      $t = $t.Substring(0,$l) # get errors if you ask for charactars after end of string!
      # "c=$c h=$h t=$t"

      if ($c -match $class_patt) {
         $class_ok = $true
      } else {
         $class_ok = $false
      }

      if ($t -match $title_patt) {
         $title_ok = $true
      } else {
         $title_ok = $false
      }

      # "c=$c h=$h t=$t class_ok=$class_ok title_ok=$title_ok"
      if ($class_ok -and $title_ok) {
         # "FOUND: c=$c h=$h t=$t"
         $found++
         $result = "$result c=$c h=$h t=$t `n"
         $script:hnd_array += $h # add to end of array
         continue
      }
   }
   log_info "find_desktop class_patt=$class_patt title_patt=$title_patt result: `n$result `ntotal=$total found=$found hnd_array=$script:hnd_array"
}


#==================== get_age_timestamp ===========================
# If a battery is present in the PC, prompts the user to unplug 
# the AC power so the battery test can run.
#
# Calling parameters: timestamp
# Returns: null
# Sets variables: $script:curr_age
#==================================================================
function get_age_timestamp ($timestamp="") {

   # Initialization
   $script:curr_age = 1000000000 # default value should trigger errors

   # Clean up calling parameter
   # "timestamp=$timestamp"
   $timestamp = $timestamp -replace "\(.*\)"
   $timestamp = $timestamp -replace "at\s*"
   $timestamp = $timestamp -replace "Today\s*"
   $timestamp = $timestamp.trim()

   # Convert timestamp to seconds.
   try {
      $EpochDiff = New-TimeSpan "01 January 1970 00:00:00" $timestamp
      $ts_sec = [INT] $EpochDiff.TotalSeconds
      remove-variable EpochDiff  # make sure no old data is leftover
   } catch {
      $ts_sec = 0 
      $msg = $error | select-object -first 1
      log_error "get_age_timestamp timestamp=$timestamp got: $msg"
   }

   # Get current date/time in seconds.
   $EpochDiff = New-TimeSpan "01 January 1970 00:00:00" $(Get-Date)
   $curr_sec = [INT] $EpochDiff.TotalSeconds
   remove-variable EpochDiff  # make sure no old data is leftover

   # Get age diffrence, in hours.
   $script:curr_age = $curr_sec - $ts_sec
   $script:curr_age = $script:curr_age / 3600 # convert to hours
   log_debug "get_age_timestamp timestamp=$timestamp ts_sec=$ts_sec curr_sec=$curr_sec curr_age=$script:curr_age hrs" 
}


#==================== get_battery_status ==========================
# If a battery is present in the PC, prompts the user to unplug 
# the AC power so the battery test can run.
#
# Calling parameters: <-log>
# -log option will log the current battery data sample
# Returns: null
# Sets variables: $script:batt_array, $script:batt_life,
#                 $script:batt_state
#==================================================================
function get_battery_status {

   # Initialization
   $script:batt_life = ""
   $script:batt_state = ""
   $EpochDiff = New-TimeSpan "01 January 1970 00:00:00" $(Get-Date)
   $curr_sec = [INT] $EpochDiff.TotalSeconds
   remove-variable EpochDiff  # make sure no old data is leftover

   # Get current battery info
   $ps = [Windows.Forms.SystemInformation]::PowerStatus
   $status = $ps.BatteryChargeStatus
   $script:batt_state = $ps.PowerLineStatus
   $script:batt_life = $ps.BatteryLifePercent
   $script:batt_life = $script:batt_life * 100
   remove-variable ps # make sure no old data is leftover

   # Check if we actually have a battery.
   if ($status -match "no.*sys.*batt") {
      # No battery found. This also covers a laptop with the battery removed.
      $script:batt_life = -1
      $script:batt_state = "NoBattery"
   }

   # Always add the current sample to batt_array.
   $script:batt_array += "$curr_sec $script:batt_state $script:batt_life"

   # Optionally log the current sample.
   if ($args -match "-log") {
      $msg = "get_battery_status curr_sec=$curr_sec status=$status batt_state=$script:batt_state batt_life={0:0.00}%" -f $script:batt_life
      log_info $msg
   }
}


#==================== get_clipboard ===============================
# Gets the current content of the windows clipboard.
#
# Calling parameters: none
# Returns: null
# Sets variables: $script:clipboard
#==================================================================
# from: http://stackoverflow.com/questions/1567112/convert-keith-hills-powershell-get-clipboard-and-set-clipboard-to-a-psm1-script
# NB: PSCX module has clipboard routine that can also copy graphics

function get_clipboard {

   $tb = New-Object System.Windows.Forms.TextBox
   $tb.Multiline = $true
   $tb.Paste()
   $script:clipboard = $tb.Text
   log_info "get_clipboard script:clipboard=$script:clipboard"
}


#==================== get_controls ================================
# Gets all the controls for the specified window object by default.
# There are options to allow the choice of main window controls
# only or the choice of child window controls only.
#
# NB: Only the first level children windows are searched. This
# routine is NOT fully recursive. If you want second or third
# level children information, you need to call this routine
# with the window object of a lower level child in order to 
# get the information of the next lower level child.
#
# Calling parameters: obj <-mainonly> <-childonly>
#
# -mainonly  option shows controls for main window only, ignores
#            child windows, if any.
# -childonly option shows controls for child windows, if any and
#            ignores the main window controls.
#
# Returns: null
# Sets variables: $script:controls
#==================================================================
function get_controls ($obj="") {

   # Initialization. @() wipes out any existing data. 
   # NB: We put a sanity check here just to be sure.
   $script:controls = @() 
   foreach ($ctl in $script:controls) {
      log_error "get_controls: obj=$obj args=$args found leftover data: $script:controls"
      return
   }

   # Sanity check.
   if (!$obj) {
      log_error "get_controls invalid obj=$obj args=$args"
      return
   }

   # Parse optional args
   $child_only = $false
   $main_only = $false
   foreach ($opt in $args) {

      # Ignore null args
      $opt = $opt.trim()
      # "opt=$opt"
      if ($opt -eq "") {
         continue
      }

      # Look for -childonly option
      if ($opt -eq "-childonly") {
         $child_only = $true
         continue
      }

      # Look for -mainonly option
      if ($opt -eq "-mainonly") {
         $main_only = $true
         continue
      }

      # Invalid option ==> error
      log_error "get_controls obj=$obj args=$args invalid option: $opt"
      return
   }
   # log_info "get_controls obj=$obj child_only=$child_only main_only=$main_only"

   # Decode the window object.
   # $obj = "" # test code
   $handle = $obj.Handle
   $name = $obj.ProcessName
   $title = $obj.Title
   # log_info "get_controls handle=$handle name=$name title=$title"

   # Sanity check.
   if (!$handle -or $handle -eq "" -or $handle -eq 0) {
      log_error "get_controls obj=$obj args=$args name=$name title=$title, invalid handle=$handle"
      return
   }

   # Look for main window controls.
   if (!$child_only) {
      $x = select-control -window $obj -recurse
      # log_info "get_controls args=$args handle=$handle name=$name title=$title main window controls: $x`n`n"
      $script:controls += $x # adds to end of existing array
   }

   # Look for the first level children windows and related controls.
   # If you want to see the information for the second or lower level
   # children, you need to call this routine with the appropriately
   # lower level window object.
   if (!$main_only) {
      $ch_w = select-childwindow -window $obj
      foreach ($ch in $ch_w) {
         # Skip null entries
         if (!$ch) {
            continue
         }

         # Decode the child window object
         # "child=$ch"
         # get-variable ch
         $h = $ch.Handle
         $n = $ch.ProcessName
         $t = $ch.Title
      
         # Look for child window controls.
         $y = select-control -window $ch -recurse
         $script:controls += $y # adds to end of existing array
         # log_info "get_controls args=$args h=$h n=$n t=$t child window controls: $y`n`n"
         # foreach ($c in $y) {
         #    if ($c) {
         #       "c=$c"
         #       get-variable c
         #    }
         # }
      }
   }

   # Logging of the controls is now done by calling routines, typically only
   # if the desired control is not found.
   # log_info "get_controls args=$args handle=$handle name=$name title=$title controls=$script:controls`n`n`n"

   # Debug code. Do we have one contiguous array of controls?
   # Really dont want an array where each element is itself an array!
   # $cnt = $script:controls.count
   # "`n`nget_controls cnt=$cnt"
   # foreach ($ctl in $script:controls) {
   #    "get_controls ctl=$ctl"
   # }
}


#==================== get_discharge_sec ===========================
# Gets total battery discharge time to date from the data in
# $script:batt_array.
#
# Calling parameters: null
# Returns: null
# Sets variables: $script:discharge_sec
#==================================================================
function get_discharge_sec {

   # NB: The battery discharge data may have more than one cycle of
   # online-offline-online. This could be accidental, or the user 
   # seeing if we can handle some extended test scenarios.

   # Dummy test data.
   # $script:batt_array = "1 online 100", "2 offline 100", "3 offline 92", "4 offline 90", "5 online 95",
   #  "6 offline 94", "7 offline 93", "8 offline 92"
   # $script:batt_array = "1 offline 90", "2 online 91"
   # $script:batt_array = "1 online 90", "2 offline 90.0001", "193 offline 87.0005"
   # $script:batt_array = "1 online 90", "2 offline 90.0", "193 offline 91"

   # Initialization for parsing data
   $script:discharge_sec = 0
   $array_cnt=$script:batt_array.count
   # "get_discharge_sec array_cnt=$array_cnt"
   $interval_cnt = 0
   $interval_start_sec = 0
   $interval_finish_sec = 0
   $total_discharge_sec = 0

   # Parse the collected data
   for ($i = 0; $i -lt $array_cnt; $i++) {

      # Get next sample and split out parameters
      $temp1 = $script:batt_array[$i]
      $temp2 = $temp1.split(" ")
      $b_sec = $temp2[0]
      $b_state = $temp2[1]
      $b_life = $temp2[2]
      # log_debug "get_discharge_sec i=$i temp1=$temp1"
      log_debug "get_discharge_sec i=$i b_sec=$b_sec b_state=$b_state b_life=$b_life%"

      # Ignore initial online data.
      if ($b_state -match "online" -and $interval_start_sec -eq 0) {
         # log_debug "get_discharge_sec ignoring initial online data i=$i b_sec=$b_sec b_state=$b_state b_life=$b_life%"
         continue
      }

      # Look for start of a discharge iterval.
      if ($b_state -match "offline" -and $interval_start_sec -eq 0) {
         $interval_start_sec = $b_sec
         $interval_cnt++
         # log_debug "get_discharge_sec starting interval i=$i interval_start_sec=$interval_start_sec"
         continue
      }

      # Collect most recent intermediate sample. That way we have it stored
      # when we hit the end of the data or we find a sample where we go online.
      if ($b_state -match "offline" -and $interval_start_sec -ne 0) {
         $interval_finish_sec = $b_sec
         # log_debug "get_discharge_sec continuing interval i=$i interval_finish_sec=$interval_finish_sec"
         continue
      }

      # When we find an online sample, this marks the end of the current discharge interval.
      if ($b_state -match "online" -and $interval_start_sec -ne 0) {
         # Do we have both start & finish data for the current discharge cycle?
         if ($interval_start_sec -ne 0 -and $interval_finish_sec -ne 0) {
            # Add the current interval discharge time to the running total.
            $delta_sec = $interval_finish_sec - $interval_start_sec
            log_info "get_discharge_sec end of interval i=$i b_state=$b_state delta_sec=$delta_sec"
            $total_discharge_sec = $total_discharge_sec + $delta_sec
            # log_debug "get_discharge_sec end interval i=$i total_discharge_sec=$total_discharge_sec"
         }

         # Reset parser state info.
         $interval_start_sec = 0
         $interval_finish_sec = 0

         # The rest of the online sample data is of no interest.
         continue
      }

      # Anything else is an error.
      log_error "get_discharge_sec i=$i should NOT go here: $b_sec $b_state $b_life% interval_cnt=$interval_cnt interval_start_sec=$interval_start_sec interval_finish_sec=$interval_finish_sec"
   }

   # We have reached the end of the aray data. Do we have an interval 
   # stored up? If so, the end of data implies the end of the sample,
   # even if we are still offline.
   if ($interval_start_sec -ne 0 -and $interval_finish_sec -ne 0) {
      $delta_sec = $interval_finish_sec - $interval_start_sec
      log_info "get_discharge_sec end of data delta_sec=$delta_sec"
      $total_discharge_sec = $total_discharge_sec + $delta_sec
      # log_debug "get_discharge_sec end of data total_discharge_sec=$total_discharge_sec"
   }

   # Log the result
   $script:discharge_sec = $total_discharge_sec
   log_info "get_discharge_sec discharge_sec=$script:discharge_sec"
}


#==================== get_explorer_disk_properties ================
# Use Windows explorer, right click on C: and choose Properties 
# to bring up the pie chart of disk utilization.
#
# Not working yet, dont use...
#
# Calling parameters: none
# Returns: null
#==================================================================
function get_explorer_disk_properties {

   # start new explorer window.
   explorer
   start-sleep -s 2
   $w = select-window -title *c:*
   "get_explorer_disk_properties w=$w"

   # Did we get an explorer window?
   if (!$w) {
      log_error "get_explorer_disk_properties could not start explorer window!"
      return
   }

   # Bring explorer window to the foreground.
   $h = $w.Handle
   restore_window $h

   # Find control for nav bar.
   get_controls $w
   "controls=$script:controls"
   search_controls "edit" "c:" 1

   # Clear address bar and goto C:   send_click $script:ctl_hnd
   start-sleep -m 500
   send_keys $script:ctl_hnd "^a{backspace}"
   start-sleep -m 500
   send_keys $script:ctl_hnd "^a{backspace}"
   start-sleep -m 500
   send_keys $script:ctl_hnd "C:\{enter}"


}


#==================== get_file_contents ===========================
# Tries repeatedly to get the contents of the specfied file.
#
# Calling parameters: file cnt <-nolog>
# Option -nolog will suppress logging of file contents.
#
# NB: cnt specifies how many bytes of data you want, default is all.
# If you dont need all the data in the file, you can keep the 
# clutter in the logfile down by only taking the first cnt bytes.
#
# Returns: null
# Sets variables: $script:file_contents
#==================================================================
function get_file_contents ($file="", $cnt="") {

   # Check file was specified.
   $script:file_contents = ""
   if (!$file) {
      log_error "get_file_contents: file is null!"
      return
   }

   # Check file exists.
   if (!(test-path "$file")) {
      log_error "get_file_contents: $file not found!"
      return
   }

   # Try repeatedly to read the specified file.
   # If file is not present to begin with, thats OK.
   for ($i = 1; $i -le $script:max_loop; $i++) {

      # Read whole file in one shot.
      $msg = ""
      $saved_rc = $False
      try {
         $script:file_contents = get-content "$file"
         $saved_rc = $?
      } catch {
         $msg = $Error | select-object -first 1
         $saved_rc = $False
      }

      # Check for errors
      if (!$saved_rc) {
         log_warning "get_file_contents i=$i msg=$msg"
      }

      # Test code
      # if ($i -le 2) {
      #    $script:file_contents = ""
      # }

      # If we got some data, we are done.
      if ($script:file_contents -and $script:file_contents -ne "") {
         if ($i -gt 1) {
            log_warning "get_file_contents: file=$file read OK on Try#: $i"
         }

         # We need to watch for html that can mess up the log file, such as
         # a complete web page. We remove various items.
         $script:file_contents = $script:file_contents -replace "<!doctype"
         $script:file_contents = $script:file_contents -replace "</"
         $script:file_contents = $script:file_contents -replace "<"
         $script:file_contents = $script:file_contents -replace ">"
         $script:file_contents = $script:file_contents -replace "--"

         # How much of the file data does the user want back?
         $len = ""
         if ($cnt -ne "") {
            if ($cnt -match "^\d+$" -and $cnt -ge 1) {
               # Build up string from multiple lines at needed.
               $bytes = 0
               $str = ""
               foreach ($l in $script:file_contents) {
                  $len = $l.length
                  # "len=$len l=$l"
                  if ($bytes -lt $cnt) {
                     # We have room for more data
                     $room = $cnt - $bytes
                     if ($room -gt $len) {
                        $room = $len
                     }
                     $x = $l.Substring(0,$room)
                     # "room=$room x=$x"
                     $str = "${str}${x}"
                     $bytes = $bytes + $room
                     # "bytes=$bytes str=$str"

                  } else {
                     break
                  }
               }

               # Set the results
               $script:file_contents = $str
               $len = $script:file_contents.length

               # Sanity check.
               if ($len -gt $cnt) {
                  log_error "get_file_contents file=$file len=$len GT cnt=$cnt!"
               }

            } else {
               log_warning "get_file_contents file=$file ignoring cnt=$cnt, must be postive integer!"
            }
         }

         # Log what we really took.
         if ($args -match "-nolog") {
            # Dont log contents!
         } else {
            log_info "get_file_contents file=$file cnt=$cnt len=$len file_contents=$script:file_contents"
         }
         return

      } else {
         # Wait.
         # "get_file_contents i=$i waiting..."
         start-sleep -s 1
      }
   }

   # Multiple tries failed ==> error.
   log_error "get_file_contents: tried $script:max_loop times to read: $file, FAILED!"
}


#==================== get_foreground_window =======================
# Gets the foreground window handle.
#
# Calling parameters: <-warnonly> <-quiet>
# option -warnonly converts errors to warnings
# option -quiet means no warnings, no errors
# Returns: null
# Sets variables: $script:scr_hnd
#==================================================================
function get_foreground_window {

   # NB: If all windows are minimized, you often get handle for the 
   # highlighted window on the taskbar.
   $script:scr_hnd = ""
   # "get_foreground_window args=$args"

   # NB: When getting handle for windows that are continuously updating,
   # such as Task Manager, you may get unexpected results here. Possibly
   # the window loses focus during the update and then takes focus again?

   # On occasion, we get $script:scr_hnd=0, which seems to be an error.
   # So we try multiple times, and warn when this occurs.
   for ($i = 1; $i -le $script:max_loop; $i++) {
      $msg = ""
      $saved_rc = $False
      try {
         $script:scr_hnd = [ApiFuncs]::GetForegroundWindow()
         $script:scr_hnd = [INT] $script:scr_hnd
         $saved_rc = $?
      } catch {
         $msg = $Error | select-object -first 1
         $saved_rc = $False
      }

      # Test code
      # if ($i -le 2) {
      #    $script:scr_hnd = 0
      # }

      # Exit loop on valid window handle.
      # NB: I have seen a few handles that are negative numbers that work OK.
      # "i=$i saved_rc=$saved_rc scr_hnd=$script:scr_hnd"
      if ($saved_rc -and $script:scr_hnd -and $script:scr_hnd -ne 0) {
         break
      }

      # Wait a bit before trying again.
      if ($i -lt $script:max_loop) {
         start-sleep -s 1
      }
   }

   # Look for errors.
   if (!$saved_rc -or !$script:scr_hnd -or $script:scr_hnd -eq 0) {
      if ($args -eq "-warnonly") {
         log_warning "get_foreground_window: tried $script:max_loop times, scr_hnd=$script:scr_hnd got: $msg"
      } elseif ($args -eq "-quiet") {
         # Say nothing!
      } else {
         log_error "get_foreground_window: tried $script:max_loop times, scr_hnd=$script:scr_hnd got: $msg"
      }
      $script:scr_hnd = ""
      return
   }

   # Warn if we had to try multiple times.
   if ($i -ne 1 -and $args -ne "-quiet") {
      log_warning "get_foreground_window tried $i times to get valid scr_hnd=$script:scr_hnd"
   }
}


#==================== get_mouse_position ==========================
# Gets the current position of the mouse pointer.
#
# Calling parameters: none
# Returns: null
# Sets variables: $script:mouse_x, $script:mouse_y
#==================================================================
function get_mouse_position {

   # Get the current mouse position.
   $msg = ""
   $script:mouse_x = ""
   $script:mouse_y = ""
   $saved_rc = $False
   try {
      $pos = [System.Windows.Forms.Cursor]::Position
      $saved_rc = $?
   } catch {
      $msg = $Error | select-object -first 1
      $saved_rc = $False
   }

   # Look for errors.
   log_info "get_mouse_position saved_rc=$saved_rc pos=$pos"
   if ($saved_rc) {
      $script:mouse_x = $pos.X
      $script:mouse_y = $pos.Y
      log_info "get_mouse_position: pos=$pos mouse_x=$script:mouse_x mouse_y=$script:mouse_y"
   } else {
      log_error "get_mouse_position: pos=$pos got: $msg"
   }
}


#==================== get_next_filename ===========================
# Gets the next file name for a screenshot in sequence, based on
# logname & tc. Present working directory is used.
#
# Calling parameters: tc
# Returns: null
# Sets variables: $script:scr_fn
#==================================================================
function get_next_filename {

   # Define filename, taking next sequence number available.
   # We dont want to overwrite any existing files!!!
   # log_info "get_next_filename tc=$tc"
   # $script:logname = "" # test code
   $i = 0
   while ( 1 ) {
      $i++
      $script:scr_fn = "${script:logname}_${tc}_${i}.jpg"
      if (!(Test-Path "$script:scr_fn")) {
         break
      }
   }
   # log_info "get_next_filename tc=$tc scr_fn=$script:scr_fn"
}


#==================== get_screenshot ==============================
# Gets a screenshot of the desired process, window or entire desktop.
# If desired window title or process name is not found, an error is
# logged and a screenshot of the entire desktop is done. 
#
# Calling parameters: tc, type, name
# tc:          - number of currently running test case, used to 
#                select a unique filename for screenshot
# type=fg      - means get the foreground window, whatever it may be
# type=process - means look for a process matching the specifed name
#                and find the corresponding window
# type=window  - means look for a window whose title matches the 
#                specified name
# type=full    - means take screenshot of complete desktop, all window,
#                 task bar, sys tray, as is, no changes.
# name:        - for type=fg, name is the title of the window you  
#                expect to be in the foreground
#              - for type=process, name is the process name you 
#                expect to be able to find 
#              - for type=window, name is the window title you 
#                expect to be able to find
#              - for type=full, name is ignored
#
# Returns: null
# Sets variables: $script:scr_fn, $script:scr_type
#==================================================================
function get_screenshot ($tc="tc_1", $type="fg", $name="") {

   # Always log calling data.
   log_info "get_screenshot tc=$tc type=$type name=$name"

   # Get the next screenshot filename in sequence for this tc.
   get_next_filename $tc
   # log_info "get_screenshot scr_fn=$script:scr_fn"

   # Save the screenshot type actually done, used by log_screenshot.
   # If we get errors, this will be updated to "full".
   $script:scr_type = $type

   # How much of screen we copy depends on the specified type.
   if ($type -eq "fg") {
      get_foreground_window
      get_window_title $script:scr_hnd
      # $script:scr_hnd = "" # test code
      # $script:scr_title = "" # test code
      if ($script:scr_hnd -eq "") {
         # No foreground window found, switch to full screenshot mode.
         log_debug "get_screenshot tc=$tc type=$type name=$name scr_hnd=null, get_foreground_window should have already logged an error, getting full screenshot."
         $WindowBounds = [Windows.Forms.SystemInformation]::VirtualScreen
         $script:scr_type = "full"

      } elseif ($name -ne "" -and -not ($script:scr_title -match $name)) {
         # Foreground window title does not match what we expected, switch to full screenshot mode.
         log_error "get_screenshot tc=$tc type=$type name=$name NOT MATCHED scr_hnd=$script:scr_hnd scr_title=$script:scr_title, getting full screenshot."
         $WindowBounds = [Windows.Forms.SystemInformation]::VirtualScreen
         $script:scr_type = "full"

      } else {
         # Get foregound window position info.
         $WindowBounds=Get-WindowPosition $script:scr_hnd
      }

   } elseif ($type -eq "process") {
      # Find a window based on the desired process name.
      $saved_rc = $False
      try {
         $proc_pid = get-process -name *$name* -erroraction silentlycontinue 
         $saved_rc = $?
      } catch {
         $saved_rc = $False
      }

      # Look for errors.
      # log_info "get_screenshot saved_rc=$saved_rc proc_pid=$proc_pid"
      # $proc_pid = "" # test code
      if (!$saved_rc -or !$proc_pid -or $proc_pid -eq "") {

         # Log error and switch to full screenshot mode.
         log_error "get_screenshot tc=$tc type=$type name=$name proc_pid=$proc_pid not found, getting full screenshot."
         $WindowBounds = [Windows.Forms.SystemInformation]::VirtualScreen
         $script:scr_type = "full"

      } else {
         # Watch out for multiple processes with same name. Warn if more than one!
         $cnt = $proc_pid.Count
         # "cnt=$cnt"
         if ($cnt -gt 1) {
            log_warning "get_screenshot tc=$tc type=$type name=$name found $cnt matching pids: $proc_pid, taking first one."
            $proc_pid = $proc_pid | select-object -first 1
         }

         # Get window handle and position info based on the chosen pid.
         $proc_hnd = $proc_pid.MainWindowHandle
         # log_info "get_screenshot tc=$tc type=$type name=$name proc_pid=$proc_pid proc_hnd=$proc_hnd"

         # Window needs to be restored and in the foreground. Otherwise we get an empty  
         # screenshot or possibly other windows will be overlapping the desired window.
         restore_window $proc_hnd
         $WindowBounds=Get-WindowPosition $proc_hnd
      }

   } elseif ($type -eq "window") {
      # Find a window with the desired title.
      resolve_window_handle $name

      # Look for errors.
      # log_info "get_screenshot curr_hnd=$script:curr_hnd"
      # $script:curr_hnd = "" # test code
      if (!$script:curr_hnd) {

         # Log error and switch to full screenshot mode.
         log_error "get_screenshot tc=$tc type=$type name=$name curr_hnd=$script:curr_hnd not found, getting full screenshot."
         $WindowBounds = [Windows.Forms.SystemInformation]::VirtualScreen
         $script:scr_type = "full"

      } else {
         # Window needs to be restored and in the foreground. Otherwise we get an empty  
         # screenshot or possibly other windows will be overlapping the desired window.
         restore_window $script:curr_hnd
         $WindowBounds=Get-WindowPosition $script:curr_hnd
      }

   } elseif ($type -eq "full") {
      # Want full screen area, showing all windows, task bar, etc.
      $WindowBounds = [Windows.Forms.SystemInformation]::VirtualScreen

   } else {
      log_error "get_screenshot: Invalid type=$type, tc=$tc, name=$name"
      $script:scr_fn = ""
      $script:scr_type = ""
      return
   }

   # Extract the screen size & location info.
   $w = $WindowBounds.Width
   $h = $WindowBounds.Height
   $l = $WindowBounds.Location
   $s = $WindowBounds.Size
   log_debug "get_screenshot w=$w h=$h l=$l s=$s"

   # Sanity check.
   # $w = 0 # test code
   # $h = 0 # test code
   if ($w -lt 100 -or $h -lt 100) {
      log_error "get_screenshot small window, w=$w h=$h l=$l s=$s"
   } 

   # Check if either dimension less than 100.
   if ($w -le 100) {
      $w = 100
      $s.Width = 100
   }
   if ($h -le 100) {
      $h = 100
      $s.Height = 100
   }

   # Get screenshot - based on obscuresecurity.blogspot.com
   $ScreenshotObject = New-Object Drawing.Bitmap $w, $h
   $DrawingGraphics = [Drawing.Graphics]::FromImage($ScreenshotObject)
   $DrawingGraphics.CopyFromScreen($l, [Drawing.Point]::Empty, $s)
   $DrawingGraphics.Dispose()

   # Save image to disk. Trial and error shows default format is .png.
   # The second parameter for Save() lets you choose the format, eg: jpeg
   # NB: If the actual format does not match the filename.type used, 
   #     then IE will refuse to display the picture!
   $msg = ""
   $saved_rc = $False
   try {
      $ScreenshotObject.Save("$pwd\$script:scr_fn","Jpeg")
      $saved_rc = $?
   } catch {
      $msg = $Error | select-object -first 1
      $saved_rc = $False
   }

   # Look for errors.
   # "saved_rc=$saved_rc"
   if (!$saved_rc) {
      log_error "get_screenshot tc=$tc, type=$type, name=$name save got: $msg"
      $script:scr_fn = ""
      $script:scr_type = ""
   } else {
      # Keep track of total screenshots created.
      $script:total_screenshot++
   }
   $ScreenshotObject.Dispose()
}


#==================== get_window_obj ==============================
# Gets the window object based on the specified handle.
#
# Calling parameters: hnd <-warnonly> <-quiet>
# option -warnonly converts errors to warnings
# option -quiet means no errors or warnings at all
# Returns: null
# Sets variables: $script:curr_obj
#==================================================================
function get_window_obj ($hnd="") {

   # NB: Null hnd is handled gracefully.

   # Loop thru all windows looking for hnd.
   $script:curr_obj = ""
   $wind_list = select-window -title *
   foreach ($w in $wind_list) {
      $h = $w.handle
      $p = $w.processid
      $t = $w.title
      # "get_window_obj h=$h p=$p t=$t w=$w"
      if ($h -eq $hnd) {
         $script:curr_obj = $w
         log_debug "get_window_obj: hnd=$hnd MATCH h=$h p=$p t=$t w=$w"
         return
      }
   }

   # Handle not found.
   if ($args -eq "-quiet") {
      # Say nothing!
   } elseif ($args -eq "-warnonly") {
      log_warning "get_window_obj: window NOT found, hnd=$hnd"
   } else {
      log_error "get_window_obj: window NOT found, hnd=$hnd"
   }
}


#==================== get_window_pid ==============================
# Gets the processid of window specied by hnd.
#
# Calling parameters: hnd <-warnonly>
# option -warnonly converts errors to warnings
# Returns: null
# Sets variables: $script:curr_pid
#==================================================================
function get_window_pid ($hnd="") {

   # NB: Null hnd is handled gracefully.

   # NB: Looking at handles associated with pid doesnt match anything!

   # Brute force matching is used.
   $script:curr_pid = "**error**"
   $wind_list = select-window -title *
   foreach ($obj in $wind_list) {
      $h = $obj.handle
      $p = $obj.processid
      # "get_window_pid h=$h p=$p obj=$obj"
      if ($h -eq $hnd) {
         $script:curr_pid = $p
         log_debug "get_window_pid: match hnd=$hnd h=$h p=$p"
         return
      }
   }

   # process not found ==> error/warning
   if ($args -eq "-warnonly") {
      log_warning "get_window_pid: hnd=$hnd no pid found!"
   } else {
      log_error "get_window_pid: hnd=$hnd no pid found!"
   }
}


#==================== get_window_title ============================
# Gets the title of the window specied by hnd.
#
# Calling parameters: hnd <-warnonly> <-quiet>
# option -warnonly converts errors to warnings
# option -quiet means no errors and no warnings
#
# Returns: null
# Sets variables: $script:scr_title
#==================================================================
function get_window_title ($hnd="") {

   # Check for valid handle.
   $script:scr_title = ""
   # "get_window_title hnd=$hnd args=$args"
   if (!$hnd -or $hnd -eq "" -or $hnd -eq 0) {
      if ($args -eq "-quiet") {
         # Say nothing!
      } elseif ($args -eq "-warnonly") {
         log_warning "get_window_title: invalid hnd=$hnd !"
      } else {
         log_error "get_window_title: invalid hnd=$hnd !"
      }
      return
   }

   # Try multiple times to get the window title based on the specified handle. 
   for ($i = 1; $i -le $script:max_loop; $i++) {
      # Get the window title text
      $msg = ""
      try {
         $len = [Apifuncs]::GetWindowTextLength($hnd)
         $sb = New-Object text.stringbuilder -ArgumentList ($len + 1)
         $rtnlen = [Apifuncs]::GetWindowText($hnd,$sb,$sb.Capacity)
         # "len=$len rtnlen=$rtnlen"
         $script:scr_title = $sb.tostring()
      } catch {
         # Will go here for invalid handle, eg: 0
         $msg = $Error | select-object -first 1
         log_debug "get_window_title got: $msg"
      }
      # log_info "get_window_title: hnd=$hnd len=$len scr_title=$script:scr_title"

      # Test code
      # if ($i -le 2) {
      #    $len = ""
      #    $script:scr_title = ""
      # }

      # Exit loop when title found.
      if ($script:scr_title -and $script:scr_title -ne "") {
         if ($i -gt 1 -and $args -ne "-quiet") {
            log_warning "get_window_title: tried $i times for hnd=$hnd len=$len scr_title=$script:scr_title, OK"
         }
         return
      }

      # Wait a bit before trying again.
      if ($i -lt $max_loop) {
         start-sleep -s 1
      }
   }

   # We failed...
   if ($args -eq "-quiet") {
      # Say nothing!
   } elseif ($args -eq "-warnonly") {
      log_warning "get_window_title: tried $script:max_loop times, failed for hnd=$hnd len=$len scr_title=$script:scr_title"
   } else {
      log_error "get_window_title: tried $script:max_loop times, failed for hnd=$hnd len=$len scr_title=$script:scr_title"
   }
}


#==================== go_nogo_network_link ========================
# If both ping_test & date_time_sync have failed, then halt the
# tests. The network link is not working, so stop here.
#
# Calling parameters: none
# Returns: null
#==================================================================
function go_nogo_network_link {

   # If the network tests are not being run, then quietly return.
   if (!$script:test_network) {
      return
   }

   # If either tc_1 or tc_2 are NOT in the list of tests to run, 
   # then quietly return. 
   # "go_nogo_network_link tc_list=$script:tc_list"
   $found_tc_1 = $false
   $found_tc_2 = $false
   foreach ($tc in $script:tc_list) {
      # "tc=$tc"
      if ($tc -eq "tc_1") {
         # "FOUND: $tc"
         $found_tc_1 = $true
         continue
      }
      if ($tc -eq "tc_2") {
         # "FOUND: $tc"
         $found_tc_2 = $true
         continue
      }
   }

   # We only make the go/nogo decision if both tc_1 & tc_2 have been run.
   # "go_nogo_network_link found_tc_1=$found_tc_1 found_tc_2=$found_tc_2"
   if ($found_tc_1 -eq $false -or $found_tc_2 -eq $false) {
      log_debug "go_nogo_network_link returning, found_tc_1=$found_tc_1 found_tc_2=$found_tc_2"
      return
   }

   # Sanity check on ping_cnt
   # $script:ping_ok_cnt = 0 # test code
   # $script:ping_cnt = 0 # test code
   if (!($script:ping_cnt -match "^\d+$") -or $script:ping_cnt -le 0) {
      $script:ping_cnt = 1
   }

   # Both tests were run. If the ping test passed, the network link is working, so return.
   $percent = ($script:ping_ok_cnt * 100.0) / $script:ping_cnt
   if ($percent -ge $script:ping_pass_percent) {
      log_info "go_nogo_network_link returning, ping_ok_cnt=$script:ping_ok_cnt percent=$percent ping_pass_percent=$script:ping_pass_percent"
      return
   }

   # Both tests were run. If the date sync test passed, the network link is working, so return.
   # $script:date_time_sync_rc = $false # test code
   # $script:date_time_sync_txt = "" # test code
   if ($script:date_time_sync_rc  -and $script:date_time_sync_txt -ne "") {
      log_info "go_nogo_network_link returning, date_time_sync_rc=$script:date_time_sync_rc date_time_sync_txt=$script:date_time_sync_txt"
      return
   }

   # OK, we are now completely toasted! 
   log_error "go_nogo_network_link halting tests, ping_ok_cnt=$script:ping_ok_cnt percent=$percent% ping_pass_percent=$script:ping_pass_percent% date_time_sync_rc=$script:date_time_sync_rc date_time_sync_txt=$script:date_time_sync_txt"
   # NB: must put braces around % for send_keys, eg: {%}
   $msg = "`n`nMessage from: go_nogo_network_link`n`n"
   $msg = "${msg}Both the ping test and the date/time sync tests failed!`n`n"
   $msg = "${msg}Details:`n"
   $msg = "${msg}ping_host=$script:ping_host ping_cnt=$script:ping_cnt `n"
   $msg = "${msg}ping_ok_cnt=$script:ping_ok_cnt percent=$percent{%} ping_pass_percent=$script:ping_pass_percent{%}`n"
   $msg = "${msg}date_time_sync_rc=$script:date_time_sync_rc `n" 
   $msg = "${msg}date_time_sync_txt=$script:date_time_sync_txt `n`n"
   $msg = "${msg}The network link is clearly NOT working!`n`n"
   $msg = "${msg}The tests are now being HALTED!`n`n"
   $msg = "${msg}Please fix the network link and try these tests again.`n`n`n"
   log_fatal_error $msg  # Script will exit here
}


#==================== handle_child_popup ==========================
# Handles an optional child popup window.
#
# Calling parameters: app_hnd title_patt
# title_patt is the pattern of the desired button(s) to look for.
#
# Returns: null
#==================================================================
function handle_child_popup ($app_hnd="", $title_patt="") {

   # Sanity check.
   if (!$app_hnd -or $app_hnd -eq 0 -or !$title_patt -or $title_patt -eq "") {
      log_error "handle_child_popup invalid app_hnd=$app_hnd title_patt=$title_patt"
      return
   }

   # We may get a new child popup.
   start-sleep -s 2
   $ch = select-childwindow -window $app_hnd
   if (!$ch) {
      log_info "handle_child_popup app_hnd=$app_hnd no child popup found, OK"
      return
   }

   # Click the first button matching title_patt. 
   log_info "handle_child_popup app_hnd=$app_hnd found child popup ch=$ch"
   get_controls $ch
   search_controls "button" "$title_patt" 1 -warnonly
   if ($script:ctl_hnd) {
        send_click $script:ctl_hnd
   }
}


#==================== handle_simpress_wizard ======================
# Open office simpress starts off with a Wizard to create new
# document. This routine handles the prompts.
#
# Calling parameters: app_name
# Returns: null
#==================================================================
function handle_simpress_wizard ($app_name="") {

   # If its not open office simpress running, we are done.
   if (!($app_name -match "simpress")) {
      log_debug "handle_simpress_wizard app_name=$app_name returning" 
      return
   }

   # Look for open office presentation wizard window.
   log_info "handle_simpress_wizard starting"
   resolve_window_handle "wizard"
   $h = $script:curr_hnd

   # Log entry to show we ran.
   log_info "handle_simpress_wizard found h=$h"

   # Make sure window is focussed
   restore_window $h

   # Send Alt-n to click the next button.
   send_keys $h "%n"
   start-sleep -s 2

   # Send Alt-n to click the next button.
   send_keys $h "%n"
   start-sleep -s 2

   # Send Alt-c to click the create button.
   send_keys $h "%c"
   start-sleep -s 2
   log_info "handle_simpress_wizard done"
}


#==================== load_rc =====================================
# Loads the optional run control file test-apps-rc.ps1, if found.
# This allows for different settings to be used on selected PC.
#
# For example, some PC may not have MS Office installed, and you
# want the MS Office tests to be skipped. You can also specify 
# different try counts, iteration counts, colors, etc. Everything 
# in the User Defined Variables section at the top of this script
# is fair game. 
#
# NB: While you probably could manipulate other variables elsewhere
# in the script, please DONT!
#
# Calling parameters: none
# Returns: null
# Sets variables: as specified in the run control file.
#==================================================================
function load_rc {

   # Look for optional run control file.
   # "load_rc_fn=$script:load_rc_fn"
   if (!(test-path "$script:load_rc_fn")) {
      # "load_rc optional $script:load_rc_fn not found, OK"
      return
   }

   # Read the run control file.
   get_file_contents $script:load_rc_fn

   # Execute the run control file one line at a time. This is intended
   # to prevent loading of code more complex than one line.
   $i = 0
   foreach ($l in $script:file_contents) {

      # Skip comments and blank lines.
      $i++
      $l = $l.trim()
      # "l=$l"
      if ($l -eq "" -or $l -match "^#") {
         # "load_rc skipping: $l"
         continue
      }

      # Try to execute this line of code.
      $msg = ""
      $saved_rc = $false
      try {
         invoke-expression "$l"
         $saved_rc = $?
      } catch {
         $msg = $error | select-object -first 1
         $saved_rc = $false
      }

      # Check for errors, exit on error.
      if (!$saved_rc) {
         log_fatal_error "load_rc i=$i l=$l saved_rc=$saved_rc msg=$msg"
      }
   }
}


#==================== load_user32 =================================
# Loads selected functions from user32.dll
#
# Calling parameters: none
# Returns: null
# Sets variables: $script:desk_hnd, $script:showWindowAsync
#==================================================================
function load_user32 {

   # Load slected routines from user32.dll.

   # NB: Right now add-type doesnt throw errors, but returns true/false.
   # The try-catch block may help in the future.

   # GetForegroundWindow, etc - from Monitor Flashview example at poshcode.org
   $msg = ""
   $saved_rc = $False
   try {
      Add-Type @"
         using System;
         using System.Collections.Generic;
         using System.Runtime.InteropServices;
         using System.Text;
         public class ApiFuncs   {
            [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            public static extern int GetWindowText(IntPtr hwnd,StringBuilder lpString, int cch);
            [DllImport("user32.dll", SetLastError=true, CharSet=CharSet.Auto)]
            public static extern IntPtr GetForegroundWindow();
            [DllImport("user32.dll", SetLastError=true, CharSet=CharSet.Auto)]
            public static extern Int32 GetWindowThreadProcessId(IntPtr hWnd,out Int32 lpdwProcessId);
            [DllImport("user32.dll", SetLastError=true, CharSet=CharSet.Auto)]
            public static extern Int32 GetWindowTextLength(IntPtr hWnd);
        }
"@
      $saved_rc = $?
   } catch {
      $msg = $Error | select-object -first 1
      $saved_rc = $False
   }

   # Look for errors.
   # "saved_rc=$saved_rc"
   if ($saved_rc) {
      # log_info "load_user32: GetForegroundWindow OK"
   } else {
      log_fatal_error "load_user32: $msg"
   }

   # Currently not used
   # Add-Type @"
   #    using System;
   #    using System.Runtime.InteropServices;
   #    public class SFW {
   #       [DllImport("user32.dll")]
   #       [return: MarshalAs(UnmanagedType.Bool)]
   #       public static extern bool SetForegroundWindow(IntPtr hWnd);}
   # "@

   # Currently not used
   # Add-Type @"
   #    using System;
   #    using System.Runtime.InteropServices;
   #    public class Tricks {
   #       [DllImport("user32.dll")]
   #       [return: MarshalAs(UnmanagedType.Bool)]
   #       public static extern bool AllowSetForegroundWindow(IntPtr hWnd);}
   # "@

   # GetDesktopWindow - from MSDN
   $signature = @"
      [DllImport("user32.dll")]
      public static extern IntPtr GetDesktopWindow();
"@
   $msg = ""
   $saved_rc = $False
   try {
      $getDesktopWindow = Add-Type -memberDefinition $signature -name "Win32GetDesktopWindow" -namespace Win32Functions -passThru
      $saved_rc = $?
   } catch {
      $msg = $Error | select-object -first 1
      $saved_rc = $False
   }

   # Look for errors.
   # "saved_rc=$saved_rc"
   if ($saved_rc) {
      # log_info "load_user32: GetDesktopWindow OK"
   } else {
      log_fatal_error "load_user32: $msg"
   }

   # Desktop window handle stays the same for entire Windows session,
   # so we need to get the desktop handle only once.
   $script:desk_hnd = $getDesktopWindow::GetDesktopWindow()

   # ShowWindowAsync - from get-help add-type -examples 
   $signature = @"
      [DllImport("user32.dll")]
      public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);
"@
   $msg = ""
   $saved_rc = $False
   try {
      $script:showWindowAsync = Add-Type -memberDefinition $signature -name "Win32ShowWindowAsync" -namespace Win32Functions -passThru
      $saved_rc = $?
   } catch {
      $msg = $Error | select-object -first 1
      $saved_rc = $False
   }

   # Look for errors.
   # "saved_rc=$saved_rc"
   if ($saved_rc) {
      # log_info "load_user32: ShowWindowAsync OK"
   } else {
      log_fatal_error "load_user32: $msg"
   }
}


#==================== load_wasp ===================================
# Loads wasp.dll which provide many useful GUI functions.
#
# Calling parameters: none
# Returns: null
#==================================================================
function load_wasp {

   # Check selected directories for desired dll.
   $dll = "wasp.dll"
   $dir_list = $pwd, $script:wasp_dir
   foreach ($dir in $dir_list) {
      $script:wasp_path = "$dir\$dll"
      if (test-path "$wasp_path") {
         # Found.
         # log_debug "found $script:wasp_path"
         break
      } else {
         # NOT found.
         # log_debug "NOT found $script:wasp_path"
         $script:wasp_path = ""
      }
   }

   # Stop script if we didnt find dll.
   if (!$script:wasp_path) {
      log_fatal_error "load_wasp: could not find $dll in any of: $dir_list"
   }

   # Load the wasp module
   $msg = ""
   $saved_rc = $False
   try {
      Import-Module $script:wasp_path
      $saved_rc = $?
   } catch {
      $msg = $Error |select-object -first 1
      $saved_rc = $False
   }

   # Look for errors.
   # "saved_rc=$saved_rc"
   if ($saved_rc) {
      # log_info "load_wasp: wasp OK"
   } else {
      # Stop script if dll didnt load.
      log_fatal_error "load_wasp: $msg"
   }
}


#==================== log_bold ====================================
# Puts a bold message on the logfile and the terminal window. Color
# used is defined at top of the script.
#
# Calling parameters: msg 
# Returns: null
#==================================================================
function log_bold ($msg="") {

   # Add INFO: prefix.
   $msg = "INFO: $msg"

   # Define appropriate html tags.
   $h1 = "<b><font color=`"$script:color_bold`">"
   $h2 = "</font></b>"

   # Handoff to log_msg.
   log_msg $msg $h1 $h2
}


#==================== log_debug ===================================
# Hides a debug message in the logfile as an html comment. To see
# the debug messages, right click on the browser web page, choose
# "View page source" and then the DEBUG: messages will be visible.
# The message will also be displayed on the terminal window.
#
# Calling parameters: msg 
# Returns: null
#==================================================================
function log_debug ($msg="") {

   # Add DEBUG: prefix.
   $msg = "DEBUG: $msg"

   # Set HTML comment tags.
   $h1 = "<!--"
   $h2 = "-->"

   # Handoff to log_msg.
   log_msg $msg $h1 $h2
}


#==================== log_error ===================================
# Puts a bold error message on the logfile and the terminal window.
# Color used is defined at top of the script.
#
# Calling parameters: msg 
# Returns: null
#==================================================================
function log_error ($msg="") {

   # Add ERROR: prefix.
   $msg = "ERROR: $script:tc_num $msg"

   # Define appropriate HTML tags.
   $h1 = "<b><font color=`"$script:color_error`">"
   $h2 = "</font></b>"

   # Increment error counters.
   $script:tc_err++
   $script:total_error++

   # Handoff to log_msg.
   log_msg $msg $h1 $h2
}


#==================== log_fatal_error =============================
# Puts a bold error message on the logfile, the terminal window,
# and opens a new notepad window for the message. Finally the
# the script exits. When this routine is called, you know you are
# completely toasted, with no hope of recovering the situation.
# Game over! Everything stops after this routine is
# called. Color used is defined at top of the script.
#
# Calling parameters: msg 
# Returns: null
#==================================================================
function log_fatal_error ($msg="") {

   # The logfile may not exist yet, and/or may not be displayed in a
   # browser and/or the terminal window may close when the script
   # exits momentarily, so we start a notepad window to also display
   # the fatal error message. The notepad window will continue to
   # exist even if the script terminal window closes.
   start_app notepad "" -forceapp
   $hnd = $script:curr_hnd

   # Add FATAL_ERROR: prefix.
   $msg = "$script:self FATAL_ERROR: $script:tc_num $msg"

   # Define appropriate HTML tags.
   $h1 = "<b><font color=`"$script:color_fatal`">"
   $h2 = "</font></b>"

   # Handoff to log_msg.
   log_msg $msg $h1 $h2

   # Add entry to summary log.
   log_synopsis $script:color_fatal "-" "-" "FATAL_ERROR" "-" "$msg"

   # Finally, show the msg in the notepad window.
   send_keys $hnd $msg

   # We know we are completely toasted, so exit the script.
   # Game over!
   exit
}


#==================== log_info ====================================
# Puts a normal message on the logfile and the terminal window. 
# The color used is defined at top of the script. 
#
# Calling parameters: msg
# Returns: null
#==================================================================
function log_info ($msg="") {

   # NB: Null msg is allowed, so as to create whitespace on logfile
   # and the terminal window.

   # NB: DONT trim the msg, as that removes leading/trailing new-lines
   # used for visual breaks on the terminal window and the logfile.   

   # Add INFO: prefix when appropriate.
   if ($msg -ne "") {
      $msg = "INFO: $msg"
   }

   # Define appropriate HTML tags.
   $h1 = "<font color=`"$script:color_info`">"
   $h2 = "</font>"

   # Handoff to log_msg.
   log_msg $msg $h1 $h2
}


#==================== log_iteration ===============================
# Puts an iteration message on the logfile. Color used is defined 
# at top of the script.
#
# Calling parameters: msg 
# Returns: null
#==================================================================
function log_iteration ($msg="") {

   # Add ITERATION: prefix.
   $msg = "ITERATION: $msg"

   # Define appropriate HTML tags.
   $h1 = "<b><font color=`"$script:color_iteration`">"
   $h2 = "</font></b>"

   # To keep log_msg simple, we do the all the formatting here.
   # Handoff the completely formatted msg off to log_msg.
   $msg = "<hr color=`"$script:color_iteration`"><br>$h1 $msg $h2"
   log_msg $msg
}


#==================== log_misc ====================================
# Logs various items in the logfile.
#
# Calling parameters: none
# Returns: null
#==================================================================
function log_misc {

   # Save localhost specific info.
   add_horizontal_rule
   log_bold "log_misc localhost system info"
   log_info "mfc_prod=$script:mfc_prod mfc_name=$script:mfc_name found_laptop=$script:found_laptop"
   log_info "cpu_name=$script:cpu_name cpu_ghz=$script:cpu_ghz GHz cpu_id=$script:cpu_id cpu_cnt=$script:cpu_cnt"
   log_info "ram_gb=$script:ram_gb GB"
   log_info "host_name=$script:host_name mac_addr=$script:mac_addr ip_addr=$script:ip_addr found_wifi=$script:found_wifi"
   log_info "win_prod=$script:win_prod win_major_ver=$script:win_major_ver win_minor_ver=$script:win_minor_ver"
   log_info "win_act_code=$script:win_act_code"
   log_info "ie_major_ver=$script:ie_major_ver ie_minor_ver=$script:ie_minor_ver ie_build_ver=$script:ie_build_ver"
   log_info "ms_office_path=$script:ms_office_path ms_office_ver=$script:ms_office_ver"
   log_info "open_office_path=$script:open_office_path open_office_major_ver=$script:open_office_major_ver open_office_minor_ver=$script:open_office_minor_ver"

   # Check localhost variables have values.
   # MS office may not be installed, so we dont check those variables.
   $var_list = "win_prod", "win_major_ver", "win_minor_ver", "win_act_code", "mfc_prod", "mfc_name", "cpu_name", "cpu_ghz", "cpu_id", "cpu_cnt", "ram_gb", "host_name", "mac_addr", "ip_addr", "ie_major_ver", "ie_minor_ver", "ie_build_ver"
   foreach ($var in $var_list) {
      # Dereference var name.
      $name = ""
      $name = "`$script:${var}"
      $value = ""
      $value = invoke-expression "$name"
      # log_debug "log_misc var=$var name=$name value=$value"

      # Check we have a valid value.
      # NB: some vars, like win_minor_ver can be 0, so dont demand value GT 0 ! 
      if (!$value -or $value -eq "") {
         log_error "log_misc missing value for variable $name = $value"
      }
   }

   # Where there any unmatched adapter items?
   if ($script:adap_no_match) {
      log_warning "log_misc: adap_no_match=$script:adap_no_match"
   } 

   # Log other info.
   log_info ""
   log_info "pwd=$pwd self=$script:self cmd_args=$script:cmd_args"
   log_info "logfile=$script:logfile"
   log_info "desk_hnd=$script:desk_hnd browser_results=$script:browser_results res_hnd=$script:res_hnd"
   log_info "scr_min_x=$script:scr_min_x scr_max_x=$script:scr_max_x scr_min_y=$script:scr_min_y scr_max_y=$script:scr_max_y"
   log_info "using: $script:wasp_path"
}


#==================== log_msg =====================================
# Puts a message on the terminal window and the logfile using the
# the html tags supplied.
#
# Calling parameters: msg h1 h2
# msg - text string
# h1  - opening html tags
# h2  - closing html tags
#
# Returns: null
#==================================================================
function log_msg ($msg="", $h1="", $h2="") {

   # NB: DONT trim the msg, as that removes leading/trailing new-lines
   # used for visual breaks on the terminal window and the logfile.   

   # Get current date/time in 24HR format.
   $ts = Get-Date
   $ts = $ts.ToString("yyyyMMdd.HHmmss")

   # Add timestamp as appropriate.
   # NB: Null msg is allowed, so as to create whitespace on logfile.
   # NB: Leave <hr ...> msg alone
   if ($msg -ne "" -and !($msg -match "<hr")) {
       $msg = "$ts $msg"
   }

   # Timestamped message is displayed on users terminal window.
   "$msg"

   # Add HTML tags as appropriate.
   # NB: Null msg is allowed, so as to create whitespace on logfile.
   # NB: Leave <hr ...> msg alone
   if ($h1 -ne "" -and $h2 -ne "" -and $msg -ne "" -and !($msg -match "<hr")) {
       $msg = "$h1 $msg $h2"
   }

   # Add break tag, but not for DEBUG: HTML comments, as this 
   # generates unwanted whitespace on the logfile.
   if (!($msg -match "DEBUG:")) {
      if ($msg -eq "") {
         $msg = "<br>" # generates whitespace on logfile
      } else {
         $msg = "${msg}<br>"
      }
   }

   # Cleanup
   remove-variable ts

   # Handoff to add_file_content. It will deal with transient logfile access errors.
   add_file_content $script:logfile $msg
}


#==================== log_screenshot ==============================
# Adds a screenshot in the logfile with links to the full size
# picture.
#
# Calling parameters: tc, type, name, fn
# See get_screenshot for details on tc, type, name
# fn - if null, calls get_screenshot to get a new screenshot
#    - otherwise simply logs the specified filepath
# Returns: null
#==================================================================
function log_screenshot ($tc="1", $type="fg", $name="", $fn="") {

   # If we dont have fn, then we call the get_screenshot routine.
   # This allows screenshots to be taken, saved and added to the logfile
   # at a later point in time. Useful when you want to take a screenshot
   # before the logfile has been initialized, and then add it in later.
   if ($fn -eq "") {
      get_screenshot $tc $type $name
      if ($script:scr_fn -eq "") {
         log_debug "log_screenshot tc=$tc type=$type name=$name fn=$fn get_screenshot should have already logged an error."
         return
      } else {
         $fn = $script:scr_fn
      }
   }

   # Check fn exists.
   if (!(Test-Path "$fn")) {
      log_error "log_screenshot: fn=$fn not found, tc=$tc, type=$type, name=$name"
      return
   }

   # Choose appropriate smaller size for screenshot.
   # $script:scr_type shows what really happened, as opposed to
   # what was asked for.
   if ($type -eq "full" -or $script:scr_type -eq "full") {
      # NB: Using a scale factor here for the full screnshot allows us to 
      # maintain the aspect ratio for the specific PC. Newer screens are wider.
      Add-Type -Assembly System.Windows.Forms 
      $ScreenBounds = [Windows.Forms.SystemInformation]::VirtualScreen
      $w = $script:screenshot_full_scale_factor * $ScreenBounds.Width
      $h = $script:screenshot_full_scale_factor * $ScreenBounds.Height
   } else {
      $w = $script:screenshot_window_width
      $h = $script:screenshot_window_height
   }
   # log_info "log_screenshot w=$w h=$h"

   # Add reduced screenshot to logfile, with links to full size screenshot.
   log_info "Screenshot <a href=`"$fn`">$fn</a> <br> <a href=`"$fn`"><img src=`"$fn`" width=`"$w`" height=`"$h`"></a>"
}


#==================== log_synopsis ================================
# Adds a one-line synopsis of the current test case result or other
# significant event in the summary log.
#
# Calling parameters: color, iter, try, result, tc, title
# Returns: null
#==================================================================
function log_synopsis ($color="", $iter="", $try="", $result="", $tc="", $title="") {

   # Old fixed width text formatting of the data.
   # $iter = "{0,4}" -f $iter
   # $try = "{0,3}" -f $try
   # $result = "{0,-6}" -f $result # negative means left align
   # $tc = "{0,-6}" -f $tc
   # $msg = "$iter   $try   $result   $tc   $title"

   # Get current date/time in 24HR format.
   $ts = Get-Date
   $ts = $ts.ToString("yyyyMMdd.HHmmss")

   # New html formatting of the data.
   $msg  = "   <tr>`n"
   $msg  = "$msg      <td><font color=`"$color`">$ts</font></td>`n"
   $msg  = "$msg      <td align=`"center`"><font color=`"$color`">$iter</font></td>`n"
   $msg  = "$msg      <td align=`"center`"><font color=`"$color`">$try</font></td>`n"
   $msg  = "$msg      <td><b><font color=`"$color`">$result</font></b></td>`n"
   $msg  = "$msg      <td><font color=`"$color`">$tc</font></td>`n"
   $msg  = "$msg      <td><font color=`"$color`">$title</font></td>`n"
   $msg  = "$msg   </tr>`n"

   # Handoff to add_file_content
   add_file_content $script:logsumm $msg
   remove-variable ts
}


#==================== log_warning =================================
# Puts a bold warning message on the logfile. Color used is defined
# at top of the script.
#
# Calling parameters: msg
# Returns: null
#==================================================================
function log_warning ($msg="") {

   # For really demanding tests, warnings are converted to errors.
   if ($script:wae) {
      log_error "WAE: $msg"
      return
   }

   # Add WARNING: prefix.
   $msg = "WARNING: $script:tc_num $msg"

   # Define appropriate HTML tags.
   $h1 = "<b><font color=`"$script:color_warning`">"
   $h2 = "</font></b>"

   # Increment running counters
   $script:total_warning++
   $script:tc_warn++

   # Handoff to log_msg.
   log_msg $msg $h1 $h2
}


#==================== map_app_name ================================
# Some application window titles do not directly correspond to the 
# application name that started them. This routine does the
# appropriate mapping.
#
# Calling parameters: app_name
# Returns: null
# Sets variables: $script:app_title
#==================================================================
function map_app_name ($app_name="") {

   # Selected application windows need a different name.
   if ($app_name -match "iexplore") {
      $script:app_title = "explorer"
   } elseif ($app_name -match "winword") {
      $script:app_title = "word"
   } elseif ($app_name -match "powerpnt") {
      $script:app_title = "powerpoint"
   } elseif ($app_name -match "msseces") {
      $script:app_title = "security"
   } elseif ($app_name -match $script:open_office_app_patt) {
      $script:app_title = $app_name -replace "^s" # drop leading "s"
   } else {
      # Most app window titles match the app name.
      $script:app_title = $app_name
   }
   log_debug "map_app_name app_name=$app_name app_title=$script:app_title" 
}


#==================== misc_outlook_prompts ========================
# Handles miscellaneous prompts from MS Outlook.
#
# Calling parameters: app_hnd
# Returns: null
#==================================================================
function misc_outlook_prompts ($app_hnd=""){

   # After Outlook has been configured, we may get a couple of 
   # prompts that need to be dealt with.
   log_debug "misc_outlook_prompts starting"
   $found = 0
   for ($i = 1; $i -le 3; $i++) {
      start-sleep -s 3 # wait for prompts to appear
      $ch_list = select-childwindow -window $app_hnd
      foreach ($ch in $ch_list) {

         # Decode child window object
         $c = $ch.Class
         $h = $ch.Handle
         $t = $ch.Title
         if (!$h -or $h -eq 0) {
            continue
         }
         log_info "misc_outlook_prompts found c=$c h=$h t=$t"

         # Selected child windows are handled appropriately.
         if ($t -match "outlook") {
            $found++
            log_info "misc_outlook_prompts sending Enter"
            send_keys $h "{enter}"
            continue
         }
         if ($t -match "norton.*antispam|find.*contact") {
            $found++
            close_child_windows $h
            continue
         }

         # Anything else is not expected. Log an error so it will
         # get flagged and hopefully added to the list for processing.
         log_error "misc_outlook_prompts unexpected child window c=$c h=$h t=$t"
      }

      # Did we find anything?
      if ($found -eq 0 -or $found -eq 2) {
         # We got nothing or both of them.
         break
      }
   }
   log_debug "misc_outlook_prompts done"
}


#==================== move_mouse ==================================
# Moves the mouse to the desired X & Y screen coordinates. If the 
# handle is specified, the mouse will be moved to coordinates relative
# to the specified window upper left corner, otherwise the mouse will
# be moved relative to the desktop screen upper left corner.
#
# Calling parameters: hnd, x, y
# Returns: null
#==================================================================
function move_mouse ($hnd="", $x="0", $y="0") {

   # If we have a window handle, then we offset the mouse position
   # relative to that window. Otherwise, the mouse position will be
   # an absolute screen position, regardless of any open windows.
   # log_info "move_mouse hnd=$hnd x=$x y=$y"
   if ($hnd -and $hnd -ne 0) {
      $pos = Get-WindowPosition $hnd
      if (!$?) {
         $msg = $Error | select-object -first 1
         log_error "move_mouse: pos=$pos got: $msg"
      }
      $l = $pos.Location
      $x_off = $l.X
      $y_off = $l.Y
      # log_info "move_mouse l=$l x_off=$x_off y_off=$y_off"
      # If window is shifted off screen to the left or the top, the offsets will be negative!
      # If window is minimized, the offsets will be -32000
      if ($x_off -lt $script:scr_min_x) {
         $x_off = $script:scr_min_x
      }
      if ($y_off -lt $script:scr_min_y) {
         $y_off = $script:scr_min_y
      }
      # log_info "move_mouse adjusted x_off=$x_off y_off=$y_off"
      # Put the n_off first, in case of negative coordinates!
      $x = $x_off + $x
      $y = $y_off + $y
      # log_info "move_mouse offset x=$x y=$y"
   }

   # Sanity checks that we are still inside the screen pixel range.
   if ($x -lt $script:scr_min_x) {
      log_warning "move_mouse: x=$x LT scr_min_x=$script:scr_min_x, set to: $script:scr_min_x"
      $x = $script:scr_min_x
   }
   if ($x -gt $script:scr_max_x) {
      log_warning "move_mouse: x=$x GT scr_max_x=$script:scr_max_x, set to: $script:scr_max_x"
      $x = $script:scr_max_x
   }
   if ($y -lt $script:scr_min_y) {
      log_warning "move_mouse: y=$y LT scr_min_y=$script:scr_min_y, set to: $script:scr_min_y"
      $y = $script:scr_min_y
   }
   if ($y -gt $script:scr_max_y) {
      log_warning "move_mouse: y=$y GT scr_max_y=$script:scr_max_y, set to: $script:scr_max_y"
      $y = $script:scr_max_y
   }

   # Move the mouse to the desired position.
   # log_info "move_mouse final x=$x y=$y"
   [System.Windows.Forms.Cursor]::Position = New-Object System.Drawing.Point($x, $y)
   if (!$?) {
      $msg = $Error | select-object -first 1
      log_error "move_mouse: $msg"
   }
}


#==================== move_mouse_click ============================
# Moves the mouse to the desired X & Y screen coordinates. If the 
# window handle is specified, mouse position is relative to the 
# window. The $args control the mouse action, default is left 
# single click.

# NB: I am having challenges with WASP send-click.
# Firefox will accept the click on the File menu, but not the click
# on the main web page display. IE & Chrome ignore the clicks. 
# Notepad ignores the click on the File menu, but does correctly
# click on the main edit area, select all text and copy it to the
# window clipboard.
#
# Calling parameters: hnd, x, y, args
# Returns: null
#==================================================================
function move_mouse_click ($hnd="", $x="0", $y="0") {

   # Sanity check.
   if (!$hnd -or $hnd -eq "" -or $hnd -eq 0) {
      log_error "move_mouse_click invalid hnd=$hnd"
      return
   }

   # Set the default mouse behaviour.
   if (!$args) {
      $args = "-button left"
   }
   log_info "move_mouse_click hnd=$hnd x=$x y=$y args=$args"

   # Position the mouse pointer.
   move_mouse $hnd $x $y

   # Now click the mouse.
   $msg = ""
   $saved_rc = $False
   try {
      # NB: send-click has parsing issues with $args if we dont use the invoke-expression!
      invoke-expression "send-click -left $x -top $y -window $hnd $args"
      $saved_rc = $?
   } catch {
      $err = $Error | select-object -first 1
      $saved_rc = $False
   }

   # Check for errors.
   # "saved_rc=$saved_rc"
   if (!$saved_rc) {
      $msg = $Error | select-object -first 1
      log_error "move_mouse_click: $msg"
   }
}


#==================== move_window =================================
# Moves the specified window to the desired X & Y desktop screen
# coordinates. 
#
# Calling parameters: hnd, x, y
# Returns: null
#==================================================================
function move_window ($hnd="", $x="0", $y="0") {

   # Sanity check.
   if (!$hnd -or $hnd -eq "" -or $hnd -eq 0) {
      log_error "move_window invalid hnd=$hnd"
      return
   }

   # Error handling wrapper for WASP set-windowposition.
   set-windowposition -window $hnd -X $x -Y $y
   if (!$?) {
      $msg = $Error | select-object -first 1
      log_error "move_window: $msg"
   }
   sleep-start -m 500
}


# See: http://msdn.microsoft.com/en-us/library/windows/desktop/ms633548%28v=vs.85%29.aspx
# 0 - SW_HIDE - hides window, activates another window
# 1 - SW_SHOWNORMAL - restores window to foreground, but user has to click to choose new window location
# 2 - SW_SHOWMINIMIZED - minimizes window, leaves window as active
# 3 - SW_SHOWMAXIMIZED - maximize window, but it can still be in background behind other windows
# 4 - SW_SHOWNOACTIVATE - same as 5
# 5 - SW_SHOW - restores window, but window still in background behind other windows
# 6 - SW_MINIMIZE - minimizes window, activates another window
# 7 - SW_SHOWMINNOACTIVE
# 8 - SW_SHOWNA - same as 5
# 9 - SW_RESTORE - restores window to foreground, but user has to click to choose new window size
# 10 - SW_SHOWDEFAULT - same as 5
# 11 - SW_FORCEMINIMIZE


#==================== minimize_all_windows ========================
# Minimizes all windows.
#
# Calling parameters: none
# Returns: null
#==================================================================
function minimize_all_windows {

   # Minimize all windows.
   $err  = "" # list of window titles that got errors.
   $list = select-window -title *
   foreach ($w in $list) {
     $t = $w.Title
     # "t=$t"
     $saved_rc = $false
     try {
        $script:showWindowAsync::ShowWindowAsync($w,2) | out-null
        $saved_rc = $?
     } catch {
        $saved_rc = $false
     }

     # Collect list of window titles that got errors.
     if (!$saved_rc) {
        $err = "$err $t"
     }
     start-sleep -m 200 
   }

   # Check for errors
   if ($err) {
      $msg = $Error | select-object -first 1
      log_error "minimize_all_windows err=$err got: $msg"
   }
}


#==================== minimize_window =============================
# Minimizes just the single specified window.
#
# Calling parameters: hnd
# Returns: null
#==================================================================
function minimize_window ($hnd) {

   # Sanity check.
   if (!$hnd -or $hnd -eq "" -or $hnd -eq 0) {
      log_error "minimize_window invalid hnd=$hnd"
      return
   }

   # Minimize just the specfied single window only.
   $saved_rc = $false
   $msg = ""
   try {
      $script:showWindowAsync::ShowWindowAsync($hnd,2) | out-null
      $saved_rc = $?
   } catch {
      $msg = $Error | select-object -first 1
      $saved_rc = $false
   }
   if (!$saved_rc) {
      log_error "minimize_window hnd=$hnd got: $msg"
   }
   start-sleep -m 200
}


#==================== new_tab =====================================
# Starts a new tab in the specified browser window. Optional
# app_args will be sent to the address navigation bar.
#
# Calling parameters: app_hnd, app_name, app_args
# Returns: null
#==================================================================
function new_tab ($app_hnd="", $app_name="", $app_args="") {

   # Sanity check.
   if (!$app_hnd -or $app_hnd -eq "" -or $app_hnd -eq 0 -or !$app_name -or $app_name -eq "") {
      log_error "new_tab invalid app_hnd=$app_hnd app_name=$app_name"
      return
   }

   # Open new tab with ctrl-t
   log_info "new_tab: app_hnd=$app_hnd app_name=$app_name opening new tab"
   start-sleep -s 1
   send_keys $app_hnd "^t"
   start-sleep -s 1

   # For iexplorer, clear the nav bar. This ensures that whatever is next
   # sent to the browser will go to the address navigation bar.
   if ($app_name -match "iexplore") { 
      clear_nav_bar $app_hnd
   }

   # Send app_args to application. For a web browser, these will
   # go to the address navigation bar. 
   if ($app_args -and $app_args -ne "") {
      log_info "new_tab: sending app_args=$app_args"
      send_keys $app_hnd "${app_args}{enter}"
      start-sleep -s 1
   }
}


#==================== open_office_recovery ========================
# Open Office apps occasionally do not get shut down properly. The
# next time the app runs, you get dialog boxes about recovery that
# need to be handled.
#
# Calling parameters: app_name
# Returns: null
#==================================================================
function open_office_recovery ($app_name="") {

   # NB: To test this code, have the test case NOT do Alt-fx, and
   # let stop_process kill the app. That will trigger the recovery. 

   # If its not one of the OpenOffice applications running, we are done.
   if (!($app_name -match $script:open_office_app_patt)) {
      log_debug "open_office_recovery app_name=$app_name returning" 
      return
   }

   # Look for Open Office recovery window. The recovery window
   # will NOT have the app_title in the window title!
   $x = select-window -title *open*office*
   $x = $x | select-object -first 1
   $c = $x.Class
   $h = $x.Handle
   $n = $x.ProcessName
   $p = $x.ProcessId
   $t = $x.Title
   map_app_name $app_name 
   $app_title = $script:app_title
   # The recovery window will NOT have the app_title in the window title!
   if (!$h -or !($n -match $oo_app_patt) -or $t -match $app_title) {
      log_debug "open_office_recovery app_title=$app_title c=$c h=$h n=$n p=$p t=$t, NOT RECOVERY"
      return
   }

   # Log entry to show we ran, log screenshot.
   log_warning "open_office_recovery found app_title=$app_title c=$c h=$h n=$n p=$p t=$t, looks like RECOVERY"
   log_screenshot "" "fg" "open.*office"

   # Make sure window is focussed
   restore_window $h

   # Send tab-enter to click the cancel button.
   # Done as 2 send_keys, for benefit of slower PC.
   send_keys $h "{tab}"
   start-sleep -m 500
   send_keys $h "{enter}"
   start-sleep -s 2

   # Send enter to click the yes button.
   send_keys $h "{enter}"
   start-sleep -s 2

   # Check that we are done.
   get_window_obj $h -quiet
   log_debug "open_office_recovery h=$h curr_obj=$script:curr_obj"
   if ($script:curr_obj) {
      log_error "open_office_recovery not completed, h=$h curr_obj=$script:curr_obj, see screenshot below"
      log_screenshot "" "full" # shows desktop at start of test case
   }
}


#==================== parse_cmd_line ==============================
# Parses the command line options, set variables accordingly.
#
# Calling parameters: none
# Returns: null
# Sets variables: $script:testcase_try_max, $script:testsuite_interation_max,
#                 $script:tc_list
#==================================================================
function parse_cmd_line {

   # Parse the command line tokens
   " " # add whitespace on terminal
   $state = ""
   $cnt = $script:cmd_args.Count # count works OK here for 0 or 1 items
   # "cnt=$cnt cmd_args=$script:cmd_args"
   for ($i = 0; $i -lt $cnt; $i++) {
      # Get next token
      $t = $script:cmd_args[$i]
      # "i=$i t=$t"

      # Look for online help request. Look for miniumum strings.
      if ($t -match "^-h" -or $t -match "^/\?") {
         clear-host
         script_help
         exit 1

      # Look for -try keyword, but no more.
      } elseif ($t -match "^-try$") {
         # We expect a positive integer after -try.
         $i++
         $t = $script:cmd_args[$i]
         # "i=$i t=$t"
         if ($t -match "^\d+$" -and $t -ge 1) {
            $script:testcase_try_max = $t
            # "testcase_try_max=$script:testcase_try_max"
            continue
         } else {
            log_error "parse_cmd_line: invalid value for -try: $t must be positive integer!"
            exit 1
         }

      # Look for -iter keyword, but no more.
      } elseif ($t -match "^-iter$") {
         # We expect a positive integer after -iter.
         $i++
         $t = $script:cmd_args[$i]
         # "i=$i t=$t"
         if ($t -match "^\d+$" -and $t -ge 1) {
            $script:testsuite_interation_max = $t
            # "testsuite_interation_max=$script:testsuite_interation_max"
            continue
         } else {
            log_error "parse_cmd_line: invalid value for -iter: $t must be positive integer!"
            exit 1
         }

      # Look for -tc keyword, but no more.
      } elseif ($t -match "^-tc$") {
         # Forget any previous test case list. This ensures that the command
         # line will override any list from the run control file. It also has
         # the small side effect of requiring all test cases to be specified as
         # a single grouping on the command line. If the user specified options
         # "-tc 1 2 -tc 5 6", only tc 5 & 6 would be run. However, this is 
         # consistent with parsing of other options where the last one wins.
         # Eg: "-try 4 -try 2" results in try=2.
         $script:tc_list = ""

         # We expect one or more integers after -tc.
         while ( 1 ) { 
            $i++
            $t = $script:cmd_args[$i]
            # "i=$i t=$t"
            if ($i -ge $cnt) {
               break
            }

            # If we find another -keyword, we stop grabbing tokens for -tc.
            if ($t -match "^-") {
               # "stop collecting for -tc"
               $i-- # go back one token in the list
               break
            }

            # Token is expected to be integer, 0 or more.
            if ($t -match "^\d+$") {
               $script:tc_list = "$script:tc_list tc_${t}" # Add tc_ prefix
               # "tc_list=$script:tc_list"
               continue
            } else {
               log_error "parse_cmd_line: invalid value for -tc: $t must be integer, GE 0 !"
               exit 1
            }
         }

      # Look for -batt keyword, but no more.
      } elseif ($t -match "^-batt$") {
         # Set the option flag
         $script:test_batt = $true

      # Look for -nobatt keyword, but no more.
      } elseif ($t -match "^-nobatt$") {
         # Set the option flag
         $script:test_batt = $false

      # Look for -br keyword, but no more.
      } elseif ($t -match "^-br$") {
         # Set the option flag
         $script:nobr = $false # use browser

      # Look for -nobr keyword, but no more.
      } elseif ($t -match "^-nobr$") {
         # Set the option flag
         $script:nobr = $true # dont use browser

      # Look for -conf keyword, but no more.
      } elseif ($t -match "^-conf$") {
         # Set the option flag
         $script:test_config = $true

      # Look for -noconf keyword, but no more.
      } elseif ($t -match "^-noconf$") {
         # Set the option flag
         $script:test_config = $false

      # Look for -cs keyword, but no more.
      } elseif ($t -match "^-cs$") {
         # Set the option flag
         $script:nocs = $false # copy files

      # Look for -nocs keyword, but no more.
      } elseif ($t -match "^-nocs$") {
         # Set the option flag
         $script:nocs = $true  # dont copy files 

      # Look for -mo keyword, but no more.
      } elseif ($t -match "^-mo$") {
         # Set the option flag
         $script:test_ms_office = $true

      # Look for -nomo keyword, but no more.
      } elseif ($t -match "^-nomo$") {
         # Set the option flag
         $script:test_ms_office = $false

      # Look for -msc keyword, but no more.
      } elseif ($t -match "^-msc$") {
         # Set the option flag
         $script:test_ms_sec_cl = $true

      # Look for -nomsc keyword, but no more.
      } elseif ($t -match "^-nomsc$") {
         # Set the option flag
         $script:test_ms_sec_cl = $false

      # Look for -net keyword, but no more.
      } elseif ($t -match "^-net$") {
         # Set the option flag
         $script:test_network = $true

      # Look for -nonet keyword, but no more.
      } elseif ($t -match "^-nonet$") {
         # Set the option flag
         $script:test_network = $false

      # Look for -oo keyword, but no more.
      } elseif ($t -match "^-oo$") {
         # Set the option flag
         $script:test_open_office = $true

      # Look for -nooo keyword, but no more.
      } elseif ($t -match "^-nooo$") {
         # Set the option flag
         $script:test_open_office = $false

      # Look for -oth keyword, but no more.
      } elseif ($t -match "^-oth$") {
         # Set the option flag
         $script:test_other = $true

      # Look for -nooth keyword, but no more.
      } elseif ($t -match "^-nooth$") {
         # Set the option flag
         $script:test_other = $false

      # Look for -pr keyword, but no more.
      } elseif ($t -match "^-pr$") {
         # Set the option flag
         $script:nopr = $false   # refresh results after each test case completes

      # Look for -nopr keyword, but no more.
      } elseif ($t -match "^-nopr$") {
         # Set the option flag
         $script:nopr = $true    # no refresh until tests completed

      # Look for -wae keyword, but no more.
      } elseif ($t -match "^-wae$") {
         # Set the option flag
         $script:wae = $true

      # Unknown tokens ==> error
      } else {
         clear-host
         log_error "parse_cmd_line: unknown token: $t"
         script_help
         exit 1
      }
   }

   # If test cases to run was not specified on the command line, then
   # compose the default list.
   if ($script:tc_list -eq "") {
      for ($i = 1; $i -le $script:tc_last; $i++) {
         $script:tc_list = "$script:tc_list tc_${i}"
      }
   }

   # Split test case list.
   $script:tc_list = $script:tc_list.Split(" ")
}


#==================== ping_test ===================================
# Pings the ping_host and stores the results for use later by tc_1
# when a logfile is available. For WIFI network links, a warmup
# sacrificial test is done to get the WIFI link working properly.
#
# Calling parameters: none
# Returns: null
# Sets variables: $script:wifi_warmup_cycles_max, $script:wifi_warmup
#==================================================================
function ping_test {

   # If the network tests are not being run, then quietly return.
   if (!$script:test_network) {
      return
   }

   # If tc_1 is not in the list of tests to run, then quietly return.
   # This avoids extra delays when debugging other code. We DONT need
   # to waste time running unnecessary pings every time the script runs.
   # "ping_test tc_list=$script:tc_list"
   $found = $false
   foreach ($tc in $script:tc_list) {
      # "tc=$tc"
      if ($tc -eq "tc_1") {
         # "FOUND: $tc"
         $found = $true
         break
      }
   }
   if (!$found) {
      # "ping_test not running"
      return
   }

   # Sanity check
   # $script:wifi_warmup_cycles_max = -1 # test code
   if (!($script:wifi_warmup_cycles_max -match "^\d+$") -or $script:wifi_warmup_cycles_max -le 0) {
      log_warning "ping_test invalid wifi_warmup_cycles_max=$script:wifi_warmup_cycles_max, must be integer GT 0, set to 5"
      $script:wifi_warmup_cycles_max = 5
   }

   # NB: Its possible that the PC has both a wired ethernet link and a WIFI
   # link active at the same time. In this case, the script will have recorded 
   # the wired ethernet link as the link of interest, see setup_localhost_info.
   # When pings are run, Windows seems to prefer the wired ethernet link over 
   # the WIFI link. So the WIFI warmup test is only done when there is no
   # active wired ethernet link.
 
   # WIFI network links can have issues under low RSSI conditions. We do a one time
   # warmup excercise with pings to try and get the WIFI link working properly.
   # This is viewed as a sacrificial test, meaning that the results do not influence
   # the PASS/FAIL result of the real ping test.

   # WIFI warmup test is done when WIFI link is the only active network link or
   # when we dont seem to have an active network link. Not seeming to have an
   # active link can occur if the tests are run while the WIFI link is still 
   # associating with the nearby WIFI router.
   # $script:mac_addr = "wifi_abcd" # test data
   log_debug "ping_test mac_addr=$script:mac_addr ip_addr=$script:ip_addr"
   if ((!$script:mac_addr -or $script:mac_addr -match "^wifi") -and !$script:wifi_warmup) {

      # We do the WIFI warmup in groups of $script:ping_cnt pings. This
      # allows the warmup test to complete as soon as the WIFI link
      # starts working acceptably. There is no need to run the full
      # number of WIFI warmup cycles if the link is working OK early
      # on in the warmup tests.
      for ($i = 1; $i -le $script:wifi_warmup_cycles_max; $i++) {

         log_info "ping_test doing WIFI warmup cycle#: $i of $script:wifi_warmup_cycles_max"
         run_ping
         # Test code
         # if ($i -le 2) {
         #    $script:ping_ok_percent = 0
         # }
         if ($script:ping_ok_percent -ge $script:ping_pass_percent) {
            log_info "ping_test ping_ok_percent=$script:ping_ok_percent completed WIFI warmup OK on cycle: $i of $script:wifi_warmup_cycles_max"
            break
         }

         # Wait a bit between warmup cycles.
         # "ping_test warmup i=$i waiting..."
         start-sleep -s 3
      }

      # Make sure we only do the WIFI warmup once.
      $script:wifi_warmup = $true
   }

   # This is the real ping test. If the WIFI link is having a hard time,
   # this is the last chance to get working. If we happen to have
   # good results from the WIFI warmup test, we can use those instead.
   if ($script:ping_ok_percent -lt $script:ping_pass_percent) {
      # Previous results, if any, are NOT good enough, so run pings again.
      run_ping
   }

   # NB: tc_1 will make PASS/FAIL decsions and act accordingly.
}


#==================== prompt_plug_in_ac_power =====================
# If a battery is present in the PC, prompts the user to plug in 
# the AC power at the end of the battery discharge test.
#
# Calling parameters: none
# Returns: null
#==================================================================
function prompt_plug_in_ac_power {

   # Are we still running on battery power?
   get_battery_status
   if (!($script:batt_state -match "Offline")) {
      # Either NoBattery or Online state.
      return
   }

   # Prompt the user to plug in the AC power.
   log_info ""
   log_info ""
   log_info ""
   log_bold "Please plug the AC power in again. The battery discharge test is finished."
   log_info ""
   log_info ""
   log_info ""
   refresh_results "bottom"
}


#==================== prompt_unplug_ac_power ======================
# If a battery is present in the PC, prompts the user to unplug 
# the AC power so the battery discharge test can run.
#
# Calling parameters: none
# Returns: null
#==================================================================
function prompt_unplug_ac_power {

   # NB: We need to get the battery status at least once,
   # even if we are not going to run the test. This ensures
   # that we have the correct state info when add_tc_stats
   # is run. 

   # Does the PC have a battery to be tested?
   get_battery_status -log
   if ($script:batt_state -match "NoBattery") {
      return
   }

   # If the battery test is turned off, then quietly return.
   if (!$script:test_batt) {
      return
   }

   # If tc_last is not in the list of tests to run, then quietly return.
   # This avoids unnecessary prompts when debugging other code.
   # "prompt_unplug_ac_power tc_list=$script:tc_list"
   $found = $false
   foreach ($tc in $script:tc_list) {
      # "tc=$tc"
      if ($tc -eq "tc_${script:tc_last}") {
         # "FOUND: $tc"
         $found = $true
         break
      }
   }
   if (!$found) {
      # "prompt_unplug_ac_power not running"
      return
   }

   # Sanity check
   if (!$script:battery_life_threshold -or !($script:battery_life_threshold -match "^\d+$") -or $script:battery_life_threshold -lt 0) {
      log_warning "Invalid battery_life_threshold=$script:battery_life_threshold, must be positive integer, set to 30"
      $script:battery_life_threshold = 30
   }

   # Is there enough life in the PC battery to run the test?
   if ($script:batt_life -lt $script:battery_life_threshold) {
      $msg = "battery discharge test will not be run, batt_life={0:0.00}% LT battery_life_threshold=$script:battery_life_threshold%" -f $script:batt_life
      log_error $msg
      return
   }

   # Prompt the user to unplug the AC power.
   log_info ""
   log_info ""
   log_info ""
   log_bold "Please unplug the AC power so the battery discharge test will start."
   log_info ""
   log_info ""
   log_info ""
   refresh_results "bottom"

   # Sit in loop for a while until user unplugs the AC power
   for ($i = 1; $i -le 120; $i++) {
      start-sleep -s 1
      get_battery_status
      if ($script:batt_state -match "Offline") {
         log_info ""
         log_info ""
         log_info ""
         log_bold "Thanks for unplugging the AC power. The tests will continue."
         log_info ""
         log_info ""
         log_info ""
         refresh_results "bottom"
         return
      }
   }

   # Power is still plugged in. Not much we can do about it.
   log_info ""
   log_info ""
   log_info ""
   log_bold "The AC power is still plugged in. The tests will now start."
   log_bold "If possible, unplug the AC power while the tests are running."
   log_info ""
   log_info ""
   log_info ""
   refresh_results "bottom"
}


#==================== refresh_results =============================
# Restores the browser results window, reloads the results page, 
# and positions the browser either at the top of the page or the
# bottom of the page.
#
# Calling parameters: dir
# dir=bottom - tells the browser to show the bottom of the page
# dir=top    - tells the browser to show the top of the page
#
# Returns: null
#==================================================================
function refresh_results ($dir="bottom") {

   # User may have requested no results display in browser.
   # This is usefull when debugging new code and you dont want to
   # waste time with displaying results page in the browser.
   if ($script:nobr) {
      return
   }

   # If we can get the window title, this is a good indicator
   # that we still have a valid window handle.
   get_window_title $script:res_hnd -warnonly

   # If things went really wrong during crash_check, we may not
   # have a valid window handle for the results browser, or title.
   # So we will try to get a new window handle as appropriate.
   # $script:res_hnd = "" # test code
   # $script:scr_title = "" # test code
   if (!$script:res_hnd -or $script:res_hnd -eq "" -or $script:res_hnd -eq 0 -or $script:scr_title -eq "") {
      log_warning "refresh_results (1) dir=$dir invalid: res_hnd=$script:res_hnd scr_title=$script:scr_title"
      resolve_window_handle $script:browser_results
      $script:res_hnd = $script:curr_hnd

      # Get the window title
      get_window_title $script:res_hnd -warnonly

      # Did we get a usable window handle & title?
      # $script:res_hnd = "" # test code
      # $script:scr_title = "" # test code
      if (!$script:res_hnd -or $script:res_hnd -eq "" -or $script:res_hnd -eq 0 -or $script:scr_title -eq "") {
         log_debug "refresh_results (2) dir=$dir, no luck, calling show_results_page"
         show_results_page
         return
      }
   }

   # We appear to have valid handle & title.
   log_debug "refresh_results (3) dir=$dir res_hnd=$script:res_hnd scr_title=$script:scr_title"

   # Restore browser results window.
   restore_window $script:res_hnd
   # $script:restore_window_rc = $false # test code
   if (!$script:restore_window_rc) {
      # We may not have a valid window handle. This is not guaranteed to
      # be the case, but there is no harm assuming so. 
      resolve_window_handle $script:browser_results
      $script:res_hnd = $script:curr_hnd

      # Get the window title
      get_window_title $script:res_hnd -warnonly

      # Did we get a usable window handle & title?
      # $script:res_hnd = "" # test code
      # $script:scr_title = "" # test code
      if (!$script:res_hnd -or $script:res_hnd -eq "" -or $script:res_hnd -eq 0 -or $script:scr_title -eq "") {
         log_debug "refresh_results (4) dir=$dir, no luck, returning"
         return
      }
      log_debug "refresh_results (5) dir=$dir res_hnd=$script:res_hnd scr_title=$script:scr_title"
   }

   # Send F5 to reload results page.
   # Function send_keys has its own error handling.
   start-sleep -m 200
   send_keys $script:res_hnd "{F5}" 

   # Reposition browser to top or bottom of page.
   # NB: "^" means Ctrl
   start-sleep -m 200
   if ($dir -eq "top") {
      send_keys $script:res_hnd "^{Home}"
   } else {
      send_keys $script:res_hnd "^{End}"
   }
}


#==================== remove_file =================================
# Tries repeatedly to remove the specfied file or directory.
#
# Calling parameters: file <-recurse>
# If file is really a directory, specify -recurse to remove
# all the contents.
#
# Returns: null
#==================================================================
function remove_file ($file="") {

   # Check file was specified.
   if (!$file -or $file -eq "") {
      log_error "remove_file: file is null!"
      return
   }

   # Sanity check on args.
   if ($args -ne "-recurse" -and $args -ne "") {
      log_error "remove_file: invalid args=$args"
      return
   }

   # If file is not present to begin with, thats OK.
   if (!(test-path "$file")) {
      log_info "remove_file: $file not found, OK"
      return
   }

   # Try repeatedly to delete the specified file.
   for ($i = 1; $i -le $script:max_loop; $i++) {

      # File exists, remove it.
      $msg = ""
      $saved_rc = $False
      try {
         if ($args -eq "-recurse") {
            # Typically used to recusively remove a directory
            # "remove_file file=$file args=$args"
            remove-item "$file" -recurse
            $saved_rc = $?
         } else {
            remove-item "$file"
            $saved_rc = $?
         }
      } catch {
         $msg = $Error | select-object -first 1
         $saved_rc = $False
      }

      # Check for errors
      if (!$saved_rc) {
         log_warning "remove_file i=$i msg=$msg"
      }

      # Test code
      # if ($i -le 2) {
      #    new-item $file -type file | out-null
      # }

      # Did we get rid of the file?
      if (!(test-path "$file")) {
         # File is gone.
         if ($i -eq 1) {
            log_info "remove_file: $file deleted OK on Try#: $i"
         } else {
            log_warning "remove_file: $file deleted OK on Try#: $i"
         }
         return

      } else {
         # Wait as needed.
         "remove_file i=$i waiting..."
         start-sleep -s 1
      }
   }

   # Multiple tries failed ==> error.
   log_error "remove_file: tried $script:max_loop times to delete: $file, FAILED!"
}


#==================== remove_window ===============================
# Removes the specfied window object. Preferred way is to click on  
# the Cancel or OK button, if any. Otherwise use the remove-window
# cmdlet.
#
# Calling parameters: object <-warnonly>
# option -warnonly converts most errors to warnings
#
# Returns: null
# Sets variables: $script:remove_window_rc
#==================================================================
function remove_window ($object) {

   # Initialization
   $script:remove_window_rc = $false

   # Sanity check.
   if (!$object) {
      log_error "remove_window: invalid object=$object"
      return
   }

   # Decode the calling object.
   $c = $object.Class
   $h = $object.Handle
   $n = $object.ProcessName
   $p = $object.ProcessId
   $t = $object.Title
   # log_info "remove_window c=$c h=$h n=$n p=$p t=$t args=$args"

   # Check for Cancel button first, then OK button.
   $button_hnd = ""
   $button_title = ""
   get_controls $object
   search_controls "button" "^\s*(cancel|close)\s*$" 1 -quiet # watch out for "Cancel Update"
   if (!$script:ctl_hnd) {
      search_controls "button" "^\s*(ok|ask\s*me)\s*$" 1 -warnonly # want control data if both Cancel & OK not found
   }
   $button_hnd = $script:ctl_hnd
   $button_title = $script:ctl_title

   # Try multiple times to remove the window.
   $closed = $false # success flag
   for ($i = 1; $i -le $script:max_loop; $i++) {

      # Directly clicking the Cancel or OK button is preffered.
      # $button_hnd = "" # test code
      if ($button_hnd) {
         log_debug "remove_window i=$i clicking button_hnd=$button_hnd"
         send_click $button_hnd

      } else { 
         # Use the remove-window cmdlet.
         $msg = ""
         $saved_rc = $False
         try {
            log_debug "remove_window i=$i trying remove-window cmdlet"
            $x = set-windowactive $object
            $x = remove-window $object
            $saved_rc = $?
         } catch {
            $msg = $Error | select-object -first 1
            $saved_rc = $False
         }

         # Check for errors
         if (!$saved_rc) {
            if ($args -eq "-warnonly") {
               log_warning "remove_window i=$i object=$object msg=$msg"
            } else {
               log_error "remove_window i=$i object=$object msg=$msg"
            }
         }
      }

      # Wait for window to go away.
      start-sleep -s 1

      # Check if child or modal window object  still exists. If both are gone,
      # then we are done.
      search_child_window $h
      # "remove_window i=$i search_child_window_rc=$search_child_window_rc"
      if (!$search_child_window_rc) {
         get_window_obj $h -quiet
         # "remove_window i=$i curr_obj=$script:curr_obj"
         if (!$script:curr_obj) {
            # No trace of the window was found, we succeeded.
            $closed = $true
            $script:remove_window_rc = $true # calling routine may want to know
            break
         }
      }

      # For really old style popup windows, try sending enter.
      # NB: This will take care of IE "About" modal window
      log_debug "remove_window i=$i sending enter"
      send_keys $h "{enter}"

      # Wait as appropriate.
      if ($i -lt $script:max_loop) {
         # "remove_window i=$i waiting..."
         start-sleep -s 1
      }
   }

   # What really happened?
   # "remove_window i=$i closed=$closed remove_window_rc=$script:remove_window_rc cancel_hnd=$cancel_hnd"
   if (!$closed -or $i -gt $script:max_loop) {
      if ($args -eq "-warnonly") {
         log_warning "remove_window c=$c h=$h n=$n p=$p t=$t button_hnd=$button_hnd button_title=$button_title tried $script:max_loop times to remove window, FAILED!"
      } else {
         log_error "remove_window c=$c h=$h n=$n p=$p t=$t button_hnd=$button_hnd button_title=$button_title tried $script:max_loop times to remove window, FAILED!"
      }
   } elseif ($i -eq 1) {
      # Normal success path
      log_info "remove_window c=$c h=$h n=$n p=$p t=$t button_hnd=$button_hnd button_title=$button_title removed window OK, Try#: $i"
   } else {
      log_warning "remove_window c=$c h=$h n=$n p=p t=$t button_hnd=$button_hnd button_title=$button_title removed window OK, Try#: $i"
   }
}


#==================== reserve_logname_on_server ===================
# Ensures that the test results wont overwrite any existing files
# on the server results directory. The log_suffix is supposed to
# be unique because it includes the MAC address (LAN, WIFI or BT).
# It is supposedly possible to administer the MAC address locally,
# so the MAC may not actually be unique. If we find that we will 
# overwrite files on the server, we back off for a random interval
# and choose a new logfile using a different date/time stamp along
# with the log_suffix, until we find a logfile name that is not
# in use. We then put a reservation token in the results directory
# so that other PC will see that we are about to use this name.
#
# Calling parameters: none
# Returns: null
# Sets variables: $script:logname
#==================================================================
function reserve_logname_on_server {

   # Clean up log_suffix. Some charactars are not allowed by
   # Windows file system, others cause issues when loading the
   # results page in the browser.
   $suffix = $script:log_suffix
   $suffix = $suffix -replace " "
   $suffix = $suffix -replace "\(" # Firefox doesnt like these
   $suffix = $suffix -replace "\)"
   $suffix = $suffix -replace "\+"
   $suffix = $suffix -replace "\\" # Windows doesnt allow these
   $suffix = $suffix -replace "\/"
   $suffix = $suffix -replace "\?"
   $suffix = $suffix -replace "\*"
   $suffix = $suffix -replace '"'
   $suffix = $suffix -replace "\>"
   $suffix = $suffix -replace "\<"
   log_debug "reserve_logname_on_server suffix=$suffix"

   # Clean up server results directory.
   # $script:server_results_dir = "  " # test code
   if ($script:server_results_dir) {
      $script:server_results_dir = $script:server_results_dir.trim()
   } else {
      $script:server_results_dir = ""
   }

   # Try multiple times to get a logname that is not in use or
   # not already reserved in the server results directory.
   for ($i = 1; $i -le $script:max_loop; $i++) {

      # Get current date/time and reformat using 24HR clock.
      $ts = Get-Date
      $ts = $ts.ToString("yyyyMMdd.HHmmss")

      # Compose a tentative logname.
      # NB: We must create a logname even if we dont copy files to the server!
      $script:logname ="${ts}_${suffix}"
      log_debug "reserve_logname_on_server i=$i logname=$script:logname"

      # If server results directory is not going to be used, we are done. 
      if ($script:nocs -or !$script:test_network -or !$script:server_results_dir -or $script:server_results_dir -eq "") {
         log_debug "reserve_logname_on_server i=$i nocs=$script:nocs test_network=$script:test_network server_results_dir=$script:server_results_dir, returning."
         return
      }

      # If necessary, provide credentials to server.
      if ($script:server_username -and $script:server_username -ne "" -and $script:server_password -and $script:server_password -ne "") {
         log_info "reserve_logname_on_server providing credentials to server: $script:server_results_dir"
         $x = net use $script:server_results_dir /user:${script:server_username} $script:server_password 2>&1 # redirect stderr
         # Check for net use success message.
         if ($x -match "completed.*success") {
            log_info "reserve_logname_on_server net use success: $x"
         } else {
            log_fatal_error "reserve_logname_on_server net use failed: $x `n<br>`nNB: System error 1219 can also mean wrong password !"
         }
      }

      # Set the specific subdirectory path to be used.
      # NB: The subdir should be left null if server_results_dir is also null!
      $script:server_results_subdir = "${script:server_results_dir}\${script:host_name}"

      # If necessary, create the subdirectory. We are coding around the issue that
      # new-item complains if the subdirectory already exists. Also, this approach 
      # lets us log when we really created the subdirectory or not.
      $msg = ""
      if ((test-path "$script:server_results_subdir")) {
         log_debug "reserve_logname_on_server subdirectory $script:host_name already exists"
      } else {
         log_debug "reserve_logname_on_server creating subdirectory $script:host_name"
         try {
            new-item -path $script:server_results_dir -name $script:host_name -type directory -erroraction silentlycontinue | out-null
         } catch {
            $msg = $error | select-object -first 1
            log_debug "reserve_logname_on_server new-item $script:host_name got: $msg"
         }
      }

      # Do we have a valid subdirectory?
      if (!(test-path "$script:server_results_subdir")) {
         $delay = get-random -min 5 -max 60
         log_warning "reserve_logname_on_server i=$i did NOT create subdir $script:host_name got: $msg, waiting $delay sec..."
         start-sleep -s $delay
         continue
      }

      # Check the server results directory for any files at all with this logname.
      # $script:server_results_subdir = "\\192.168.0.100\abcd" # test code
      # if ($i -eq 1) {
      #    add-content -path "${script:server_results_subdir}\${script:logname}.res" -value "a b" # test code
      # }
      log_debug "reserve_logname_on_server i=$i checking: ${script:server_results_subdir}\${script:logname}*"
      $msg = ""
      $saved_rc = $false
      try {
         $files = dir ${script:server_results_subdir}\${script:logname}*
         $saved_rc = $?
      } catch {
         $files = ""
         $msg = $error | select-object -first 1
         $saved_rc = $false
      }
      log_debug "reserve_logname_on_server i=$i files=$files"

      # Check for errors.
      if (!$saved_rc) {
         $delay = get-random -min 5 -max 60
         log_warning "reserve_logname_on_server i=$i dir got: $msg, waiting $delay sec..."
         start-sleep -s $delay
         continue
      }

      # If files were found, wait a random time, then try again.
      if ($files) {
         # What are the odds of this happening? 
         $delay = get-random -min 5 -max 60
         log_warning "reserve_logname_on_server i=$i logname=$script:logname is not unique, waiting $delay sec..."
         start-sleep -s $delay
         continue
      }

      # So far so good, no files were found. We try to reserve this logname.
      # Add a .res file with our host_name & ip_addr.
      $res_fn = "${script:server_results_subdir}\${script:logname}.res"
      # "res_fn=$res_fn"
      $msg = ""
      $saved_rc = $false
      try {
         add-content -path "$res_fn" -value "$script:host_name $script:ip_addr"
         $saved_rc = $?
      } catch {
         $msg = $error | select-object -first 1
         $saved_rc = $false
      }

      # Check for errors.
      if (!$saved_rc) {
         $delay = get-random -min 5 -max 60
         log_warning "reserve_logname_on_server i=$i add-content got: $msg, waiting $delay sec..."
         start-sleep -s $delay
         continue
      }

      # Wait a bit, then get the contents of the .res file.
      start-sleep -s 2
      $msg = ""
      $saved_rc = $false
      try {
         $x = get-content $res_fn
         $saved_rc = $?
      } catch {
         $msg = $error | select-object -first 1
         $saved_rc = $false
         $x = ""
      }

      # Check for errors.
      if (!$saved_rc) {
         $delay = get-random -min 5 -max 60
         log_warning "reserve_logname_on_server i=$i get-content got: $msg, waiting $delay sec..."
         start-sleep -s $delay
         continue
      }

      # If the .res file contents match up, then we are done.
      if ($x -eq "$script:host_name $script:ip_addr") {
         # We successfully reserved this logname!!!
         log_info "reserve_logname_on_server i=$i res_fn=$res_fn x=$x reserved OK"
         return

      } else {
         # OOPS! We collided with another PC at same date/time with same MAC address.
         # What are the odds of this happening? 
         $delay = get-random -min 5 -max 60
         log_warning "reserve_logname_on_server i=$i res_fn=$res_fn x=$x reservation collision, waiting $delay sec..."
         start-sleep -s $delay
         continue
      }
   }
   
   # We didnt reserve a unique logname. Game over!
   log_fatal_error "reserve_logname_on_server tried $script:max_loop times to reserve a unique logname in $script:server_results_subdir, FAILED!"
}


#==================== resolve_window_handle =======================
# Deals with case of finding a window handle when there are multiple
# process with same name. We always look at the foreground window
# first. iexplorer usually has multiple pids, and sometimes modal
# windows. Other cases will be extra notepad windows open for
# editing, but not used by these tests.
#
# NB: This routine will ignore selected windows specified in the 
# $script:ignore_list
#
# Calling parameters: app_name <-quiet>
# Option -quiet will suppress logging of most errors.
#
# Returns: null
# Sets variables: $script:curr_hnd, $script:curr_pid
#==================================================================
function resolve_window_handle ($app_name) {

   # Initialization
   # start-sleep -s 5 # test code
   $script:curr_hnd = ""
   $script:curr_pid = ""
   $wind_cnt = 0
   $wind_list = @()

   # Sanity check.
   if (!$app_name -or $app_name -eq "") {
      log_error "resolve_window_handle invalid app_name=$app_name"
      return
   }

   # Modify app_name for selected apps.
   map_app_name $app_name
   $app_name = $script:app_title

   # start_app may have just created a new window for us, in
   # which case this window will be in the foreground. So we
   # check the foreground window window title first to see if
   # its what we really want. 

   # We try multiple times to get the foreground window and/or
   # app_name related windows.
   for ($i = 1; $i -le $script:max_loop; $i++) {
      # Get foreground object and decode.
      # start-sleep -s 5 # test code
      get_foreground_window -quiet
      get_window_obj $script:scr_hnd -quiet
      $fg_class = $script:curr_obj.Class
      $fg_name = $script:curr_obj.ProcessName
      $fg_pid = $script:curr_obj.ProcessId
      $fg_title = $script:curr_obj.Title
      log_debug "resolve_window_handle (0) i=$i app_name=$app_name foreground scr_hnd=$script:scr_hnd fg_class=$fg_class fg_name=$fg_name fg_pid=$fg_pid fg_title=$fg_title"

      # str is used in messages indicate window used, first vs foreground.
      $str = "first"

      # Does foreground window look like what we want?
      # $fg_title = "" # test code
      if ($fg_title -match "$app_name" -or $fg_name -match "$app_name") {
         # Add debug msg if fg_title=null, meaning we matched on fg_name.
         # This happens a lot, and warning message is no longer usefull.
         if ($fg_title -eq "") {
            log_debug "resolve_window_handle (1) i=$i app_name=$app_name fg_title is null, matched fg_name=$fg_name foreground scr_hnd=$script:scr_hnd fg_class=$fg_class fg_name=$fg_name fg_pid=$fg_pid fg_title=$fg_title"
         }

         # The foreground window looks like what we want.
         # Check this window is NOT on the ignore list.
         $ignore = $false
         foreach ($patt in $script:ignore_list) {
            # "resolve_window_handle patt=$patt"
            if ($fg_title -match "$patt") {
               $ignore = $true
               log_debug "resolve_window_handle (2) i=$i app_name=$app_name ignoring: scr_hnd=$script:scr_hnd fg_class=$fg_class fg_name=$fg_name fg_pid=$fg_pid fg_title=$fg_title patt=$patt" 
               break
            }
         }

         # Should we use the foreground window?
         if ($fg_class -match "modal") {
            log_debug "resolve_window_handle (3) i=$i app_name=$app_name ignoring modal: scr_hnd=$script:scr_hnd fg_class=$fg_class fg_name=$fg_name fg_pid=$fg_pid fg_title=$fg_title" 

         } elseif (!$ignore) {
            # Need sanity checks on data before accepting foreground window!
            if ($script:scr_hnd -and $script:scr_hnd -ne "0" -and $fg_pid -and $fg_pid -ne "0") {
               $str = "foreground"
               $script:curr_hnd = $script:scr_hnd
               $script:curr_pid = $fg_pid
               log_info "resolve_window_handle (4) i=$i app_name=$app_name using $str window, curr_hnd=$script:curr_hnd fg_class=$fg_class fg_name=$fg_name cur_pid=$script:curr_pid fg_title=$fg_title"
            }
         }
      }

      # Get all windows matching app_name, which may include the foreground window.
      $msg = ""
      $saved_rc = $false
      try {
         $wind_list = select-window -title *$app_name*
         $saved_rc = $?
      } catch {
         $wind_list = @()
         $msg = $Error | select-object -first 1
         $saved_rc = $false
      }

      # Look for errors.
      if (!$saved_rc) {
         if ($args -match "-quiet") {
            # Dont complain!
         } else {
            log_error "resolve_window_handle (5) i=$i app_name=$app_name got: $msg"
         }
      }

      # Test code
      # if ($i -eq 1) {
      #    $wind_list = ""
      # }

      # If we got some valid windows with real data, we are done.
      # NB: The count method shows null for 0 or 1 items.
      # NB: If there are no windows, we will get 1 null item!
      $wind_cnt = 0
      foreach ($w in $wind_list) {
         $c = $w.Class
         $h = $w.Handle
         $n = $w.ProcessName
         $p = $w.ProcessId
         $t = $w.Title
         log_debug "resolve_window_handle (6) i=$i c=$c h=$h n=$n p=$p t=$t w=$w"
         if ($c -and $c -ne "" -and $h -and $h -ne "0" -and $p -and $p -ne "0") {
            # OK, we have a valid window object, so count it.
            $wind_cnt++  
         } else {
            log_debug "resolve_window_handle (7) i=$i ignoring: c=$c h=$h n=$n p=$p t=$t w=$w"
         }
      }
      # "wind_cnt=$wind_cnt"
      if ($wind_cnt -gt 0) {
         log_debug "resolve_window_handle (8) i=$i app_name=$app_name wind_cnt=$wind_cnt wind_list=$wind_list"
         break
      } else {
         # log_debug "resolve_window_handle (9) i=$i app_name=$app_name waiting..."
         start-sleep -s 1
      }
   }

   # Thought for the future: If the foreground window matches app_name, why bother 
   # with looking at all the other app_name related windows? Do we really need to
   # know about the windows we are ignorning? We could just return after we take
   # foreground window.

   # Right now the value of logging all the windows that we ignore is it provides
   # more data in the logfile when there is a failure, and all you have to look at
   # is the logfile. So far, the windows we ignore typically dont have any impact
   # on the test cases, so the value of knowing they exist but were ignored is
   # minimal at best.

   # Did we get valid windows?
   if ($wind_cnt -eq 0) {
      if ($args -match "-quiet") {
         # Dont complain!
      } else {
         log_error "resolve_window_handle (10) app_name=$app_name tried $script:max_loop times, no windows found!"
      }
      return
   } elseif ($i -gt 1) {
      log_warning "resolve_window_handle (11) found app_name=$app_name wind_cnt=$wind_cnt window(s) OK on Try#: $i"
   }

   # Loop thru win_list.
   foreach ($w in $wind_list) {
      $c = $w.Class
      $h = $w.Handle
      $n = $w.ProcessName
      $p = $w.ProcessId
      $t = $w.Title
      log_debug "resolve_window_handle (12) c=$c h=$h n=$n p=$p t=$t w=$w"

      # NB: The foreground window can show up with no handle or title info
      # and will be ignored above. Later on the required info will become
      # available, so we need to check again here if we should accept or
      # ignore the foreground window. If we really did already take the 
      # foreground window, then dont process it again here, as this will
      # result in false warnings.
      if ($h -eq $script:scr_hnd -and $script:curr_hnd -eq $h -and $script:curr_pid -ne "") {
         log_debug "resolve_window_handle (13) app_name=$app_name foreground already done c=$c h=$h n=$n p=$p t=$t"
         continue
      } 

      # Ignore selected windows.
      $ignore = $false
      foreach ($patt in $script:ignore_list) {
         # log_info "resolve_window_handle patt=$patt"
         if ($t -match "$patt") {
            $ignore = $true
            log_debug "resolve_window_handle (14) app_name=$app_name ignoring: t=$t patt=$patt" 
            break
         }
      }

      # Did inner loop signal to ignore this window?
      if ($ignore) {
         continue
      }

      # Ignore modal windows
      if ($c -match "modal") {
         log_debug "resolve_window_handle (15) i=$i app_name=$app_name ignoring modal: c=$c h=$h n=$n p=$p t=$t" 
         continue
      }

      # Take the first acceptable window with real data. Warn user about remaining windows.
      if ($script:curr_hnd -eq "" -and $h -and $h -ne "0" -and $p -and $p -ne "0") {
         $script:curr_hnd = $h
         $script:curr_pid = $p
         log_info "resolve_window_handle (16) taking $str app_name=$app_name window, curr_hnd=$script:curr_hnd curr_pid=$script:curr_pid c=$c n=$n t=$t"
         break
      } else {
         log_warning "resolve_window_handle (17) took $str app_name=$app_name window, ignoring: h=$h n=$n p=$p t=$t" 
      }
   }

   # Did we find something acceptable?
   if ($script:curr_hnd -eq "" -or $script:curr_pid -eq "") {
      if ($args -match "-quiet") {
         # Dont complain!
      } else {
         log_error "resolve_window_handle (18) app_name=$app_name no windows found, curr_hnd=$script:curr_hnd curr_pid=$script:curr_pid"
      }
   }
}


#==================== restore_session =============================
# Looks for browser window/tab indicating previous session can be
# restored. Clicks OK. Also tries to handle Default Browser popup.
#
# Calling parameters: hnd
# Returns: null
# Sets variables: $script:ff_def_br
#==================================================================
function restore_session ($hnd) {

   # Sanity check.
   if (!$hnd -or $hnd -eq "" -or $hnd -eq 0) {
      log_error "restore_session invalid hnd=$hnd"
      return
   }

   # Get window title
   get_window_title $hnd
   # log_info "restore_session hnd=$hnd scr_title=$script:scr_title"

   # Bandaid for Firefox Default Browser popup.
   if ($script:ff_def_br -lt 4 -and $script:scr_title -match "firefox") {
      # Since we dont yet have a way to detect the non-standard Firefox
      # popup window asking about making Firefox the default browser,
      # we simply send Alt-y to Firefox several times for this test session.
      # This seems to do the job with no side effects.
      $script:ff_def_br++
      log_info "restore_session default browser prompt hnd=$hnd scr_title=$script:scr_title cnt=$script:ff_def_br, sending Alt-y"
      send_keys $hnd "%y"
      start-sleep -s 5
   }

   # NB: To test this code below, do: kill -name firefox -force
   # NB: Yes, to trigger this behaviour, you really DO need to add the option -force !!!

   # Look for restore session.
   if (!($script:scr_title -match "restore.*session")) {
      # The usual case
      return
   }

   # Neither Firefox or IE show any controls, so we just 
   # send Enter and hope for the best.
   log_info "restore_session hnd=$hnd scr_title=$script:scr_title, sending Enter"
   send_keys $hnd "{enter}"
   start-sleep -s 10
}


#==================== restore_window =============================
# Currently minimizes all windows, then restores the desired window
# as the active, focussed foreground window.
#
# Calling parameters: hnd
# Returns: null
# Sets variables: $script:restore_window_rc
#==================================================================
function restore_window ($hnd) {

   # Sanity check.
   $script:restore_window_rc = $false
   if (!$hnd -or $hnd -eq "" -or $hnd -eq 0) {
      log_error "restore_window invalid hnd=$hnd"
      return
   }

   # Get title of desired window.
   get_window_title $hnd
   $desired_title = $script:scr_title

   # First we check to see which window is currently in the foreground.
   # NB: There may not be a foreground window, so use the -quiet option.
   get_foreground_window -quiet
   get_window_title $script:scr_hnd -quiet
   if ($script:scr_hnd -eq $hnd) {
      # We got lucky and dont have to do anything.
      log_debug "restore_window foreground title=$script:scr_title hnd=$script:scr_hnd EQ desired title=$desired_title hnd=$hnd, nothing to do"
      $script:restore_window_rc = $true
      return
   } else {
      # Continue onwards in routine.
      log_debug "restore_window foreground title=$script:scr_title hnd=$script:scr_hnd NE desired title=$desired_title hnd=$hnd, restoring window"
   }

   # This is the sheer brute force approach to get a specific window
   # into the foreground and active/focussed.

   # Minimize all existing windows.
   minimize_all_windows

   # Try multiple times to restore just the desired window. It should be
   # in the foreground as here are no other windows showing.
   for ($i = 1; $i -le $script:max_loop; $i++) {

      # Restore the desired window.
      $msg = ""
      $saved_rc = $false
      try {
         $script:showWindowAsync::ShowWindowAsync($hnd,4) | out-null
         $saved_rc = $?
      } catch {
         $msg = $Error | select-object -first 1
         $saved_rc = $false
      }

      # Warning for errors.
      if (!$saved_rc) {
         log_warning "restore_window (1) hnd=$hnd got: $msg"
      }

      # Make the desired window active.
      $saved_rc = $false
      $msg = ""
      try {
         set-windowactive $hnd |out-null
         $saved_rc = $?
      } catch {
         $msg = $Error | select-object -first 1
         $saved_rc = $false
      }

      # Warning for errors.
      if (!$saved_rc) {
         log_warning "restore_window (2) hnd=$hnd got: $msg"
      }

      # On occasion, the window may not come into foreground. 
      # So we check what really happened.
      start-sleep -s 2
      get_foreground_window

      # Test code.
      # if ($i -lt 2) {
      #    $script:scr_hnd = ""
      # }

      # Exit loop if we got the desired window as the foreground window.
      if ($script:scr_hnd -eq $hnd) {
         $script:restore_window_rc = $true
         break 
      }

      # Wait a bit...
      if ($i -lt $script:max_loop) {
         start-sleep -s 1
         # This seems to help when things dont work the first time.
         minimize_all_windows
      }
   }

   # Check for errors, warn if we had to retry.
   get_window_title $script:scr_hnd
   if ($script:scr_hnd -ne $hnd) {
      log_error "restore_window failed, tried $script:max_loop times to restore hnd=$hnd, foreground is hnd=$script:scr_hnd title=$script:scr_title, but desired is hnd=$hnd title=$desired_title"
   } elseif ($i -ne 1) {
      log_warning "restore_window tried $i times to restore hnd=$hnd, title=$script:scr_title  OK"
   } else {
      log_debug "restore_window hnd=$hnd, title=$script:scr_title OK i=$i"
   }
}


#==================== run_ping ====================================
# Pings the ping_host and stores the results for use by calling
# routine. This routine is used for the real ping test and the WIFI
# sacrificial warmup ping tests.
#
# Calling parameters: none
# Returns: null
# Sets variables: $script:ping_cnt, $script:ping_ok_cnt,
# $script:ping_ok_txt, $script:ping_ok_percent, $script:ping_fail_cnt,
# $script:ping_fail_txt
#==================================================================
function run_ping {

   # NB: This routine does not call log_error. It is up to the calling
   # routine to make PASS/FAIL decisions and act accordingly. This 
   # allows the WIFI warmup tests to discard data without triggering
   # a failure and automatic retry.

   # Initialization. Wipe the error stack, so we will know that any
   # and all errors whatsoever came from here.
   $error.clear()
   $script:ping_ok_cnt = 0
   $script:ping_ok_txt = ""
   $script:ping_ok_percent = 0
   $script:ping_fail_cnt = 0
   $script:ping_fail_txt = ""

   # Sanity check, avoids divide by 0 error.
   # $script:ping_cnt = -1 # test code
   if (!($script:ping_cnt -match "^\d+$") -or $script:ping_cnt -le 0) {
      log_warning "run_ping invalid script:ping_cnt=$script:ping_cnt, must be integer GT 0, set to 1"
      $script:ping_cnt = 1
   }

   # Run the pings via powershell test-connection cmdlet.
   log_info "run_ping starting ping_host=$script:ping_host ping_cnt=$script:ping_cnt"
   try {
      $lines = test-connection -computername $script:ping_host -count $script:ping_cnt -erroraction silentlycontinue
   } catch {
      $lines = ""
   }
   
   # We get one line of stdout for each ping that succeeded.
   # Reformat the lines we got.
   foreach ($l in $lines) {
      if ($l) {
         $l = $l.tostring().trim()
         # "l = $l"
         $script:ping_ok_txt = "$script:ping_ok_txt $l <br>`n"
         $script:ping_ok_cnt++
      }
   }

   # Compute ping_ok_percent, log data. 
   $script:ping_ok_percent = ($script:ping_ok_cnt * 100.0) / $script:ping_cnt
   log_info "run_ping ping_ok_cnt=$script:ping_ok_cnt ping_ok_percent=$script:ping_ok_percent% ping_ok_txt=$script:ping_ok_txt" 

   # We get one item in the $error stack for each ping that failed. Reformat 
   # the errors we got. We take any and all errors, no matter what!
   foreach ($l in $error) {
      if ($l) {
         $l = $l.tostring().trim()
         # "l = $l"
         $script:ping_fail_txt = "$script:ping_fail_txt $l <br>`n"
         $script:ping_fail_cnt++
      }
   }
   if ($script:ping_fail_cnt -gt 0) {
      log_bold "run_ping ping_fail_cnt=$script:ping_fail_cnt ping_fail_txt=$script:ping_fail_txt"
   } else {
      log_info "run_ping ping_fail_cnt=$script:ping_fail_cnt ping_fail_txt=$script:ping_fail_txt"
   }

   # NB: calling routine will make PASS/FAIL decsions and act accordingly.
   log_info "run_ping done"
}


#==================== save_file_as ================================
# Saves the specified file for the first time. Application window
# is expected to be the active, focussed window.
#
# Tested on only IE so far.
#
# Calling parameters: app_name app_hnd filepath ft_cl ft_t ft_ks fn_cl fn_t s_m
#
# NB: To use the default saveas filetype, leave all these parameters null.
# ft_cl  - class of the combobox needed to choose filetype.
# ft_t   - title of the combobox needed to choose filetype.
# ft_cnt - sequence count of the combobox needed to choose
#          filetype, in case of multiple controls with same
#          class & title
# ft_ks  - keystroke sequence needed to select the desired 
#          filetype in the combobox list of filetype choices.
# fn_cl  - class of the edit box needed to choose filename.
# fn_t   - title of the edit box needed to choose filename.
# fn_cnt - sequence count of the edit box needed to choose
#          filename, in case of multiple controls with same
#          class & title
# s_t    - save type: ctl - use control for Save button
#                    anything else is the keystrokes to access 
#                    the Save button
# popup - ctl - use controls to  handle popup optional windows
#       - N   - number of times to send enter to handle various
#               popup windows
# no_ctrl_s - the SaveAs window is expected to be already open,
#       so dont send the initial ctrl-s sequence
#
# Returns: null
#==================================================================
function save_file_as ($app_name="", $app_hnd="", $filepath="", $ft_cl="", $ft_t="", $ft_cnt="", $ft_ks="", $fn_cl="", $fn_t="", $fn_cnt="", $s_t="", $popup="", $no_ctrl_s=$false) {

   # Sanity checks
   # NB: filetype related parameters can be null.
   if (!$app_name -or $app_name -eq "" -or !$app_hnd -or $hnd -eq "" -or $app_hnd -eq 0 -or !$filepath -or $filepath -eq "") {
      log_error "save_file_as null parm, app_name=$app_name, app_hnd=$app_hnd, filepath=$filepath"
      return
   }
   log_info "save_file_as app_name=$app_name app_hnd=$app_hnd filepath=$filepath ft_cl=$ft_cl ft_t=$ft_t ft_cnt=$ft_cnt ft_ks=$ft_ks fn_cl=$fn_cl fn_t=$fn_t fn_cnt=$fn_cnt s_t=$s_t popup=$popup no_ctrl_s=$no_ctrl_s"

   # While the SaveAs item on the IE File menu is in the same place,
   # it takes less Down keystrokes to get there on the older version.
   # $script:ie_major_ver = 8 # test code
   if ($script:ie_major_ver -le 8) {
      $ie_cmd = "{Down}{Down}{Down}{Down}{Down}{Down}{Down}{Enter}"
   } else { 
      $ie_cmd = "{Down}{Down}{Down}{Down}{Down}{Down}{Down}{Down}{Enter}"
   }

   # Send Ctrl-s to window to start the SaveAs procedure.
   # NB: Some apps, like OO sbase, have already triggered the SaveAs window. 
   # NB: Older Internet Explorer ignores Ctrl-S, typically on WinXP.
   if (!$no_ctrl_s) {
      if ($app_name -match "iexplore" -and $script:ie_major_ver -le 8) {
         # Older version of IE (typically on WinXP).
         log_info "save_file_as using Alt-f, down..."
         send_keys $app_hnd "%f" # Alt-f
         start-sleep -s 1
         send_keys $app_hnd "$ie_cmd"
      } else {
         # Other apps & newer versions of IE.
         log_info "save_file_as using Ctrl-s"
         send_keys $app_hnd "^s" # Ctrl-s
      }
      start-sleep -s 2
   }

   # Try multiple times to find child window or modal window.
   # Sometimes browsers dont respond to the first send_keys
   # with the filename and need a retry.
   $closed_child = $false # flag
   $found_child = 0 # counter
   for ($i = 1; $i -le $script:max_loop; $i++) {

      # Look for a modal windows.
      $ch = ""
      $type = ""
      if ($app_name -match "iexplore" -and $script:ie_major_ver -gt 8) {
         # New versions of IE may pop up a modal window, or may pop up a
         # child window. Its always the same on a given PC, but can
         # vary from PC to PC. The close_child_windows should have gotten
         # rid of all the IE modal windows at the start of the script.
         # For now, we match on the app_name for a slightly better, but
         # still imperfect result.
         $x = select-window -class *modal* # cant specify 2 search paramters at once!
         $cnt = $x.count
         # "modal: i=$i cnt=$cnt x=$x"
         foreach ($w in $x) {
            $name = $w.ProcessName
            # "save_file_as (1) i=$i name=$name w=$w"
            if ($name -match $app_name) {
               # "MATCHED (1) i=$i app_name=$app_name"
               if (!$ch) {
                  # "SAVING (1) i=$i w=$w"
                  $ch = $w
                  $type = "modal"
               } else { 
                  log_warning "save_file_as (2) found multiple $type windows app_name=$app_name, ignoring w=$w"
               }
            }
         }
      }

      # If no modal window found, look for a child window.
      if (!$ch) {
         # Other apps and some versions of IE.
         $ch = select-childwindow -window $app_hnd
         # test code for multiple child window loop below
         # $temp = @()  # test code
         # $temp += $ch # test code
         # $temp += $ch # test code
         # $ch = $temp  # test code
         $cnt = $ch.count
         $type = "child"
         # log_info "child: i=$i cnt=$cnt ch=$ch"
         if ($cnt -ge 2) {
            # We expect only 1 window max, but occasionally get 2.
            log_warning "save_file_as i=$i type=$type windows cnt=$cnt app_name=$app_name ch=$ch"
            $temp = $ch
            foreach ($w in $temp) {
               # If possible, take a child window with Save As title.
               # On occasion, MS office tool tips show up here and are to be avoided.
               # If all windows have null title, this algorithm takes the last one.
               $ch = $w # always take a window, no matter what
               $t = $ch.title
               if ($t -match "Save.*As") {
                  break
               }
            }
         }
      }

      # If we have a child or modal window, try to save the file.
      # "save_file_as (3) i=$i found_child=$found_child type=$type ch=$ch"
      # $ch = "" # test code
      $h = $ch.Handle
      if ($ch -and $h -and $h -ne "" -and $h -ne 0 ) {
         # Decode the window object.
         $found_child++
         $c = $ch.Class
         $n = $ch.ProcessName
         $t = $ch.Title
         log_info "save_file_as i=$i found app_name=$app_name type=$type c=$c n=$n h=$h t=$t"

         # Handling filetype selection is very different from useing default filetype.
         if ($ft_cl -ne "") {
            # Get controls
            get_controls $ch

            # Set the filetype combobox.
            if ($ft_cl -eq "keys") { 
               # Hook for older MS office apps that dont have controls to 
               # access their save as filetype combobox.
               log_info "save_file_as i=$i sending keys to select filetype box ft_cl=$ft_cl ft_t=$ft_t"
               send_keys $h $ft_t
               start-sleep -s 1
               log_info "save_file_as i=$i sending keys to set filetype: $ft_ks" 
               send_keys $h "$ft_ks"

            } else {
               # Proper way to access filetype combobox.
               log_info "save_file_as i=$i searching for control to select filetype box ft_cl=$ft_cl ft_t=$ft_t ft_cnt=$ft_cnt"
               search_controls $ft_cl $ft_t $ft_cnt
               send_click $script:ctl_hnd
               start-sleep -s 1
               log_info "save_file_as i=$i sending keys to set filetype: $ft_ks" 
               send_keys $script:ctl_hnd "$ft_ks"
            }
            start-sleep -s 1

            # Set the filename edit box.
            search_controls $fn_cl $fn_t $fn_cnt 
            send_click $script:ctl_hnd
            start-sleep -s 1
            send_keys $script:ctl_hnd "^a{backspace}"
            start-sleep -s 1
            send_keys $script:ctl_hnd "$filepath"
            start-sleep -s 1

            # Now save the file.
            if ($s_t -eq "" -or $s_t -eq "ctl") {
               # Proper way to access the Save button control and click it.
               log_info "save_file_as i=$i searching for control for Save button s_t=$s_t"
               search_controls "button" "save$" 1 # "save$" is used to avoid "save thumbnail"
               send_click $script:ctl_hnd
            } else {
               # Hook for older MS office apps that dont have controls to 
               # access their Save button.
               log_info "save_file_as i=$i sending keys for Save button s_t=$s_t"
               send_keys $h "$s_t"
            }
            start-sleep -s 2

            # The popup window(s) that occur next depend on the app.
            if ($popup -ne "ctl") {
               # MS word has a File Conversion dialogue that has no button controls.
               # OO scalc has 3 popups for .csv format.
               # Send Enter to click OK.
               for ($j = 1; $j -le $popup; $j++) {
                  log_info "save_file_as i=$i j=$j sending Enter for popup"
                  start-sleep -s 2
                  send_keys $h "{enter}"
               }

            } else {
               # These optional popup windows have button controls.
               # We usually get a new popup. If so, click OK or Yes.
               handle_child_popup $app_hnd "OK|Yes" # use orginal app_hnd, not child h

               # We may get a second new popup. If so, click Ok or Yes.
               handle_child_popup $app_hnd "OK|Yes" # use orginal app_hnd, not child h
            }

         } else {
            # Default filetype handling is very simple. 
            # Set path/filename selection.
            if ($found_child -eq 1) {
               # First time, send file path and enter.
               send_keys $h "${filepath}{enter}"

            } else {
               # Second and subsequent tries, just send enter.
               send_keys $h "{enter}" 
            }
         }
         start-sleep -s 3

         # Check if the window object still exists.
         $y = ""
         if ($type -eq "child") {
            # We had a child window.
            search_child_window $h
            $y = $search_child_window_rc
         } else {
            # We had a modal window.
            get_window_obj $h -quiet
            $y = $script:curr_obj
         }
         # If no window object, then the window has been closed.
         if($y) {
            # Window still exists.
            log_debug "save_file_as i=$i app_name=$app_name type=$type not closed h=$h y=$y"
         } else {
            # Window really is closed, we are done.
            $closed_child = $true
            break
         }
      }

      # If no modal or child window has been found yet, then wait a bit,
      # and try to open SaveAs window again.
      if ($found_child -eq 0) {
         # "save_file_as i=$i found_child=$found_child waiting..."
         start-sleep -s 3
         # Try alternate keystrokes for IE, all versions.
         if ($app_name -match "iexplore") {
            log_warning "save_file_as i=$i sending Alt-F, down..."
            send_keys $app_hnd "%f"
            start-sleep -s 1
            send_keys $app_hnd "$ie_cmd"
         } else {
            # Other apps.
            log_warning "save_file_as i=$i sending Ctrl-s again"
            send_keys $app_hnd "^s" # Ctrl-s
         }
         start-sleep -s 2
      }
   }

   # Check what really happened.
   # "save_file_as (7) i=$i found_child=$found_child closed_child=$closed_child"
   if ($found_child -eq 0) {
      log_error "save_file_as app_name=$app_name tried $script:max_loop times to open child window, FAILED!"
      # Diagnostic code - pull down file menu, throw error so TC stops here with full screenshot.
      send_keys $app_hnd "%f"
      throw_error "save_file_as diagnostic stopping TC here!"

   } elseif (!$closed_child) {
      log_error "save_file_as app_name=$app_name tried $script:max_loop times to save/close $type window, FAILED, still open: $y"
   } elseif ($i -eq 1) {
      log_info "save_file_as app_name=$app_name $type window opened/saved/closed OK on Try#: $i"
   } else {
      log_warning "save_file_as app_name=$app_name $type window opened/saved/closed OK on Try#: $i"
   }
}


#==================== script_help =================================
# Provides user with online help syntax & details.
#
# Calling parameters: none
# Returns: null
#==================================================================
function script_help {
   " "
   "Basic usage: powershell .\${script:self} <-options>"
   " "
   "By default, all tests will be run. You may modify the defaults at the"
   "top of this script, or set options in the run control file described below."
   " "
   "The <-options> below allow you to modify the script behaviour."
   "-try N           - N defaults to: $script:testcase_try_max, controls retries for test cases that"
   "                   get errors"
   "-iter M          - M defaults to: $script:testsuite_interation_max, controls how many times the entire"
   "                   test suite is run in one shot"
   "-tc integer list - integer list defaults to all $script:tc_last existing test cases, allows"
   "                   running a subset of the available test cases"
   "-batt            - test battery life, requires the user to unplug AC power"
   "                   for the duration of the tests, then plug AC power back"
   "                   in again at the end of the tests"
   "-nobatt          - do NOT run battery life test, NO user action needed"
   "-br              - use browser to show logfile results"
   "-nobr            - do NOT use browser to show logfile results, usefull for"
   "                   debugging new code"
   "-conf            - test RAM, CPU & HDD configuration"
   "-noconf          - do NOT run configuration tests"
   "-cs              - copy result files to a central server"
   "-nocs            - do NOT copy result files to a central server"
   "-mo              - test Microsoft Office"
   "-nomo            - do NOT run Microsoft Office tests"
   "-msc             - test Microsoft Security Client"
   "-nomsc           - do NOT test Microsoft Security Client"
   "-oo              - test Open Office"
   "-nooo            - do NOT run Open Office tests"
   "-oth             - test Notepad, Wordpad & Screenshot"
   "-nooth           - do NOT run other tests"
   "-net             - test ping, clock sync, web browsers (Chrome, Firefox, IE)"
   "-nonet           - do NOT run network tests"
   "-pr              - refresh of logfile results after each test case completes"
   "-nopr            - NO refresh of logfile results after each test case completes,"
   "                   wait until the end of the tests to refresh results, avoids"
   "                   stress on browser for long running tests of many"
   "                   iterations, tests run much, much faster"
   "-wae             - treat warnings as errors, demanding that the tests run"
   "                   absolutely squeaky clean"
   " "
   "There is support for an optional run control file, $script:load_rc_fn to allow"
   "customization of the script behaviour for selected PC. If this file is found,"
   "it will be executed so that different settings can be used on selected PC as"
   "specified in the run control file."
   " "
   "For example, some PC may not have MS Office installed, and you want the MS"
   "Office tests to be skipped. You can also specify different try counts,"
   "iteration counts, colors, etc in the run control file. Everything in the"
   "User Defined Variables section at the top of this script is fair game." 
   " "
   "NB: While you probably could manipulate other variables elsewhere in the"
   "script, please DONT!"
   " "
   "NB: For the MS Outlook email test to run, you need to setup a dummy email"
   "server. The process for configuring the free hMailServer is described in"
   "the User Defined Variables section at the top of this script."
   " "
}


#==================== screenshot ==================================
# Dont use, still broken code!!!
#==================================================================
function screenshot {
   # From example on the internet

   $fn="1.jpg"

   # NB: Function is broken, $bitmap is always null, get error when saving.

   Add-Type -Assembly System.Drawing

   $jpegCodec = [Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() | Where-Object { $_.FormatDescription -eq "JPEG" }
   "rc1=$?"
   $jpegCodec

   # Capture just the current window.
   [Windows.Forms.Sendkeys]::SendWait("%{PrtSc}")
   "rc2=$?"
   start-sleep -Milliseconds 250 # Let capture complete
   "rc3=$?"
   $bitmap = [Windows.Forms.Clipboard]::GetImage()
   "rc4=$?"
   $bitmap

   $ep = New-Object Drawing.Imaging.EncoderParameters
   "rc5=$?"
   $ep.Param[0] = New-Object Drawing.Imaging.EncoderParameter ([System.Drawing.Imaging.Encoder]::Quality, [long]100)
   "rc6=$?"
   $ep

   $FileType = [System.Drawing.Imaging.ImageFormat]::Jpeg
   "rc7=$?"
   $FileType
   $bitmap.Save($fn, $jpegCodec, $ep)
   [Windows.Forms.Clipboard]::Clear()
}


#==================== search_child_window =========================
# Searches for a child window based on handle. Used to detect if
# a child window still exists or not.
#
# Returns: null
# Sets variables: $script:search_child_window_rc
#==================================================================
function search_child_window ($hnd="") {

   # Initialization
   $script:search_child_window_rc = $false

   # Sanity check.
   if (!$hnd -or $hnd -eq 0) {
      log_error "search_child_window invalid hnd=$hnd"
      return
   }

   # Use brute force to see if the specified child window still exists or not.
   $ch = select-window -title * | select-childwindow 
   # "ch=$ch"
   foreach ($w in $ch) {
      # Decode the child window
      $c = $w.Class
      $h = $w.Handle
      $n = $w.ProcessName
      $p = $w.ProcessId
      $t = $w.Title
      if (!$h) {
         continue
      }

      # Look for matching child window based on specified handle.
      # "search_child_window c=$c h=$h n=$n p=$p t=$t"
      if ($h -eq $hnd) {
         # Desired window still exists, set return code and keep going.
         log_debug "search_child_window hnd=$hnd found desired child window c=$c h=$h n=$n p=$p t=$t"
         $script:search_child_window_rc = $true
      } else {
         # Log windows that didnt match for debugging purposes.
         log_debug "search_child_window hnd=$hnd ignoring child window c=$c h=$h n=$n p=$p t=$t"
      }
   }
}


#==================== search_controls =============================
# Searches the $script:controls for the first control that matches
# the specified patterns and related options.
#
# NB: Calling routine is expected to have called get_controls to
# provide up to date relevant data.
#
# Calling parameters: class_patt title_patt count <-notclass>
#                     <-nottitle> <-warnonly> <-quiet>
#
# Both patterns default to: .*
# Count defaults to 1, allows you to search farther down the list
# of controls. Usefull when you have multiple controls with same
# class & title and you need the 2nd or 3rd one in the list.
#
# The options -notclass & -nottitle allow you to search for anything
# but the specific pattern.
# 
# Option -warnonly converts most errors to warnings.
# Option -quiet suppresses most errors.
#
# Returns: null
# Sets variables: $script:ctl_class, $script:ctl_hnd, $script:ctl_title
#==================================================================
function search_controls ($class_patt="", $title_patt="", $count="") {
   # log_info "search_controls class_patt=$class_patt title_patt=$title_patt count=$count args=$args"

   # Intialization
   $script:ctl_class = ""
   $script:ctl_hnd = ""
   $script:ctl_title = ""
   try {
      $class_patt = $class_patt.Trim() # Chokes on unquoted numbers
   } catch {
      log_error "search_controls title_patt=$title_patt count=$count args=$args invalid class_patt=$class_patt, set to: .*"
      $class_patt = ".*"
   }
   if ($class_patt -eq "") {
      $class_patt = ".*"
   }
   try {
      $title_patt = $title_patt.Trim() # Chokes on unquoted numbers
   } catch {
      log_error "search_controls class_patt=$class_patt count=$count args=$args invalid title_patt=$title_patt, set to: .*"
      $title_patt = ".*"
   }
   if ($title_patt -eq "") {
      $title_patt = ".*"
   }
   if (!$count -or $count -eq "") {
      # "setting default count=1"
      $count = 1
   }
   if (!($count -match "^\d+$") -or $count -eq "0" -or $count -eq 0) {
      log_error "search_controls class_patt=$class_patt title_patt=$title_patt args=$args invalid count=$count, must be postive integer, set to 1"
      $count = 1
   }

   # Parse $args options.
   $not_class = $false
   $not_title = $false
   $quiet = $false
   $warn_only = $false
   foreach ($opt in $args) {
      $opt = $opt.Trim()
      # "opt=$opt"
      if ($opt -eq "") {
         continue
      }

      # Look for -notclass flag
      if ($opt -eq "-notclass") {
         $not_class = $true
         continue
      }

      # Look for -nottitle flag
      if ($opt -eq "-nottitle") {
         $not_title = $true
         continue
      }

      # Look for -quiet flag
      if ($opt -eq "-quiet") {
         $quiet = $true
         continue
      }

      # Look for -warnonly flag
      if ($opt -eq "-warnonly") {
         $warn_only = $true
         continue
      }

      # Invalid option ==> error
      log_error "search_controls class_patt=$class_patt title_patt=$title_patt count=$count args=$args invalid option: $opt"
      return
   }
   # log_info "search_controls class_patt=$class_patt title_patt=$title_patt count=$count args=$args not_class=$not_class not_title=$not_title quiet=$quiet warn_only=$warn_only"

   # Look for the first control that meets spec.
   $match_cnt = 0 # counter for correct matches
   foreach ($ctl in $script:controls) {

      # Get control data
      # $ctl | get-member *
      # get-variable ctl
      $c = $ctl.Class
      $h = $ctl.Handle
      $t = $ctl.Title
      # log_info "search_controls c=$c h=$h t=$t ctl=$ctl"

      # Check for class pattern match.
      if ($c -match $class_patt) {
         # Class MATCHES OK.
         $class_ok = $true
      } else {
         $class_ok = $false
      }

      # If necessary, flip the class result.
      if ($not_class) {
         if ($class_ok) {
            $class_ok = $false
         } else {
            $class_ok = $true
         }
      }

      # Check for title pattern match.
      if ($t -match $title_patt) {
         # Title MATCHES OK.
         $title_ok = $true
      } else {
         $title_ok = $false
      }

      # If necessary, flip the title result.
      if ($not_title) {
         if ($title_ok) {
            $title_ok = $false
         } else {
            $title_ok = $true
         }
      }

      # Did both class & title match appropriately?
      # log_info "search_controls class_ok=$class_ok title_ok=$title_ok"
      if ($class_ok -and $title_ok) {
         # We have a match. Do we need to keep going farther down the list?
         $match_cnt++
         if ($match_cnt -ne $count) {
            # Showing skipped items as DEBUG: makes debugging MS Office issues easier.
            log_debug "search_controls class_patt=$class_patt title_patt=$title_patt match_cnt=$match_cnt NE count=$count, skipping c=$c h=$h t=$t"
            continue
         }

         # This is the control we want.
         $script:ctl_class = $c
         $script:ctl_hnd = $h
         $script:ctl_title = $t
         log_info "search_controls class_patt=$class_patt title_patt=$title_patt count=$count args=$args found: c=$c h=$h t=$t"
         return
      }
   }

   # The desired control was not found, show all the controls data. 
   if (!$quiet) {
      log_info "search_controls controls=$script:controls"
   }

   # The desired control was not found ==> quiet/warning/error.
   if ($quiet) {
      # Say nothing!
   } elseif ($warn_only) {
      log_warning "search_controls NOT found: class_patt=$class_patt title_patt=$title_patt count=$count args=$args"
   } else {
      log_error "search_controls NOT found: class_patt=$class_patt title_patt=$title_patt count=$count args=$args"
   }
}


#==================== send_click ==================================
# Sends a click to desired control handle.
#
# Calling parameters: hnd
# Returns: null
#==================================================================
function send_click ($hnd="") {

   # Sanity check.
   if (!$hnd -or $hnd -eq "" -or $hnd -eq 0) {
      log_error "send_click invalid hnd=$hnd"
      return
   }
   # log_info "send_click hnd=$hnd"

   # Error handling wrapper for WASP send-click.
   # NB: send-click doesnt seem to ever complain about invalid handles or anything!
   $msg = ""
   $saved_rc = $false
   try {
      send-click -window $hnd
      $saved_rc = $?

   } catch {
      $msg = $Error | select-object -first 1
      $saved_rc = $false
   }
   if (!$saved_rc) {
      log_error "send_click: hnd=$hnd got: $msg"
   }
}


#==================== send_keys ===================================
# Sends key-stroke sequence to desired window.
#
# NB: If you want {enter} or "`n" new-line sent, you need to add
# this to the keys string!
#
# Calling parameters: hnd keys
# Returns: null
#==================================================================
function send_keys ($hnd="", $keys="") {

   # Sanity check.
   if (!$hnd -or $hnd -eq "" -or $hnd -eq 0) {
      log_error "send_keys invalid hnd=$hnd"
      return
   }

   # Error handling wrapper for WASP send-keys
   $msg = ""
   $saved_rc = $false
   try {
      send-keys -window $hnd -keys $keys
      $saved_rc = $?
   } catch {
      $msg = $Error | select-object -first 1
      $saved_rc = $false
   }
   if (!$saved_rc) {
      log_error "send_keys hnd=$hnd keys=$keys got: $msg"
   }
}


#==================== setup ======================================
# Initializes script level variables, other misc things.
#
# Calling parameters: none
# Returns: null
# Sets variable: many...
#==================================================================
function setup {

   # Clear the screen buffer. Works on Powershell & DOS command windows.
   clear-host

   # Save script start time.
   $EpochDiff = New-TimeSpan "01 January 1970 00:00:00" $(Get-Date)
   # get-variable EpochDiff
   $script:start_sec = [INT] $EpochDiff.TotalSeconds
   remove-variable EpochDiff  # make sure no old data is leftover
   # "setup start_sec=$script:start_sec"

   # Need this assembly for full screen shots & some error handling. 
   Add-Type -Assembly System.Windows.Forms 

   # Initialize script level variables here to avoid errors in common functions.
   $script:add_content_issue = 0        # total debug msgs issued for add-content retries
   $script:add_content_max_delay = 0    # max delay used to recover add-content issues
   $script:app_title = ""               # used by map_app_name
   $script:batt_array = @()             # used for monitoring the PC battery, if any
   $script:batt_est_time_min = -1       # negative number means test was not run
   $script:batt_life = ""
   $script:batt_state = ""
   $script:clipboard = ""               # current contents of windows clipboard
   $script:controls = @()               # array for storing controls of current window
   $script:ctl_class = ""               # for passing back to other routines
   $script:ctl_hnd = ""   
   $script:ctl_title = ""
   $script:curr_age = 0 
   $script:curr_hnd = ""
   $script:curr_obj = ""
   $script:curr_pid = ""
   $script:date_time_sync_rc = ""       # used by date_time_sync to save data for tc_2
   $script:date_time_sync_txt = ""
   $script:date_time_sync_jpg = ""
   $script:desk_hnd = ""                # window handle for complete windows desktop
   $script:discharge_sec = 0            # used by get_discharge_sec
   $script:diskpart_hdd_total = 0       # used to cache the diskpart total disk space value
   $script:ff_def_br = 0                # counter used by firefox restore_session
   $script:file_contents = ""           # used by get_file_content to pass data  to calling routine
   $script:found_laptop = $false        # flag to indicate this is a laptop
   $script:found_wifi = $false          # flag to indicate WIFI adaptor was found
   $script:load_rc_fn = $script:self -replace ".ps1" # used by load_rc
   $script:load_rc_fn = "${script:load_rc_fn}-rc.ps1"
   $script:logname = ""                 # file name only, needed for screenshot name prefix 
   $script:logfile = ""                 # full file name and .html extension, for fully detailed log
   $script:logsumm = ""                 # full file name and .txt extension, for result synopsis log
   $script:max_loop = 5                 # Number of times for selected routines to retry before logging an error
   $script:mouse_x = 0                  # current mouse x position, in pixels
   $script:mouse_y = 0                  # current mouse y position, in pixels
   $script:msg_q = @()                  # array for messages to be added to logfile after logfile is initialized
   $script:nobr = $false                # flag to control display of browser results page
   $script:nocs = $false                # flag to control copying result files to central server
   $script:nopr = $false                # flag to control periodic refresh of results page in a browser
   $script:open_office_app_patt = "sbase|scalc|sdraw|simpress|smath|soffice|swriter" # list of open office apps
   $script:ping_fail_cnt = 0            # used by run_ping & ping_test to save data for tc_1
   $script:ping_fail_txt = ""
   $script:ping_ok_cnt = 0
   $script:ping_ok_txt = ""
   $script:ping_ok_percent = 0
   $script:res_hnd = ""                 # window handle for results browser
   $script:browser_results = $script:browser_results -replace ".exe" # short form name
   $script:scr_fn = ""                  # file name of latest screenshot
   $script:scr_hnd = ""                 # foreground window handle for screenshot
   $script:scr_title = ""               # foreground window title for screenshot
   $script:scr_type = ""                # screenshot type actually done
   $temp = [Windows.Forms.SystemInformation]::VirtualScreen
   $script:scr_min_x = 0                # screen min x position, in pixels
   $script:scr_max_x = $temp.Width      # screen max x position, in pixels
   $script:scr_min_y = 0                # screen min y position, in pixels
   $script:scr_max_y = $temp.Height     # screen max y position, in pixels
   $script:server_results_subdir = ""   # subdirectory for test results storage
   $script:temp_csv = "$pwd\temp.csv"   # temporary files for test cases to use as needed
   $script:temp_htm = "$pwd\temp.htm"
   $script:temp_mml = "$pwd\temp.mml"
   $script:temp_odb = "$pwd\temp.odb"
   $script:temp_odg = "$pwd\temp.odg"
   $script:temp_odp = "$pwd\temp.odp"
   $script:temp_rtf = "$pwd\temp.rtf"
   $script:temp_scr = "$pwd\temp.scr"
   $script:temp_txt = "$pwd\temp.txt"
   $script:temp_dir = "$pwd\temp_files" # browser usually leaves related images in this directory
   $script:wae = $false                 # treat warnings as errors
   $script:wasp_path = ""               # wasp.dll path
   $script:wifi_warmup = $false         # flag to ensure wifi warmup done only once.

   # To keep the communication between functions simple and relatively clean,
   # we have a separate $script:<function>_rc variable for just those routines 
   # that need to send pass/fail status up to the higher level calling routine.
   # The danger of useing a single rc variable is that one routine could unknowingly
   # stomp on the results of another routine before it is used by the routine that
   # really wants it.
   $script:copy_file_rc = $false
   $script:remove_window_rc = $false
   $script:restore_window_rc = $false
   $script:search_child_window_rc = $false
   $script:wait_for_control_rc = $false

   # Counters needed to keep track of test case execution and results
   $script:cleanup_rtn = ""
   $script:tc_err = 0
   $script:tc_error_stats = @{} 
   $script:tc_fail = 0
   $script:tc_fail_stats = @{} 
   $script:tc_list = ""
   $script:tc_max_sec = -1 # Needed to match when max time is 0
   $script:tc_max_name = ""
   $script:tc_max_title = ""
   $script:tc_max_iter = 0
   $script:tc_max_try = 0
   $script:tc_num = ""
   $script:tc_pass = 0
   $script:tc_pending = 0
   $script:tc_pending_stats = @{}
   $script:tc_result_rc = $false
   $script:tc_skip = 0
   $script:tc_skip_reason = ""
   $script:tc_skip_stats = @{}
   $script:tc_start_sec = 0
   $script:tc_stop_sec = 0
   $script:tc_title = ""
   $script:tc_total = 0
   $script:tc_try_cnt = 0
   $script:tc_warn = 0
   $script:tc_warning_stats = @{} 
   $script:testsuite_interation = 0
   $script:total_bug = 0
   $script:total_crash = 0
   $script:total_error = 0
   $script:total_screenshot = 0
   $script:total_warning = 0

   # Get the localhost specific info
   setup_localhost_info

   # Loading the optional run control file here ensures that the 
   # command line options are parsed afterwards and will be able
   # to override settings in the run control file.
   load_rc
}


#==================== setup_import_firefox ========================
# Firefox has several import dialog boxes the first time it runs
# that need to be handled.
#
# Calling parameters: app_name
# Returns: null
#==================================================================
function setup_import_firefox ($app_name="") {

   # NB: To test this code, uninstall then reinstall Firefox.
   # Alternatively, you can just delete the Profile folder in
   # C:\users\<username>\AppData\Roaming\Mozilla\Firefox,
   # also need to delete the profiles.ini file.

   # If its not firefox running, we are done.
   if (!($app_name -match "firefox")) {
      log_debug "setup_import_firefox app_name=$app_name returning" 
      return
   }

   # Look for Import Wizard window from Firefox.
   $x = select-window -title *import*wizard*
   $x = $x | select-object -first 1
   $c = $x.Class
   $h = $x.Handle
   $n = $x.ProcessName
   $p = $x.ProcessId
   $t = $x.Title
   if (!$h -or !($n -match "firefox")) {
      log_debug "setup_import_firefox c=$c h=$h n=$n p=$p t=$t NO MATCH" 
      return
   }

   # Log entry to show we ran
   log_info "setup_import_firefox found c=$c h=$h n=$n p=$p t=$t"

   # Make sure import window is focussed
   restore_window $h

   # NB: Firefox import wizard does NOT like new-line. Use enter instead!!!

   # Send enter to click the next button to import default data.
   send_keys $h "{enter}"
   start-sleep -s 2

   # Send enter to click the next button for home page selection.
   send_keys $h "{enter}"
   start-sleep -s 6 # let firefox import the data now.

   # Send enter to click the finish button.
   send_keys $h "{enter}"
   start-sleep -s 2

   # Check that we are done.
   get_window_obj $h -quiet
   log_debug "setup_import_firefox h=$h curr_obj=$script:curr_obj"
   if ($script:curr_obj) {
      log_error "setup_import_firefox not completed, h=$h curr_obj=$script:curr_obj, see screenshot below"
      log_screenshot "" "full"
   }
}


#==================== setup_localhost_info ========================
# Saves localhost info in script level variables.
#
# Calling parameters: none
# Returns: null
# Sets variable: many...
#==================================================================
function setup_localhost_info {

   # NB: log_misc has the checks to verify we get values for the
   # variables we setup in here.

   # NB: get-childitem returns lists of subkeys, not values!

   # Get Windows product & version numbers, available for XP onwards.
   $r = "hklm:\Software\Microsoft\Windows NT\CurrentVersion"
   $x = get-itemproperty -path $r
   log_debug "setup_localhost_info r=$r x=$x"
   $script:win_prod = $x.ProductName
   $temp = $x.CurrentVersion.split(".")
   $script:win_major_ver = $temp[0]
   $script:win_minor_ver = $temp[1]
   $script:win_act_code = $x.ProductId
   $reg_org = $x.RegisteredOrganization # shows mfc name, but not model, not populated for XP!
   # "reg_org=$reg_org"

   # Get manufacturer name and product.
   $script:mfc_name = ""
   $script:mfc_prod = ""
   if ($script:win_major_ver -le 5) {
      # These registry entries are not available for XP.
      # There is a tool \program files\common\microsoft shared\msinfo\msinfo32.exe
      # which will let you specify \report c:\temp.txt giving ram and mfc info,
      # along with tons of other stuff we dont really need, but its really, really
      # slow and would be a real pain to keep waiting for it every time the script
      # is run before we can compose the log_suffix name.
      $script:mfc_name = "unknown"
      $script:mfc_prod = "unknown"

   } else {
      # Available in WinVista, Win7, hopefully onwards.
      $r = "hklm:\HARDWARE\DESCRIPTION\System\BIOS"
      $x = get-itemproperty -path $r
      log_debug "setup_localhost_info r=$r x=$x"
      if ($x) {
         $script:mfc_name = $x.SystemManufacturer
         $script:mfc_prod = $x.SystemProductName
      }

      # OEM organizations may have put their name in a different place.
      # $script:mfc_name = "" # test code
      if ($script:mfc_name -eq "" -or $script:mfc_prod -eq "") {
         $r = "hklm:\Software\Microsoft\Windows\CurrentVersion\OEMInformation"
         $x = get-itemproperty -path $r
         log_debug "setup_localhost_info r=$r x=$x"
         if ($x) {
            $script:mfc_name = $x.Manufacturer
            $script:mfc_prod = $x.Model # May not be populated
            if ($script:mfc_name -ne "" -and !$script:mfc_prod) {
               $script:mfc_prod = "unknown"
            }
            # "OEM mfc_name=$script:mfc_name mfc_prod=$script:mfc_prod"
         }
      }

      # Last try, using registered organization.
      # $script:mfc_name = "" # test code
      if ($script:mfc_name -eq "" -and $reg_org -ne "") {
         $script:mfc_name = $reg_org
         $script:mfc_prod = "unknown"
         # "REG_ORG mfc_name=$script:mfc_name mfc_prod=$script:mfc_prod"
      }
   }

   # The mgf_prod may give us a clue that this really is a laptop
   if ($script:mfc_prod -match "book|laptop|note|pad") {
      $script:found_laptop = $true
   }

   # Get CPU info, available for XP onwards.
   $cpu_path = "hklm:\HARDWARE\DESCRIPTION\System\CentralProcessor"
   $x = get-itemproperty -path "$cpu_path\0" # CPU 0
   log_debug "setup_localhost_info cpu_path=$cpu_path x=$x"
   $script:cpu_id = $x.Identifier
   $script:cpu_name = $x.ProcessorNameString
   $script:cpu_ghz = ""
   # Cant seem to access the ~MHz property the usual way, so parse it out.
   if ($x -match "(mhz=)(\d+)") {
      # $matches
      $script:cpu_ghz = $matches[2] / 1000
   }

   # Count the number of CPU in the BIOS.
   # NB: It seems a separate thread shows up as a CPU.
   $script:cpu_cnt = 0
   for ($i = 0; $i -le 256; $i++) {
      $x = get-itemproperty -path "$cpu_path\$i" -erroraction silentlycontinue
      # "i=$i x=$x"
      $y = $x.Identifier
      if ($y) {
         $script:cpu_cnt++
      } else {
         # CPU are always in a continuous sequence, so break on first one not found.
         # "cpu_cnt i=$i break"
         break
      }
   }
   # "cpu_cnt=$script:cpu_cnt"

   # CPU Id is same for all CPU of same type, eg: i7-4770s, so it is not
   # unique to this specific PC instance. Also, it is a long hex string,
   # so its not particularly useful.
   # NB: wmic.exe is not available on XP!
   # $x = wmic.exe CPU get ProcessorId
   # "x=$x"
   # foreach ($l in $x) {
   #    $l = $l.Trim()
   #    # "l=$l"
   #    if ($l -ne "" -and $l -ne "ProcessorID") {
   #       $script:cpu_procid = $l
   #    }
   # }

   # CPU UniqueId is usually blank.
   # $x = wmic.exe CPU get UniqueId

   # Get RAM info. There will be a value for each bank, installed or not.
   if ($script:win_major_ver -le 5) {
      # NB: wmic.exe is not available on XP!
      # There is a tool \program files\common\microsoft shared\msinfo\msinfo32.exe
      # which will let you specify \report c:\temp.txt giving ram and mfc info,
      # along with tons of other stuff we dont really need, but its really, really
      # slow and would be a real pain to keep waiting for it.
      $script:ram_gb = "unknown"

   } else {
      # Available for Vista, Win7, hopefully onwards.
      $x = wmic.exe MemoryChip get Capacity
      if ($x -and $?) {
         $script:ram_gb = 0.0
         foreach ($ram in $x) {
            $ram = $ram.Trim()
            # "ram=$ram"
            # Save only the numeric values.
            if ($ram -match "^\d+$") {
               $script:ram_gb += $ram
            }
         }
         $script:ram_gb = $script:ram_gb / 1073741824 # Convert to GB
       }
   }

   # Get MAC & matching IP address.
   # The command "Get-WMIObject win32_NetworkAdapter" shows everything that
   # is configured on the PC, but does not show which items are active items
   # or their corresponding IP addresses. So we parse the ipconfig output for
   # the active adapters.
   $lines = ipconfig /all
   $adap_bth_ip = ""           # BlueTooth data
   $adap_bth_mac = ""
   $adap_eth_ip = ""           # Wired ethernet data
   $adap_eth_mac = ""
   $adap_wifi_ip = ""          # WIFI data
   $adap_wifi_mac = ""
   $adap_ip = ""               # parser state info
   $adap_mac = ""
   $adap_name = ""             
   $adap_type = ""
   $script:adap_no_match = ""  # Collect items not matched
   foreach ($l in $lines) {

      # Skip blank lines.
      $l = $l.Trim()
      if ($l -eq "") {
         # "skipping l=$l"
         continue
      }

      # Skip connection-specific DNS suffix.
      if ($l -match "connection-specific.*DNS.*suffix") {
         # "skipping DNS Suffix l=$l"
         continue
      }

      # Look for header lines with connection: or tunnel isatap:
      if ($l -match "connection.*:$|tunnel.*isatap.*:$") {
         # We found a new connection.

         # If we found both mac & ip data for the previous connection,
         # we save it now as a matched pair. This ensures that we dont
         # save an ip address for an unrelated mac address.
         if ($adap_type -ne "" -and $adap_mac -ne "" -and $adap_ip -ne "") {
            # "saving (1) adap_type=$adap_type adap_mac=$adap_mac adap_ip=$adap_ip adap_name=$adap_name"
            $x = "`$adap_${adap_type}_mac"
            invoke-expression "$x = `"$adap_mac`""
            $y = "`$adap_${adap_type}_ip"
            invoke-expression "$y = `"$adap_ip`""
         }

         # Forget any previous data collected.
         $adap_ip = ""
         $adap_mac = ""
         $adap_name = ""
         $adap_type = ""

         # Figure out adap_type for the new connection.
         $adap_name = $l
         # "new connection adap_name=$adap_name"
         if ($l -match "blue|tooth|personal") {
            $adap_type = "bth" # lower case!
         } elseif ($l -match "wireless|wifi|wlan") {
            $adap_type = "wifi" # lower case!
            # Record the fact that there is a WIFI link present, even if the WIFI link
            # does not have an IP address currently assigned to the WIFI link. The 
            # presence of WIFI is used by battery discharge test as an indicator
            # that this is a probably a laptop PC and should have a battery installed.
            $script:found_wifi = $true
         } elseif ($l -match "gigabit|ethernet.*local") {
            $adap_type = "eth" # lower case!
         } elseif ($l -match "tunnel") {
            # Ignore tunnel adapters.
         } else {
            # Collect unmatched items for display later on by log_misc.
            $script:adap_no_match = "$script:adap_no_match $l <br>`n"
            # "******** NOT matched: $l"
         }
         # "found adap_type=$adap_type adap_name=$adap_name"
         continue
      }

      # For adap_type=null, skip subsequent lines, they are of no interest.
      if ($adap_type -eq "") {
         # "adap_type=null skipping: $l"
         continue
      }

      # Look for MAC address.
      if ($l -match "(physical.*address.*:\s*)([\-0-9a-f]+)") {
         $adap_mac = $matches[2]
         $adap_mac = $adap_mac -replace "-" # compact for use in filename
         $adap_mac = $adap_mac -replace ":" # colons not allowed in filename
         $adap_mac = "${adap_type}_${adap_mac}" # include adapter type
         # "found adap_mac=$adap_mac l=$l"
         continue
      }

      # Look for IPV4 address. Avoid IPV6.
      # On XP, it only shows IP Address, no V4. 
      if ($l -match "(IPV?4?\s*address.*:\s*)([\.0-9]+)") {
         $adap_ip  = $matches[2]
         # "found adap_ip=$adap_ip l=$l"
         continue
      }

      # Everything else is ignored.
      # "end-loop skipping: $l"
   }

   # We may have data to save from last connection processed.
   # $adap_type = "eth" # test code
   # $adap_mac = "fred" # test code
   # $adap_ip = "barney" # test code
   if ($adap_type -ne "" -and $adap_mac -ne "" -and $adap_ip -ne "") {
      # "saving (2) adap_type=$adap_type adap_mac=$adap_mac adap_ip=$adap_ip adap_name=$adap_name"
      $x = "`$adap_${adap_type}_mac"
      invoke-expression "$x = `"$adap_mac`""
      $y = "`$adap_${adap_type}_ip"
      invoke-expression "$y = `"$adap_ip`""
   }
   # "adap_no_match=$script:adap_no_match"

   # Choose ETH mac/ip first, second choice is WIFI, finally BT.
   # $adap_wifi_ip = "" # test code
   if ($adap_eth_mac -and $adap_eth_ip) {
      $script:mac_addr = $adap_eth_mac
      $script:ip_addr  = $adap_eth_ip
   } elseif ($adap_wifi_mac -and $adap_wifi_ip) {
      $script:mac_addr = $adap_wifi_mac
      $script:ip_addr  = $adap_wifi_ip
   } elseif ($adap_bth_mac -and $adap_bth_ip) {
      $script:mac_addr = $adap_bth_mac
      $script:ip_addr  = $adap_bth_ip
   } 
   # "mac_addr=$script:mac_addr ip_addr=$script:ip_addr"

   # Get local hostname
   $script:host_name = hostname
   # "host_name=$script:host_name"

   # Get Internet Explorer version, available for XP onwards.
   $r = "hklm:\Software\Microsoft\Internet Explorer"
   $x = get-itemproperty -path $r
   log_debug "setup_localhost_info r=$r x=$x"
   $temp = $x.Version.split(".")
   $script:ie_major_ver = $temp[0]
   $script:ie_minor_ver = $temp[1]
   $script:ie_build_ver = $temp[2] + "." + $temp[3]

   # NB: log_misc has the checks to verify we get values for the
   # variables we setup in here.

   # Compose suffix for use in logfile name using manufacturer name, model & cpu info.
   # Start with full manufacturer name, take the first token only. That will drop
   # the "Inc." from "Dell Inc.", may solve other issues.
   # $script:mfc_name = "Dell Inc." # test code
   $temp = $script:mfc_name
   $temp = $temp.Split(" ")
   $temp = $temp[0]
   if ($temp -match "hew.*pack") {
      $temp = "HP" # Use well known short form
   }
   # "temp=$temp"
   # Add in model & cpu info.
   $temp = "$temp $script:mfc_prod $script:cpu_name" 
   $temp = $temp.Split(" ")
   # "temp=$temp"

   # For log_suffix, take 3 tokens, avoiding consecutive duplicates.
   $i = 0
   $script:log_suffix = $script:host_name # start with local hostname.
   $prev = ""
   foreach ($y in $temp) {
      # "y=$y"
      if ($y -eq "" -or $y -eq $prev -or $y -eq "unknown") {
         # "skipping y=$y"
         continue
      }
      $prev = $y
      $i++
      if ($script:log_suffix -eq "") {
         $script:log_suffix = $y
      } else {
         $script:log_suffix = "${script:log_suffix}_${y}"
      }
      if ($i -ge 3) {
         break
      }
   }

   # Add mac address to maximize uniqueness.
   $script:log_suffix = "${script:log_suffix}_${script:mac_addr}"
   # "log_suffix=$script:log_suffix"

   # Look in registry for InstallRoot path for MS office.
   # NB: MS office may not be installed!
   # Put the latest Office version in list first, so we will find latest 
   # first. There can be multiple versions installed on the PC. 
   $script:ms_office_path = ""
   $ver_list = 15, 14, 12, 11 # Microsoft skipped 13 (unlucky!)
   foreach ($v in $ver_list) {
      try {
         $r = "hklm:\Software\Microsoft\Office\${v}.0\Common\InstallRoot"
         $x = get-itemproperty -path $r -erroraction silentlycontinue
      } catch {
         $x = ""
      }
      $script:ms_office_path = $x.Path
      log_debug "setup_localhost_info v=$v r=$r x=$x found: ms_office_path=$script:ms_office_path"
      if ($script:ms_office_path -and $script:ms_office_path -ne "") {
         break
      }
   }

   # One other place to check for MS office, if needed.
   if (!$script:ms_office_path -or $script:ms_office_path -eq "") {
      $r = "hklm:\Software\Microsoft\Windows\CurrentVersion\App Paths\excel.exe"
      try {
         $x = get-itemproperty -path $r -erroraction silentlycontinue
      } catch {
         $x = ""
      }
      $script:ms_office_path = $x.Path
      log_debug "setup_localhost_info r=$r x=$x found: ms_office_path=$script:ms_office_path"
   }

   # Set MS office version based on path found.
   $script:ms_office_ver = ""
   if ($script:ms_office_path -match "office11\\$") {
      $script:ms_office_ver = 2003 # set as number
   } elseif ($script:ms_office_path -match "office12\\$") {
      $script:ms_office_ver = 2007 # set as number
   } elseif ($script:ms_office_path -match "office14\\$") {
      $script:ms_office_ver = 2010 # set as number
   } elseif ($script:ms_office_path -match "office15\\$") {
      $script:ms_office_ver = 2013 # set as number
   }

   # Look in registry for OpenOffice version.
   # NB: Open Office may not be installed!
   $script:open_office_major_ver = ""
   $script:open_office_minor_ver = ""
   $r = "hklm:\SOFTWARE\RegisteredApplications" # Available for XP, Vista, Win7
   try {
      $x = get-itemproperty -path $r -erroraction silentlycontinue 
   } catch {
      $x = ""
   }
   log_debug "setup_localhost_info r=$r x=$x"
   # NB: x sure looks like a hash, but the .keys method doesnt work!
   $y = $x | get-member * # alternate way to break out data
   # "y=$y"

   # Parse out OpenOffice major/minor version info, keeping most recent version.
   foreach ($l in $y) {
      $l = $l.ToString()
      # "l=$l"
      if ($l -match "OpenOffice(.org)?\s+(\d+)\.(\d+)?") {
         # $matches
         $major = $matches[2]
         $minor = $matches[3]
         # "OpenOffice found major=$major minor=$minor"
         if (!$script:open_office_major_ver -or $script:open_office_major_ver -eq "") {
            # Always save the first values found.
            $script:open_office_major_ver = $major
            $script:open_office_minor_ver = $minor
            continue
         }
         if ($major -gt $script:open_office_major_ver) {
            # When there are multiple versions available, save the most recent.
            $script:open_office_major_ver = $major
            $script:open_office_minor_ver = $minor
            continue
         }
      }
   }
   # "open_office_major_ver=$script:open_office_major_ver open_office_minor_ver=$script:open_office_minor_ver"

   # Define new PS drive to access registry HKEY_CLASSES_ROOT
   New-PSDrive -PSProvider registry -Root HKEY_CLASSES_ROOT -Name HKCR | out-null

   # Find Open Office path by locating scalc.exe. 
   # NB: This is NOT consistent across WinXP, Vista or Win7!
   $script:open_office_path = ""
   $reg_list = "hklm:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\scalc.exe",
      "hkcr:\Applications\scalc.exe\shell\new\command",
      "hkcr:\opendocument.CalcDocument.1\shell\open\command"
   foreach ($r in $reg_list) {
      # Get specific registry entry.
      try {
         $x = get-itemproperty -path $r -erroraction silentlycontinue 
      } catch {
         $x = ""
      }
      log_debug "setup_localhost_info r=$r x=$x"

      # Look for default path & clean up.
      $path = $x."(default)"
      # "path=$path"
      $path = $path -replace "-(o|n).*"
      $path = $path -replace "scalc.exe"
      $path = $path -replace '"' # remove double quotes
      $path = $path.trim()
      # "path=$path"

      # Always save the first value found.
      if (!$script:open_office_path -or $script:open_office_path -eq "") {
         if ($path -ne "") {
            # "openoffice initial path match"
            $script:open_office_path = $path
         }
         continue
      }

      # Keep path matching the major version.
      if ($path -match "OpenOffice(.org)?\s+$script:open_office_major_ver") {
         # "openoffice path major version match"
         $script:open_office_path = $path
         continue
      }
   }
   # "open_office_path=$script:open_office_path"

   # NB: If I ever needed to select amongst multiple OpenOffice versions installed
   # on the same PC, I would add a new command line option -oopath to allow user to 
   # specify which one to test. Then the user could rerun just the desired OpenOffice
   # testcases with the specified path.
}


#==================== setup_ms_apps ===============================
# MS Office apps, including Outlook, have a dialog box to prompt
# for users name and initials the first time any of these apps run.
# On occasion, this info will get lost, and apps will prompt again.
#
# Calling parameters: app_name
# Returns: null
#==================================================================
function setup_ms_apps ($app_name="") {

   # NB: To test this code:
   # 1) For Powerpoint 2003, goto Tools Menu, Options, General Tab
   #    and delete the users name & initials
   # 2) For Powerpoint 2010, goto File Menu, Options window,
   #    General Tab and delete the users name & initials.

   # NB: While the users name and initials are shared amongst Excel, 
   # Powerpoint and Word, there are some inconsistencies. Excel will
   # only show the username, but not the initials. Word 2003 will 
   # let you delete the username & initials, but Word 2010 will 
   # NOT let you delete the username & initials. 

   # If app_name is not excel, powerpnt or winword, we are done.
   if (!($app_name -match "excel|powerpnt|winword|outlook")) {
      log_debug "setup_ms_apps app_name=$app_name returning" 
      return
   }

   # When the prompt for username & intials occurs, it takes a few seconds
   # and appears as a child window.
   start-sleep -s 3
   map_app_name $app_name
   $ch_list = select-window -title *${script:app_title}* | select-childwindow

   # Look for child window title "User Name"
   $cnt = 0
   foreach ($ch in $ch_list) {
      $c = $ch.Class
      $h = $ch.Handle
      $n = $ch.ProcessName
      $p = $ch.ProcessId
      $t = $ch.Title
      if (!$h) {
         continue
      }
      $cnt++
      if ($t -match "user.*name") {
         log_bold "setup_ms_apps found child window c=$c h=$h n=$n p=$p t=$t, sending Enter"
         send_keys $h "{enter}"
         break
      } else {
         log_debug "setup_ms_apps ignoring child window c=$c h=$h n=$n p=$p t=$t"
      }
   }
   log_debug "setup_ms_apps found $cnt child window(s), returning."
}


#==================== setup_ms_outlook ============================
# MS Outlook has several dialog boxes the first time it runs
# that need to be handled.
#
# Calling parameters: app_name
# Returns: null
#==================================================================
function setup_ms_outlook ($app_name="") {

   # NB: To completely remove an email profile, goto Control Panels,
   # click on Mail control panel, click Show Profiles, then
   # you can delete an existing Outlook profile.

   # NB: The .PST file gets left in the C:\users\<username>\documents\Outlook Files
   # directory. It is NOT deleted even though the other profile info
   # is deleted.

   # When an email profile is deleted, Outlook will give you get a prompt
   # for a New Profile name. The code below will handle the slightly 
   # different set of prompts, and the associtated window handle/object
   # resyncs that are needed.

   # When you need to test the original first time startup procedure, simply
   # cancel out of the wizard after you gave it a New Profile name. The
   # next time Outlook starts, it will use the original first time setup
   # set of prompts.

   # If its not MS Outlook application running, we are done.
   if (!($app_name -match "outlook")) {
      log_debug "setup_ms_outlook app_name=$app_name returning" 
      return
   }

   # Time delay for slow PC
   start-sleep -s 5

   # MS Outlook starts up by presenting a child window to the user.
   # For 2003, there are 2 child windows, one of which has a child window
   # of its own.
   $ch_list = select-window -title *microsoft*outlook* | select-childwindow
   foreach ($ch in $ch_list) {
      # Decode the child window object.
      $c = $ch.Class
      $h = $ch.Handle
      $n = $ch.ProcessName
      $p = $ch.ProcessId
      $t = $ch.Title

      # MS Outlook may show child windows when receiving email, ignore them.
      # There are many titles for these numerous tooltips, so we ignore them all.
      if ($c -match "tooltip") {
         log_debug "setup_ms_outlook ignoring child window c=$c h=$h n=$n p=$p t=$t"
         $c = ""
         $h = ""
         $n = ""
         $p = ""
         $t = ""
         continue 
      }

      # Log the child windows that were not ignored.
      if ($h -and $h -ne 0) {
         log_bold "setup_ms_outlook found child window c=$c h=$h n=$n p=$p t=$t"
      }

      # For MS Outlook 2003, take the msosplash window.
      if ($c -match "msosplash") {
         break
      }
   }

   # Sanity check.
   if (!$h -or $h -eq "" -or $h -eq 0) {
      # This is NOT the trigger we need to setup a new email account.
      log_debug "setup_ms_outlook c=$c h=$h n=$n p=$p t=$t NO MATCH" 
      return
   }

   # MS Outlook 2003 setup needs different code.
   log_info "setup_ms_outlook ver=$script:ms_office_ver"
   if ($script:ms_office_ver -eq "2003") {
      # The "New Profile" prompt, if any, will show up as second level child window.
      $x = select-childwindow -window $ch
      $y = $x.title
      if ($y -match "new\s*profile") {
         # The new profile prompt occurs after you create a profile and
         # then delete it.
         $ch = $x
         log_info "setup_ms_outlook setting new profile name"

         # Get the controls & set the profile name.
         get_controls $ch
         search_controls "richedit" ".*" 1
         send_click $script:ctl_hnd
         send_keys $script:ctl_hnd $script:email_display 
         start-sleep -s 1

         # Click the OK button.
         search_controls "button" "OK" 1
         send_click $script:ctl_hnd

         # Handle changes here, need to resync to second level child window.
         resolve_window_handle outlook
         $h = $script:curr_hnd
         get_window_obj $h
         $w = $script:curr_obj
         $y = select-childwindow -window $w  # first level child
         $ch = select-childwindow -window $y # second level child 
         $h = $ch.handle
         log_debug "setup_ms_outlook h=$h ch=$ch"

         # Get controls & click next button to choose add new email account.
         get_controls $ch
         search_controls "button" "next" 1
         send_click $script:ctl_hnd
         start-sleep -s 2

      } else {

         # Get controls & click next button on initial setup window.
         get_controls $ch
         search_controls "button" "next" 1
         send_click $script:ctl_hnd
         start-sleep -s 2

         # Get controls & click next button on email accounts window.
         get_controls $ch
         search_controls "button" "next" 1
         send_click $script:ctl_hnd
         start-sleep -s 2
      }

      # Get controls & select POP3 on server type window.
      log_info "setup_ms_outlook setting choosing POP3"
      get_controls $ch
      search_controls "button" "POP3" 1
      send_click $script:ctl_hnd
      start-sleep -s 1

      # Click next button
      search_controls "button" "next" 1
      send_click $script:ctl_hnd
      start-sleep -s 2

      # There is no Manually option to choose here.

      # Create an email account.
      # This childwindow has many richedit controls, with no titles.
      log_info "setup_ms_outlook account setup"

      # So we choose them progressively by numbers 1, 2, 3 ...
      get_controls $ch
      search_controls "richedit" ".*" 1 # display name
      send_click $script:ctl_hnd
      send_keys $script:ctl_hnd $script:email_display
      start-sleep -s 1

      search_controls "richedit" ".*" 2 # email address
      send_click $script:ctl_hnd
      send_keys $script:ctl_hnd $script:email_address
      start-sleep -s 1

      search_controls "richedit" ".*" 3 # incoming mail server
      send_click $script:ctl_hnd
      send_keys $script:ctl_hnd $script:email_server_name
      start-sleep -s 1

      search_controls "richedit" ".*" 4 # outgoing mail server
      send_click $script:ctl_hnd
      send_keys $script:ctl_hnd $script:email_server_name
      start-sleep -s 1

      search_controls "richedit" ".*" 5 # username = full email
      send_click $script:ctl_hnd
      send_keys $script:ctl_hnd "^a{backspace}" # clear default text
      send_keys $script:ctl_hnd $script:email_address
      start-sleep -s 1

      search_controls "richedit" ".*" 6 # password
      send_click $script:ctl_hnd
      send_keys $script:ctl_hnd $script:email_password
      start-sleep -s 1

      # The remember password check box is defaulted on.

      # If appropriate, select the More Settings popup window.
      if ($script:email_needs_authentication) {
         log_info "setup_ms_outlook more settings"
         search_controls "button" "more.*set"
         send_click $script:ctl_hnd
         start-sleep -s 2

         # Use hot-keys to select the Outgoing Server tab.
         send_keys $h "^{tab}" # Ctrl-tab
         start-sleep -s 2

         # Check the "My outgoing server requires authentication" option box.
         get_controls $ch
         search_controls "button" "my.*outgoing"
         send_click $script:ctl_hnd
         start-sleep -s 1

         # Click the OK button.
         search_controls "button" "^OK$"
         send_click $script:ctl_hnd
         start-sleep -s 2
      }

      # Click test account settings button.
      log_info "setup_ms_outlook start testing settings"
      get_controls $ch
      search_controls "button" "test\s*account"
      send_click $script:ctl_hnd

      # The account settings get tested now.
      start-sleep -s 10

      # Click the Close button.
      log_info "setup_ms_outlook done test settings"
      get_controls $ch
      search_controls "button" "close"
      send_click $script:ctl_hnd
      start-sleep -s 2

      # Click the next button.
      log_info "setup_ms_outlook start testing settings"
      get_controls $ch
      search_controls "button" "&next" # ampersand ensures choice of correct next button.
      send_click $script:ctl_hnd
      start-sleep -s 2

      # Now click Finish button.
      get_controls $ch
      search_controls "button" "finish" 1 -quiet
      if ($script:ctl_hnd) {
         send_click $script:ctl_hnd
         start-sleep -s 2
      } else {
         # There is no point letting test case continue, so throw error here.
         throw_error "setup_ms_outlook not completed, no finish button found"
      }

      # Check that we are done.
      search_child_window $h
      log_debug "setup_ms_outlook h=$h search_child_window_rc=$script:search_child_window_rc"
      if ($script:search_child_window_rc) {
         # There is no point letting test case continue, so throw error here.
         throw_error "setup_ms_outlook not completed, h=$h search_child_window_rc=$script:search_child_window_rc, see screenshot below"
      } else {
         log_bold "setup_ms_outlook completed OK, h=$h search_child_window_rc=$script:search_child_window_rc"
      }
      return # done Outlook 2003 setup!
   }

   # Code for MS Outlook 2010 onwards.
   # Make sure MS Outlook window is focussed.
   restore_window $h

   # The initial prompts vary slightly depending on whether this is 
   # really the first time initialization versus you already created
   # a profile and then deleted it.
   $need_resync = $false
   if ($t -match "new\s*profile") {
      # The new profile prompt occurs after you create a profile and
      # then delete it.
      log_info "setup_ms_outlook setting new profile name"

      # Get the controls & set the profile name.
      get_controls $ch
      search_controls "richedit" ".*" 1
      send_click $script:ctl_hnd
      send_keys $script:ctl_hnd $script:email_display 
      start-sleep -s 1

      # Click the OK button.
      search_controls "button" "OK" 1
      send_click $script:ctl_hnd

      # At this point the window handle changes. So we need to resync
      # with the new window handle & object here.
      $need_resync = $true
      resolve_window_handle outlook
      $h = $script:curr_hnd
      get_window_obj $h
      $ch = $script:curr_obj

   } else {
      # First time setup.
      log_info "setup_ms_outlook first time setup"

      # Get the controls and click the next button to start the setup.
      get_controls $ch
      search_controls "button" "next" 1
      send_click $script:ctl_hnd
      start-sleep -s 2

      # Get the controls and click the next button to start configuring email account.
      get_controls $ch
      search_controls "button" "next" 1
      send_click $script:ctl_hnd
   }

   # Get the controls, click the Manually option, so we can input server name.
   log_info "setup_ms_outlook selecting manual setup"
   start-sleep -s 2
   get_controls $ch
   search_controls "button" "manually" 1
   send_click $script:ctl_hnd
   start-sleep -s 2

   # Click the next button.
   get_controls $ch
   search_controls "button" "next" 1
   send_click $script:ctl_hnd
   start-sleep -s 2

   # Now choose the service, default is internet email.
   log_info "setup_ms_outlook choose internet email"
   get_controls $ch
   search_controls "button" "next" 1
   send_click $script:ctl_hnd
   start-sleep -s 2

   # Create an email account.
   # This childwindow has many richedit controls, with no titles.
   log_info "setup_ms_outlook account setup"

   # So we choose them progressively by numbers 1, 2, 3 ...
   get_controls $ch
   search_controls "richedit" ".*" 1 # display name
   send_click $script:ctl_hnd
   send_keys $script:ctl_hnd $script:email_display
   start-sleep -s 1

   search_controls "richedit" ".*" 2 # email address
   send_click $script:ctl_hnd
   send_keys $script:ctl_hnd $script:email_address
   start-sleep -s 1

   search_controls "richedit" ".*" 3 # incoming mail server
   send_click $script:ctl_hnd
   send_keys $script:ctl_hnd $script:email_server_name
   start-sleep -s 1

   search_controls "richedit" ".*" 4 # outgoing mail server
   send_click $script:ctl_hnd
   send_keys $script:ctl_hnd $script:email_server_name
   start-sleep -s 1

   search_controls "richedit" ".*" 6 # username = full email
   send_click $script:ctl_hnd
   send_keys $script:ctl_hnd "^a{backspace}" # clear default text
   send_keys $script:ctl_hnd $script:email_address
   start-sleep -s 1

   search_controls "richedit" ".*" 7 # password
   send_click $script:ctl_hnd
   send_keys $script:ctl_hnd $script:email_password
   start-sleep -s 1

   # The remember password check box is defaulted on.

   # If appropriate, select the More Settings popup window.
   if ($script:email_needs_authentication) {
      log_info "setup_ms_outlook more settings"
      search_controls "button" "more.*set"
      send_click $script:ctl_hnd
      start-sleep -s 2

      # At this point the window handle may change. So we need to resync
      # with the new chlild window handle & object here.
      if ($need_resync) {
         resolve_window_handle outlook
         $h = $script:curr_hnd
         get_window_obj $h
         $w = $script:curr_obj
         $ch = select-childwindow -window $w
         $h = $ch.handle
      }

      # Use hot-keys to select the Outgoing Server tab.
      send_keys $h "^{tab}" # Ctrl-tab
      start-sleep -s 2

      # Check the "My outgoing server requires authentication" option box.
      get_controls $ch
      search_controls "button" "my.*outgoing"
      send_click $script:ctl_hnd
      start-sleep -s 1

      # Click the OK button.
      search_controls "button" "^OK$"
      send_click $script:ctl_hnd
      start-sleep -s 2
   }

   # Final resync with the new window handle & object here.
   if ($need_resync) {
      resolve_window_handle outlook
      $h = $script:curr_hnd
      get_window_obj $h
      $w = $script:curr_obj
      $ch = select-childwindow -window $w
      $h = $ch.handle
   }

   # Click the next button to test settings.
   log_info "setup_ms_outlook start testing settings"
   get_controls $ch
   search_controls "button" "&next" # ampersand ensures choice of correct next button.
   send_click $script:ctl_hnd

   # The account settings get tested now.
   start-sleep -s 10

   # Click the Close button.
   log_info "setup_ms_outlook done test settings"
   get_controls $ch
   search_controls "button" "close"
   send_click $script:ctl_hnd
   start-sleep -s 2

   # Now click Finish button.
   get_controls $ch
   search_controls "button" "finish" 1 -quiet
   if ($script:ctl_hnd) {
      send_click $script:ctl_hnd
      start-sleep -s 2
   } else {
      # There is no point letting test case continue, so throw error here.
      throw_error "setup_ms_outlook not completed, no finish button found"
   }

   # Check that we are done.
   search_child_window $h
   log_debug "setup_ms_outlook h=$h search_child_window_rc=$script:search_child_window_rc"
   if ($script:search_child_window_rc) {
      # There is no point letting test case continue, so throw error here.
      throw_error "setup_ms_outlook not completed, h=$h search_child_window_rc=$script:search_child_window_rc"
   } else {
      log_bold "setup_ms_outlook completed OK, h=$h search_child_window_rc=$script:search_child_window_rc"
   }
   return
}


#==================== setup_open_office ===========================
# Open Office has several dialog boxes the first time it runs that
# need to be handled.
#
# Calling parameters: app_name
# Returns: null
#==================================================================
function setup_open_office ($app_name="") {

   # NB: To test this code, goto C:\users\<username>\AppData\Roaming
   # and delete the OpenOffice folder and all its contents. Then OO 
   # will popup the window for names & initials. 

   # If its not one of the OpenOffice applications running, we are done.
   if (!($app_name -match $script:open_office_app_patt)) {
      log_debug "setup_open_office app_name=$app_name returning" 
      return
   }

   # Look for Open Office welcome window.
   $x = select-window -title *welcome*open*office*
   $x = $x | select-object -first 1
   $c = $x.Class
   $h = $x.Handle
   $n = $x.ProcessName
   $p = $x.ProcessId
   $t = $x.Title
   if (!$h -or !($n -match $oo_app_patt)) {
      log_debug "setup_open_office open_office_app_patt=$script:open_office_app_patt c=$c h=$h n=$n p=$p t=$t NO MATCH" 
      return
   }

   # Log entry to show we ran
   log_info "setup_open_office open_office_app_patt=$script:open_office_app_patt found c=$c h=$h n=$n p=$p t=$t"

   # Make sure welcome window is focussed
   restore_window $h

   # Send alt-n to click the next button.
   send_keys $h "%n"
   start-sleep -s 2

   # Get user, setup initials
   $user = $env:username
   if ($user -match "^(.)") {
      $init = $matches[1]
   } else {
      $init = "j"
   }
   # "user=$user init=$init"

   # Set first name
   send_keys $h $init
   start-sleep -m 500

   # Set last name
   send_keys $h "{tab}"
   start-sleep -m 500
   send_keys $h $user
   start-sleep -m 500

   # Set initials
   start-sleep -m 500
   send_keys $h "{tab}"
   start-sleep -m 500
   send_keys $h $init
   start-sleep -m 500

   # OO V3 has more prompts.
   if ($script:open_office_major_ver -eq 3) {
      # Send enter to hit next for the automatic update prompt.
      send_keys $h "{enter}"
      start-sleep -s 2

      # Send down twice to select "do not register" radio button.
      start-sleep -m 500
      send_keys $h "{down}"
      start-sleep -m 500
      send_keys $h "{down}"
   }

   # Send enter to click the finish button.
   # NB: Alt-f doesnt seem to work!
   send_keys $h "{enter}"
   start-sleep -s 2

   # Check that we are done.
   get_window_obj $h -quiet
   log_debug "setup_open_office h=$h curr_obj=$script:curr_obj"
   if ($script:curr_obj) {
      log_error "setup_open_office not completed, h=$h curr_obj=$script:curr_obj, see screenshot below"
      log_screenshot "" "full"
   }
}


#==================== set_clipboard ===============================
# Sets the contents of the windows clipboard.
#
# Calling parameters: msg
# Returns: null
#==================================================================
# from: http://stackoverflow.com/questions/1567112/convert-keith-hills-powershell-get-clipboard-and-set-clipboard-to-a-psm1-script
# NB: PSCX module has clipboard routine that can also copy graphics

function set_clipboard ($msg="") {
   $tb = New-Object System.Windows.Forms.TextBox
   $tb.Multiline = $true
   $tb.Text = $msg
   $tb.SelectAll()
   $tb.Copy()
   log_info "set_clipboard msg=$msg"
}


#==================== show_results_page ===========================
# Displays current logfile in the desired web browser.
#
# Calling parameters: none
# Returns: null
#==================================================================
function show_results_page {

   # Option -nobr means dont show logfile in results browser.
   # This is usefull when debugging new code. Also when you 
   # want the tests to run much, much faster, with no browser
   # crashes.
   if ($script:nobr) {
      return
   }

   # Display full pathaname logfile in the preferred browser.
   # Always force a newtab so that start_app has a chance to recover
   # any existing session pages.
   $script:browser_results = $script:browser_results -replace ".exe" # short form name
   try {
      start_app "$script:browser_results" "file:///$pwd\$script:logfile" -forcetab
   } catch {
      $msg = $error | select-object -first 1
      log_error "show_results_page start_app got: $msg"
   }
   $script:res_hnd = $script:curr_hnd
   # log_info "res_hnd=$script:res_hnd"
}


#==================== start_app ===================================
# Checks if specified application app_name is already running.
# Starts the application if necessary. When app_name is started,
# the app_args are passed directly on the command line. Otherwise
# the app_args are passed to the app_name window if the option
# -newtab is also specified.
#
# Calling parameters: app_name, app_args <-newtab> <-forcetab> <-forceapp>
# app_name can be short name if it is in the path statement, or
# fullpath if it is not in the path statement
# -newtab   - for existing browser, opens a new tab window.
# -forcetab - for new browser instance, opens a new tab window 
#             in addition to the main new window tab, app_args 
#             will be passed in the new tab, not to the browser
#             via the command line
# -forceapp - starts another instance of app_name process
#             even if one is already running
#
# Returns: null or throws error
# Sets variables: $script:curr_hnd, $script:app_started_list
#==================================================================
function start_app ($app_name, $app_args="") {

   # Initialize items passed back to calling routine.
   $script:curr_hnd = ""
   $script:curr_pid = ""

   # We may have a fullpath to the app name or a relative path.
   # We need the short app name when finding matching processes.
   # We need the full path to start a new process if the app name
   # is not found in the path statement.
   $app_path = $app_name 
   $temp = $app_name.split("\")
   $cnt = $temp.count
   $app_name = $temp[$cnt-1]
   $app_name = $app_name -replace ".exe" # Drop the .exe, if any
   # log_info "start_app: temp=$temp cnt=$cnt"
   log_info "start_app: app_path=$app_path app_name=$app_name app_args=$app_args args=$args"

   # Sanity check.
   if (!$app_name -or $app_name -eq "") {
      log_error "start_app invalid app_name=$app_name"
      return
   }

   # Check for valid args
   foreach ($opt in $args) {
      $opt = $opt.Trim()
      # "opt=$opt"
      if ($opt -ne "" -and $opt -ne "-forceapp" -and $opt -ne "-forcetab" -and $opt -ne "-newtab") {
         throw_error "start_app: app_name=$app_name app_args=$app_args args=$args invalid option: $opt"
      }
   }

   # Check for option -forceapp
   $forceapp = $false
   if ($args -match "-forceapp") {
      $forceapp = $true
   }
   # "start_app forceapp=$forceapp"

   # Check for existing application processes.
   $app_pid = get-process -name $app_name -erroraction silentlycontinue
   $cnt = $app_pid.count
   # "cnt=$cnt"
   $pid_list = ""
   foreach ($p in $app_pid) {
      $p_pid = $p.id
      # "p_pid=$p_pid p=$p"
      if ($p_pid -ge 0) {
         $pid_list = "$pid_list $p_pid"
      }
   }
   if ($pid_list -ne "") {
      log_info "start_app: found $cnt existing $app_name pid(s): $pid_list"
   }

   # Add leading/trailing spaces to help with exact match later.
   $pid_list = " $pid_list " 

   # Do we use existing app_name?
   if ($app_pid -and !$forceapp) {

      # Case of multiple pids is handled by the separate routine below.
      # This also filters out selected windows & modal windows.
      log_info "start_app: using existing process"
      resolve_window_ha 

RE: Powershell test of applications on PC image

(OP)
NB: Initial post of files were truncated, please use web links below for complete files.

Powershell test of applications on PC image

Synopsis
========
- test-apps.ps1 is used to verify applications are correctly installed on a refurbished PC
with a new RPK image before the PC is deployed to the end customer
- tests cover network link, web browsers, notepad, wordpad, MS Office, Open Office, etc
- missing / inoperative drivers are detected
- laptop batteries are exercised and an estimate of the usable battery life is provided
- a detailed html logfile shows test results and supporting screenshots as the tests progress
- test results files are optionally copied to a central server
- there is a summary file to facilitate integration of results into inventory control systems

Notes
=====
- download the script related files:
http://www3.bell.net/brearley/public/test-apps.ps1
http://www3.bell.net/brearley/public/test-apps.bat
http://www3.bell.net/brearley/public/powershell-ex...
- download wasp.dll from: http://wasp.codeplex.com/releases/view/22118
- put wasp.dll in the same directory as the .ps1 file
- you may have to unblock downloaded files
- from windows explorer, right click on each file, select properties, click the Unblock button
- if you are deploying this script as part of a PC image, it is best to turn on
powershell scripts in the registry via the supplied .reg file
- if you are using the default windows user account control setting, then you will need to
open the powershell window by right clicking and selecting the Run as Administrator option
- if the script does not have administrator privileges, some of the tests will fail, the error
messages will flag the administrator privilege issue
- if you have changed the user account control setting to be more permissive, there is a .bat
file to run the tests from windows explorer
- if you uncomment line 34 of the .bat file, it will also update the registry entry needed to
enable powershell scripts
- for more details and options, in a powershell window, type: ./test-apps.ps1 -h

Red Flag This Post

Please let us know here why this post is inappropriate. Reasons such as off-topic, duplicates, flames, illegal, vulgar, or students posting their homework.

Red Flag Submitted

Thank you for helping keep Tek-Tips Forums free from inappropriate posts.
The Tek-Tips staff will check this out and take appropriate action.

Reply To This Thread

Posting in the Tek-Tips forums is a member-only feature.

Click Here to join Tek-Tips and talk with other members!

Resources

Close Box

Join Tek-Tips® Today!

Join your peers on the Internet's largest technical computer professional community.
It's easy to join and it's free.

Here's Why Members Love Tek-Tips Forums:

Register now while it's still free!

Already a member? Close this window and log in.

Join Us             Close