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

3. Executes the block

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.