Execution environment overview
Pharo provides an
execution environment
mechanism that allows injecting a context into a process. This context is automatically propagated to forked processes, enabling the environment to intercept and manage process forking. This is primarily used for testing, where it enables features like process monitoring, watchdog timeouts, and controlled cleanup of forked processes.
The execution environment system consists of these key classes:
- ExecutionEnvironment
Object << #ExecutionEnvironment
slots: {};
tag: 'Processes';
package: 'Kernel'
- Abstract base class representing an execution context for a process. Provides hooks like activated, deactivated, and prepareForNewProcess:.
- CurrentExecutionEnvironment
ProcessLocalVariable << #CurrentExecutionEnvironment
slots: {};
tag: 'Processes';
package: 'Kernel'
- A process-local variable that provides access to the current execution environment. Returns DefaultExecutionEnvironment
ExecutionEnvironment << #DefaultExecutionEnvironment
slots: {};
tag: 'Processes';
package: 'Kernel'
when nothing special is installed.
- DefaultExecutionEnvironment
ExecutionEnvironment << #DefaultExecutionEnvironment
slots: {};
tag: 'Processes';
package: 'Kernel'
- A singleton environment used when no special environment is active. Does nothing special for forked processes.
- TestExecutionEnvironment
ExecutionEnvironment << #TestExecutionEnvironment
slots: { #watchDogProcess . #watchDogSemaphore . #testCase . #maxTimeForTest . #testCompleted . #services . #mainTestProcess };
tag: 'Kernel';
package: 'SUnit-Core'
- A managed environment for running tests and examples with features like watchdog timeouts and process monitoring.
An execution environment is activated for a block using ExecutionEnvironment>>#beActiveDuring:
beActiveDuring: aBlock
CurrentExecutionEnvironment activate: self for: aBlock
, which delegates to CurrentExecutionEnvironment>>#activate:for:
activate: anExecutionEnvironment for: aBlock
| current |
self value == anExecutionEnvironment ifTrue: [ ^aBlock value ].
current := self soleInstance valueOrNil.
[ self value: anExecutionEnvironment.
anExecutionEnvironment activated.
aBlock value ] ensure: [
self value: current.
anExecutionEnvironment deactivated]
. This:
1. Stores the environment in the process-local variable
2. Calls activated on the environment
4. Restores the previous environment and calls deactivated
TestExecutionEnvironment new beActiveDuring: [
"Code runs with test environment active"
CurrentExecutionEnvironment value isTest "=> true" ]
When a new process is forked, CurrentExecutionEnvironment>>#installValue:intoForked:from:
installValue: anExecutionEnvironment intoForked: newProcess from: ownerProcess
super installValue: anExecutionEnvironment intoForked: newProcess from: ownerProcess.
anExecutionEnvironment prepareForNewProcess: newProcess
is called, which notifies the environment via ExecutionEnvironment>>#prepareForNewProcess:
prepareForNewProcess: aProcess
self subclassResponsibility
. This allows the test environment to track all forked processes and install exception handlers on them.
TestExecutionEnvironment
ExecutionEnvironment << #TestExecutionEnvironment
slots: { #watchDogProcess . #watchDogSemaphore . #testCase . #maxTimeForTest . #testCompleted . #services . #mainTestProcess };
tag: 'Kernel';
package: 'SUnit-Core'
provides:
- Watchdog: A high-priority process (TestExecutionEnvironment>>#startWatchDog
startWatchDog
watchDogSemaphore := Semaphore new.
watchDogProcess := [self watchDogLoop] newProcess.
"Watchdog needs to run at high priority to do its job (but not at timing priority)"
watchDogProcess
name: 'Tests execution watch dog';
priority: Processor timingPriority-1;
resume
) that monitors test execution time and signals TestTookTooMuchTime if the test exceeds its time limit.
- Services: An extensible service infrastructure (TestExecutionService
Object << #TestExecutionService
slots: { #executionEnvironment . #isEnabled };
tag: 'Kernel';
package: 'SUnit-Core'
) for monitoring test execution. The main service is ProcessMonitorTestService
TestExecutionService << #ProcessMonitorTestService
slots: { #forkedProcesses . #testFailures . #shouldSuspendBackgroundFailures . #shouldFailTestLeavingProcesses . #shouldTerminateProcesses };
tag: 'Kernel';
package: 'SUnit-Core'
which tracks forked processes and their exceptions.
- isTest: Returns true (via TestExecutionEnvironment>>#isTest
isTest
^true
) allowing code to check if running in a test context.
The test execution envionment is set in by the test suite in TestSuite>>#run:
run: aResult
CurrentExecutionEnvironment runTestsBy: [
self runWith: [ :test | test run: aResult ] ]
.
After that the suite delegates through the TestResult
Object << #TestResult
slots: { #timeStamp . #failures . #errors . #passed . #skipped };
tag: 'Kernel';
package: 'SUnit-Core'
using TestResult>>#runCase:
runCase: aTestCase
[
aTestCase announce: TestCaseStarted withResult: self.
aTestCase runCaseManaged.
aTestCase announce: TestCaseEnded withResult: self.
self addPass: aTestCase]
on: self class failure , self class skip, self class warning, self class error
do: [:ex | ex sunitAnnounce: aTestCase toResult: self]
.
The test result then calls TestCase>>#runCaseManaged
runCaseManaged
CurrentExecutionEnvironment runTestCase: self
on the test to allow the test to decide how it should be managed.
TestCase>>#runCase
runCase
self resources do: [ :each | each availableFor: self ].
[ [
self setUp.
self performTest ]
ensure: [ self tearDown ] ]
ensure: [ self cleanUpInstanceVariables ]
is then the method called from TestExecutionEnvironment>>#runTestCaseUnderWatchdog:
runTestCaseUnderWatchdog: aTestCase
[
[aTestCase runCase] ensure: [
"Terminated test is not considered as completed (user just closed a debugger for example)"
mainTestProcess isTerminating ifFalse: [
self handleCompletedTest ]]
] on: Exception do: [ :err |
self handleException: err
]
to execute the actual test.
For GT examples, there are two execution paths:
Managed execution (with watchdog): Used via CurrentExecutionEnvironment>>#runExampleEvaluator:
runExampleEvaluator: anExampleEvaluator
^ self value runExampleEvaluator: anExampleEvaluator
which calls TestExecutionEnvironment>>#runExampleEvaluator:
runExampleEvaluator: anExampleEvaluator
| exampleResult |
testCase := anExampleEvaluator.
maxTimeForTest := anExampleEvaluator timeLimit.
testCompleted := false.
watchDogSemaphore signal. "signal about new test case"
[ exampleResult := self runExampleUnderWatchdogUsingEvaluator: anExampleEvaluator] ensure: [
testCompleted := true.
watchDogSemaphore signal. "signal that test case is completed"
self cleanUpAfterTest ].
^ exampleResult
.
Unmanaged execution (no watchdog): Used via CurrentExecutionEnvironment>>#runUnmanagedExampleEvaluator:
runUnmanagedExampleEvaluator: anExampleEvaluator
^ self value runUnmanagedExampleEvaluator: anExampleEvaluator
which calls TestExecutionEnvironment>>#runUnmanagedExampleEvaluator:
runUnmanagedExampleEvaluator: anExampleEvaluator
| exampleResult |
testCase := anExampleEvaluator.
testCompleted := false.
[ exampleResult := anExampleEvaluator value ] ensure: [
testCompleted := true ].
^ exampleResult
. The test environment is still installed, but without the watchdog timeout checks.
When running from the command line (GtExamplesTestingHudsonReport>>#runAll
runAll
CurrentExecutionEnvironment runTestsBy: [
self testCasesToRun do: [ :each |
self runTestCase: each ] ]
), a test environment is pre-installed via CurrentExecutionEnvironment>>#runTestsBy:
runTestsBy: aBlock
self value runTestsBy: aBlock
.
One practical use of execution environments is controlling Epicea logging during tests. In EpMonitor>>#subscribeToSystemAnnouncer
subscribeToSystemAnnouncer
{
(PackageAdded -> [ :ann | self packageAdded: ann ]).
(PackageRenamed -> [ :ann | self packageRenamed: ann ]).
(PackageRemoved -> [ :ann | self packageRemoved: ann ]).
(PackageTagAdded -> [ :ann | self packageTagAdded: ann ]).
(PackageTagRemoved -> [ :ann | self packageTagRemoved: ann ]).
(PackageTagRenamed -> [ :ann | self packageTagRenamed: ann ]).
(ClassAdded -> [ :ann | self behaviorAdded: ann ]).
(ClassRemoved -> [ :ann | self behaviorRemoved: ann ]).
(MethodAdded -> [ :ann | self methodAdded: ann ]).
(MethodRemoved -> [ :ann | self methodRemoved: ann ]).
(ProtocolAdded -> [ :ann | self protocolAdded: ann ]).
(ProtocolRemoved -> [ :ann | self protocolRemoved: ann ]).
(ClassModifiedClassDefinition -> [ :ann | self behaviorModified: ann ]).
(MethodModified -> [ :ann | self methodModified: ann ]).
(ClassRepackaged -> [ :ann | self classRepackaged: ann ]).
(ClassRenamed -> [ :ann | self classRenamed: ann ]).
(ClassCommented -> [ :ann | self classCommented: ann ]).
(MethodRecategorized -> [ :ann | self methodRecategorized: ann ]) } asDictionary keysAndValuesDo: [ :announcement :block |
self systemAnnouncer weak
when: announcement
do: [ :ann | "During the tests, we should only log with Epicea if the test case declare it wants logging."
(CurrentExecutionEnvironment value isTest and: [ CurrentExecutionEnvironment value testCase shouldLogWithEpicea not ]) ifFalse: [ block value: ann ] ]
for: self ]
, code changes are only logged if:
1. Not running in a test environment, OR
2. The test case explicitly enables logging via shouldLogWithEpicea
This prevents test-generated code changes from polluting the change history.