Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 158 additions & 31 deletions testing/Project/Sources/Classes/TestRunner.4dm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -62,38 +67,160 @@ 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)

Comment thread
KyleKincer marked this conversation as resolved.
// 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

Comment thread
coderabbitai[bot] marked this conversation as resolved.
// 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

Comment thread
coderabbitai[bot] marked this conversation as resolved.
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
If ($cacheObj.functions#Null:C1517)
This:C1470._functionCache:=$cacheObj.functions
Else
This:C1470._functionCache:=New object:C1471
End if
End if
End if

Comment thread
coderabbitai[bot] marked this conversation as resolved.
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._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(\
Expand Down
24 changes: 18 additions & 6 deletions testing/Project/Sources/Classes/_TestFunction.4dm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
KyleKincer marked this conversation as resolved.

Function run()
This:C1470.startTime:=Milliseconds:C459
Expand Down
88 changes: 80 additions & 8 deletions testing/Project/Sources/Classes/_TestRunnerTest.4dm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Expand All @@ -344,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")
$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")
Loading