Showing posts with label Testing. Show all posts
Showing posts with label Testing. Show all posts

Sunday, December 23, 2012

The Power of Failure

Wow.  It's been a while since my last post.  I was... delayed.


If you ever bought a house and had to deal with bankers, brokers, realtors, contractors, movers, utilities, inspectors, insurance agents, lawyers, and sundry government agencies, you realize this is kind of how it is.

But let's deal with a different type of failure.  Powershell uses ErrorAction to define how a script responds to errors.  This can be defined globally by assigning a value to the $ErrorActionPreference variable.  You can override default behavior for a specific cmdlet by using the -ErrorAction common parameter.  Possible values are Stop, Inquire, Continue, and SilentlyContinue.  These are explained in the snippet from help about_preference_variables.

$ErrorActionPreference ErrorAction Stop Inquire Continue SilentlyContinue about_preference_variables -ErrorAction

Note that the default ErrorAction is actually Continue, not SilentlyContinue as stated in the help.  You can see examples of these settings by issuing the help about_preference_variables and help about_commonparameters commands.  As I debug scripts I set the $ErrorActionPreference to Inquire or Continue.  But once I put error handling in place I set it to SilentlyContinue.

What can I do to evaluate errors?  One way is to check the value of the variable $?.  This is set to $True if the previous command completed successfully and $False if it failed.  The variable $LastExitCode has the value of the exit code for the most recently executed command.  These variables give me all the power I need to simulate a DOS batch file, if that's what you need to do.

But wait, there's more!

If an error is raised the information about it gets added to the $Error variable.  $Error can be referenced like an array but it is actually a circular buffer with a default length of 256.  Once your script generates the 257th error the first error gets deleted to make room for the new error.  If you really want to keep more more than 256 errors you can set the $MaximumErrorCount variable.  The most recent error is referenced as $Error[0].  The $Error buffer can be cleared by using the $Error.Clear() method.  So now I have all the power I need to simulate a vbScript, not that there's anything wrong with that.

But wait, there's more!

PowerShell lets us create a trap.  This is a block of code that gets executed whenever an error is raised so you don't have to check for errors at each critical step.  If you need to raise an error use the Throw command.  Use the Continue command in the trap block to keep processing the rest of the script or use the Break command to exit the current block of code.

trap throw continue echo $Error Exception.Message
 
Traps only work inside scripts so don't use them in an interactive session.  If you don't include the Continue command at the end of the trap block then PowerShell will still display the default error message in red text in addition to the tasks in the trap.  Traps are scope aware so you can use different traps inside user defined functions.  In the following example the trap inside the function does not trigger the global trap that is outside the function.

function newtrap trap throw continue $Error Exception.Message

You can specify different traps for different types of exceptions.  For example you might have one trap that deals with file system errors and another that deals with network errors.  The powershell.com guys do a good job of explaining this and other intricacies.  Joel Bennet explores other anomalies.

But wait, there's more!

With PowerShell 2.0 and beyond we have the try-catch-finally construct used in modern object oriented languages.  This not only gives you a sweet way to methodically handle errors but also lets you break the script into logical chunks and use the Write-Progress cmdlet to show users how close we are to completion.

try catch finally write-progress break

And like the Trap, you can have different Catch blocks for different types of errors.  In the first section of the previous example where I read the data, I could have one catch block if the file is not found, another catch block if I can't read the file, and another as a catch all catch to catch whatever else I didn't catch:

Try {
    # Read the data
}
Catch [System.IO.FileNotFoundException] {
    echo "The file wasn't found"
    break
}
Catch [System.IO.IOException] {
    echo "The file could not be opened"
    break
}
Catch {
    echo "An unexpected error happened.  Check the Mayan calendar."
    break
}

As usual, most of these examples are stupid.  If all you do is tell the user that an error happened you don't need any of these facilities.  Just let PowerShell show the default error message and take the appropriate ErrorAction.  The real power in error handling is to take corrective action: prompt the user for a file that exists, fix the input so it has the correct format, reset a failed network connection, cast the ring of power into the fires of Mount Doom, etc.

