├── .gitignore ├── README.md ├── appveyor.yml ├── lib └── Yunit │ ├── Stdout.ahk │ ├── Window.ahk │ └── Yunit.ahk └── tests ├── test.bat ├── testCustomSimple.ahk └── testMain.ahk /.gitignore: -------------------------------------------------------------------------------- 1 | testoutput.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ahk Continuous Integration using AppVeyor 2 | 3 | [![Build status](https://ci.appveyor.com/api/projects/status/5510uj2ey77lc09i/branch/master?svg=true)](https://ci.appveyor.com/project/aviaryan/ahk-ci-example/branch/master) 4 | 5 | This is an example project to demonstrate how to use [AppVeyor](https://www.appveyor.com/) and Uberi's [Yunit](https://github.com/Uberi/Yunit) to set up continuous testing for your AutoHotkey project. 6 | 7 | 8 | ### Steps 9 | 10 | 1. Clone or download this repo. 11 | 2. Copy appveyor.yml to your project. `appveyor.yml` is the appveyor configuration files and it includes steps like install ahk on server, run the test script etc. 12 | 3. Copy lib\Yunit to your project maintaining the directory structure. It should be noted that the Yunit library used in this project is slightly modified version of the original's so please use this one. 13 | 4. Copy the `tests` directory to your project. The tests/test.bat is the script responsible to run all .ahk test files in the directory. 14 | 5. testMain.ahk is the test script. You can have as many test scripts in the `tests/` directory. 15 | 6. Spend some time going through its code. It should be easy enough to understand how to write tests. Additionally [Yunit docs](https://github.com/Uberi/Yunit) should be helpful. 16 | 7. Once you have written the tests, run them locally by executing `tests\test.bat` on the terminal. 17 | 8. After pushing the tests to GitHub, you need to sign in to [appveyor](https://www.appveyor.com/) and add the repo to start the CI process. 18 | 19 | 20 | ### Using custom test framework 21 | 22 | You can use a custom test framework as long as it does the following - 23 | 24 | 1. It sends test details to stdout 25 | 2. It exits the script with ExitCode 1 in case one of the test fails. 26 | 27 | See [testCustomSimple.ahk](tests/testCustomSimple.ahk) for an example. 28 | 29 | 30 | ### Need Help ? 31 | 32 | Post in the [AHK topic](https://autohotkey.com/boards/viewtopic.php?f=6&t=16168) or tweet me at [@aviaryan123](https://twitter.com/aviaryan123) -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 1.0.{build} 2 | 3 | install: 4 | - cinst autohotkey.install 5 | 6 | build: off 7 | 8 | before_test: 9 | - cmd: cd tests 10 | 11 | test_script: 12 | - test.bat -------------------------------------------------------------------------------- /lib/Yunit/Stdout.ahk: -------------------------------------------------------------------------------- 1 | class YunitStdOut 2 | { 3 | Update(Category, Test, Result) ;wip: this only supports one level of nesting? 4 | { 5 | if IsObject(Result) 6 | { 7 | Details := " at line " Result.Line " " Result.Message 8 | Status := "FAIL" 9 | } 10 | else 11 | { 12 | Details := "" 13 | Status := "PASS" 14 | } 15 | FileAppend, %Status%: %Category%.%Test% %Details%`n, * 16 | } 17 | } -------------------------------------------------------------------------------- /lib/Yunit/Window.ahk: -------------------------------------------------------------------------------- 1 | class YunitWindow 2 | { 3 | __new(instance) 4 | { 5 | global YunitWindowTitle, YunitWindowEntries, YunitWindowStatusBar 6 | Gui, Yunit:Font, s16, Arial 7 | Gui, Yunit:Add, Text, x0 y0 h30 vYunitWindowTitle Center, Test Results 8 | 9 | hImageList := IL_Create() 10 | IL_Add(hImageList,"shell32.dll",132) ;red X 11 | IL_Add(hImageList,"shell32.dll",78) ;yellow triangle with exclamation mark 12 | IL_Add(hImageList,"shell32.dll",138) ;green circle with arrow facing right 13 | IL_Add(hImageList,"shell32.dll",135) ;two sheets of paper 14 | this.icons := {fail: "Icon1", issue: "Icon2", pass: "Icon3", detail: "Icon4"} 15 | 16 | Gui, Yunit:Font, s10 17 | Gui, Yunit:Add, TreeView, x10 y30 vYunitWindowEntries ImageList%hImageList% 18 | 19 | Gui, Yunit:Font, s8 20 | Gui, Yunit:Add, StatusBar, vYunitWindowStatusBar -Theme BackgroundGreen 21 | Gui, Yunit:+Resize +MinSize320x200 22 | Gui, Yunit:Show, w500 h400, Yunit Testing 23 | Gui, Yunit:+LastFound 24 | 25 | this.Categories := {} 26 | Return this 27 | 28 | YunitGuiSize: 29 | GuiControl, Yunit:Move, YunitWindowTitle, w%A_GuiWidth% 30 | GuiControl, Yunit:Move, YunitWindowEntries, % "w" . (A_GuiWidth - 20) . " h" . (A_GuiHeight - 60) 31 | Gui, Yunit:+LastFound 32 | DllCall("user32.dll\InvalidateRect", "uInt", WinExist(), "uInt", 0, "uInt", 1) 33 | Return 34 | 35 | YunitGuiClose: 36 | ExitApp 37 | } 38 | 39 | Update(Category, TestName, Result) 40 | { 41 | Gui, Yunit:Default 42 | If !this.Categories.HasKey(Category) 43 | this.AddCategories(Category) 44 | Parent := this.Categories[Category] 45 | If IsObject(result) 46 | { 47 | hChildNode := TV_Add(TestName,Parent,this.icons.fail) 48 | TV_Add("Line #" result.line ": " result.message,hChildNode,this.icons.detail) 49 | GuiControl, Yunit: +BackgroundRed, YunitWindowStatusBar 50 | key := category 51 | pos := 1 52 | while (pos) 53 | { 54 | TV_Modify(this.Categories[key], this.icons.issue) 55 | pos := InStr(key, ".", false, (A_AhkVersion < "2") ? 0 : -1, 1) 56 | key := SubStr(key, 1, pos-1) 57 | } 58 | } 59 | Else 60 | TV_Add(TestName,Parent,this.icons.pass) 61 | TV_Modify(Parent, "Expand") 62 | } 63 | 64 | AddCategories(Categories) 65 | { 66 | Parent := 0 67 | Category := "" 68 | Categories_Array := StrSplit(Categories, ".") 69 | for k,v in Categories_Array 70 | { 71 | Category .= (Category == "" ? "" : ".") v 72 | If (!this.Categories.HasKey(Category)) 73 | this.Categories[Category] := TV_Add(v, Parent, this.icons.pass) 74 | Parent := this.Categories[Category] 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/Yunit/Yunit.ahk: -------------------------------------------------------------------------------- 1 | ;#NoEnv 2 | 3 | class Yunit 4 | { 5 | static Modules := [Yunit.StdOut] 6 | 7 | class Tester extends Yunit 8 | { 9 | __New(Modules) 10 | { 11 | this.Modules := Modules 12 | } 13 | } 14 | 15 | Use(Modules*) 16 | { 17 | return new this.Tester(Modules) 18 | } 19 | 20 | Test(classes*) ; static method 21 | { 22 | instance := new this("") 23 | instance.results := {} 24 | instance.classes := classes 25 | instance.Modules := [] 26 | for k,module in instance.base.Modules 27 | instance.Modules[k] := new module(instance) 28 | while (A_Index <= (A_AhkVersion < "2" ? classes.MaxIndex() : classes.Length())) 29 | { 30 | cls := classes[A_Index] 31 | instance.current := A_Index 32 | instance.results[cls.__class] := obj := {} 33 | instance.TestClass(obj, cls) 34 | } 35 | return instance 36 | } 37 | 38 | Update(Category, Test, Result) 39 | { 40 | for k,module in this.Modules 41 | module.Update(Category, Test, Result) 42 | } 43 | 44 | TestClass(results, cls) 45 | { 46 | environment := new cls() ; calls __New 47 | for k,v in cls 48 | { 49 | if IsObject(v) && IsFunc(v) ;test 50 | { 51 | if (k = "Begin") or (k = "End") 52 | continue 53 | if ObjHasKey(cls,"Begin") 54 | && IsFunc(cls.Begin) 55 | environment.Begin() 56 | result := 0 57 | try 58 | { 59 | %v%(environment) 60 | if ObjHasKey(environment, "ExpectedException") 61 | throw Exception("ExpectedException") 62 | } 63 | catch error 64 | { 65 | if !ObjHasKey(environment, "ExpectedException") 66 | || !this.CompareValues(environment.ExpectedException, error) 67 | result := error 68 | } 69 | results[k] := result 70 | ObjRemove(environment, "ExpectedException") 71 | this.Update(cls.__class, k, results[k]) 72 | if ObjHasKey(cls,"End") 73 | && IsFunc(cls.End) 74 | environment.End() 75 | } 76 | else if IsObject(v) 77 | && ObjHasKey(v, "__class") ;category 78 | { 79 | if (A_AhkVersion < "2") 80 | this.classes.Insert(++this.current, v) 81 | else 82 | this.classes.InsertAt(++this.current, v) 83 | } 84 | } 85 | } 86 | 87 | Assert(Value, params*) 88 | { 89 | Message := (params[1] = "") ? "FAIL" : params[1] 90 | if (!Value) 91 | throw Exception(Message, -1) 92 | } 93 | 94 | CompareValues(v1, v2) 95 | { ; Support for simple exceptions. May need to be extended in the future. 96 | if !IsObject(v1) || !IsObject(v2) 97 | return v1 = v2 ; obey StringCaseSense 98 | if !ObjHasKey(v1, "Message") || !ObjHasKey(v2, "Message") 99 | return False 100 | return v1.Message = v2.Message 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /tests/test.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | set err_level=0 3 | 4 | rem http://ss64.com/nt/start.html 5 | rem http://stackoverflow.com/questions/138497/iterate-all-files-in-a-directory-using-a-for-loop for file loops 6 | rem "C:/Program Files/AutoHotkey/AutoHotkeyU32.exe" /ErrorStdOut "testMain.ahk" 2>&1 |more 7 | 8 | rem Loop over all ahk files in tests directory 9 | for /r %%i in (*.ahk) do ( 10 | start "testing" /B /wait "C:\Program Files\AutoHotkey\AutoHotkeyU32.exe" /ErrorStdOut %%~nxi > testoutput.txt 2>&1 11 | echo ** Running %%~nxi ** 12 | if errorlevel 1 ( 13 | echo *** Test file %%~nxi failed *** 14 | set err_level=1 15 | ) 16 | type testoutput.txt 17 | echo. 18 | ) 19 | 20 | 21 | rem EXIT SCRIPT 22 | exit /b %err_level% -------------------------------------------------------------------------------- /tests/testCustomSimple.ahk: -------------------------------------------------------------------------------- 1 | /* 2 | This file shows a custom, barebones test script made functional without using any other libs 3 | */ 4 | 5 | global errorcode := 0 6 | 7 | /* 8 | Tests 9 | */ 10 | 11 | assert( 4+3 == 2+5, "Simple_Addition_Check") 12 | 13 | s := "28" 14 | s += "42" 15 | i := 28 16 | i += 42 17 | assert( s == i, "Number_In_String_Concat_Test") 18 | 19 | assert(Instr("abcd", "b") == 2, "Instr_Check") 20 | 21 | /* 22 | Exit 23 | */ 24 | 25 | ExitApp, % errorcode 26 | return 27 | 28 | 29 | assert(eq, testName, msgFail=""){ 30 | if (!eq){ 31 | FileAppend, FAIL: %testName% %msgFail%`n, * 32 | errorcode := 1 33 | } else { 34 | FileAppend, PASS: %testName%`n, * 35 | } 36 | } -------------------------------------------------------------------------------- /tests/testMain.ahk: -------------------------------------------------------------------------------- 1 | ; #Include 2 | ; #Include 3 | #Include %A_ScriptDir%\..\lib\Yunit\Yunit.ahk 4 | #Include %A_ScriptDir%\..\lib\Yunit\StdOut.ahk 5 | 6 | t := Yunit.Use(YunitStdOut).Test(NumberTestSuite, StringTestSuite) 7 | 8 | ; loop through the results, check if there was a FAIL case 9 | errorcode := 0 10 | for k,v in t.results { 11 | if IsObject(v){ 12 | for k2, v2 in v { 13 | if (v2 != "0") 14 | errorcode := 1 15 | } 16 | } 17 | } 18 | ExitApp, % errorcode 19 | return 20 | 21 | 22 | /* 23 | ********* 24 | The Tests 25 | ********* 26 | */ 27 | 28 | class NumberTestSuite 29 | { 30 | Begin() 31 | { 32 | this.x := 123 33 | this.y := 456 34 | } 35 | 36 | Test_Sum() 37 | { 38 | Yunit.assert(this.x + this.y == 579) 39 | } 40 | 41 | Test_Division() 42 | { 43 | Yunit.assert(this.x / this.y < 1) 44 | Yunit.assert(this.x / this.y > 0.25) 45 | } 46 | 47 | End() 48 | { 49 | this.remove("x") 50 | this.remove("y") 51 | } 52 | 53 | class Negatives 54 | { 55 | Begin() 56 | { 57 | this.x := -123 58 | this.y := 456 59 | } 60 | 61 | Test_Sum() 62 | { 63 | Yunit.assert(this.x + this.y == 333) 64 | } 65 | 66 | Test_Division() 67 | { 68 | Yunit.assert(this.x / this.y > -1) 69 | Yunit.assert(this.x / this.y < -0.25) 70 | } 71 | 72 | ; uncomment this for a failing test -123 - 456 != 0 73 | ; Test_Fails_NoMessage() 74 | ; { 75 | ; Yunit.assert(this.x - this.y == 0) 76 | ; } 77 | 78 | End() 79 | { 80 | this.remove("x") 81 | this.remove("y") 82 | } 83 | } 84 | } 85 | 86 | class StringTestSuite 87 | { 88 | Begin() 89 | { 90 | this.a := "abc" 91 | this.b := "cdef" 92 | } 93 | 94 | Test_Concat() 95 | { 96 | Yunit.assert(this.a . this.b == "abccdef") 97 | } 98 | 99 | Test_Substring() 100 | { 101 | Yunit.assert(SubStr(this.b, 2, 2) == "de") 102 | } 103 | 104 | Test_ExpectedException_Success() 105 | { 106 | this.ExpectedException := Exception("SomeCustomException") 107 | if SubStr(this.a, 3, 1) == SubStr(this.b, 1, 1) 108 | throw Exception("SomeCustomException") 109 | } 110 | 111 | End() 112 | { 113 | this.remove("a") 114 | this.remove("b") 115 | } 116 | } 117 | --------------------------------------------------------------------------------