Modified tinku99's version a bit -- there is some semblance of line number support and Notepad++ Portable support.
WARNING: In UTest.ahk, there are at least two instances of the value 0xA0 -- which doesn't show up very well in some editors here. Not sure they will survive pasting...They occur after instances of ~`a in the function definition for UTest.
UTest.ahk
Code:
/* Title: UTest
modified by Guest_AutoHotkey_L
changes: formatting
preliminary line number support
Notepad++ Portable support
modified by Naveen Garg
changes: removed dependency on lowlevel functions which were brittle.
added stack trace information for failed tests
originally by majkinetor: http://www.autohotkey.com/forum/author-majkinetor.html
forum: http://www.autohotkey.com/forum/viewtopic.php?t=49262
Unit testing framework.
(see Utest.png)
Usage:
UTest will scan the script for functions which name starts with "Test_".
Test functions have no parameter and use one of the Assert functions.
If Assert function fails, test will fail and you will
see that in the result output.txt with stack trace of failed tests.
To test your script, use the following template :
(start code)
ahktest()
Return
#include UTest.ahk
Test_MyTest1()
{
assert(1, expr2, expr3)
}
Test_MyTest2(expr1, expr2)
{
}
...
...
#include FunctionsToTest.ahk
(end code)
*/
ahktest()
{
#SingleInstance, force
; XXX: used elsewhere too
OutFile := "output.txt"
UTest("Result", UTest_Start(UTest("NoGui"))) ;execute tests
Run, % OutFile
FileRead, Output, % OutFile
Return Output
}
Assert(b1 = "", b2 = 1, b3 = 1, b4 = 1, b5 = 1, b6 = 1, b7 = 1, b8 = 1
, b9 = 1, b10 = 1)
{
e := {}
Loop, 10
{
If (A_Index == 1)
{
Continue
}
If !b%A_Index%
{
e.insert("Test " . A_Index . " failed`n")
}
}
If e[1]
{
stack := getStackTrace()
stack := RemoveUTestFunctionsFromStackTrace(stack)
te := exception("fail", -1)
s := ToString(e)
. "at " . getLineSource(te.line, te.file)
. "`nin file " . te.file
. "`nstacktrace: "
stack[0] := s
UTest_SetFail(Name, ",")
Throw stack
}
}
UTest_Edit(Path, LineNumber)
{
OutputDebug, % "UTest_Edit entered"
; XXX
Command := "x:\apps\Notepad++Portable\Notepad++Portable.exe "
. "-multiInst "
If (LineNumber != "")
{
Command .= "-n" . LineNumber . " "
}
Command .= """" . Path . """"
OutputDebug, % Command
;
Run, % Command
/*
If (LineNumber != "")
{
Mode := A_TitleMatchMode
SetTitleMatchMode, RegEx
WinWaitActive, % "^.* - Notepad\+\+$"
OutputDebug, % "Found an instance of Notepad++"
SetTitleMatchMode, % Mode
OutputDebug, "Restored title match mode to: " . A_TitleMatchMode
Send ^g
WinWaitActive, % "Go To..."
Send %LineNumber%{Enter}
}
*/
}
;================================== PRIVATE ================================
UTest_RunTests()
{
OutputDebug, % "UTest_RunTests entered"
; XXX: used elsewhere too
OutFile := "output.txt"
FileDelete, % OutFile
tests := UTest_GetTests()
bNoGui := UTest("NoGui")
If (tests == "")
{
MsgBox, % "No tests found!"
ExitApp
}
bTestsFail := 0
OutputDebug, % tests
Loop, Parse, tests, `n
{
StringSplit, f, A_LoopField, % A_Space
Try
{
%f1%()
}
Catch e
{
If !UTest("Name")
{
FileAppend % "test " . A_Index . ":`n"
. ToString(e) . "`n"
. "******************************************`n"
, % OutFile
}
}
bFail := UTest("F")
OutputDebug, % "bFail: " . bFail
Param := UTest("Param")
Name := UTest("Name")
fName := SubStr(f1, 6)
IfEqual, bFail, 1, SetEnv, bTestsFail, 1
s .= (bFail ? "FAIL" : "OK")
. "," . fName
. "," . f2
. "," . Name
. "," . Param . "`n"
OutputDebug, % "s: " . s
UTest("F", 0)
UTest("Param", "")
UTest("Name", "")
If !bNoGui
{
LV_Add(bFail ? "Select" : ""
, bFail ? "FAIL" : "OK"
, fName, f2, Name, Param)
}
}
If !bNoGui
{
LV_ModifyCol()
LV_ModifyCol(1, 100)
LV_ModifyCol(3, 50)
LV_ModifyCol(4, 150)
}
UTest("TestsFail", bTestsFail)
Return SubStr(s, 1, -1)
}
UTest_GetTests()
{
s := UTest_GetFunctions()
Loop, Parse, s, `n
{
If (SubStr(A_LoopField, 1, 5) == "Test_")
{
t .= A_LoopField . "`n"
}
}
Return SubStr(t, 1, -1)
}
UTest_GetFunctions()
{
fnames := ""
FObj := FileOpen(A_ScriptName, "r")
; XXX: check success?
LineNo := 1
While (!FObj.AtEOF)
{
Line := FObj.ReadLine()
LineNo += 1
FoundPos := RegExMatch(Line, "^([\w_\d]+)\(", m)
; XXX: check for problems?
FoundPos += StrLen(m1)
f := Func(m1)
name := f.name
If !f.name
{
Continue
}
If !f.IsBuiltIn
{
fnames .= f.name . " " . LineNo . "`n"
}
}
Return SubStr(fNames, 1, -1)
}
UTest_GetFreeGuiNum()
{
Loop, 99
{
Gui %A_Index%:+LastFoundExist
IfWinNotExist
{
Return A_Index
}
}
Return 0
}
UTest_Start(bNoGui = false)
{
If !bNoGui
{
hGui := UTest_CreateGui()
}
s := UTest_RunTests()
If (hGui)
{
Result := UTest("TestsFail") ? "FAIL" : "OK"
ControlSetText, Static1, % Result, % "ahk_id " . hGui
}
Return s
}
UTest_CreateGui()
{
w := 500
h := 400
n := UTest_GetFreeGuiNum()
Gui, %n%: +LastFound +LabelUTest_
hGui := WinExist()
Gui, %n%: Add, ListView, w%w% h%h% gUTest_OnList, Result|Test|Line|Name|Param
Gui, %n%: Font, s20 bold cRED, Courier New
Gui, %n%: Add, Text, w%w% h40
Gui, %n%: Show, autosize, UTest - %A_ScriptName%
UTest("GUINO", n)
Hotkey, IfWinActive, % "ahk_id " . hGui
Hotkey, ESC, UTest_Close
Hotkey, IfWinActive
Return hGui
UTest_Close:
ExitApp
; XXX: what is the following line?
Return
}
UTest_SetFail(Name = "", Param = "")
{
UTest("Param", UTest("Param") . " " . Param)
UTest("Name", UTest("Name") . " " . Name)
UTest("F", 1)
Return 1
}
UTest_OnList:
{
OutputDebug, "UText_OnLine entered"
IfNotEqual, A_GuiEvent, DoubleClick, return
LV_GetText(lineNumber, LV_GetNext(), 3)
UTest_Edit(A_ScriptFullPath, lineNumber)
Return
}
; XXX: careful of instances of ~`a... below, some editors don't appear to
; display appropriately...the hex value following ~`a appears to be A0
UTest(var = "", value = "~`a "
, ByRef o1 = "", ByRef o2 = "", ByRef o3 = ""
, ByRef o4 = "", ByRef o5 = "", ByRef o6 = "")
{
static
_ := %var%
IfNotEqual, value, ~`a , SetEnv, %var%, %value%
Return _
; XXX: what is the following line?
return
}
#include util.ahk
util.ahk
Code:
getFile(name)
{
static files := {}
If files[name]
{
Return files[name]
}
file := {}
Loop, Read, % name
{
file[A_Index] := A_LoopReadLine
}
files[name] := file
Return file
}
getLinesNear(lineNumber, filename)
{
If lineNumber is not number
{
Throw exception("lineNumber not provided")
}
file := getFile(filename)
lines := ""
loop, 5
{
lines .= file[lineNumber - 3 + A_Index] . "`n"
}
Return lines
}
getLineSource(lineNumber, filename)
{
If lineNumber is not number
{
Throw exception("lineNumber not provided")
}
file := getFile(filename)
line := file[lineNumber]
Return line
}
getStackTrace(max = 10)
{
If (max > 50)
{
max := 50
}
stack := Object()
Loop
{
If (A_Index < 2) ; don't need stack for these utility functions
{
Continue
}
If (A_Index > max) ; in case we are in a long running coroutine
{
Break
}
s := exception("level " . A_Index, 0 - A_Index)
If (s.what < 0)
{
Break
}
s.extra := getLinesNear(s.line, s.file)
stack[A_Index] := s
}
Return stack
}
RemoveUTestFunctionsFromStackTrace(stack)
{
max := stack.maxindex()
Loop, % max
{
s := stack[A_Index]
If instr(s.What, "runTests")
{
stack[A_Index] := 0
stack[A_Index - 1] := 0
level := A_Index
}
If (level < max)
{
Loop, % (max - level)
{
stack[level + A_Index] := 0
}
}
}
Return stack
}