Remember, failure is a natural and happens all the time.  Recovering by turning failure into success is what error handling is all about.  PowerShell has abundant tools to help you achieve that goal.


Thursday, September 13, 2012

Did I do that?

In one of my first college computer science classes the professor asked the question, "Half of your code should be what?"  To which I replied, "functioning".  The professor wasn't amused but I wasn't far off.  The answer he was looking for was error handling.  Anybody can program a tic-tac-toe game but a well written game will give the user useful information when they try to input something other than an X or O.
http://www.kellie.de/jw1/steve4.jpg
If I am writing a script that only I will use then I won't bother with adding error handling.  In those cases I usually leave all output turned on so if an error arises I can see and troubleshoot it.  But if a script will be used by others I try to make it as friendly as possible.

When I first started writing DOS batch files the only error handling was through the IF ERRORLEVEL command.  ERRORLEVEL is populated with the return code from the previous command.  The return code is zero if the command completed successfully and something else if it failed.  IF ERRORLEVEL evaluates to TRUE if the ERRORLEVEL is greater than or equal to the value supplied.  So error handling in batch files consisted of a bunch of IF statements with the known ERRORLEVELs and associated GOTO statements.

IF ERRORLEVEL 10 GOTO Error10
IF ERRORLEVEL 5  GOTO Error5
IF ERRORLEVEL 1  GOTO Error1
GOTO Success

:Error10
ECHO You got your peanut butter in my chocolate
GOTO Success

:Error5
ECHO You got your chocolate in my peanut butter
GOTO Success

:Error1
ECHO I don't have chocolate or peanut butter

:Success
ECHO And now I'm hungry

Batch file processing has improved over the years.  You can still use the IF ERRORLEVEL statement but if you have command extensions enabled then ERRORLEVEL is also an environment variable.  The first three lines in the previous script could be reduced to the single line:

IF ERRORLEVEL 1 GOTO Error%ERRORLEVEL%

Also the IF statement is now more robust and allows for standard numerical comparisons as well grouping multiple commands in parentheses.

IF %ERRORLEVEL% EQU 10 (
    ECHO You got your chocolate in my peanut butter
) ELSE (
IF %ERRORLEVEL% EQU 5 (
    ECHO You got your peanut butter in my chocolate
) ELSE (
IF %ERRORLEVEL% GEQ 1 (
    ECHO I don't have chocolate or peanut butter
)))
ECHO And now I'm hungry

Those are stupid examples.  You have capabilities to not only provide user feedback, but to retry failed operations, solicit input from users, or enable other resolutions.  Still, the facilities are rudimentary.

In vbScript the facilities improve.  vbScript error handling starts with the On Error statement which defines whether error handling is on or off.  On Error Goto 0 turns off internal error handling and lets your script fail on error.  On Error Resume Next turns on error handling and enables the internal Err object.  This object contains the error number as well as descriptive information.

On Error Resume Next

' Do something useful here
If Err.Number <> 0 Then
    WScript.Echo "Error: " & Err.Number
    WScript.Echo "Source: " &  Err.Source
    WScript.Echo "Description: " &  Err.Description
    Err.Clear
End If

Note that we clear the error after reporting it.  If the Err object doesn't get cleared then our next test of Err.Number might report the previous error.

This generic error handling can be enclosed in a user defined function and called for each error detected.

On Error Resume Next

' Do something useful
If Err > 0 Then
        DisplayErrorInfo
End If

' Do something else useful
If Err > 0 Then
        DisplayErrorInfo
End If

Sub DisplayErrorInfo
    WScript.Echo "Error:      : " & Err
    WScript.Echo "Source      : " & Err.Source
    WScript.Echo "Description : " & Err.Description
    Err.Clear
End Sub

If you know which error codes are returned by the application being called then you can do something more elegant than just return generic error information.  Here is some code pinched from The Scripting Guys.

