Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
211 changes: 180 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,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(\
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

Function run()
This:C1470.startTime:=Milliseconds:C459
Expand Down
Loading