diff --git a/README.md b/README.md index 4e16be3..7248396 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,9 @@ tool4d --project YourProject.4DProject --startup-method "test" --user-param "tes # Filter by tags tool4d --project YourProject.4DProject --startup-method "test" --user-param "tags=unit" tool4d --project YourProject.4DProject --startup-method "test" --user-param "tags=unit excludeTags=slow" + +# Force the runner to refresh cached discovery data (accepts true/1) +tool4d --project YourProject.4DProject --startup-method "test" --user-param "refreshCache=true" ``` ## Table-Driven Tests diff --git a/docs/guide.md b/docs/guide.md index 60f4437..6bcb738 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -78,8 +78,13 @@ tool4d --project YourProject.4DProject --startup-method "test" --user-param "exc # Combine multiple parameters tool4d --project YourProject.4DProject --startup-method "test" --user-param "format=json tags=unit,integration verbose=true" + +# Force the runner to ignore cached discovery data on this run +tool4d --project YourProject.4DProject --startup-method "test" --user-param "refreshCache=true" ``` +`refreshCache` accepts `true`/`false` (any casing) or `1`/`0` so you can script cache refreshes without clearing files manually. + ## Required Setup ### Creating the Startup Method diff --git a/testing/Project/Sources/Classes/TestRunner.4dm b/testing/Project/Sources/Classes/TestRunner.4dm index fb279e7..ee87e02 100644 --- a/testing/Project/Sources/Classes/TestRunner.4dm +++ b/testing/Project/Sources/Classes/TestRunner.4dm @@ -7,14 +7,19 @@ property testPatterns : Collection // Collection of test patterns to match property includeTags : Collection // Tags to include (OR logic) property excludeTags : Collection // Tags to exclude property requireAllTags : Collection // Tags that must all be present (AND logic) +property _cachedTestClasses : Collection // Cached collection of discovered test classes +property _classStoreSignature : Text // Signature of the class store for cache validation +property _functionCache : Object // Cache of test functions per class +property forceCacheRefresh : Boolean // Whether to ignore caches and recompute Class constructor($cs : 4D:C1709.Object) This:C1470.classStore:=$cs || cs:C1710 This:C1470.testSuites:=[] - This:C1470._initializeResults() - This:C1470._determineOutputFormat() - This:C1470._parseTestPatterns() - This:C1470._parseTagFilters() + This:C1470._initializeResults() + This:C1470._determineOutputFormat() + This:C1470._parseTestPatterns() + This:C1470._parseTagFilters() + This:C1470._parseCacheOptions() Function run() // Set up global error handler for the test run @@ -62,38 +67,182 @@ Function discoverTests() End for each Function _getTestClasses()->$classes : Collection - // Returns collection of 4D.Class - var $classStore : Object - $classStore:=This:C1470._getClassStore() - - return This:C1470._filterTestClasses($classStore) + // Returns collection of 4D.Class with caching persisted to disk + var $classStore : Object + var $classes : Collection + $classStore:=This:C1470._getClassStore() + + If (This:C1470.forceCacheRefresh) + This:C1470._cachedTestClasses:=Null:C1517 + This:C1470._classStoreSignature:="" + This:C1470._functionCache:=New object:C1471 + End if + + // Build a signature based on the current class names so we can detect changes + var $classNames : Collection + $classNames:=OB Keys:C1719($classStore) + // Ensure deterministic order so signature is stable across runs + $classNames.sort() + var $signature : Text + $signature:=JSON Stringify:C1217($classNames) + + // Try to load from disk cache if not already in memory + If (Not:C34(This:C1470.forceCacheRefresh)) && (This:C1470._cachedTestClasses=Null:C1517) + This:C1470._loadCache($signature; $classStore) + End if + + // If the class store hasn't changed and we have a cache, return it + If (Not:C34(This:C1470.forceCacheRefresh)) && ($signature=This:C1470._classStoreSignature) && (This:C1470._cachedTestClasses#Null:C1517) + return This:C1470._cachedTestClasses + End if + + // Otherwise, compute and store the cache + $classes:=This:C1470._filterTestClasses($classStore; $classNames) + This:C1470._cachedTestClasses:=$classes + This:C1470._classStoreSignature:=$signature + This:C1470._saveCache() + + return $classes Function _getClassStore() : Object // Extracted method to make testing easier - can be mocked return This:C1470.classStore -Function _filterTestClasses($classStore : Object) : Collection - var $classes : Collection - $classes:=[] - var $className : Text - For each ($className; $classStore) - // Skip classes without superclass property (malformed classes) - If ($classStore[$className].superclass=Null:C1517) - continue - End if - - // Skip Dataclasses for now - If ($classStore[$className].superclass.name="DataClass") - continue - End if - - // Test classes end with "Test", e.g. "MyClassTest" - If ($className="@Test") - $classes.push($classStore[$className]) - End if - End for each - - return $classes +Function _filterTestClasses($classStore : Object; $classNames : Collection) : Collection + var $classes : Collection + $classes:=[] + var $className : Text + var $classInfo : Object + If ($classNames=Null:C1517) + $classNames:=OB Keys:C1719($classStore) + End if + For each ($className; $classNames) + $classInfo:=$classStore[$className] + // Skip missing or malformed entries + If ($classInfo=Null:C1517) + continue + End if + // Skip classes without superclass property (malformed classes) + If ($classInfo.superclass=Null:C1517) + continue + End if + + // Skip Dataclasses for now + If ($classInfo.superclass.name="DataClass") + continue + End if + + // Test classes end with "Test", e.g. "MyClassTest" + If (This:C1470._matchesPattern($className; "*Test")) + $classes.push($classInfo) + End if + End for each + + return $classes + +Function _cacheFile()->$file : 4D:C1709.File + // Cache stored in DerivedData so it persists across runs but isn't versioned + return Folder:C1567(fk database folder:K87:14).folder("Project").folder("DerivedData").file("testClassCache.json") + +Function _loadCache($signature : Text; $classStore : Object) + If (This:C1470.forceCacheRefresh) + return + End if + var $cacheFile : 4D:C1709.File + $cacheFile:=This:C1470._cacheFile() + If ($cacheFile.exists) + var $cacheText : Text + $cacheText:=$cacheFile.getText("UTF-8") + var $cacheObj : Object + $cacheObj:=JSON Parse:C1218($cacheText) + If ($cacheObj#Null:C1517) && ($cacheObj.signature=$signature) + var $classes : Collection + $classes:=This:C1470._filterTestClasses($classStore; $cacheObj.classes) + This:C1470._cachedTestClasses:=$classes + This:C1470._classStoreSignature:=$signature + If ($cacheObj.functions#Null:C1517) + This:C1470._functionCache:=$cacheObj.functions + Else + This:C1470._functionCache:=New object:C1471 + End if + End if + End if + +Function _saveCache() + var $cacheFile : 4D:C1709.File + $cacheFile:=This:C1470._cacheFile() + var $parent : 4D:C1709.Folder + $parent:=$cacheFile.parent + If (Not:C34($parent.exists)) + $parent.create() + End if + var $names : Collection + $names:=[] + var $classInfo : 4D:C1709.Class + For each ($classInfo; This:C1470._cachedTestClasses) + $names.push($classInfo.name) + End for each + var $cacheObj : Object + $cacheObj:=New object:C1471("signature"; This:C1470._classStoreSignature; "classes"; $names; "functions"; This:C1470._getFunctionCache()) + $cacheFile.setText(JSON Stringify:C1217($cacheObj); "UTF-8") + +Function _parseCacheOptions() + var $params : Object + $params:=This:C1470._parseUserParams() + This:C1470.forceCacheRefresh:=($params.refreshCache="1") || ($params.refreshCache="true") + +Function _getFunctionCache() : Object + If (This:C1470.forceCacheRefresh) + If (This:C1470._functionCache=Null:C1517) + This:C1470._functionCache:=New object:C1471 + End if + return This:C1470._functionCache + End if + + If (This:C1470._functionCache=Null:C1517) + // Ensure cache is loaded from disk if available + If (This:C1470._cachedTestClasses=Null:C1517) + This:C1470._getTestClasses() + Else + This:C1470._loadCache(This:C1470._classStoreSignature; This:C1470._getClassStore()) + End if + If (This:C1470._functionCache=Null:C1517) + This:C1470._functionCache:=New object:C1471 + End if + End if + return This:C1470._functionCache + +Function _classFileSignature($className : Text) : Text + var $file : 4D:C1709.File + $file:=Folder:C1567(fk database folder:K87:14).folder("Project").folder("Sources").folder("Classes").file($className+".4dm") + If ($file.exists) + return String:C10($file.modificationDate)+"-"+String:C10($file.modificationTime)+"-"+String:C10($file.size) + End if + return "" + +Function _getCachedFunctionsForClass($class : 4D:C1709.Class) : Collection + var $entry : Object + var $className : Text + $className:=$class.name + var $sig : Text + $sig:=This:C1470._classFileSignature($className) + $entry:=This:C1470._getFunctionCache()[$className] + If (Not:C34(This:C1470.forceCacheRefresh)) && ($entry#Null:C1517) && ($entry.signature=$sig) + return $entry.functions + End if + return Null:C1517 + +Function _updateFunctionCache($className : Text; $signature : Text; $testFunctions : Collection) + var $functions : Collection + $functions:=[] + var $tf : cs:C1710._TestFunction + For each ($tf; $testFunctions) + $functions.push(New object:C1471("name"; $tf.functionName; "tags"; $tf.tags; "useTransactions"; $tf.useTransactions)) + End for each + var $entry : Object + $entry:=New object:C1471("signature"; $signature; "functions"; $functions) + This:C1470._getFunctionCache()[$className]:=$entry + This:C1470._saveCache() Function _initializeResults() This:C1470.results:=New object:C1471(\ diff --git a/testing/Project/Sources/Classes/_TestFunction.4dm b/testing/Project/Sources/Classes/_TestFunction.4dm index 196f7df..be25dff 100644 --- a/testing/Project/Sources/Classes/_TestFunction.4dm +++ b/testing/Project/Sources/Classes/_TestFunction.4dm @@ -10,17 +10,29 @@ property skipped : Boolean property tags : Collection // Collection of tag strings property useTransactions : Boolean // Whether to auto-manage transactions for this test -Class constructor($class : 4D:C1709.Class; $classInstance : 4D:C1709.Object; $function : 4D:C1709.Function; $name : Text; $classCode : Text) - This:C1470.class:=$class - This:C1470.classInstance:=$classInstance - This:C1470.function:=$function +Class constructor($class : 4D:C1709.Class; $classInstance : 4D:C1709.Object; $function : 4D:C1709.Function; $name : Text; $classCode : Text; $tags : Collection; $useTransactions : Boolean) + This:C1470.class:=$class + This:C1470.classInstance:=$classInstance + This:C1470.function:=$function This:C1470.functionName:=$name This:C1470.t:=cs:C1710.Testing.new() This:C1470.t.classInstance:=$classInstance This:C1470.runtimeErrors:=[] This:C1470.skipped:=False:C215 - This:C1470.tags:=This:C1470._parseTags($classCode || "") - This:C1470.useTransactions:=This:C1470._shouldUseTransactions($classCode || "") + + var $paramCount : Integer + $paramCount:=Count parameters:C259 + If ($paramCount>=6) && ($tags#Null:C1517) + This:C1470.tags:=$tags + Else + This:C1470.tags:=This:C1470._parseTags($classCode || "") + End if + + If ($paramCount>=7) + This:C1470.useTransactions:=$useTransactions + Else + This:C1470.useTransactions:=This:C1470._shouldUseTransactions($classCode || "") + End if Function run() This:C1470.startTime:=Milliseconds:C459 diff --git a/testing/Project/Sources/Classes/_TestRunnerTest.4dm b/testing/Project/Sources/Classes/_TestRunnerTest.4dm index 2d8477f..7fc5130 100644 --- a/testing/Project/Sources/Classes/_TestRunnerTest.4dm +++ b/testing/Project/Sources/Classes/_TestRunnerTest.4dm @@ -75,7 +75,7 @@ Function test_filter_test_classes($t : cs:C1710.Testing) ) var $testClasses : Collection - $testClasses:=$runner._filterTestClasses($mockClassStore) + $testClasses:=$runner._filterTestClasses($mockClassStore; Null:C1517) // Should find ExampleTest and ErrorHandlingTest from the mock var $foundNames : Collection @@ -112,9 +112,35 @@ Function test_test_class_discovery($t : cs:C1710.Testing) // Verify that all returned classes have names ending with "Test" var $class : 4D:C1709.Class - For each ($class; $testClasses) - $t.assert.isTrue($t; $class.name="@Test"; "All discovered classes should end with 'Test', found: "+$class.name) - End for each + For each ($class; $testClasses) + $t.assert.isTrue($t; $class.name="@Test"; "All discovered classes should end with 'Test', found: "+$class.name) + End for each + +Function test_persistent_class_cache($t : cs:C1710.Testing) + + var $runner : cs:C1710.TestRunner + $runner:=cs:C1710.TestRunner.new() + + // Ensure cache file is removed before testing + var $cacheFile : 4D:C1709.File + $cacheFile:=$runner._cacheFile() + If ($cacheFile.exists) + $cacheFile.delete() + End if + + // First run should create the cache file + var $classes : Collection + $classes:=$runner._getTestClasses() + $t.assert.isTrue($t; $cacheFile.exists; "Cache file should be created on first run") + + // Clear in-memory cache to simulate fresh run + $runner._cachedTestClasses:=Null:C1517 + $runner._classStoreSignature:="" + + // Second run should load from disk and return same number of classes + var $classes2 : Collection + $classes2:=$runner._getTestClasses() + $t.assert.areEqual($t; $classes.length; $classes2.length; "Cache should provide same classes on subsequent runs") Function test_pattern_matching_exact($t : cs:C1710.Testing) @@ -239,7 +265,7 @@ Function test_filterTestClasses_comprehensive($t : cs:C1710.Testing) ) var $testClasses : Collection - $testClasses:=$runner._filterTestClasses($mockClassStore) + $testClasses:=$runner._filterTestClasses($mockClassStore; Null:C1517) // Should find all test classes (ExampleTest, ErrorHandlingTest, TestRunnerTest, ComprehensiveErrorTest) $t.assert.isTrue($t; $testClasses.length>=4; "Should find at least 4 test classes") @@ -283,7 +309,7 @@ Function test_dependency_injection_pattern($t : cs:C1710.Testing) var $emptyStore : Object $emptyStore:=New object:C1471 var $noClasses : Collection - $noClasses:=$runner._filterTestClasses($emptyStore) + $noClasses:=$runner._filterTestClasses($emptyStore; Null:C1517) $t.assert.areEqual($t; 0; $noClasses.length; "Should handle empty class store") Function test_pattern_matching_with_dependency_extraction($t : cs:C1710.Testing) @@ -323,7 +349,7 @@ Function test_error_handling_in_extracted_methods($t : cs:C1710.Testing) "ValidTest"; New object:C1471("name"; "ValidTest"; "superclass"; New object:C1471("name"; "Object"))\ ) var $filteredClasses : Collection - $filteredClasses:=$runner._filterTestClasses($malformedStore) + $filteredClasses:=$runner._filterTestClasses($malformedStore; Null:C1517) // Should find ValidTest but skip InvalidTest (missing superclass) $t.assert.areEqual($t; 1; $filteredClasses.length; "Should handle classes without superclass gracefully") $t.assert.areEqual($t; "ValidTest"; $filteredClasses[0].name; "Should include ValidTest") @@ -344,4 +370,116 @@ Function test_skip_tag_counts_as_skipped($t : cs:C1710.Testing) $t.assert.areEqual($t; 1; $results.totalTests; "Total should count skipped test") $t.assert.areEqual($t; 1; $results.skipped; "Skipped test should be counted") $t.assert.areEqual($t; 0; $results.failed; "Skipped test should not fail") - $t.assert.areEqual($t; 0; $results.passed; "Skipped test should not pass") \ No newline at end of file + $t.assert.areEqual($t; 0; $results.passed; "Skipped test should not pass") + +Function test_function_cache_persistence($t : cs:C1710.Testing) + + var $runner : cs:C1710.TestRunner + $runner:=cs:C1710.TestRunner.new() + + // Remove existing cache file + var $cacheFile : 4D:C1709.File + $cacheFile:=$runner._cacheFile() + If ($cacheFile.exists) + $cacheFile.delete() + End if + + // Build a suite to populate function cache + var $class : 4D:C1709.Class + $class:=cs:C1710._ExampleTest + var $suite : cs:C1710._TestSuite + $suite:=cs:C1710._TestSuite.new($class; "human"; []; $runner) + + $t.assert.isTrue($t; $cacheFile.exists; "Cache file should be created after suite discovery") + + // Clear in-memory cache and ensure functions load from disk + $runner._functionCache:=Null:C1517 + var $cached : Collection + $cached:=$runner._getCachedFunctionsForClass($class) + $t.assert.isNotNull($t; $cached; "Cached functions should be loaded from disk") + $t.assert.isTrue($t; $cached.length>0; "Cached functions should be present") + +Function test_function_cache_retains_unfiltered_functions_with_patterns($t : cs:C1710.Testing) + + var $runner : cs:C1710.TestRunner + $runner:=cs:C1710.TestRunner.new() + + var $cacheFile : 4D:C1709.File + $cacheFile:=$runner._cacheFile() + If ($cacheFile.exists) + $cacheFile.delete() + End if + + var $class : 4D:C1709.Class + $class:=cs:C1710._ExampleTest + + // Populate cache using a restrictive pattern filter + var $filteredSuite : cs:C1710._TestSuite + $filteredSuite:=cs:C1710._TestSuite.new($class; "human"; ["*areEqual*"]; $runner) + $t.assert.areEqual($t; 1; $filteredSuite.testFunctions.length; "Pattern filter should limit discovered tests") + + var $cachedAfterFilter : Collection + $cachedAfterFilter:=$runner._getCachedFunctionsForClass($class) + $t.assert.isNotNull($t; $cachedAfterFilter; "Cache should exist after filtered discovery") + + // A fresh runner without patterns should still see every test via the cache + var $runnerAll : cs:C1710.TestRunner + $runnerAll:=cs:C1710.TestRunner.new() + var $suiteAll : cs:C1710._TestSuite + $suiteAll:=cs:C1710._TestSuite.new($class; "human"; []; $runnerAll) + + $t.assert.isTrue($t; $suiteAll.testFunctions.length>$filteredSuite.testFunctions.length; "Unfiltered run should include more tests than filtered run") + $t.assert.areEqual($t; $suiteAll.testFunctions.length; $cachedAfterFilter.length; "Cache should retain all test functions even when initial discovery used patterns") + +Function test_function_cache_retains_unfiltered_functions_with_tag_filters($t : cs:C1710.Testing) + + var $runner : cs:C1710.TestRunner + $runner:=cs:C1710.TestRunner.new() + + var $cacheFile : 4D:C1709.File + $cacheFile:=$runner._cacheFile() + If ($cacheFile.exists) + $cacheFile.delete() + End if + + // Restrict discovery to tests tagged as "unit" + $runner.includeTags:=["unit"] + + var $class : 4D:C1709.Class + $class:=cs:C1710._TaggingExampleTest + + var $tagFilteredSuite : cs:C1710._TestSuite + $tagFilteredSuite:=cs:C1710._TestSuite.new($class; "human"; []; $runner) + $t.assert.areEqual($t; 3; $tagFilteredSuite.testFunctions.length; "Include tag filter should limit discovered tests") + + var $cachedAfterTags : Collection + $cachedAfterTags:=$runner._getCachedFunctionsForClass($class) + $t.assert.isNotNull($t; $cachedAfterTags; "Cache should exist after tag-filtered discovery") + + var $runnerNoTags : cs:C1710.TestRunner + $runnerNoTags:=cs:C1710.TestRunner.new() + $runnerNoTags.excludeTags:=[] + var $suiteAll : cs:C1710._TestSuite + $suiteAll:=cs:C1710._TestSuite.new($class; "human"; []; $runnerNoTags) + + $t.assert.isTrue($t; $suiteAll.testFunctions.length>$tagFilteredSuite.testFunctions.length; "Removing tag filters should expose additional tests") + $t.assert.areEqual($t; $suiteAll.testFunctions.length; $cachedAfterTags.length; "Cache should retain every test regardless of tag filters") + +Function test_force_cache_refresh_option($t : cs:C1710.Testing) + + var $runner : cs:C1710.TestRunner + $runner:=cs:C1710.TestRunner.new() + + var $class : 4D:C1709.Class + $class:=cs:C1710._ExampleTest + + // Populate cache + var $suite : cs:C1710._TestSuite + $suite:=cs:C1710._TestSuite.new($class; "human"; []; $runner) + + // Force refresh + $runner.forceCacheRefresh:=True:C214 + $runner._functionCache:=Null:C1517 + var $cached : Collection + $cached:=$runner._getCachedFunctionsForClass($class) + $t.assert.areEqual($t; Null:C1517; $cached; "Cache should be ignored when refresh is forced") \ No newline at end of file diff --git a/testing/Project/Sources/Classes/_TestSuite.4dm b/testing/Project/Sources/Classes/_TestSuite.4dm index 5e84aaf..ca53508 100644 --- a/testing/Project/Sources/Classes/_TestSuite.4dm +++ b/testing/Project/Sources/Classes/_TestSuite.4dm @@ -32,31 +32,73 @@ Function run() This:C1470._callTeardown() Function discoverTests() - var $testFunctions : Collection - $testFunctions:=This:C1470._getTestClassFunctions() - - // Get class source code once for all functions - var $classCode : Text - $classCode:=This:C1470._getClassCode() - - var $function : Object - For each ($function; $testFunctions) - // Filter individual test methods based on patterns - If (This:C1470._shouldIncludeTestMethod($function.name)) - var $testFunction : cs:C1710._TestFunction - $testFunction:=cs:C1710._TestFunction.new(This:C1470.class; This:C1470.classInstance; $function.function; $function.name; $classCode) - - // Apply tag filtering if TestRunner is available - If (This:C1470.testRunner#Null:C1517) - If (This:C1470.testRunner._shouldIncludeTestByTags($testFunction)) - This:C1470.testFunctions.push($testFunction) - End if - Else - // If no TestRunner reference, include all tests that pass pattern filtering - This:C1470.testFunctions.push($testFunction) - End if - End if - End for each + var $cachedFunctions : Collection + If (This:C1470.testRunner#Null:C1517) + $cachedFunctions:=This:C1470.testRunner._getCachedFunctionsForClass(This:C1470.class) + End if + + If ($cachedFunctions#Null:C1517) + var $info : Object + For each ($info; $cachedFunctions) + If (This:C1470._shouldIncludeTestMethod($info.name)) + var $func : 4D:C1709.Function + $func:=This:C1470.classInstance[$info.name] + If ($func#Null:C1517) && (OB Instance of:C1731($func; 4D:C1709.Function)) + var $cachedTest : cs:C1710._TestFunction + $cachedTest:=cs:C1710._TestFunction.new(This:C1470.class; This:C1470.classInstance; $func; $info.name; ""; $info.tags; $info.useTransactions) + If (This:C1470.testRunner#Null:C1517) + If (This:C1470.testRunner._shouldIncludeTestByTags($cachedTest)) + This:C1470.testFunctions.push($cachedTest) + End if + Else + This:C1470.testFunctions.push($cachedTest) + End if + End if + End if + End for each + return + End if + + var $testFunctions : Collection + $testFunctions:=This:C1470._getTestClassFunctions() + + // Get class source code once for all functions + var $classCode : Text + $classCode:=This:C1470._getClassCode() + + var $allWrappers : Collection + $allWrappers:=[] + + var $function : Object + For each ($function; $testFunctions) + var $testFunction : cs:C1710._TestFunction + $testFunction:=cs:C1710._TestFunction.new(This:C1470.class; This:C1470.classInstance; $function.function; $function.name; $classCode) + + // Always retain complete discovery set for caching + If ($allWrappers#Null:C1517) + $allWrappers.push($testFunction) + End if + + // Filter individual test methods based on patterns + If (This:C1470._shouldIncludeTestMethod($function.name)) + // Apply tag filtering if TestRunner is available + If (This:C1470.testRunner#Null:C1517) + If (This:C1470.testRunner._shouldIncludeTestByTags($testFunction)) + This:C1470.testFunctions.push($testFunction) + End if + Else + // If no TestRunner reference, include all tests that pass pattern filtering + This:C1470.testFunctions.push($testFunction) + End if + End if + End for each + + If ($allWrappers#Null:C1517) + var $sig : Text + $sig:=This:C1470.testRunner._classFileSignature(This:C1470.class.name) + // Cache the complete set of discovered test functions so filters don't drop entries + This:C1470.testRunner._updateFunctionCache(This:C1470.class.name; $sig; $allWrappers) + End if Function _getTestClassFunctions() : Collection // Returns collection of {function: 4D.Function; name: String}