On Error Resume Next

strComputer = "."
arrTargetProcs = Array("calc.exe","freecell.exe")

Set objWMIService = GetObject("winmgmts:" _
 & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")

For Each strTargetProc In arrTargetProcs
    Set colProcesses = objWMIService.ExecQuery _
      ("SELECT * FROM Win32_Process WHERE Name='" & strTargetProc & "'")

    If colProcesses.Count = 0 Then
        WScript.Echo VbCrLf & "No processes named " & strTargetProc & " found."
    Else
        For Each objProcess in colProcesses
            WScript.Echo VbCrLf & "Process Name: " & objProcess.Name
            Wscript.Echo "Process ID: " & objProcess.Handle
            Wscript.Echo "Attempting to terminate process ..."
            intTermProc = TerminateProcess(objProcess)
        Next
    End If
Next

'***********************************************************************
Function TerminateProcess(objProcess)

    On Error Resume Next
    intReturn = objProcess.Terminate
    Select Case intReturn
        Case 0 Wscript.Echo "Return code " & intReturn & _
                " - Terminated"
        Case 2 Wscript.Echo "Return code " & intReturn & _
                " - Access denied"
        Case 3 Wscript.Echo "Return code " & intReturn & _
                " - Insufficient privilege"
        Case 8 Wscript.Echo "Return code " & intReturn & _
                " - Unknown failure"
        Case 9 Wscript.Echo "Return code " & intReturn & _
                " - Path not found"
        Case 21 Wscript.Echo "Return code " & intReturn & _
                " - Invalid parameter"
        Case Else Wscript.Echo "Return code " & intReturn & _
                " - Unable to terminate for undetermined reason"
    End Select
    TerminateProcess = intReturn

End Function

Something else to note is that the Err object has the method Raise which allows you to generate an error on demand.  This is useful for debugging error handling code or if you just want to punk your users.  It is also useful if you need to pass errors to other modules.  Note that you have to add the constant vbObjectError to the error number you want to raise to prevent collision with existing errors.

In the next post I will discuss error handling in PowerShell.  In case you couldn't guess, it is far superior to DOS and vbScript.

Monday, July 16, 2012

A good time

Time is what keeps everything from happening all at once.  I think Confucius said that.  Or maybe it was one of those stoner guys I met at college.  Either way most humans agree that time is frequently useful.


Sometimes you need to know how long it takes a script to run.  This comes in handy if you are comparing various techniques to accomplish a task and want to find the most efficient approach.  Or if you need to benchmark a script with a small data set to estimate how long it will take with the full data set.  Or maybe you are just curious like a cat.

vbScript has the function Time that returns the (you guessed it) current system time.  It is returned as a datetime value so use the DateDiff function to compare the difference between two values.  My sample script is:

StartTime = Time
wscript.echo StartTime


for i = 1 to 100000000
next


EndTime = Time
wscript.echo EndTime


TotTime = DateDiff("s",StartTime,EndTime)wscript.echo TotTime

wscript.echo "The operation took " + cStr(TotTime) + " seconds."


The Time and DateDiff functions only offer time accurate to the second.  Instead we should use the Timer function.  This not only offers greater accuracy but it also lets us do simple math to see the results.

StartTime = Timer
wscript.echo StartTime


for i = 1 to 50000000
next


EndTime = Timer
wscript.echo EndTime


TotTime = cStr(EndTime-StartTime)
wscript.echo "The operation took " + TotTime + " seconds."



The DOS environment variable %TIME% is accurate to the hundredth of a second.  You can use this if you want to time the processing of a batch file, but you have to do some serious parsing of the variable by using the FOR command.  In this example I use FOR /F to break the %TIME% string into 4 pieces (hours, minutes, seconds, hundredths) and do a bunch of math to turn the whole thing into the number of hundredths of seconds since midnight.  (The two FOR /F commands are wrapped in the example below.)

echo OFF
echo %TIME%
for /F "tokens=1-4 delims=:." %%A in ('echo %TIME%') do set /A Start=(%%A*60*60*100)+(%%B*60*100)+(%%C*100)+%%D


for /L %%X in (0,1,10000) do rem

for /F "tokens=1-4 delims=:." %%A in ('echo %TIME%') do set /A Stop=(%%A*60*60*100)+(%%B*60*100)+(%%C*100)+%%D
ECHO ON


set /A TotTime=%Stop%-%Start%
set /A Secs=%TotTime%/100
set /A Hund=%TotTime% %% 100
echo "The operation took %Secs%.%Hund% seconds"


set Hund=0%Hund%
set Hund=%Hund:~-2%

echo "The operation took %Secs%.%Hund% seconds"

The math is pretty straightforward and I can calculate the elapsed time with simple subtraction.  I use the modulo operator (%) to separate hundredths from seconds.  But note that the environment variables are strings, so I still need to add a leading zero then take the last 2 characters of the string to make sure I have a 2 digit integer after the decimal point.


But PowerShell is the undisputed champ of scripting and and timing script execution is no exception.  Sure, there are cmdlets for finding the current time and methods for doing datetime math.  But why go to all that trouble when you have the Measure-Command cmdlet?  Enclose the code you need to benchmark in brackets and PowerShell gives you detailed timing information.



As always, the cmdlet returns an object that you can process through the pipeline.  Here I use the shortcut of finding just the number of seconds it took to run the code.


If you have plenty of time then you have time to kill. But if you are out of time you are out of luck. So don't take your time for granted unless you are granted more time, in which case you can take all the time you need.  So until next time, take your time and take care.




Sunday, March 4, 2012

Why Script It?

Before we get into the nuts-and-bolts of making scripts to make you more productive, I want to take a minute to consider why you would or not build a scripted solution.  I run into many IT professionals who have great technical knowledge but never develop skills at automating their tasks.  They get the job done, but not always efficiently.

There are valid reasons for not scripting your solutions.  Maybe it really is a task you will do once.  Or something you do so infrequently that there is little value to spending the time to create, test, and debug your script.  Or maybe it is a task for which there are no facilities that allow scripting such as a GUI only interface.

But more often than not you can build an automated solution.  And the more you build scripts the more adept you become at doing it, which makes you more productive, which means you (hopefully) make more money.

Repeatability
In IT if you have to do something once you usually have to do it dozens or hundreds or thousands of times over the course of a contract or project or career.  By scripting your solution you ultimately save time. 

Say a task takes a minute to do manually.  And assume it takes you an hour to create, test, and debug a script that does the same task in a second.  If you have to do that task once a week you get your time back in a year and by the second year you've earned yourself an extra lunch hour.

Consistency
Sometimes consistency is just a stylistic issue such as ensuring consistent capitalization or naming conventions.  But it can be critical in situations where case sensitivity is important, objects need to be in specific locations, or other systems require reliable output from your process.

Delegation
If you need to pass tasks on to others it is to your advantage to make those tasks as simple and bulletproof as possible.  Sometimes the people performing the tasks will not have the best technical skills, sometimes they won't have the extra time, and sometimes they just won't care.  If you can give your delegate the ability to push a few buttons you have a better chance for success than asking them to follow a detailed list of manual tasks.

Reliability
By creating scripts in a test environment you can make sure everything works as expected before you run the tasks against production systems and live data.  By working out issues beforehand you can reduce (and hopefully eliminate) negative impact to end users and reliance on backups.

Change Management
By showing the steps you will take before you take them, and showing confirmation that the steps produce the desired results in your test environment, you generate a clear procedure that can be reliably approved by internal change management facilities.

Audit ability
I always make sure my scripts produce output showing the progress and results of the scripts.  I then save the logs with the project or change management records.  Two years from now when I get blamed for questioned about something my scripts did I can show a clear audit trail to refute the allegations.  And if it is my fault I have the script and output to provide the required details to make corrections.

Commenters: What other reasons do you have for creating scripted solutions for you administrative tasks?