From 14c7bc1c90d671743c2f25b6958bb54f03140597 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Ledure Date: Fri, 3 Jun 2022 17:51:57 +0200 Subject: [PATCH] ScriptForge - New 'UnitTest' service for Basic The "UnitTest" service is implemented as a new Basic library called 'SFUnitTests'. ScriptForge unit tests (SF_UnitTest class module) ====================== Class providing a framework to execute and check sets of unit tests. The UnitTest unit testing framework was originally inspired by unittest.py in Python and has a similar flavor as major unit testing frameworks in other languages. It supports - test automation - sharing of setupand shutdown code - aggregation of tests into collections. Both the - code describing the unit tests - code to be tested must be written exclusively in Basic (the code might call functions written in other languages). The code to be tested may be released as an extension. It does not need to make use of ScriptForge services. The test reporting device is the Console. Definitions: - Test Case: each test case is a Basic Sub. - Test Suite: a collection of test cases stored in 1 Basic module. - Unit test: a set of test suites stored in 1 library. Two modes: - the normal mode ("full mode"), using test suites and test cases The UnitTest service is passed as argument to each test case. - the "simple mode" limited to the use of the Assert...() methods. Service invocation examples: - In full mode, the service creation is external to test cases Dim myUnitTest As Variant myUnitTest = CreateScriptService("UnitTest", ThisComponent, "Tests") ' Test code is in the library "Tests" ' located in the current document - In simple mode, the service creation is internal to every test case Dim myUnitTest As Variant myUnitTest = CreateScriptService("UnitTest") With myUnitTest If Not .AssertTrue(...) Then ... ' ... .Dispose() End With Error handling To support the debugging of the tested code, the UnitTest service, in cases of - assertion failure - Basic run-time error in the tested code - Basic run-time error in the testing code (the unit tests) will comment the error location and description in a message box and in the console log, providing every test case (in either mode) implements an error handler containing at least a call to the ReportError() method. Change-Id: I9d9b889b148f172cd868af455493c8c696d1e953 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/135365 Tested-by: Jean-Pierre Ledure Tested-by: Jenkins Reviewed-by: Jean-Pierre Ledure --- Repository.mk | 1 + desktop/CppunitTest_desktop_lib.mk | 1 + scp2/source/ooo/directory_ooo.scp | 5 + wizards/Module_wizards.mk | 1 + wizards/Package_sfunittests.mk | 30 + wizards/source/configshare/dialog.xlc | 1 + wizards/source/configshare/script.xlc | 1 + wizards/source/scriptforge/SF_Exception.xba | 10 + wizards/source/scriptforge/SF_Root.xba | 15 + wizards/source/scriptforge/SF_Services.xba | 1 + wizards/source/scriptforge/SF_Utils.xba | 7 +- wizards/source/scriptforge/po/ScriptForge.pot | 22 +- wizards/source/scriptforge/po/en.po | 22 +- wizards/source/sfunittests/SF_Register.xba | 202 ++ wizards/source/sfunittests/SF_UnitTest.xba | 1820 +++++++++++++++++ wizards/source/sfunittests/__License.xba | 26 + wizards/source/sfunittests/dialog.xlb | 3 + wizards/source/sfunittests/script.xlb | 7 + 18 files changed, 2171 insertions(+), 4 deletions(-) create mode 100644 wizards/Package_sfunittests.mk create mode 100644 wizards/source/sfunittests/SF_Register.xba create mode 100644 wizards/source/sfunittests/SF_UnitTest.xba create mode 100644 wizards/source/sfunittests/__License.xba create mode 100644 wizards/source/sfunittests/dialog.xlb create mode 100644 wizards/source/sfunittests/script.xlb diff --git a/Repository.mk b/Repository.mk index 5532b993dfde..4004cbe142df 100644 --- a/Repository.mk +++ b/Repository.mk @@ -1002,6 +1002,7 @@ $(eval $(call gb_Helper_register_packages_for_install,ooo,\ wizards_basicsrvsfdatabases \ wizards_basicsrvsfdialogs \ wizards_basicsrvsfdocuments \ + wizards_basicsrvsfunittests \ wizards_basicsrvsfwidgets \ wizards_basicsrvstandard \ wizards_basicsrvtemplate \ diff --git a/desktop/CppunitTest_desktop_lib.mk b/desktop/CppunitTest_desktop_lib.mk index 61d5cb1f31c5..d8f58cac18da 100644 --- a/desktop/CppunitTest_desktop_lib.mk +++ b/desktop/CppunitTest_desktop_lib.mk @@ -75,6 +75,7 @@ $(eval $(call gb_CppunitTest_use_packages,desktop_lib, \ wizards_basicsrvsfdatabases \ wizards_basicsrvsfdialogs \ wizards_basicsrvsfdocuments \ + wizards_basicsrvsfunittests \ wizards_basicsrvsfwidgets \ wizards_basicsrvtemplate \ wizards_basicsrvtools \ diff --git a/scp2/source/ooo/directory_ooo.scp b/scp2/source/ooo/directory_ooo.scp index 66c895575a7f..71bc2f2f76eb 100644 --- a/scp2/source/ooo/directory_ooo.scp +++ b/scp2/source/ooo/directory_ooo.scp @@ -285,6 +285,11 @@ Directory gid_Dir_Basic_SFDocuments DosName = "SFDocuments"; End +Directory gid_Dir_Basic_SFUnitTests + ParentID = gid_Dir_Basic; + DosName = "SFUnitTests"; +End + Directory gid_Dir_Basic_SFWidgets ParentID = gid_Dir_Basic; DosName = "SFWidgets"; diff --git a/wizards/Module_wizards.mk b/wizards/Module_wizards.mk index 14757e57bb10..580eafcdea29 100644 --- a/wizards/Module_wizards.mk +++ b/wizards/Module_wizards.mk @@ -33,6 +33,7 @@ $(eval $(call gb_Module_add_targets,wizards,\ Package_sfdatabases \ Package_sfdialogs \ Package_sfdocuments \ + Package_sfunittests \ Package_sfwidgets \ Package_standard \ Package_template \ diff --git a/wizards/Package_sfunittests.mk b/wizards/Package_sfunittests.mk new file mode 100644 index 000000000000..54b50e016e5a --- /dev/null +++ b/wizards/Package_sfunittests.mk @@ -0,0 +1,30 @@ +# -*- Mode: makefile-gmake; tab-width: 4; indent-tabs-mode: t -*- +# +# This file is part of the LibreOffice project. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This file incorporates work covered by the following license notice: +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed +# with this work for additional information regarding copyright +# ownership. The ASF licenses this file to you under the Apache +# License, Version 2.0 (the "License"); you may not use this file +# except in compliance with the License. You may obtain a copy of +# the License at http://www.apache.org/licenses/LICENSE-2.0 . +# + +$(eval $(call gb_Package_Package,wizards_basicsrvsfunittests,$(SRCDIR)/wizards/source/sfunittests)) + +$(eval $(call gb_Package_add_files,wizards_basicsrvsfunittests,$(LIBO_SHARE_FOLDER)/basic/SFUnitTests,\ + SF_Register.xba \ + SF_UnitTest.xba \ + __License.xba \ + dialog.xlb \ + script.xlb \ +)) + +# vim: set noet sw=4 ts=4: diff --git a/wizards/source/configshare/dialog.xlc b/wizards/source/configshare/dialog.xlc index 8db62f073800..2849d7a9b79e 100644 --- a/wizards/source/configshare/dialog.xlc +++ b/wizards/source/configshare/dialog.xlc @@ -14,5 +14,6 @@ + diff --git a/wizards/source/configshare/script.xlc b/wizards/source/configshare/script.xlc index d1de5ea4f948..ff05ff37b026 100644 --- a/wizards/source/configshare/script.xlc +++ b/wizards/source/configshare/script.xlc @@ -14,5 +14,6 @@ + diff --git a/wizards/source/scriptforge/SF_Exception.xba b/wizards/source/scriptforge/SF_Exception.xba index aeac2fb91aab..11e97b02b753 100644 --- a/wizards/source/scriptforge/SF_Exception.xba +++ b/wizards/source/scriptforge/SF_Exception.xba @@ -133,6 +133,10 @@ Const SQLSYNTAXERROR = "SQLSYNTAXERROR" ' Python Const PYTHONSHELLERROR = "PYTHONSHELLERROR" +' SF_UnitTest +Const UNITTESTLIBRARYERROR = "UNITTESTLIBRARYERROR" +Const UNITTESTMETHODERROR = "UNITTESTMETHODERROR" + REM ============================================================= PRIVATE MEMBERS ' User defined errors @@ -1025,6 +1029,12 @@ Try: Case PYTHONSHELLERROR ' SF_Exception.PythonShell (Python only) sMessage = sLocation _ & "\n" & "\n" & .GetText("PYTHONSHELL") + Case UNITTESTLIBRARYERROR ' SFUnitTests._NewUnitTest(LibraryName) + sMessage = sLocation _ + & "\n" & "\n" & .GetText("UNITTESTLIBRARY", pvArgs(0)) + Case UNITTESTMETHODERROR ' SFUnitTests.SF_UnitTest(Method) + sMessage = sLocation _ + & "\n" & "\n" & .GetText("UNITTESTMETHOD", pvArgs(0)) Case Else End Select End With diff --git a/wizards/source/scriptforge/SF_Root.xba b/wizards/source/scriptforge/SF_Root.xba index f0d81252469e..4db0efb42c1d 100644 --- a/wizards/source/scriptforge/SF_Root.xba +++ b/wizards/source/scriptforge/SF_Root.xba @@ -1012,6 +1012,21 @@ Try: , Comment := "SF_Exception.PythonShell error message" _ & "APSO: to leave unchanged" _ ) + ' SFUnitTests._NewUnitTest + .AddText( Context := "UNITTESTLIBRARY" _ + , MsgId := "The requested library could not be located.\n" _ + & "The UnitTest service has not been initialized.\n\n" _ + & "Library name : « %1 »" _ + , Comment := "SFUnitTest could not locate the library gven as argument\n" _ + & "%1: The name of the library" _ + ) + ' SFUnitTests.SF_UnitTest + .AddText( Context := "UNITTESTMETHOD" _ + , MsgId := "The method '%1' is unexpected in the current context.\n" _ + & "The UnitTest service cannot proceed further with the on-going test." _ + , Comment := "SFUnitTest finds a RunTest() call in a inappropriate location\n" _ + & "%1: The name of a method" _ + ) End With End If diff --git a/wizards/source/scriptforge/SF_Services.xba b/wizards/source/scriptforge/SF_Services.xba index a2ab63828819..627dc4d2e8fe 100644 --- a/wizards/source/scriptforge/SF_Services.xba +++ b/wizards/source/scriptforge/SF_Services.xba @@ -130,6 +130,7 @@ Try: sLibrary = "SFDocuments" Case "dialog", "dialogevent" : sLibrary = "SFDialogs" Case "database" : sLibrary = "SFDatabases" + Case "unittest" : sLibrary = "SFUnitTests" Case "menu", "popupmenu" : sLibrary = "SFWidgets" Case Else End Select diff --git a/wizards/source/scriptforge/SF_Utils.xba b/wizards/source/scriptforge/SF_Utils.xba index e26cca66a776..91b703c46431 100644 --- a/wizards/source/scriptforge/SF_Utils.xba +++ b/wizards/source/scriptforge/SF_Utils.xba @@ -51,8 +51,9 @@ Global Const V_SFOBJECT = 103 ' ScriptForge object: has Object Global Const V_BASICOBJECT = 104 ' User Basic object Type _ObjectDescriptor ' Returned by the _VarTypeObj() method - iVarType As Integer ' One of the V_NOTHING, V_xxxOBJECT constants - sObjectType As String ' Either "" or "com.sun.star..." or a ScriptForge object type (ex. "SF_SESSION" or "DICTIONARY") + iVarType As Integer ' One of the V_NOTHING, V_xxxOBJECT constants + sObjectType As String ' Either "" or "com.sun.star..." or a ScriptForge object type (ex. "SF_SESSION" or "DICTIONARY") + sServiceName As String ' Either "" or the service name of a ScriptForge object type (ex. "ScriptForge.Exception"- End Type REM ================================================================== EXCEPTIONS @@ -1065,6 +1066,7 @@ Try: With oObjDesc .iVarType = VarType(pvValue) .sObjectType = "" + .sServiceName = "" bUno = False If .iVarType = V_OBJECT Then If IsNull(pvValue) Then @@ -1089,6 +1091,7 @@ Try: bUno = False ' Try if the Basic object has an ObjectType property .sObjectType = oValue.ObjectType + .sServiceName = oValue.ServiceName End If ' Derive the return value from the object type Select Case True diff --git a/wizards/source/scriptforge/po/ScriptForge.pot b/wizards/source/scriptforge/po/ScriptForge.pot index b1727527da58..248d800c017c 100644 --- a/wizards/source/scriptforge/po/ScriptForge.pot +++ b/wizards/source/scriptforge/po/ScriptForge.pot @@ -14,7 +14,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: https://bugs.libreoffice.org/enter_bug.cgi?product=LibreOffice&bug_status=UNCONFIRMED&component=UI\n" -"POT-Creation-Date: 2022-03-09 14:52:15\n" +"POT-Creation-Date: 2022-05-04 18:07:20\n" "PO-Revision-Date: YYYY-MM-DD HH:MM:SS\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -952,4 +952,24 @@ msgctxt "PYTHONSHELL" msgid "" "The APSO extension could not be located in your LibreOffice " "installation." +msgstr "" + +#. SFUnitTest could not locate the library gven as argument +#. %1: The name of the library +#, kde-format +msgctxt "UNITTESTLIBRARY" +msgid "" +"The requested library could not be located.\n" +"The UnitTest service has not been initialized.\n" +"\n" +"Library name : « %1 »" +msgstr "" + +#. SFUnitTest finds a RunTest() call in a inappropriate location +#. %1: The name of a method +#, kde-format +msgctxt "UNITTESTMETHOD" +msgid "" +"The method '%1' is unexpected in the current context.\n" +"The UnitTest service cannot proceed further with the on-going test." msgstr "" \ No newline at end of file diff --git a/wizards/source/scriptforge/po/en.po b/wizards/source/scriptforge/po/en.po index b1727527da58..248d800c017c 100644 --- a/wizards/source/scriptforge/po/en.po +++ b/wizards/source/scriptforge/po/en.po @@ -14,7 +14,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: https://bugs.libreoffice.org/enter_bug.cgi?product=LibreOffice&bug_status=UNCONFIRMED&component=UI\n" -"POT-Creation-Date: 2022-03-09 14:52:15\n" +"POT-Creation-Date: 2022-05-04 18:07:20\n" "PO-Revision-Date: YYYY-MM-DD HH:MM:SS\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -952,4 +952,24 @@ msgctxt "PYTHONSHELL" msgid "" "The APSO extension could not be located in your LibreOffice " "installation." +msgstr "" + +#. SFUnitTest could not locate the library gven as argument +#. %1: The name of the library +#, kde-format +msgctxt "UNITTESTLIBRARY" +msgid "" +"The requested library could not be located.\n" +"The UnitTest service has not been initialized.\n" +"\n" +"Library name : « %1 »" +msgstr "" + +#. SFUnitTest finds a RunTest() call in a inappropriate location +#. %1: The name of a method +#, kde-format +msgctxt "UNITTESTMETHOD" +msgid "" +"The method '%1' is unexpected in the current context.\n" +"The UnitTest service cannot proceed further with the on-going test." msgstr "" \ No newline at end of file diff --git a/wizards/source/sfunittests/SF_Register.xba b/wizards/source/sfunittests/SF_Register.xba new file mode 100644 index 000000000000..360abba50381 --- /dev/null +++ b/wizards/source/sfunittests/SF_Register.xba @@ -0,0 +1,202 @@ + + +REM ======================================================================================================================= +REM === The ScriptForge library and its associated libraries are part of the LibreOffice project. === +REM === The SFUnitTests library is one of the associated libraries. === +REM === Full documentation is available on https://help.libreoffice.org/ === +REM ======================================================================================================================= + +Option Compatible +Option Explicit + +''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' +''' SF_Register +''' =========== +''' The ScriptForge framework includes +''' the master ScriptForge library +''' a number of "associated" libraries SF* +''' any user/contributor extension wanting to fit into the framework +''' +''' The main methods in this module allow the current library to cling to ScriptForge +''' - RegisterScriptServices +''' Register the list of services implemented by the current library +''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' + +REM ================================================================== EXCEPTIONS + +Private Const UNITTESTLIBRARYERROR = "UNITTESTLIBRARYERROR" + +REM ============================================================== PUBLIC METHODS + +REM ----------------------------------------------------------------------------- +Public Sub RegisterScriptServices() As Variant +''' Register into ScriptForge the list of the services implemented by the current library +''' Each library pertaining to the framework must implement its own version of this method +''' +''' It consists in successive calls to the RegisterService() and RegisterEventManager() methods +''' with 2 arguments: +''' ServiceName: the name of the service as a case-insensitive string +''' ServiceReference: the reference as an object +''' If the reference refers to a module, then return the module as an object: +''' GlobalScope.Library.Module +''' If the reference is a class instance, then return a string referring to the method +''' containing the New statement creating the instance +''' "libraryname.modulename.function" + + With GlobalScope.ScriptForge.SF_Services + .RegisterService("UnitTest", "SFUnitTests.SF_Register._NewUnitTest") ' Reference to the function initializing the service + End With + +End Sub ' SFUnitTests.SF_Register.RegisterScriptServices + +REM =========================================================== PRIVATE FUNCTIONS + +REM ----------------------------------------------------------------------------- +Public Function _NewUnitTest(Optional ByVal pvArgs As Variant) As Object +''' Create a new instance of the SF_UnitTest class +' Args: +''' Location: if empty, the location of the library is presumed to be in GlobalScope.BasicLibraries +''' Alternatives are: +''' - the name of a document: see SF_UI.WindowName +''' - an explicit SFDocuments.Document instance +''' - the component containing the library, typically ThisComponent +''' LibraryName: the name of the library containing the test code +''' Returns: +''' The instance or Nothing +''' Exceptions: +''' UNITTESTLIBRARYNOTFOUND The library could not be found + +Dim oUnitTest As Object ' Return value +Dim vLocation As Variant ' Alias of pvArgs(0) +Dim vLibraryName As Variant ' alias of pvArgs(1) +Dim vLocations As Variant ' "user", "share" or document +Dim sLocation As String ' A single location +Dim sTargetLocation As String ' "user" or the document name +Dim vLanguages As Variant ' "Basic", "Python", ... programming languages +Dim sLanguage As String ' A single programming language +Dim vLibraries As Variant ' Library names +Dim sLibrary As String ' A single library +Dim vModules As Variant ' Module names +Dim sModule As String ' A single module +Dim vModuleNames As Variant ' Module names +Dim oRoot As Object ' com.sun.star.script.browse.BrowseNodeFactory +Dim iLibrary As Integer ' The index of the target location in vLibraries + +Dim FSO As Object ' SF_FileSystem +Dim i As Integer, j As Integer, k As Integer, l As Integer + +Const cstService = "SFUnitTests.UnitTest" + + If ScriptForge.SF_Utils._ErrorHandling() Then On Local Error GoTo Catch + +Check: + If IsMissing(pvArgs) Or IsEmpty(pvArgs) Then pvArgs = Array() + If UBound(pvArgs) >= 0 Then vLocation = pvArgs(0) Else vLocation = "" + If IsEmpty(vLocation) Then vLocation = "" + If UBound(pvArgs) >= 1 Then vLibraryName = pvArgs(1) Else vLibraryName = "" + If IsEmpty(vLibraryName) Then vLibraryName = "" + If Not ScriptForge.SF_Utils._Validate(vLocation, "Location", Array(V_STRING, ScriptForge.V_OBJECT)) Then GoTo Finally + If Not ScriptForge.SF_Utils._Validate(vLibraryName, "LibraryName", V_STRING) Then GoTo Finally + + Set oUnitTest = Nothing + Set FSO = CreateScriptService("ScriptForge.FileSystem") + + ' Determine the library container hosting the test code + + ' Browsing starts from root element + Set oRoot = SF_Utils._GetUNOService("BrowseNodeFactory").createView(com.sun.star.script.browse.BrowseNodeFactoryViewTypes.MACROORGANIZER) + + If Len(vLibraryName) > 0 Then + + ' Determine the target location, as a string. The location is either: + ' - the last component of a document's file name + ' - "user" = My Macros & Dialogs + If VarType(vLocation) = ScriptForge.V_OBJECT Then + sTargetLocation = FSO.GetName(vLocation.URL) + ElseIf Len(vLocation) = 0 Then + sTargetLocation = "user" ' Testing code is presumed NOT in "share" + Else + sTargetLocation = FSO.GetName(vLocation) + End If + + ' Exploration is done via tree nodes + iLibrary = -1 + If Not IsNull(oRoot) Then + If oRoot.hasChildNodes() Then + vLocations = oRoot.getChildNodes() + For i = 0 To UBound(vLocations) + sLocation = vLocations(i).getName() + If sLocation = sTargetLocation Then + If vLocations(i).hasChildNodes() Then + vLanguages = vLocations(i).getChildNodes() + For j = 0 To UBound(vLanguages) + sLanguage = vLanguages(j).getName() + ' Consider Basic libraries only + If sLanguage = "Basic" Then + If vLanguages(j).hasChildNodes() Then + vLibraries = vLanguages(j).getChildNodes() + For k = 0 To UBound(vLibraries) + sLibrary = vLibraries(k).getName() + ' Consider the targeted library only + If sLibrary = vLibraryName Then + iLibrary = k + If vLibraries(k).hasChildNodes() Then + vModules = vLibraries(k).getChildNodes() + vModuleNames = Array() + For l = 0 To UBound(vModules) + sModule = vModules(l).getName() + vModuleNames = ScriptForge.SF_Array.Append(vModuleNames, sModule) + Next l + End If + Exit For + End If + Next k + End If + End If + If iLibrary >= 0 Then Exit For + Next j + End If + End If + If iLibrary >= 0 Then Exit For + Next i + End If + End If + If iLibrary < 0 Then GoTo CatchLibrary + + End If + +Try: + ' Create the unittest Basic object and initialize its attributes + Set oUnitTest = New SF_UnitTest + With oUnitTest + Set .[Me] = oUnitTest + If Len(vLibraryName) > 0 Then + .LibrariesContainer = sTargetLocation + .Scope = Iif(sTargetLocation = "user", "application", "document") + .Libraries = vLibraries + .LibraryName = sLibrary + .LibraryIndex = iLibrary + .Modules = vModules + .ModuleNames = vModuleNames + ._ExecutionMode = .FULLMODE + ._WhenAssertionFails = .FAILSTOPSUITE + ' Launch the test timer + .TestTimer = CreateScriptService("ScriptForge.Timer", True) + Else + ._ExecutionMode = .SIMPLEMODE + ._WhenAssertionFails = .FAILIMMEDIATESTOP + End If + End With + +Finally: + Set _NewUnitTest = oUnitTest + Exit Function +Catch: + GoTo Finally +CatchLibrary: + ScriptForge.SF_Exception.RaiseFatal(UNITTESTLIBRARYERROR, vLibraryName) + GoTo Finally +End Function ' SFUnitTests.SF_Register._NewUnitTest + +REM ============================================== END OF SFUNITTESTS.SF_REGISTER + \ No newline at end of file diff --git a/wizards/source/sfunittests/SF_UnitTest.xba b/wizards/source/sfunittests/SF_UnitTest.xba new file mode 100644 index 000000000000..c3a1daa9dc9d --- /dev/null +++ b/wizards/source/sfunittests/SF_UnitTest.xba @@ -0,0 +1,1820 @@ + + +REM ======================================================================================================================= +REM === The ScriptForge library and its associated libraries are part of the LibreOffice project. === +REM === Full documentation is available on https://help.libreoffice.org/ === +REM ======================================================================================================================= + +Option Compatible +Option ClassModule + +Option Explicit + +''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' +''' SF_UnitTest +''' =========== +''' Class providing a framework to execute and check sets of unit tests. +''' +''' The UnitTest unit testing framework was originally inspired by unittest.py in Python +''' and has a similar flavor as major unit testing frameworks in other languages. +''' +''' It supports test automation, sharing of setup and shutdown code for tests, +''' aggregation of tests into collections. +''' +''' Both the +''' - code describing the unit tests +''' - code to be tested +''' must be written exclusively in Basic (the code might call functions written in other languages). +''' Even if either code may be contained in the same module, a much better practice is to +''' store them in separate libraries. +''' Typically: +''' - in a same document when the code to be tested is contained in that document +''' - either in a "test" document or in a "My Macros" library when the code +''' to be tested is a shared library (My Macros or LibreOffice Macros). +''' The code to be tested may be released as an extension. It does not need to make +''' use of ScriptForge services in any way. +''' +''' The test reporting device is the Console. Read abount the console in the ScriptForge.Exception service. +''' +''' Definitions: +''' - Test Case +''' A test case is the individual unit of testing. +''' It checks for a specific response to a particular set of inputs. +''' A test case in the UnitTest service is represented by a Basic Sub. +''' The name of the Sub starts conventionally with "Test_". +''' The test fails if one of the included AssertXXX methods returns False +''' - Test Suite +''' A test suite is a collection of test cases that should be executed together. +''' A test suite is represented by a Basic module. +''' A suite may include the tasks needed to prepare one or more tests, and any associated cleanup actions. +''' This may involve, for example, creating temporary files or directories, opening a document, loading libraries. +''' Conventionally those tasks are part pf the SetUp') and TearDown() methods. +''' - Unit test +''' A full unit test is a set of test suites (each suite in a separate Basic module), +''' each of them being a set of test cases (each case is located in a separate Basic Sub). +''' +''' Two modes: +''' Beside the normal mode ("full mode"), using test suites and test cases, a second mode exists, called "simple mode" +''' limited to the use exclusively of the Assert...() methods. +''' Their boolean returned value may support the execution of limited unit tests. +''' +''' Service invocation examples: +''' In full mode, the service creation is external to test cases +''' Dim myUnitTest As Variant +''' myUnitTest = CreateScriptService("UnitTest", ThisComponent, "Tests") +''' ' Test code is in the library "Tests" located in the current document +''' In simple mode, the service creation is internal to every test case +''' Dim myUnitTest As Variant +''' myUnitTest = CreateScriptService("UnitTest") +''' With myUnitTest +''' If Not .AssertTrue(...) Then ... ' Only calls to the Assert...() methods are allowed +''' ' ... +''' .Dispose() +''' End With +''' +''' Minimalist full mode example +''' Code to be tested (stored in library "Standard" of document "MyDoc.ods") : +''' Function ArraySize(arr As Variant) As Long +''' If IsArray(arr) Then ArraySize = UBound(arr) - LBound(arr) + 1 Else ArraySize = -1 +''' End Function +''' Test code (stored in module "AllTests" of library "Tests" of document "MyDoc.ods") : +''' Sub Main() ' Sub to trigger manually, f.i. from the Tools + Run Macro tabbed bar +''' GlobalScope.BasicLibraries.loadLibrary("ScriptForge") +''' Dim test : test = CreateScriptService("UnitTest", ThisComponent, "Tests") +''' test.RunTest("AllTests") ' AllTests is a module name ; test cases are named "Test_*" (default) +''' test.Dispose() +''' End Sub +''' REM ------------------------------------------------------------------------------ +''' Sub Setup(test) ' The unittest service is passed as argument +''' ' Optional Sub to initialize processing of the actual test suite +''' Dim exc : exc = CreateScriptService("Exception") +''' exc.Console(Modal := False) ' Watch test progress in the console +''' End Sub +''' REM ------------------------------------------------------------------------------ +''' Sub Test_ArraySize(test) +''' On Local Error GoTo CatchErr +''' test.AssertEqual(ArraySize(10), -1, "When not array") +''' test.AssertEqual(ArraySize(Array(1, 2, 3)), 3, "When simple array") +''' test.AssertEqual(ArraySize(DimArray(3)), 4, "When array with empty items") +''' Exit Sub +''' CatchErr: +''' test.ReportError("ArraySize() is corrupt") +''' End Sub +''' REM ------------------------------------------------------------------------------ +''' Sub TearDown(test) +''' ' Optional Sub to finalize processing of the actual test suite +''' End Sub +''' +''' Error handling +''' To support the debugging of the tested code, the UnitTest service, in cases of +''' - assertion failure +''' - Basic run-time error in the tested code +''' - Basic run-time error in the testing code (the unit tests) +''' will comment the error location and description in a message box and in the console log, +''' providing every test case (in either mode) implements an error handler containing at least: +''' Sub Test_Case1(test As Variant) +''' On Local Error GoTo Catch +''' ' ... (AssertXXX(), Fail(), ...) +''' Exit Sub +''' Catch: +''' test.ReportError() +''' End Sub +''' +''' Detailed user documentation: +''' https://help.libreoffice.org/latest/en-US/text/sbasic/shared/03/sf_unittest.html?DbPAR=BASIC +''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' + +REM ================================================================== EXCEPTIONS + +Private Const UNITTESTMETHODERROR = "UNITTESTMETHODERROR" + +REM ============================================================= PRIVATE MEMBERS + +Private [Me] As Object +Private [_Parent] As Object +Private ObjectType As String ' Must be "UNITTEST" +Private ServiceName As String + +' Testing code +Private LibrariesContainer As String ' Document or user Basic library containing the test library +Private Scope As String ' Scope when running a Basic script with Session.ExecuteBasicScript() +Private Libraries As Variant ' Set of libraries +Private LibraryName As String ' Name of the library containing the test code +Private LibraryIndex As Integer ' Index in Libraries +Private Modules As Variant ' Set of modules +Private ModuleNames As Variant ' Set of module names +Private MethodNames As Variant ' Set of methods in a given module + +' Internals +Private _Verbose As Boolean ' When True, every assertion is reported,failing or not +Private _LongMessage As Boolean ' When False, only the message provided by the tester is considered + ' When True (default), that message is appended to the standard message +Private _WhenAssertionFails As Integer ' Determines what to do when a test fails + +' Test status +Private _Status As Integer ' 0 = standby + ' 1 = test suite started + ' 2 = setup started + ' 3 = test case started + ' 4 = teardown started +Private _ExecutionMode As Integer ' 1 = Test started with RunTest() + ' 2 = Test started with CreateScriptService() Only Assert() methods allowed +Private _Module As String ' Exact name of module currently running +Private _TestCase As String ' Exact name of test case currently running +Private _ReturnCode As Integer ' 0 = Normal end + ' 1 = Assertion failed + ' 2 = Skip request (in Setup() only) + '-1 = abnormal end +Private _FailedAssert As String ' Assert function that returned a failure + +' Timers +Private TestTimer As Object ' Started by CreateScriptService() +Private SuiteTimer As Object ' Started by RunTest() +Private CaseTimer As Object ' Started by new case + +' Services +Private Exception As Object ' SF_Exception +Private Session As Object ' SF_Session + +REM ============================================================ MODULE CONSTANTS + +' When assertion fails constants: error is reported + ... +Global Const FAILIGNORE = 0 ' Ignore the failure +Global Const FAILSTOPSUITE = 1 ' Module TearDown is executed, then next suite may be started (default in full mode) +Global Const FAILIMMEDIATESTOP = 2 ' Stop immediately (default in simple mode) + +' Unit tests status (internal use only => not Global) +Const STATUSSTANDBY = 0 ' No test active +Const STATUSSUITESTARTED = 1 ' RunTest() started +Const STATUSSETUP = 2 ' A Setup() method is running +Const STATUSTESTCASE = 3 ' A test case is running +Const STATUSTEARDOWN = 4 ' A TearDown() method is running + +' Return codes +Global Const RCNORMALEND = 0 ' Normal end of test or test not started +Global Const RCASSERTIONFAILED = 1 ' An assertion within a test case returned False +Global Const RCSKIPTEST = 2 ' A SkipTest() was issued by a Setup() method +Global Const RCABORTTEST = 3 ' Abnormal end of test + +' Execution modes +Global Const FULLMODE = 1 ' 1 = Test started with RunTest() +Global Const SIMPLEMODE = 2 ' 2 = Test started with CreateScriptService() Only Assert() methods allowed + +Const INVALIDPROCEDURECALL = "5" ' Artificial error raised when an assertion fails + +REM ===================================================== CONSTRUCTOR/DESTRUCTOR + +REM ----------------------------------------------------------------------------- +Private Sub Class_Initialize() + Set [Me] = Nothing + Set [_Parent] = Nothing + ObjectType = "UNITTEST" + ServiceName = "SFUnitTests.UnitTest" + LibrariesContainer = "" + Scope = "" + Libraries = Array() + LibraryName = "" + LibraryIndex = -1 + _Verbose = False + _LongMessage = True + _WhenAssertionFails = -1 + _Status = STATUSSTANDBY + _ExecutionMode = SIMPLEMODE + _Module = "" + _TestCase = "" + _ReturnCode = RCNORMALEND + _FailedAssert = "" + Set TestTimer = Nothing + Set SuiteTimer = Nothing + Set CaseTimer = Nothing + Set Exception = CreateScriptService("ScriptForge.Exception") + Set Session = CreateScriptService("ScriptForge.Session") +End Sub ' SFUnitTests.SF_UnitTest Constructor + +REM ----------------------------------------------------------------------------- +Private Sub Class_Terminate() + If Not IsNull(CaseTimer) Then CaseTimer = CaseTimer.Dispose() + If Not IsNull(SuiteTimer) Then SuiteTimer = SuiteTimer.Dispose() + If Not IsNull(TestTimer) Then TestTimer = TestTimer.Dispose() + Call Class_Initialize() +End Sub ' SFUnitTests.SF_UnitTest Destructor + +REM ----------------------------------------------------------------------------- +Public Function Dispose() As Variant + Call Class_Terminate() + Set Dispose = Nothing +End Function ' SFUnitTests.SF_UnitTest Explicit destructor + +REM ================================================================== PROPERTIES + +REM ----------------------------------------------------------------------------- +Property Get LongMessage() As Variant +''' When False, only the message provided by the tester is considered +''' When True (default), that message is appended to the standard message + LongMessage = _PropertyGet("LongMessage") +End Property ' SFUnitTests.SF_UnitTest.LongMessage (get) + +REM ----------------------------------------------------------------------------- +Property Let LongMessage(Optional ByVal pvLongMessage As Variant) +''' Set the updatable property LongMessage + _PropertySet("LongMessage", pvLongMessage) +End Property ' SFUnitTests.SF_UnitTest.LongMessage (let) + +REM ----------------------------------------------------------------------------- +Property Get ReturnCode() As Integer +''' RCNORMALEND = 0 ' Normal end of test or test not started +''' RCASSERTIONFAILED = 1 ' An assertion within a test case returned False +''' RCSKIPTEST = 2 ' A SkipTest() was issued by a Setup() method +''' RCABORTTEST = 3 ' Abnormal end of test + ReturnCode = _PropertyGet("ReturnCode") +End Property ' SFUnitTests.SF_UnitTest.ReturnCode (get) + +REM ----------------------------------------------------------------------------- +Property Get Verbose() As Variant +''' The Verbose property indicates if all sertions are reported + Verbose = _PropertyGet("Verbose") +End Property ' SFUnitTests.SF_UnitTest.Verbose (get) + +REM ----------------------------------------------------------------------------- +Property Let Verbose(Optional ByVal pvVerbose As Variant) +''' Set the updatable property Verbose + _PropertySet("Verbose", pvVerbose) +End Property ' SFUnitTests.SF_UnitTest.Verbose (let) + +REM ----------------------------------------------------------------------------- +Property Get WhenAssertionFails() As Variant +''' What when an AssertXXX() method returns False +''' FAILIGNORE = 0 ' Ignore the failure +''' FAILSTOPSUITE = 1 ' Module TearDown is executed, then next suite may be started (default in FULL mode) +''' FAILIMMEDIATESTOP = 2 ' Stop immediately (default in SIMPLE mode) +''' In simple mode, only FAILIGNORE and FAILIMMEDIATESTOP are allowed. +''' In both modes, when WhenAssertionFails has not the value FAILIGNORE, +''' each test case MUST have a run-time error handler calling the ReportError() method. +''' Example: +''' Sub Test_sometest(Optional test) +''' On Local Error GoTo CatchError +''' ' ... one or more assert verbs +''' Exit Sub +''' CatchError: +''' test.ReportError() +''' End Sub + WhenAssertionFails = _PropertyGet("WhenAssertionFails") +End Property ' SFUnitTests.SF_UnitTest.WhenAssertionFails (get) + +REM ----------------------------------------------------------------------------- +Property Let WhenAssertionFails(Optional ByVal pvWhenAssertionFails As Variant) +''' Set the updatable property WhenAssertionFails + _PropertySet("WhenAssertionFails", pvWhenAssertionFails) +End Property ' SFUnitTests.SF_UnitTest.WhenAssertionFails (let) + +REM ===================================================================== METHODS + +REM ----------------------------------------------------------------------------- +Public Function AssertAlmostEqual(Optional ByRef A As Variant _ + , Optional ByRef B As Variant _ + , Optional ByVal Tolerance As Variant _ + , Optional ByVal Message As Variant _ + ) As Boolean +''' Returns True when A and B are numerical values and are found close to each other. +''' It is typically used to compare very large or very small numbers. +''' Equality is confirmed when +''' - A and B can be converted to doubles +''' - The absolute difference between a and b, relative to the larger absolute value of a or b, +''' is lower or equal to the tolerance. The default tolerance is 1E-09, +''' Examples: 1E+12 and 1E+12 + 100 are almost equal +''' 1E-20 and 2E-20 are not almost equal +''' 100 and 95 are almost equal when Tolerance = 0.05 + +Dim bAssert As Boolean ' Return value +Const cstTolerance = 1E-09 +Const cstThisSub = "UnitTest.AssertAlmostEqual" +Const cstSubArgs = "A, B, [Tolerance=1E-09], [Message=""""]" + +Check: + If IsMissing(A) Then A = Empty + If IsMissing(B) Then B = Empty + If IsMissing(Tolerance) Then Tolerance = cstTolerance + If IsMissing(Message) Then Message = "" + ScriptForge.SF_Utils._EnterFunction(cstThisSub, cstSubArgs) ' Unconditional ! + If Not ScriptForge.SF_Utils._Validate(Tolerance, "Tolerance", ScriptForge.V_NUMERIC) Then GoTo Catch + +Try: + bAssert = _Assert("AssertAlmostEqual", True, A, B, Message, Tolerance) + +Finally: + AssertAlmostEqual = bAssert + ScriptForge.SF_Utils._ExitFunction(cstThisSub) + Exit Function +Catch: + bAssert = False + GoTo Finally +End Function ' SFUnitTests.SF_UnitTest.AssertAlmostEqual + +REM ----------------------------------------------------------------------------- +Public Function AssertEqual(Optional ByRef A As Variant _ + , Optional ByRef B As Variant _ + , Optional ByVal Message As Variant _ + ) As Boolean +''' Returns True when A and B are found equal. +''' Equality is confirmed when +''' If A and B are scalars: +''' They should have the same VarType or both be numeric +''' Booleans and numeric values are compared with the = operator +''' Strings are compared with the StrComp() builtin function. The comparison is case-sensitive +''' Dates and times are compared up to the second +''' Null, Empty and Nothing are not equal, but AssertEqual(Nothing, Nothing) returns True +''' UNO objects are compared with the EqualUnoObjects() method +''' Basic objects are NEVER equal +''' If A and B are arrays: +''' They should have the same number of dimensions (maximum 2) +''' The lower and upper bounds must be identical for each dimension +''' Two empty arrays are equal +''' Their items must be equal one by one + +Dim bAssert As Boolean ' Return value +Const cstThisSub = "UnitTest.AssertEqual" +Const cstSubArgs = "A, B, [Message=""""]" + +Check: + If IsMissing(A) Then A = Empty + If IsMissing(B) Then B = Empty + If IsMissing(Message) Then Message = "" + ScriptForge.SF_Utils._EnterFunction(cstThisSub, cstSubArgs) ' Unconditional ! + +Try: + bAssert = _Assert("AssertEqual", True, A, B, Message) + +Finally: + AssertEqual = bAssert + ScriptForge.SF_Utils._ExitFunction(cstThisSub) + Exit Function +End Function ' SFUnitTests.SF_UnitTest.AssertEqual + +REM ----------------------------------------------------------------------------- +Public Function AssertFalse(Optional ByRef A As Variant _ + , Optional ByVal Message As Variant _ + ) As Boolean +''' Returns True when A is a Boolean and its value is False + +Dim bAssert As Boolean ' Return value +Const cstThisSub = "UnitTest.AssertFalse" +Const cstSubArgs = "A, [Message=""""]" + +Check: + If IsMissing(A) Then A = Empty + If IsMissing(Message) Then Message = "" + ScriptForge.SF_Utils._EnterFunction(cstThisSub, cstSubArgs) ' Unconditional ! + +Try: + bAssert = _Assert("AssertFalse", True, A, Empty, Message) + +Finally: + AssertFalse = bAssert + ScriptForge.SF_Utils._ExitFunction(cstThisSub) + Exit Function +End Function ' SFUnitTests.SF_UnitTest.AssertFalse + +REM ----------------------------------------------------------------------------- +Public Function AssertGreater(Optional ByRef A As Variant _ + , Optional ByRef B As Variant _ + , Optional ByVal Message As Variant _ + ) As Boolean +''' Returns True when A is greater than B. +''' To compare A and B: +''' They should have the same VarType or both be numeric +''' Elgible datatypes are String, Date or numeric. +''' String comparisons are case-sensitive. + +Dim bAssert As Boolean ' Return value +Const cstThisSub = "UnitTest.AssertGreater" +Const cstSubArgs = "A, B, [Message=""""]" + +Check: + If IsMissing(A) Then A = Empty + If IsMissing(B) Then B = Empty + If IsMissing(Message) Then Message = "" + ScriptForge.SF_Utils._EnterFunction(cstThisSub, cstSubArgs) ' Unconditional ! + +Try: + bAssert = _Assert("AssertGreater", True, A, B, Message) + +Finally: + AssertGreater = bAssert + ScriptForge.SF_Utils._ExitFunction(cstThisSub) + Exit Function +End Function ' SFUnitTests.SF_UnitTest.AssertGreater + +REM ----------------------------------------------------------------------------- +Public Function AssertGreaterEqual(Optional ByRef A As Variant _ + , Optional ByRef B As Variant _ + , Optional ByVal Message As Variant _ + ) As Boolean +''' Returns True when A is greater than or equal to B. +''' To compare A and B: +''' They should have the same VarType or both be numeric +''' Elgible datatypes are String, Date or numeric. +''' String comparisons are case-sensitive. + +Dim bAssert As Boolean ' Return value +Const cstThisSub = "UnitTest.AssertGreaterEqual" +Const cstSubArgs = "A, B, [Message=""""]" + +Check: + If IsMissing(A) Then A = Empty + If IsMissing(B) Then B = Empty + If IsMissing(Message) Then Message = "" + ScriptForge.SF_Utils._EnterFunction(cstThisSub, cstSubArgs) ' Unconditional ! + +Try: + bAssert = _Assert("AssertGreaterEqual", True, A, B, Message) + +Finally: + AssertGreaterEqual = bAssert + ScriptForge.SF_Utils._ExitFunction(cstThisSub) + Exit Function +End Function ' SFUnitTests.SF_UnitTest.AssertGreaterEqual + +REM ----------------------------------------------------------------------------- +Public Function AssertIn(Optional ByRef A As Variant _ + , Optional ByRef B As Variant _ + , Optional ByVal Message As Variant _ + ) As Boolean +''' Returns True when A, a string, is found within B +''' B may be a 1D array, a ScriptForge dictionary or a string. +''' When B is an array, A may be a date or a numeric value. +''' String comparisons are case-sensitive. + +Dim bAssert As Boolean ' Return value +Const cstThisSub = "UnitTest.AssertIn" +Const cstSubArgs = "A, B, [Message=""""]" + +Check: + If IsMissing(A) Then A = Empty + If IsMissing(B) Then B = Empty + If IsMissing(Message) Then Message = "" + ScriptForge.SF_Utils._EnterFunction(cstThisSub, cstSubArgs) ' Unconditional ! + +Try: + bAssert = _Assert("AssertIn", True, A, B, Message) + +Finally: + AssertIn = bAssert + ScriptForge.SF_Utils._ExitFunction(cstThisSub) + Exit Function +End Function ' SFUnitTests.SF_UnitTest.AssertIn + +REM ----------------------------------------------------------------------------- +Public Function AssertIsInstance(Optional ByRef A As Variant _ + , Optional ByRef ObjectType As Variant _ + , Optional ByVal Message As Variant _ + ) As Boolean +''' Returns True when A is an object instance of the class ObjectType or a variable of type ObjectType. +''' A may be: +''' - a ScriptForge object +''' ObjectType is a string like "DICTIONARY", "calc", "Dialog", "exception", etc. +''' - a UNO object +''' ObjectType is a string identical with values returned by the SF_Session.UnoObjectType() +''' - any variable, providing it is neither an object nor an array +''' ObjectType is a string identifying a value returned by the TypeName() builtin function +''' - an array +''' ObjectType is expected to be "array" + +Dim bAssert As Boolean ' Return value +Const cstThisSub = "UnitTest.AssertIsInstance" +Const cstSubArgs = "A, ObjectType, [Message=""""]" + +Check: + If IsMissing(A) Then A = Empty + If IsMissing(ObjectType) Then ObjectType = Empty + If IsMissing(Message) Then Message = "" + ScriptForge.SF_Utils._EnterFunction(cstThisSub, cstSubArgs) ' Unconditional ! + If Not ScriptForge.SF_Utils._Validate(ObjectType, "ObjectType", V_STRING) Then GoTo Catch + + +Try: + bAssert = _Assert("AssertIsInstance", True, A, Empty, Message, ObjectType) + +Finally: + AssertIsInstance = bAssert + ScriptForge.SF_Utils._ExitFunction(cstThisSub) + Exit Function +Catch: + bAssert = False + GoTo Finally +End Function ' SFUnitTests.SF_UnitTest.AssertIsInstance + +REM ----------------------------------------------------------------------------- +Public Function AssertIsNothing(Optional ByRef A As Variant _ + , Optional ByVal Message As Variant _ + ) As Boolean +''' Returns True when A is an object that has the Nothing value + +Dim bAssert As Boolean ' Return value +Const cstThisSub = "UnitTest.AssertIsNothing" +Const cstSubArgs = "A, [Message=""""]" + +Check: + If IsMissing(A) Then A = Empty + If IsMissing(Message) Then Message = "" + ScriptForge.SF_Utils._EnterFunction(cstThisSub, cstSubArgs) ' Unconditional ! + +Try: + bAssert = _Assert("AssertIsNothing", True, A, Empty, Message) + +Finally: + AssertIsNothing = bAssert + ScriptForge.SF_Utils._ExitFunction(cstThisSub) + Exit Function +End Function ' SFUnitTests.SF_UnitTest.AssertIsNothing + +REM ----------------------------------------------------------------------------- +Public Function AssertIsNull(Optional ByRef A As Variant _ + , Optional ByVal Message As Variant _ + ) As Boolean +''' Returns True when A has the Null value + +Dim bAssert As Boolean ' Return value +Const cstThisSub = "UnitTest.AssertIsNull" +Const cstSubArgs = "A, [Message=""""]" + +Check: + If IsMissing(A) Then A = Empty + If IsMissing(Message) Then Message = "" + ScriptForge.SF_Utils._EnterFunction(cstThisSub, cstSubArgs) ' Unconditional ! + +Try: + bAssert = _Assert("AssertIsNull", True, A, Empty, Message) + +Finally: + AssertIsNull = bAssert + ScriptForge.SF_Utils._ExitFunction(cstThisSub) + Exit Function +End Function ' SFUnitTests.SF_UnitTest.AssertIsNull + +REM ----------------------------------------------------------------------------- +Public Function AssertLess(Optional ByRef A As Variant _ + , Optional ByRef B As Variant _ + , Optional ByVal Message As Variant _ + ) As Boolean +''' Returns True when A is less than B. +''' To compare A and B: +''' They should have the same VarType or both be numeric +''' Elgible datatypes are String, Date or numeric. +''' String comparisons are case-sensitive. + +Dim bAssert As Boolean ' Return value +Const cstThisSub = "UnitTest.AssertLess" +Const cstSubArgs = "A, B, [Message=""""]" + +Check: + If IsMissing(A) Then A = Empty + If IsMissing(B) Then B = Empty + If IsMissing(Message) Then Message = "" + ScriptForge.SF_Utils._EnterFunction(cstThisSub, cstSubArgs) ' Unconditional ! + +Try: + bAssert = _Assert("AssertLess", False, A, B, Message) + +Finally: + AssertLess = bAssert + ScriptForge.SF_Utils._ExitFunction(cstThisSub) + Exit Function +End Function ' SFUnitTests.SF_UnitTest.AssertLess + +REM ----------------------------------------------------------------------------- +Public Function AssertLessEqual(Optional ByRef A As Variant _ + , Optional ByRef B As Variant _ + , Optional ByVal Message As Variant _ + ) As Boolean +''' Returns True when A is less than or equal to B. +''' To compare A and B: +''' They should have the same VarType or both be numeric +''' Elgible datatypes are String, Date or numeric. +''' String comparisons are case-sensitive. + +Dim bAssert As Boolean ' Return value +Const cstThisSub = "UnitTest.AssertLessEqual" +Const cstSubArgs = "A, B, [Message=""""]" + +Check: + If IsMissing(A) Then A = Empty + If IsMissing(B) Then B = Empty + If IsMissing(Message) Then Message = "" + ScriptForge.SF_Utils._EnterFunction(cstThisSub, cstSubArgs) ' Unconditional ! + +Try: + bAssert = _Assert("AssertLessEqual", False, A, B, Message) + +Finally: + AssertLessEqual = bAssert + ScriptForge.SF_Utils._ExitFunction(cstThisSub) + Exit Function +End Function ' SFUnitTests.SF_UnitTest.AssertLessEqual + +REM ----------------------------------------------------------------------------- +Public Function AssertLike(Optional ByRef A As Variant _ + , Optional ByRef Pattern As Variant _ + , Optional ByVal Message As Variant _ + ) As Boolean +''' Returns True if string A matches a given pattern containing wildcards +''' Admitted wildcard are: the "?" represents any single character +''' the "*" represents zero, one, or multiple characters +''' The comparison is case-sensitive. + +Dim bAssert As Boolean ' Return value +Const cstThisSub = "UnitTest.AssertLike" +Const cstSubArgs = "A, Pattern, [Message=""""]" + +Check: + If IsMissing(A) Then A = Empty + If IsMissing(Pattern) Then Pattern = "" + If IsMissing(Message) Then Message = "" + ScriptForge.SF_Utils._EnterFunction(cstThisSub, cstSubArgs) ' Unconditional ! + If Not ScriptForge.SF_Utils._Validate(Pattern, "Pattern", V_STRING) Then GoTo Catch + +Try: + bAssert = _Assert("AssertLike", True, A, Empty, Message, Pattern) + +Finally: + AssertLike = bAssert + ScriptForge.SF_Utils._ExitFunction(cstThisSub) + Exit Function +Catch: + bAssert = False + GoTo Finally +End Function ' SFUnitTests.SF_UnitTest.AssertLike + +REM ----------------------------------------------------------------------------- +Public Function AssertNotAlmostEqual(Optional ByRef A As Variant _ + , Optional ByRef B As Variant _ + , Optional ByVal Tolerance As Variant _ + , Optional ByVal Message As Variant _ + ) As Boolean +''' Returns True when A and B are numerical values and are not found close to each other. +''' Read about almost equality in the comments linked to the AssertEqual() method. + +Dim bAssert As Boolean ' Return value +Const cstTolerance = 1E-09 +Const cstThisSub = "UnitTest.AssertNotAlmostEqual" +Const cstSubArgs = "A, B, [Tolerance=1E-09], [Message=""""]" + +Check: + If IsMissing(A) Then A = Empty + If IsMissing(B) Then B = Empty + If IsMissing(Tolerance) Then Tolerance = cstTolerance + If IsMissing(Message) Then Message = "" + ScriptForge.SF_Utils._EnterFunction(cstThisSub, cstSubArgs) ' Unconditional ! + If Not ScriptForge.SF_Utils._Validate(Tolerance, "Tolerance", ScriptForge.V_NUMERIC) Then GoTo Catch + +Try: + bAssert = _Assert("AssertNotAlmostEqual", False, A, B, Message, Tolerance) + +Finally: + AssertNotAlmostEqual = bAssert + ScriptForge.SF_Utils._ExitFunction(cstThisSub) + Exit Function +Catch: + bAssert = False + GoTo Finally +End Function ' SFUnitTests.SF_UnitTest.AssertNotAlmostEqual + +REM ----------------------------------------------------------------------------- +Public Function AssertNotEqual(Optional ByRef A As Variant _ + , Optional ByRef B As Variant _ + , Optional ByVal Message As Variant _ + ) As Boolean +''' Returns True when A and B are found unequal. +''' Read about equality in the comments linked to the AssertEqual() method. + +Dim bAssert As Boolean ' Return value +Const cstThisSub = "UnitTest.AssertNotEqual" +Const cstSubArgs = "A, B, [Message=""""]" + +Check: + If IsMissing(A) Then A = Empty + If IsMissing(B) Then B = Empty + If IsMissing(Message) Then Message = "" + ScriptForge.SF_Utils._EnterFunction(cstThisSub, cstSubArgs) ' Unconditional ! + +Try: + bAssert = _Assert("AssertNotEqual", False, A, B, Message) + +Finally: + AssertNotEqual = bAssert + ScriptForge.SF_Utils._ExitFunction(cstThisSub) + Exit Function +End Function ' SFUnitTests.SF_UnitTest.AssertNotEqual + +REM ----------------------------------------------------------------------------- +Public Function AssertNotIn(Optional ByRef A As Variant _ + , Optional ByRef B As Variant _ + , Optional ByVal Message As Variant _ + ) As Boolean +''' Returns True when A, a string, is not found within B +''' B may be a 1D array, a ScriptForge dictionary or a string. +''' When B is an array, A may be a date or a numeric value. +''' String comparisons are case-sensitive. + +Dim bAssert As Boolean ' Return value +Const cstThisSub = "UnitTest.AssertNotIn" +Const cstSubArgs = "A, B, [Message=""""]" + +Check: + If IsMissing(A) Then A = Empty + If IsMissing(B) Then B = Empty + If IsMissing(Message) Then Message = "" + ScriptForge.SF_Utils._EnterFunction(cstThisSub, cstSubArgs) ' Unconditional ! + +Try: + bAssert = _Assert("AssertNotIn", False, A, B, Message) + +Finally: + AssertNotIn = bAssert + ScriptForge.SF_Utils._ExitFunction(cstThisSub) + Exit Function +End Function ' SFUnitTests.SF_UnitTest.AssertNotIn + +REM ----------------------------------------------------------------------------- +Public Function AssertNotInstance(Optional ByRef A As Variant _ + , Optional ByRef ObjectType As Variant _ + , Optional ByVal Message As Variant _ + ) As Boolean +''' Returns True when A is an object instance of the class ObjectType or a variable of type ObjectType. +''' More details to be read under the AssertInstance() function. + +Dim bAssert As Boolean ' Return value +Const cstThisSub = "UnitTest.AssertNotInstance" +Const cstSubArgs = "A, ObjectType, [Message=""""]" + +Check: + If IsMissing(A) Then A = Empty + If IsMissing(ObjectType) Then ObjectType = Empty + If IsMissing(Message) Then Message = "" + ScriptForge.SF_Utils._EnterFunction(cstThisSub, cstSubArgs) ' Unconditional ! + If Not ScriptForge.SF_Utils._Validate(ObjectType, "ObjectType", V_STRING) Then GoTo Catch + +Try: + bAssert = _Assert("AssertNotInstance", False, A, Empty, Message, ObjectType) + +Finally: + AssertNotInstance = bAssert + ScriptForge.SF_Utils._ExitFunction(cstThisSub) + Exit Function +Catch: + bAssert = False + GoTo Finally +End Function ' SFUnitTests.SF_UnitTest.AssertNotInstance + +REM ----------------------------------------------------------------------------- +Public Function AssertNotLike(Optional ByRef A As Variant _ + , Optional ByRef Pattern As Variant _ + , Optional ByVal Message As Variant _ + ) As Boolean +''' Returns True if A is not a string or does not match a given pattern containing wildcards +''' Admitted wildcard are: the "?" represents any single character +''' the "*" represents zero, one, or multiple characters +''' The comparison is case-sensitive. + +Dim bAssert As Boolean ' Return value +Const cstThisSub = "UnitTest.AssertNotLike" +Const cstSubArgs = "A, Pattern, [Message=""""]" + +Check: + If IsMissing(A) Then A = Empty + If IsMissing(Pattern) Then Pattern = "" + If IsMissing(Message) Then Message = "" + ScriptForge.SF_Utils._EnterFunction(cstThisSub, cstSubArgs) ' Unconditional ! + If Not ScriptForge.SF_Utils._Validate(Pattern, "Pattern", V_STRING) Then GoTo Catch + +Try: + bAssert = _Assert("AssertNotLike", False, A, Empty, Message, Pattern) + +Finally: + AssertNotLike = bAssert + ScriptForge.SF_Utils._ExitFunction(cstThisSub) + Exit Function +Catch: + bAssert = False + GoTo Finally +End Function ' SFUnitTests.SF_UnitTest.AssertNotLike + +REM ----------------------------------------------------------------------------- +Public Function AssertNotNothing(Optional ByRef A As Variant _ + , Optional ByVal Message As Variant _ + ) As Boolean +''' Returns True except when A is an object that has the Nothing value + +Dim bAssert As Boolean ' Return value +Const cstThisSub = "UnitTest.AssertNotNothing" +Const cstSubArgs = "A, [Message=""""]" + +Check: + If IsMissing(A) Then A = Empty + If IsMissing(Message) Then Message = "" + ScriptForge.SF_Utils._EnterFunction(cstThisSub, cstSubArgs) ' Unconditional ! + +Try: + bAssert = _Assert("AssertNotNothing", False, A, Empty, Message) + +Finally: + AssertNotNothing = bAssert + ScriptForge.SF_Utils._ExitFunction(cstThisSub) + Exit Function +End Function ' SFUnitTests.SF_UnitTest.AssertNotNothing + +REM ----------------------------------------------------------------------------- +Public Function AssertNotNull(Optional ByRef A As Variant _ + , Optional ByVal Message As Variant _ + ) As Boolean +''' Returns True except when A has the Null value + +Dim bAssert As Boolean ' Return value +Const cstThisSub = "UnitTest.AssertNotNull" +Const cstSubArgs = "A, [Message=""""]" + +Check: + If IsMissing(A) Then A = Empty + If IsMissing(Message) Then Message = "" + ScriptForge.SF_Utils._EnterFunction(cstThisSub, cstSubArgs) ' Unconditional ! + +Try: + bAssert = _Assert("AssertNotNull", False, A, Empty, Message) + +Finally: + AssertNotNull = bAssert + ScriptForge.SF_Utils._ExitFunction(cstThisSub) + Exit Function +End Function ' SFUnitTests.SF_UnitTest.AssertNotNull + +REM ----------------------------------------------------------------------------- +Public Function AssertNotRegex(Optional ByRef A As Variant _ + , Optional ByRef Regex As Variant _ + , Optional ByVal Message As Variant _ + ) As Boolean +''' Returns True when A is not a string or does not match the given regular expression. +''' The comparison is case-sensitive. + +Dim bAssert As Boolean ' Return value +Const cstThisSub = "UnitTest.AssertNotRegex" +Const cstSubArgs = "A, Regex, [Message=""""]" + +Check: + If IsMissing(A) Then A = Empty + If IsMissing(Regex) Then Regex = "" + If IsMissing(Message) Then Message = "" + ScriptForge.SF_Utils._EnterFunction(cstThisSub, cstSubArgs) ' Unconditional ! + If Not ScriptForge.SF_Utils._Validate(Regex, "Regex", V_STRING) Then GoTo Catch + +Try: + bAssert = _Assert("AssertNotRegex", False, A, Empty, Message, Regex) + +Finally: + AssertNotRegex = bAssert + ScriptForge.SF_Utils._ExitFunction(cstThisSub) + Exit Function +Catch: + bAssert = False + GoTo Finally +End Function ' SFUnitTests.SF_UnitTest.AssertNotRegex + +REM ----------------------------------------------------------------------------- +Public Function AssertRegex(Optional ByRef A As Variant _ + , Optional ByRef Regex As Variant _ + , Optional ByVal Message As Variant _ + ) As Boolean +''' Returns True when string A matches the given regular expression. +''' The comparison is case-sensitive. + +Dim bAssert As Boolean ' Return value +Const cstThisSub = "UnitTest.AssertRegex" +Const cstSubArgs = "A, Regex, [Message=""""]" + +Check: + If IsMissing(A) Then A = Empty + If IsMissing(Regex) Then Regex = "" + If IsMissing(Message) Then Message = "" + ScriptForge.SF_Utils._EnterFunction(cstThisSub, cstSubArgs) ' Unconditional ! + If Not ScriptForge.SF_Utils._Validate(Regex, "Regex", V_STRING) Then GoTo Catch + +Try: + bAssert = _Assert("AssertRegex", True, A, Empty, Message, Regex) + +Finally: + AssertRegex = bAssert + ScriptForge.SF_Utils._ExitFunction(cstThisSub) + Exit Function +Catch: + bAssert = False + GoTo Finally +End Function ' SFUnitTests.SF_UnitTest.AssertRegex + +REM ----------------------------------------------------------------------------- +Public Function AssertTrue(Optional ByRef A As Variant _ + , Optional ByVal Message As Variant _ + ) As Boolean +''' Returns True when A is a Boolean and its value is True + +Dim bAssert As Boolean ' Return value +Const cstThisSub = "UnitTest.AssertTrue" +Const cstSubArgs = "A, [Message=""""]" + +Check: + If IsMissing(A) Then A = Empty + If IsMissing(Message) Then Message = "" + ScriptForge.SF_Utils._EnterFunction(cstThisSub, cstSubArgs) ' Unconditional ! + +Try: + bAssert = _Assert("AssertTrue", True, A, Empty, Message) + +Finally: + AssertTrue = bAssert + ScriptForge.SF_Utils._ExitFunction(cstThisSub) + Exit Function +End Function ' SFUnitTests.SF_UnitTest.AssertTrue + +REM ----------------------------------------------------------------------------- +Public Sub Fail(Optional ByVal Message As Variant) +''' Forces a test failure +''' Args: + + +Dim bAssert As Boolean ' Fictive return value +Const cstThisSub = "UnitTest.Fail" +Const cstSubArgs = "[Message=""""]" + +Check: + If IsMissing(Message) Then Message = "" + ScriptForge.SF_Utils._EnterFunction(cstThisSub, cstSubArgs) ' Unconditional ! + +Try: + bAssert = _Assert("Fail", False, Empty, Empty, Message) + +Finally: + ScriptForge.SF_Utils._ExitFunction(cstThisSub) + Exit Sub +End Sub ' SFUnitTests.SF_UnitTest.Fail + +REM ----------------------------------------------------------------------------- +Public Sub Log(Optional ByVal Message As Variant) +''' Forces a test Logure + +Dim bAssert As Boolean ' Fictive return value +Dim bVerbose As Boolean : bVerbose = _Verbose +Const cstThisSub = "UnitTest.Log" +Const cstSubArgs = "[Message=""""]" + +Check: + If IsMissing(Message) Then Message = "" + ScriptForge.SF_Utils._EnterFunction(cstThisSub, cstSubArgs) ' Unconditional ! + +Try: + ' Force the display of the message in the console + _Verbose = True + bAssert = _Assert("Log", True, Empty, Empty, Message) + _Verbose = bVerbose + +Finally: + ScriptForge.SF_Utils._ExitFunction(cstThisSub) + Exit Sub +End Sub ' SFUnitTests.SF_UnitTest.Log + +REM ----------------------------------------------------------------------------- +Public Function GetProperty(Optional ByVal PropertyName As Variant) As Variant +''' Return the actual value of the given property +''' Args: +''' PropertyName: the name of the property as a string +''' Returns: +''' The actual value of the property +''' Exceptions +''' ARGUMENTERROR The property does not exist +''' Examples: +''' myUnitTest.GetProperty("Duration") + +Const cstThisSub = "UnitTest.GetProperty" +Const cstSubArgs = "PropertyName" + + If ScriptForge.SF_Utils._ErrorHandling() Then On Local Error GoTo Catch + GetProperty = Null + +Check: + If ScriptForge.SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then + If Not ScriptForge.SF_Utils._Validate(PropertyName, "PropertyName", V_STRING, Properties()) Then GoTo Catch + End If + +Try: + GetProperty = _PropertyGet(PropertyName) + +Finally: + ScriptForge.SF_Utils._ExitFunction(cstThisSub) + Exit Function +Catch: + GoTo Finally +End Function ' SFUnitTests.SF_UnitTest.Properties + +REM ----------------------------------------------------------------------------- +Public Function Methods() As Variant +''' Return the list or methods of the UnitTest class as an array + + Methods = Array( _ + "AssertAlmostEqual" _ + , "AssertEqual" _ + , "AssertFalse" _ + , "AssertGreater" _ + , "AssertGreaterEqual" _ + , "AssertIn" _ + , "AssertIsInstance" _ + , "AssertIsNothing" _ + , "AssertLike" _ + , "AssertNotRegex" _ + , "AssertIsNull" _ + , "AssertLess" _ + , "AssertLessEqual" _ + , "AssertNotAlmostEqual" _ + , "AssertNotEqual" _ + , "AssertNotIn" _ + , "AssertNotInstance" _ + , "AssertNotLike" _ + , "AssertNotNothing" _ + , "AssertNotNull" _ + , "AssertRegex" _ + , "AssertTrue" _ + , "Fail" _ + , "Log" _ + , "RunTest" _ + , "SkipTest" _ + ) + +End Function ' SFUnitTests.SF_UnitTest.Methods + +REM ----------------------------------------------------------------------------- +Public Function Properties() As Variant +''' Return the list or properties of the UnitTest class as an array + + Properties = Array( _ + "LongMessage" _ + , "ReturnCode" _ + , "Verbose" _ + , "WhenAssertionFails" _ + ) + +End Function ' SFUnitTests.SF_UnitTest.Properties + +REM ----------------------------------------------------------------------------- +Public Sub ReportError(Optional ByVal Message As Variant) +''' DIsplay a message box with the current property values of the "Exception" service. +''' Depending on the WhenAssertionFails property, a Raise() or RaiseWarning() +''' is issued. The Raise() method stops completely the Basic running process. +''' The ReportError() method is presumed present in a user script in an error +''' handling part of the actual testcase. +''' Args: +''' Message: a string to replace or to complete the standard message description +''' Example: +''' See the Test_ArraySize() sub in the module's heading example + +Dim sLine As String ' Line number where the error occurred +Dim sError As String ' Exception description +Dim sErrorCode As String ' Exception number +Const cstThisSub = "UnitTest.ReportError" +Const cstSubArgs = "[Message=""""]" + +Check: + If IsMissing(Message) Or IsEmpty(Message) Then Message = "" + ScriptForge.SF_Utils._EnterFunction(cstThisSub, cstSubArgs) ' Unconditional ! + If VarType(Message) <> V_STRING Then Message = "" + +Try: + sLine = "ln " & CStr(Exception.Source) + If _ExecutionMode = FULLMODE Then sLine = _Module & "." & _TestCase & " " & sLine + If Len(Message) > 0 Then + sError = Message + Else + If Exception.Number = INVALIDPROCEDURECALL Then + sError = "Test case failure" + sErrorCode = "ASSERTIONFAILED" + Else + sError = Exception.Description + sErrorCode = CStr(Exception.Number) + End If + End If + + Select Case _WhenAssertionFails + Case FAILIGNORE + Case FAILSTOPSUITE + Exception.RaiseWarning(sErrorCode, sLine, sError) + Case FAILIMMEDIATESTOP + Exception.Raise(sErrorCode, sLine, sError) + End Select + +Finally: + ScriptForge.SF_Utils._ExitFunction(cstThisSub) + Exit Sub +End Sub ' SFUnitTests.SF_UnitTest.ReportError +REM ----------------------------------------------------------------------------- +Public Function RunTest(Optional ByVal TestSuite As Variant _ + , Optional ByVal TestCasePattern As Variant _ + , Optional ByVal Message As Variant _ + ) As Integer +''' Execute a test suite pointed out by a module name. +''' Each test case will be run independently from each other. +''' The names of the test cases to be run may be selected with a string pattern. +''' The test is "orchestrated" by this method: +''' 1. Execute the optional Setup() method present in the module +''' 2. Execute once each test case, in any order +''' 3, Execute the optional TearDown() method present in the module +''' Args: +''' TestSuite: the name of the module containing the set of test cases to run +''' TestCasePattern: the pattern that the test cases must match. The comparison is not case-sensitive. +''' Non-matching finctions and subs are ignored. +''' Admitted wildcard are: the "?" represents any single character +''' the "*" represents zero, one, or multiple characters +''' The default pattern is "Test_*" +''' Message: the message to be displayed in the console when the test starts. +''' Returns: +''' The return code of the esecution (RCxxx constants +''' Examples: +''' GlobalScope.BasicLibraries.loadLibrary("ScriptForge") +''' Dim test : test = CreateScriptService("UnitTest", ThisComponent, "Tests") +''' test.RunTest("AllTests") ' AllTests is a module name ; test cases are named "Test_*" (default) + +Dim iRun As Integer ' Return value +Dim sRunMessage As String ' Reporting +Dim iModule As Integer ' Index of module currently running +Dim vMethods As Variant ' Set of methods +Dim sMethod As String ' A single method +Dim iMethod As Integer ' Index in MethodNames +Dim m As Integer + +Const cstThisSub = "UnitTest.RunTest" +Const cstSubArgs = "TestSuite, [TestCasePattern=""Test_*""], [Message=""""]" + + iRun = RCNORMALEND + If ScriptForge.SF_Utils._ErrorHandling() Then On Local Error GoTo Catch + +Check: + If IsMissing(TestCasePattern) Or IsEmpty(TestCasePattern) Then TestCasePattern = "Test_*" + If IsMissing(Message) Or IsEmpty(Message) Then Message = "" + ScriptForge.SF_Utils._EnterFunction(cstThisSub, cstSubArgs) ' Unconditional ! + If Not ScriptForge.SF_Utils._Validate(TestSuite, "TestSuite", V_STRING, ModuleNames) Then GoTo Catch + If Not ScriptForge.SF_Utils._Validate(TestCasePattern, "TestCasePattern", V_STRING) Then GoTo Catch + If Not ScriptForge.SF_Utils._Validate(Message, "Message", V_STRING) Then GoTo Catch + + ' A RunTest() is forbidden inside a test suite or when simple mode + If _Status <> STATUSSTANDBY Or _ExecutionMode <> FULLMODE Then GoTo CatchMethod + + ' Ignore any call when an abnormal end has been encountered + If _ReturnCode = RCABORTTEST Then GoTo Catch + +Try: + iModule = ScriptForge.SF_Array.IndexOf(ModuleNames, TestSuite, CaseSensitive := False, SortOrder := "ASC") + _Module = ModuleNames(iModule) + + ' Start timer + If Not IsNull(SuiteTimer) Then SuiteTimer = SuiteTimer.Dispose() + Set SuiteTimer = CreateScriptService("ScriptForge.Timer", True) + + ' Report the start of a new test suite + sRunMessage = "RUNTEST ENTER testsuite='" & LibraryName & "." & _Module & "', pattern='" & TestCasePattern & "'" + _ReportMessage(sRunMessage, Message) + _Status = STATUSSUITESTARTED + + ' Collect all the methods of the module + If Modules(iModule).hasChildNodes() Then + vMethods = Modules(iModule).getChildNodes() + MethodNames = Array() + For m = 0 To UBound(vMethods) + sMethod = vMethods(m).getName() + MethodNames = ScriptForge.SF_Array.Append(MethodNames, sMethod) + Next m + End If + + ' Execute the Setup() method, if it exists + iMethod = ScriptForge.SF_Array.IndexOf(MethodNames, "Setup", CaseSensitive := False, SortOrder := "ASC") + If iMethod >= 0 Then + _TestCase = MethodNames(iMethod) ' _TestCase is used in ReportError() + If Not _ExecuteScript(_TestCase) Then GoTo Catch + End If + + ' Execute the test cases that match the pattern + For iMethod = 0 To UBound(MethodNames) + If _ReturnCode = RCSKIPTEST Or _ReturnCode = RCASSERTIONFAILED Then Exit For + sMethod = MethodNames(iMethod) + If ScriptForge.SF_String.IsLike(sMethod, TestCasePattern, CaseSensitive := False) Then + _TestCase = sMethod + ' Start timer + If Not IsNull(CaseTimer) Then CaseTimer = CaseTimer.Dispose() + Set CaseTimer = CreateScriptService("ScriptForge.Timer", True) + If Not _ExecuteScript(sMethod) Then GoTo Catch + CaseTimer.Terminate() + _TestCase = "" + End If + Next iMethod + + If _ReturnCode <> RCSKIPTEST Then + ' Execute the TearDown() method, if it exists + iMethod = ScriptForge.SF_Array.IndexOf(MethodNames, "TearDown", CaseSensitive := False, SortOrder := "ASC") + If iMethod >= 0 Then + _TestCase = MethodNames(iMethod) ' _TestCase is used in ReportError() + If Not _ExecuteScript(_TestCase) Then GoTo Catch + End If + End If + + ' Report the end of the current test suite + sRunMessage = "RUNTEST EXIT testsuite='" & LibraryName & "." & _Module & "' " & _Duration("Suite", True) + _ReportMessage(sRunMessage, Message) + + ' Stop timer + SuiteTimer.Terminate() + + ' Housekeeping + MethodNames = Array() + _Module = "" + _Status = STATUSSTANDBY + +Finally: + _ReturnCode = iRun + RunTest = iRun + ScriptForge.SF_Utils._ExitFunction(cstThisSub) + Exit Function +Catch: + iRun = RCABORTTEST + GoTo Finally +CatchMethod: + ScriptForge.SF_Exception.RaiseFatal(UNITTESTMETHODERROR, "RunTest") + GoTo Catch +End Function ' SFUnitTests.SF_UnitTest.RunTest + +REM ----------------------------------------------------------------------------- +Public Function SetProperty(Optional ByVal PropertyName As Variant _ + , Optional ByRef Value As Variant _ + ) As Boolean +''' Set a new value to the given property +''' Args: +''' PropertyName: the name of the property as a string +''' Value: its new value +''' Exceptions +''' ARGUMENTERROR The property does not exist + +Const cstThisSub = "UnitTest.SetProperty" +Const cstSubArgs = "PropertyName, Value" + + If ScriptForge.SF_Utils._ErrorHandling() Then On Local Error GoTo Catch + SetProperty = False + +Check: + If ScriptForge.SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then + If Not ScriptForge.SF_Utils._Validate(PropertyName, "PropertyName", V_STRING, Properties()) Then GoTo Catch + End If + +Try: + SetProperty = _PropertySet(PropertyName, Value) + +Finally: + ScriptForge.SF_Utils._ExitFunction(cstThisSub) + Exit Function +Catch: + GoTo Finally +End Function ' SFUnitTests.SF_UnitTest.SetProperty + +REM ----------------------------------------------------------------------------- +Public Function SkipTest(Optional ByVal Message As Variant) As Boolean +''' Interrupt the running test suite. The TearDown() method is NOT executed. +''' The SkipTest() method is normally meaningful only in a Setup() method when not all the +''' conditions to run the test are met. +''' It is up to the Setup() script to exit shortly after the SkipTest() call.. +''' The method may also be executed in a test case. Next test cases will not be executed. +''' Remember however that the test cases are executed is an arbitrary order. +''' Args: +''' Message: the message to be displayed in the console +''' Returns: +''' The return code of the esecution (RCxxx constants +''' Examples: +''' GlobalScope.BasicLibraries.loadLibrary("ScriptForge") +''' Dim test : test = CreateScriptService("UnitTest", ThisComponent, "Tests") +''' test.SkipTest("AllTests") ' AllTests is a module name ; test cases are named "Test_*" (default) + +Dim bSkip As Boolean ' Return value +Dim sSkipMessage As String ' Reporting + +Const cstThisSub = "UnitTest.SkipTest" +Const cstSubArgs = "[Message=""""]" + + bSkip = False + If ScriptForge.SF_Utils._ErrorHandling() Then On Local Error GoTo Catch + +Check: + If IsMissing(Message) Or IsEmpty(Message) Then Message = "" + ScriptForge.SF_Utils._EnterFunction(cstThisSub, cstSubArgs) ' Unconditional ! + If Not ScriptForge.SF_Utils._Validate(Message, "Message", V_STRING) Then GoTo Catch + + ' A SkipTest() is forbidden when simple mode + If _ExecutionMode <> FULLMODE Then GoTo CatchMethod + + ' Ignore any call when an abnormal end has been encountered + If _ReturnCode = RCABORTTEST Then GoTo Catch + +Try: + If _Status = STATUSSETUP Or _Status = STATUSTESTCASE Then + _ReturnCode = RCSKIPTEST + bSkip = True + ' Exit message + sSkipMessage = _Duration("Test", True) & "SKIPTEST TESTSUITE='" & _Module & "'" + _ReportMessage(sSkipMessage, Message) + End If + +Finally: + SkipTest = bSkip + ScriptForge.SF_Utils._ExitFunction(cstThisSub) + Exit Function +Catch: + _ReturnCode = RCABORTTEST + GoTo Finally +CatchMethod: + ScriptForge.SF_Exception.RaiseFatal(UNITTESTMETHODERROR, "SkipTest") + GoTo Catch +End Function ' SFUnitTests.SF_UnitTest.SkipTest + +REM =========================================================== PRIVATE FUNCTIONS + +REM ----------------------------------------------------------------------------- +Private Function _Assert(ByVal psAssert As String _ + , ByVal pvReturn As Variant _ + , ByRef A As Variant _ + , ByRef B As Variant _ + , Optional ByVal pvMessage As Variant _ + , Optional ByVal pvArg As Variant _ + ) As Boolean +''' Evaluation of the assertion and management of the success or the failure +''' Args: +''' psAssert: the assertion verb as a string +''' pvReturn: may be True, False or Empty +''' When True (resp. False), the assertion must be evaluated as True (resp. False) +''' e.g. AssertEqual() will call _Assert("AssertEqual", True, ...) +''' AssertNotEqual() will call _Assert("AssertNotEqual", False, ...) +''' Empty may be used for recursive calls of the function (for comparing arrays, ...) +''' A: always present +''' B: may be empty +''' pvMessage: the message to display on the console +''' pvArg: optional additional argument of the assert function +''' Returns: +''' True when success + +Dim bAssert As Boolean ' Return value +Dim bEval As Boolean ' To be compared with pvReturn +Dim iVarTypeA As Integer ' Alias of _VarTypeExt(A) +Dim iVarTypeB As Integer ' Alias of _VarTypeExt(B) +Dim oVarTypeObjA As Object ' SF_Utils.ObjectDescriptor +Dim oVarTypeObjB As Object ' SF_Utils.ObjectDescriptor +Dim oUtils As Object : Set oUtils = ScriptForge.SF_Utils +Dim iDims As Integer ' Number of dimensions of array +Dim oAliasB As Object ' Alias of B to bypass the "Object variable not set" issue +Dim dblA As Double ' Alias of A +Dim dblB As Double ' Alias of B +Dim dblTolerance As Double ' Alias of pvArg +Dim oString As Object : Set oString = ScriptForge.SF_String +Dim sArgName As String ' Argument description +Dim i As Long, j As Long + +Check: + bAssert = False + If IsMissing(pvMessage) Then pvMessage = "" + If Not oUtils._Validate(pvMessage, "Message", V_STRING) Then GoTo Finally + If IsMissing(pvArg) Then pvArg = "" + +Try: + iVarTypeA = oUtils._VarTypeExt(A) + iVarTypeB = oUtils._VarTypeExt(B) + sArgName = "" + + Select Case UCase(psAssert) + Case UCase("AssertAlmostEqual"), UCase("AssertNotAlmostEqual") + bEval = ( iVarTypeA = iVarTypeB And iVarTypeA = ScriptForge.V_NUMERIC ) + If bEval Then + dblA = CDbl(A) + dblB = CDbl(B) + dblTolerance = Abs(CDbl(pvArg)) + bEval = ( Abs(dblA - dblB) <= (dblTolerance * Iif(Abs(dblA) > Abs(DblB), Abs(dblA), Abs(dblB))) ) + End If + Case UCase("AssertEqual"), UCase("AssertNotEqual") + If Not IsArray(A) Then + bEval = ( iVarTypeA = iVarTypeB ) + If bEval Then + Select Case iVarTypeA + Case V_EMPTY, V_NULL + Case V_STRING + bEval = ( StrComp(A, B, 1) = 0 ) + Case ScriptForge.V_NUMERIC, ScriptForge.V_BOOLEAN + bEval = ( A = B ) + Case V_DATE + bEval = ( Abs(DateDiff("s", A, B)) = 0 ) + Case ScriptForge.V_OBJECT + Set oVarTypeObjA = oUtils._VarTypeObj(A) + Set oVarTypeObjB = oUtils._VarTypeObj(B) + bEval = ( oVarTypeObjA.iVarType = oVarTypeObjB.iVarType ) + If bEval Then + Select Case oVarTypeObjA.iVarType + Case ScriptForge.V_NOTHING + Case ScriptForge.V_UNOOBJECT + bEval = EqualUnoObjects(A, B) + Case ScriptForge.V_SFOBJECT, ScriptForge.V_BASICOBJECT + bEval = False + End Select + End If + End Select + End If + Else ' Compare arrays + bEval = IsArray(B) + If bEval Then + iDims = ScriptForge.SF_Array.CountDims(A) + bEval = ( iDims = ScriptForge.SF_Array.CountDims(B) And iDims <= 2 ) + If bEval Then + Select Case iDims + Case -1, 0 ' Scalars (not possible) or empty arrays + Case 1 ' 1D array + bEval = ( LBound(A) = LBound(B) And UBound(A) = UBound(B) ) + If bEval Then + For i = LBound(A) To UBound(A) + bEval = _Assert(psAssert, Empty, A(i), B(i)) + If Not bEval Then Exit For + Next i + End If + Case 2 ' 2D array + bEval = ( LBound(A, 1) = LBound(B, 1) And UBound(A, 1) = UBound(B, 1) _ + And LBound(A, 2) = LBound(B, 2) And UBound(A, 2) = UBound(B, 2) ) + If bEval Then + For i = LBound(A, 1) To UBound(A, 1) + For j = LBound(A, 2) To UBound(A, 2) + bEval = _Assert(psAssert, Empty, A(i, j), B(i, j)) + If Not bEval Then Exit For + Next j + If Not bEval Then Exit For + Next i + End If + End Select + End If + End If + End If + Case UCase("AssertFalse") + If iVarTypeA = ScriptForge.V_BOOLEAN Then bEval = Not A Else bEval = False + Case UCase("AssertGreater"), UCase("AssertLessEqual") + bEval = ( iVarTypeA = iVarTypeB _ + And (iVarTypeA = ScriptForge.V_NUMERIC Or iVarTypeA = V_STRING Or iVarTypeA = V_DATE) ) + If bEval Then bEval = ( A > B ) + Case UCase("AssertGreaterEqual"), UCase("AssertLess") + bEval = ( iVarTypeA = iVarTypeB _ + And (iVarTypeA = ScriptForge.V_NUMERIC Or iVarTypeA = V_STRING Or iVarTypeA = V_DATE) ) + If bEval Then bEval = ( A >= B ) + Case UCase("AssertIn"), UCase("AssertNotIn") + Set oVarTypeObjB = oUtils._VarTypeObj(B) + Select Case True + Case iVarTypeA = V_STRING And iVarTypeB = V_STRING + bEval = ( Len(A) > 0 And Len(B) > 0 ) + If bEval Then bEval = ( InStr(1, B, A, 0) > 0 ) + Case (iVarTypeA = V_DATE Or iVarTypeA = V_STRING Or iVarTypeA = ScriptForge.V_NUMERIC) _ + And iVarTypeB >= ScriptForge.V_ARRAY + bEval = ( ScriptForge.SF_Array.CountDims(B) = 1 ) + If bEval Then bEval = ScriptForge.SF_Array.Contains(B, A, CaseSensitive := True) + Case oVarTypeObjB.iVarType = ScriptForge.V_SFOBJECT And oVarTypeObjB.sObjectType = "DICTIONARY" + bEval = ( Len(A) > 0 ) + If bEval Then + Set oAliasB = B + bEval = ScriptForge.SF_Array.Contains(oAliasB.Keys(), A, CaseSensitive := True) + End If + Case Else + bEval = False + End Select + Case UCase("AssertIsInstance"), UCase("AssertNotInstance") + Set oVarTypeObjA = oUtils._VarTypeObj(A) + sArgName = "ObjectType" + With oVarTypeObjA + Select Case .iVarType + Case ScriptForge.V_UNOOBJECT + bEval = ( pvArg = .sObjectType ) + Case ScriptForge.V_SFOBJECT + bEval = ( UCase(pvArg) = UCase(.sObjectType) Or UCase(pvArg) = "SF_" & UCase(.sObjectType) _ + Or UCase(pvArg) = UCase(.sServiceName) ) + Case ScriptForge.V_NOTHING, ScriptForge.V_BASICOBJECT + bEval = False + Case >= ScriptForge.V_ARRAY + bEval = ( UCase(pvArg) = "ARRAY" ) + Case Else + bEval = ( UCase(TypeName(A)) = UCase(pvArg) ) + End Select + End With + Case UCase("AssertIsNothing"), UCase("AssertNotNothing") + bEval = ( iVarTypeA = ScriptForge.V_OBJECT ) + If bEval Then bEval = ( A Is Nothing ) + Case UCase("AssertIsNull"), UCase("AssertNotNull") + bEval = ( iVarTypeA = V_NULL ) + Case UCase("AssertLike"), UCase("AssertNotLike") + sArgName = "Pattern" + bEval = ( iVarTypeA = V_STRING And Len(pvArg) > 0 ) + If bEval Then bEval = oString.IsLike(A, pvArg, CaseSensitive := True) + Case UCase("AssertRegex"), UCase("AssertNotRegex") + sArgName = "Regex" + bEval = ( iVarTypeA = V_STRING And Len(pvArg) > 0 ) + If bEval Then bEval = oString.IsRegex(A, pvArg, CaseSensitive := True) + Case UCase("AssertTrue") + If iVarTypeA = ScriptForge.V_BOOLEAN Then bEval = A Else bEval = False + Case UCase("FAIL"), UCase("Log") + bEval = True + Case Else + End Select + + ' Check the result of the assertion vs. what it should be + If IsEmpty(pvReturn) Then + bAssert = bEval ' Recursive call => Reporting and failure management are done by calling _Assert() procedure + Else ' pvReturn is Boolean => Call from user script + bAssert = Iif(pvReturn, bEval, Not bEval) + ' Report the assertion evaluation + If _Verbose Or Not bAssert Then + _ReportMessage(" " & psAssert _ + & Iif(IsEmpty(A), "", " = " & bAssert & ", A = " & oUtils._Repr(A)) _ + & Iif(IsEmpty(B), "", ", B = " & oUtils._Repr(B)) _ + & Iif(Len(sArgName) = 0, "", ", " & sArgName & " = " & pvArg) _ + , pvMessage) + End If + ' Manage assertion failure + If Not bAssert Then + _FailedAssert = psAssert + Select Case _WhenAssertionFails + Case FAILIGNORE ' Do nothing + Case Else + _ReturnCode = RCASSERTIONFAILED + ' Cause artificially a run-time error + Dim STRINGBADUSE As String + + '++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + '+ To avoid a run-time error on next executable statement, + + '+ insert an error handler in the code of your test case: + + '+ Like in next code: + + '+ On Local Error GoTo Catch + + '+ ... + + '+ Catch: + + '+ myTest.ReportError() + + '+ Exit Sub + + '++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + STRINGBADUSE = Right("", -1) ' Raises "#5 - Invalid procedure call" error + + End Select + End If + End If + +Finally: + _Assert = bAssert + Exit Function + +End Function ' SFUnitTests.SF_UnitTest._Assert + +REM ----------------------------------------------------------------------------- +Private Function _Duration(ByVal psTimer As String _ + , Optional ByVal pvBrackets As Variant _ + ) As String +''' Return the Duration property of the given timer +''' or the empty string if the timer is undefined or not started +''' Args: +''' psTimer: "Test", "Suite" or "TestCase" +''' pbBrackets: surround with brackets when True. Default = False + +Dim sDuration As String ' Return value +Dim oTimer As Object ' Alias of psTimer + +Check: + If IsMissing(pvBrackets) Or IsEmpty(pvBrackets) Then pvBrackets = False + +Try: + Select Case psTimer + Case "Test" : Set oTimer = TestTimer + Case "Suite" : Set oTimer = SuiteTimer + Case "TestCase", "Case" : Set oTimer = CaseTimer + End Select + If Not IsNull(oTimer) Then + sDuration = CStr(oTimer.Duration) & " " + If pvBrackets Then sDuration = "(" & Trim(sDuration) & " sec) " + Else + sDuration = "" + End If + +Finally: + _Duration = sDuration +End Function ' SFUnitTests.SF_UnitTest._Duration + +REM ----------------------------------------------------------------------------- +Private Function _ExecuteScript(psMethod As String) As Boolean +''' Run the given method and report start and stop +''' The targeted method is presumed not to return anything (Sub) +''' Args: +''' psMethod: the scope, the library and the module are predefined in the instance internals +''' Returns: +''' True when successful + +Dim bExecute As Boolean ' Return value +Dim sRun As String ' SETUP, TEARDOWN or TESTCASE + + On Local Error GoTo Catch + bExecute = True + +Try: + ' Set status before the effective execution + sRun = UCase(psMethod) + Select Case UCase(psMethod) + Case "SETUP" : _Status = STATUSSETUP + Case "TEARDOWN" : _Status = STATUSTEARDOWN + Case Else : _Status = STATUSTESTCASE + sRun = "TESTCASE" + End Select + + ' Report and execute + _ReportMessage(" " & sRun & " " & LibraryName & "." & _Module & "." & psMethod & "() ENTER") + Session.ExecuteBasicScript(Scope, LibraryName & "." & _Module & "." & psMethod, [Me]) + _ReportMessage(" " & sRun & " " & LibraryName & "." & _Module & "." & psMethod & "() EXIT" _ + & Iif(_STATUS = STATUSTESTCASE, " " & _Duration("Case", True), "")) + ' Reset status + _Status = STATUSSUITESTARTED + +Finally: + _ExecuteScript = bExecute + Exit Function +Catch: + bExecute = False + _ReturnCode = RCABORTTEST + GoTo Finally +End Function ' SFUnitTests.SF_UnitTest._ExecuteScript + +REM ----------------------------------------------------------------------------- +Private Function _PropertyGet(Optional ByVal psProperty As String) +''' Return the named property +''' Args: +''' psProperty: the name of the property + +Dim cstThisSub As String +Dim cstSubArgs As String + + cstThisSub = "UnitTest.get" & psProperty + cstSubArgs = "" + SF_Utils._EnterFunction(cstThisSub, cstSubArgs) + + Select Case UCase(psProperty) + Case UCase("LongMessage") + _PropertyGet = _LongMessage + Case UCase("ReturnCode") + _PropertyGet = _ReturnCode + Case UCase("Verbose") + _PropertyGet = _Verbose + Case UCase("WhenAssertionFails") + _PropertyGet = _WhenAssertionFails + Case Else + _PropertyGet = Null + End Select + +Finally: + SF_Utils._ExitFunction(cstThisSub) + Exit Function +End Function ' SFUnitTests.SF_UnitTest._PropertyGet + +REM ----------------------------------------------------------------------------- +Private Function _PropertySet(Optional ByVal psProperty As String _ + , Optional ByVal pvValue As Variant _ + ) As Boolean +''' Set the new value of the named property +''' Args: +''' psProperty: the name of the property +''' pvValue: the new value of the given property +''' Returns: +''' True if successful + +Dim bSet As Boolean ' Return value +Dim vWhenFailure As Variant ' WhenAssertionFails allowed values +Dim cstThisSub As String +Const cstSubArgs = "Value" + + If ScriptForge.SF_Utils._ErrorHandling() Then On Local Error GoTo Catch + bSet = False + + cstThisSub = "SFUnitTests.UnitTest.set" & psProperty + ScriptForge.SF_Utils._EnterFunction(cstThisSub, cstSubArgs) + + bSet = True + Select Case UCase(psProperty) + Case UCase("LongMessage") + If Not ScriptForge.SF_Utils._Validate(pvValue, "LongMessage", ScriptForge.V_BOOLEAN) Then GoTo Finally + _LongMessage = pvValue + Case UCase("Verbose") + If Not ScriptForge.SF_Utils._Validate(pvValue, "Verbose", ScriptForge.V_BOOLEAN) Then GoTo Finally + _Verbose = pvValue + Case UCase("WhenAssertionFails") + If _ExecutionMode = SIMPLEMODE Then vWhenFailure = Array(0, 3) Else vWhenFailure = Array(0, 1, 2, 3) + If Not ScriptForge.SF_Utils._Validate(pvValue, "WhenAssertionFails", ScriptForge.V_NUMERIC, vWhenFailure) Then GoTo Finally + _WhenAssertionFails = pvValue + Case Else + bSet = False + End Select + +Finally: + _PropertySet = bSet + ScriptForge.SF_Utils._ExitFunction(cstThisSub) + Exit Function +Catch: + GoTo Finally +End Function ' SFUnitTests.SF_UnitTest._PropertySet + +REM ----------------------------------------------------------------------------- +Private Function _ReportMessage(ByVal psSysMessage As String _ + , Optional ByVal pvMessage As Variant _ + ) As Boolean +''' Report in the console: +''' - either the standard message +''' - either the user message when not blank +''' - or both +''' Args: +''' psSysMessage: the standard message as built by the calling routine +''' psMessage: the message provided by the user script +''' Returns: +''' True when successful + +Dim bReport As Boolean ' Return value +Dim sIndent As String ' Indentation spaces + + bReport = False + On Local Error GoTo Catch + If IsMissing(pvMessage) Or IsEmpty(pvMessage) Then pvMessage = "" + +Try: + Select Case True + Case Len(pvMessage) = 0 + Exception.DebugPrint(psSysMessage) + Case _LongMessage + Exception.DebugPrint(psSysMessage, pvMessage) + Case Else + Select Case _Status + Case STATUSSTANDBY, STATUSSUITESTARTED : sIndent = "" + Case STATUSSUITESTARTED : sIndent = Space(2) + Case Else : sIndent = Space(4) + End Select + Exception.DebugPrint(sIndent & pvMessage) + End Select + +Finally: + _ReportMessage = bReport + Exit Function +Catch: + bReport = False + GoTo Finally +End Function ' SFUnitTests.SF_UnitTest._ReportMessage + +REM ----------------------------------------------------------------------------- +Private Function _Repr() As String +''' Convert the UnitTest instance to a readable string, typically for debugging purposes (DebugPrint ...) +''' Args: +''' Return: +''' "[UnitTest] + +Const cstUnitTest = "[UnitTest]" +Const cstMaxLength = 50 ' Maximum length for items + + _Repr = cstUnitTest + +End Function ' SFUnitTests.SF_UnitTest._Repr + +REM ============================================== END OF SFUNITTESTS.SF_UNITTEST + \ No newline at end of file diff --git a/wizards/source/sfunittests/__License.xba b/wizards/source/sfunittests/__License.xba new file mode 100644 index 000000000000..a8e6a7779c7b --- /dev/null +++ b/wizards/source/sfunittests/__License.xba @@ -0,0 +1,26 @@ + + + +''' Copyright 2019-2022 Jean-Pierre LEDURE, Rafael LIMA, Alain ROMEDENNE + +REM ======================================================================================================================= +REM === The ScriptForge library and its associated libraries are part of the LibreOffice project. === +REM === The SFUnitTests library is one of the associated libraries. === +REM === Full documentation is available on https://help.libreoffice.org/ === +REM ======================================================================================================================= + +''' ScriptForge is distributed in the hope that it will be useful, +''' but WITHOUT ANY WARRANTY; without even the implied warranty of +''' MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +''' ScriptForge is free software; you can redistribute it and/or modify it under the terms of either (at your option): + +''' 1) The Mozilla Public License, v. 2.0. If a copy of the MPL was not +''' distributed with this file, you can obtain one at http://mozilla.org/MPL/2.0/ . + +''' 2) The GNU Lesser General Public License as published by +''' the Free Software Foundation, either version 3 of the License, or +''' (at your option) any later version. If a copy of the LGPL was not +''' distributed with this file, see http://www.gnu.org/licenses/ . + + \ No newline at end of file diff --git a/wizards/source/sfunittests/dialog.xlb b/wizards/source/sfunittests/dialog.xlb new file mode 100644 index 000000000000..2d4a57045bb4 --- /dev/null +++ b/wizards/source/sfunittests/dialog.xlb @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/wizards/source/sfunittests/script.xlb b/wizards/source/sfunittests/script.xlb new file mode 100644 index 000000000000..3292dc12c1e7 --- /dev/null +++ b/wizards/source/sfunittests/script.xlb @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file