From 5226cc2684ce757955873dcea2a1e2a5046cd0f7 Mon Sep 17 00:00:00 2001 From: Kyle Kincer <69128842+KyleKincer@users.noreply.github.com> Date: Fri, 12 Sep 2025 15:36:39 -0400 Subject: [PATCH 1/7] feat: persist test class cache --- .../Project/Sources/Classes/TestRunner.4dm | 122 ++++++++++++++---- .../Sources/Classes/_TestRunnerTest.4dm | 40 +++++- 2 files changed, 128 insertions(+), 34 deletions(-) diff --git a/testing/Project/Sources/Classes/TestRunner.4dm b/testing/Project/Sources/Classes/TestRunner.4dm index fb279e7..21e9164 100644 --- a/testing/Project/Sources/Classes/TestRunner.4dm +++ b/testing/Project/Sources/Classes/TestRunner.4dm @@ -7,6 +7,8 @@ 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 Class constructor($cs : 4D:C1709.Object) This:C1470.classStore:=$cs || cs:C1710 @@ -62,38 +64,104 @@ 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() + + // Build a signature based on the current class names so we can detect changes + var $classNames : Collection + $classNames:=OB Keys:C1719($classStore) + var $signature : Text + $signature:=JSON Stringify:C1217($classNames) + + // Try to load from disk cache if not already in memory + If (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 ($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 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 ($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) + 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 + 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) + $cacheFile.setText(JSON Stringify:C1217($cacheObj); "UTF-8") Function _initializeResults() This:C1470.results:=New object:C1471(\ diff --git a/testing/Project/Sources/Classes/_TestRunnerTest.4dm b/testing/Project/Sources/Classes/_TestRunnerTest.4dm index 2d8477f..633f95a 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") From e639e04182d5612c2f90877d041471cf96a828ec Mon Sep 17 00:00:00 2001 From: Kyle Kincer <69128842+KyleKincer@users.noreply.github.com> Date: Fri, 12 Sep 2025 16:20:41 -0400 Subject: [PATCH 2/7] feat: cache test functions --- .../Project/Sources/Classes/TestRunner.4dm | 69 +++++++++++++-- .../Project/Sources/Classes/_TestFunction.4dm | 24 ++++-- .../Sources/Classes/_TestRunnerTest.4dm | 48 ++++++++++- .../Project/Sources/Classes/_TestSuite.4dm | 83 +++++++++++++------ 4 files changed, 187 insertions(+), 37 deletions(-) diff --git a/testing/Project/Sources/Classes/TestRunner.4dm b/testing/Project/Sources/Classes/TestRunner.4dm index 21e9164..fb19c45 100644 --- a/testing/Project/Sources/Classes/TestRunner.4dm +++ b/testing/Project/Sources/Classes/TestRunner.4dm @@ -9,14 +9,17 @@ 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 @@ -142,6 +145,11 @@ Function _loadCache($signature : Text; $classStore : Object) $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 @@ -160,8 +168,59 @@ Function _saveCache() $names.push($classInfo.name) End for each var $cacheObj : Object - $cacheObj:=New object:C1471("signature"; This:C1470._classStoreSignature; "classes"; $names) + $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._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 633f95a..1fb725f 100644 --- a/testing/Project/Sources/Classes/_TestRunnerTest.4dm +++ b/testing/Project/Sources/Classes/_TestRunnerTest.4dm @@ -370,4 +370,50 @@ 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_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..0a2fdbd 100644 --- a/testing/Project/Sources/Classes/_TestSuite.4dm +++ b/testing/Project/Sources/Classes/_TestSuite.4dm @@ -32,31 +32,64 @@ 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 $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 + + If (This:C1470.testRunner#Null:C1517) + var $sig : Text + $sig:=This:C1470.testRunner._classFileSignature(This:C1470.class.name) + This:C1470.testRunner._updateFunctionCache(This:C1470.class.name; $sig; This:C1470.testFunctions) + End if Function _getTestClassFunctions() : Collection // Returns collection of {function: 4D.Function; name: String} From cfcc3fa7d86b0d42ed8626c5fff4a23bd064dda1 Mon Sep 17 00:00:00 2001 From: Kyle Kincer <69128842+KyleKincer@users.noreply.github.com> Date: Tue, 16 Sep 2025 07:01:43 -0400 Subject: [PATCH 3/7] Sort class names before computing cache signature --- testing/Project/Sources/Classes/TestRunner.4dm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/testing/Project/Sources/Classes/TestRunner.4dm b/testing/Project/Sources/Classes/TestRunner.4dm index fb19c45..7b4e473 100644 --- a/testing/Project/Sources/Classes/TestRunner.4dm +++ b/testing/Project/Sources/Classes/TestRunner.4dm @@ -75,6 +75,8 @@ Function _getTestClasses()->$classes : Collection // 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) From 821280ab261129a952f2c182fddbc2d3e39a0154 Mon Sep 17 00:00:00 2001 From: Kyle Kincer <69128842+KyleKincer@users.noreply.github.com> Date: Tue, 16 Sep 2025 07:04:40 -0400 Subject: [PATCH 4/7] fix: guard cached class entries --- testing/Project/Sources/Classes/TestRunner.4dm | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/testing/Project/Sources/Classes/TestRunner.4dm b/testing/Project/Sources/Classes/TestRunner.4dm index 7b4e473..71f7583 100644 --- a/testing/Project/Sources/Classes/TestRunner.4dm +++ b/testing/Project/Sources/Classes/TestRunner.4dm @@ -112,6 +112,10 @@ Function _filterTestClasses($classStore : Object; $classNames : Collection) : Co 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 @@ -123,7 +127,7 @@ Function _filterTestClasses($classStore : Object; $classNames : Collection) : Co End if // Test classes end with "Test", e.g. "MyClassTest" - If ($className="@Test") + If (This:C1470._matchesPattern($className; "*Test")) $classes.push($classInfo) End if End for each From 07005029128b51f5b737285d7a1e5ad0675ef1b1 Mon Sep 17 00:00:00 2001 From: Kyle Kincer <69128842+KyleKincer@users.noreply.github.com> Date: Tue, 16 Sep 2025 07:10:36 -0400 Subject: [PATCH 5/7] fix: cache all discovered test functions --- testing/Project/Sources/Classes/_TestSuite.4dm | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/testing/Project/Sources/Classes/_TestSuite.4dm b/testing/Project/Sources/Classes/_TestSuite.4dm index 0a2fdbd..fd5dada 100644 --- a/testing/Project/Sources/Classes/_TestSuite.4dm +++ b/testing/Project/Sources/Classes/_TestSuite.4dm @@ -66,13 +66,17 @@ Function discoverTests() 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) + $allWrappers.push($testFunction) + // 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)) @@ -88,7 +92,8 @@ Function discoverTests() If (This:C1470.testRunner#Null:C1517) var $sig : Text $sig:=This:C1470.testRunner._classFileSignature(This:C1470.class.name) - This:C1470.testRunner._updateFunctionCache(This:C1470.class.name; $sig; This:C1470.testFunctions) + // 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 From e4670dcd9b08fca5e233a626e98d131bdfb49279 Mon Sep 17 00:00:00 2001 From: Kyle Kincer Date: Tue, 16 Sep 2025 07:42:40 -0400 Subject: [PATCH 6/7] feat: honor forceCacheRefresh in more cases. Cache all classes even with filters --- .../Project/Sources/Classes/TestRunner.4dm | 20 +++++- .../Sources/Classes/_TestRunnerTest.4dm | 66 +++++++++++++++++++ .../Project/Sources/Classes/_TestSuite.4dm | 8 ++- 3 files changed, 90 insertions(+), 4 deletions(-) diff --git a/testing/Project/Sources/Classes/TestRunner.4dm b/testing/Project/Sources/Classes/TestRunner.4dm index 71f7583..ee87e02 100644 --- a/testing/Project/Sources/Classes/TestRunner.4dm +++ b/testing/Project/Sources/Classes/TestRunner.4dm @@ -72,6 +72,12 @@ Function _getTestClasses()->$classes : Collection 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) @@ -81,12 +87,12 @@ Function _getTestClasses()->$classes : Collection $signature:=JSON Stringify:C1217($classNames) // Try to load from disk cache if not already in memory - If (This:C1470._cachedTestClasses=Null:C1517) + 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 ($signature=This:C1470._classStoreSignature) && (This:C1470._cachedTestClasses#Null:C1517) + If (Not:C34(This:C1470.forceCacheRefresh)) && ($signature=This:C1470._classStoreSignature) && (This:C1470._cachedTestClasses#Null:C1517) return This:C1470._cachedTestClasses End if @@ -139,6 +145,9 @@ Function _cacheFile()->$file : 4D:C1709.File 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) @@ -183,6 +192,13 @@ Function _parseCacheOptions() 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) diff --git a/testing/Project/Sources/Classes/_TestRunnerTest.4dm b/testing/Project/Sources/Classes/_TestRunnerTest.4dm index 1fb725f..7fc5130 100644 --- a/testing/Project/Sources/Classes/_TestRunnerTest.4dm +++ b/testing/Project/Sources/Classes/_TestRunnerTest.4dm @@ -399,6 +399,72 @@ Function test_function_cache_persistence($t : cs:C1710.Testing) $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 diff --git a/testing/Project/Sources/Classes/_TestSuite.4dm b/testing/Project/Sources/Classes/_TestSuite.4dm index fd5dada..ca53508 100644 --- a/testing/Project/Sources/Classes/_TestSuite.4dm +++ b/testing/Project/Sources/Classes/_TestSuite.4dm @@ -73,7 +73,11 @@ Function discoverTests() 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) - $allWrappers.push($testFunction) + + // 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)) @@ -89,7 +93,7 @@ Function discoverTests() End if End for each - If (This:C1470.testRunner#Null:C1517) + 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 From 5924620d9e0f1e747cce69e6cf637d2ec6956da0 Mon Sep 17 00:00:00 2001 From: Kyle Kincer <69128842+KyleKincer@users.noreply.github.com> Date: Tue, 16 Sep 2025 07:54:12 -0400 Subject: [PATCH 7/7] docs: document cache refresh user parameter --- README.md | 3 +++ docs/guide.md | 5 +++++ 2 files changed, 8 insertions(+) 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