Repository: pantsbuild/pants

Issue:
Include environment in process "source" field, and consider splitting out cache status
The [process result source](https://github.com/pantsbuild/pants/blob/7986c739a11b12fac32e3d3c565658754baae8ec/src/rust/engine/process_execution/src/lib.rs#L781-L798) is [rendered in test output](https://github.com/pantsbuild/pants/blob/7986c739a11b12fac32e3d3c565658754baae8ec/src/python/pants/core/goals/test.py#L937-L942), and [included in workunit metadata](https://github.com/pantsbuild/pants/blob/7986c739a11b12fac32e3d3c565658754baae8ec/src/rust/engine/src/nodes.rs#L471-L474).

But "ran locally" or "ran remotely" no longer encodes enough information to make it clear which environment a process ran in, and that makes both use cases of this information less helpful than they could be otherwise. We should determine how to include an environment (for all but the `__local__` environment), including accounting for docker environments. And we should consider splitting the cache status out of the field to avoid more permutations.


Discussion:
While doing this, we should also include the time-saved or runtime of the process inline (currently we only record it in global metrics histograms).
> While doing this, we should also include the time-saved or runtime of the process inline (currently we only record it in global metrics histograms).

maybe call it something that will indicate the source of the value rather than what it means.
i.e. last_execution_time, or something better.
I feel this will be better.


Resolving patch:
diff --git a/src/python/pants/core/util_rules/environments.py b/src/python/pants/core/util_rules/environments.py
--- a/src/python/pants/core/util_rules/environments.py
+++ b/src/python/pants/core/util_rules/environments.py
@@ -17,7 +17,7 @@
 from pants.engine.environment import EnvironmentName as EnvironmentName
 from pants.engine.internals.docker import DockerResolveImageRequest, DockerResolveImageResult
 from pants.engine.internals.graph import WrappedTargetForBootstrap
-from pants.engine.internals.native_engine import ProcessConfigFromEnvironment
+from pants.engine.internals.native_engine import ProcessExecutionEnvironment
 from pants.engine.internals.scheduler import SchedulerSession
 from pants.engine.internals.selectors import Params
 from pants.engine.platform import Platform
@@ -463,6 +463,7 @@ class AllEnvironmentTargets(FrozenDict[str, Target]):
 
 @dataclass(frozen=True)
 class EnvironmentTarget:
+    name: str | None
     val: Target | None
 
     def executable_search_path_cache_scope(
@@ -799,7 +800,7 @@ async def get_target_for_environment_name(
     env_name: EnvironmentName, environments_subsystem: EnvironmentsSubsystem
 ) -> EnvironmentTarget:
     if env_name.val is None:
-        return EnvironmentTarget(None)
+        return EnvironmentTarget(None, None)
     if env_name.val not in environments_subsystem.names:
         raise AssertionError(
             softwrap(
@@ -840,7 +841,7 @@ async def get_target_for_environment_name(
                 """
             )
         )
-    return EnvironmentTarget(tgt)
+    return EnvironmentTarget(env_name.val, tgt)
 
 
 async def _maybe_add_docker_image_id(image_name: str, platform: Platform, address: Address) -> str:
@@ -880,7 +881,7 @@ async def extract_process_config_from_environment(
     platform: Platform,
     global_options: GlobalOptions,
     environments_subsystem: EnvironmentsSubsystem,
-) -> ProcessConfigFromEnvironment:
+) -> ProcessExecutionEnvironment:
     docker_image = None
     remote_execution = False
     raw_remote_execution_extra_platform_properties: tuple[str, ...] = ()
@@ -919,7 +920,8 @@ async def extract_process_config_from_environment(
                     )
                 )
 
-    return ProcessConfigFromEnvironment(
+    return ProcessExecutionEnvironment(
+        environment_name=tgt.name,
         platform=platform.value,
         docker_image=docker_image,
         remote_execution=remote_execution,
diff --git a/src/python/pants/engine/process.py b/src/python/pants/engine/process.py
--- a/src/python/pants/engine/process.py
+++ b/src/python/pants/engine/process.py
@@ -12,7 +12,7 @@
 from pants.engine.engine_aware import SideEffecting
 from pants.engine.fs import EMPTY_DIGEST, Digest, FileDigest
 from pants.engine.internals.native_engine import (  # noqa: F401
-    ProcessConfigFromEnvironment as ProcessConfigFromEnvironment,
+    ProcessExecutionEnvironment as ProcessExecutionEnvironment,
 )
 from pants.engine.internals.session import RunId
 from pants.engine.platform import Platform
@@ -156,9 +156,12 @@ class ProcessResult:
     stderr: bytes
     stderr_digest: FileDigest
     output_digest: Digest
-    platform: Platform
     metadata: ProcessResultMetadata = field(compare=False, hash=False)
 
+    @property
+    def platform(self) -> Platform:
+        return self.metadata.platform
+
 
 @dataclass(frozen=True)
 class FallibleProcessResult:
@@ -173,17 +176,19 @@ class FallibleProcessResult:
     stderr_digest: FileDigest
     exit_code: int
     output_digest: Digest
-    platform: Platform
     metadata: ProcessResultMetadata = field(compare=False, hash=False)
 
+    @property
+    def platform(self) -> Platform:
+        return self.metadata.platform
+
 
 @dataclass(frozen=True)
 class ProcessResultMetadata:
     """Metadata for a ProcessResult, which is not included in its definition of equality."""
 
     class Source(Enum):
-        RAN_LOCALLY = "ran_locally"
-        RAN_REMOTELY = "ran_remotely"
+        RAN = "ran"
         HIT_LOCALLY = "hit_locally"
         HIT_REMOTELY = "hit_remotely"
         MEMOIZED = "memoized"
@@ -191,12 +196,18 @@ class Source(Enum):
     # The execution time of the process, in milliseconds, or None if it could not be captured
     # (since remote execution does not guarantee its availability).
     total_elapsed_ms: int | None
+    # The environment that the process ran in (or would have run in, if it was not a cache hit).
+    execution_environment: ProcessExecutionEnvironment
     # Whether the ProcessResult (when it was created in the attached run_id) came from the local
     # or remote cache, or ran locally or remotely. See the `self.source` method.
     _source: str
     # The run_id in which a ProcessResult was created. See the `self.source` method.
     source_run_id: int
 
+    @property
+    def platform(self) -> Platform:
+        return Platform[self.execution_environment.platform]
+
     def source(self, current_run_id: RunId) -> Source:
         """Given the current run_id, return the calculated "source" of the ProcessResult.
 
@@ -274,7 +285,6 @@ def fallible_to_exec_result_or_raise(
             stderr=fallible_result.stderr,
             stderr_digest=fallible_result.stderr_digest,
             output_digest=fallible_result.output_digest,
-            platform=fallible_result.platform,
             metadata=fallible_result.metadata,
         )
     raise ProcessExecutionFailure(
diff --git a/src/python/pants/testutil/rule_runner.py b/src/python/pants/testutil/rule_runner.py
--- a/src/python/pants/testutil/rule_runner.py
+++ b/src/python/pants/testutil/rule_runner.py
@@ -42,7 +42,7 @@
 from pants.engine.fs import Digest, PathGlobs, PathGlobsAndRoot, Snapshot, Workspace
 from pants.engine.goal import Goal
 from pants.engine.internals import native_engine
-from pants.engine.internals.native_engine import ProcessConfigFromEnvironment, PyExecutor
+from pants.engine.internals.native_engine import ProcessExecutionEnvironment, PyExecutor
 from pants.engine.internals.scheduler import ExecutionError, Scheduler, SchedulerSession
 from pants.engine.internals.selectors import Effect, Get, Params
 from pants.engine.internals.session import SessionValues
@@ -589,7 +589,8 @@ def run_interactive_process(self, request: InteractiveProcess) -> InteractivePro
             return native_engine.session_run_interactive_process(
                 self.scheduler.py_session,
                 request,
-                ProcessConfigFromEnvironment(
+                ProcessExecutionEnvironment(
+                    environment_name=None,
                     platform=Platform.create_for_localhost().value,
                     docker_image=None,
                     remote_execution=False,


Test patch:
diff --git a/src/python/pants/backend/docker/goals/package_image_test.py b/src/python/pants/backend/docker/goals/package_image_test.py
--- a/src/python/pants/backend/docker/goals/package_image_test.py
+++ b/src/python/pants/backend/docker/goals/package_image_test.py
@@ -61,6 +61,7 @@
 from pants.engine.process import (
     FallibleProcessResult,
     Process,
+    ProcessExecutionEnvironment,
     ProcessExecutionFailure,
     ProcessResultMetadata,
 )
@@ -139,8 +140,18 @@ def run_process_mock(process: Process) -> FallibleProcessResult:
             stderr=b"stderr",
             stderr_digest=EMPTY_FILE_DIGEST,
             output_digest=EMPTY_DIGEST,
-            platform=Platform.create_for_localhost(),
-            metadata=ProcessResultMetadata(0, "ran_locally", 0),
+            metadata=ProcessResultMetadata(
+                0,
+                ProcessExecutionEnvironment(
+                    environment_name=None,
+                    platform=Platform.create_for_localhost().value,
+                    docker_image=None,
+                    remote_execution=False,
+                    remote_execution_extra_platform_properties=[],
+                ),
+                "ran_locally",
+                0,
+            ),
         )
 
     def mock_get_info_file(request: CreateDigest) -> Digest:
diff --git a/src/python/pants/core/goals/check_test.py b/src/python/pants/core/goals/check_test.py
--- a/src/python/pants/core/goals/check_test.py
+++ b/src/python/pants/core/goals/check_test.py
@@ -24,7 +24,11 @@
 from pants.engine.environment import EnvironmentName
 from pants.engine.fs import EMPTY_DIGEST, EMPTY_FILE_DIGEST, Workspace
 from pants.engine.platform import Platform
-from pants.engine.process import FallibleProcessResult, ProcessResultMetadata
+from pants.engine.process import (
+    FallibleProcessResult,
+    ProcessExecutionEnvironment,
+    ProcessResultMetadata,
+)
 from pants.engine.target import FieldSet, MultipleSourcesField, Target, Targets
 from pants.engine.unions import UnionMembership
 from pants.testutil.option_util import create_options_bootstrapper, create_subsystem
@@ -280,8 +284,18 @@ def test_from_fallible_process_result_output_prepping(
             stderr=b"stderr \033[0;31m/var/pants-sandbox-123/red/path.py\033[0m \033[1mbold\033[0m",
             stderr_digest=EMPTY_FILE_DIGEST,
             output_digest=EMPTY_DIGEST,
-            platform=Platform.create_for_localhost(),
-            metadata=ProcessResultMetadata(0, "ran_locally", 0),
+            metadata=ProcessResultMetadata(
+                0,
+                ProcessExecutionEnvironment(
+                    environment_name=None,
+                    platform=Platform.create_for_localhost().value,
+                    docker_image=None,
+                    remote_execution=False,
+                    remote_execution_extra_platform_properties=[],
+                ),
+                "ran_locally",
+                0,
+            ),
         ),
         strip_chroot_path=strip_chroot_path,
         strip_formatting=strip_formatting,
diff --git a/src/python/pants/core/goals/export_test.py b/src/python/pants/core/goals/export_test.py
--- a/src/python/pants/core/goals/export_test.py
+++ b/src/python/pants/core/goals/export_test.py
@@ -127,7 +127,7 @@ def run_export_rule(rule_runner: RuleRunner, targets: List[Target]) -> Tuple[int
                 MockGet(
                     output_type=EnvironmentTarget,
                     input_types=(EnvironmentNameRequest,),
-                    mock=lambda req: EnvironmentTarget(None),
+                    mock=lambda req: EnvironmentTarget(req.raw_value, None),
                 ),
                 rule_runner.do_not_use_mock(Digest, (MergeDigests,)),
                 rule_runner.do_not_use_mock(Digest, (AddPrefix,)),
@@ -277,10 +277,12 @@ def test_warnings_for_non_local_target_environments(
 
 def _give_an_environment(enr: EnvironmentNameRequest) -> EnvironmentTarget:
     if enr.raw_value.startswith("l"):
-        return EnvironmentTarget(LocalEnvironmentTarget({}, Address("local", target_name="local")))
+        return EnvironmentTarget(
+            enr.raw_value, LocalEnvironmentTarget({}, Address("local", target_name="local"))
+        )
     elif enr.raw_value.startswith("r"):
         return EnvironmentTarget(
-            RemoteEnvironmentTarget({}, Address("remote", target_name="remote"))
+            enr.raw_value, RemoteEnvironmentTarget({}, Address("remote", target_name="remote"))
         )
     else:
         raise Exception()
diff --git a/src/python/pants/core/goals/test.py b/src/python/pants/core/goals/test.py
--- a/src/python/pants/core/goals/test.py
+++ b/src/python/pants/core/goals/test.py
@@ -934,7 +934,7 @@ async def run_tests(
 
 _SOURCE_MAP = {
     ProcessResultMetadata.Source.MEMOIZED: "memoized",
-    ProcessResultMetadata.Source.RAN_REMOTELY: "ran remotely",
+    ProcessResultMetadata.Source.RAN: "ran",
     ProcessResultMetadata.Source.HIT_LOCALLY: "cached locally",
     ProcessResultMetadata.Source.HIT_REMOTELY: "cached remotely",
 }
@@ -952,8 +952,19 @@ def _format_test_summary(result: TestResult, run_id: RunId, console: Console) ->
         sigil = console.sigil_failed()
         status = "failed"
 
-    source = _SOURCE_MAP.get(result.result_metadata.source(run_id))
-    source_print = f" ({source})" if source else ""
+    environment = result.result_metadata.execution_environment.name
+    environment_type = result.result_metadata.execution_environment.environment_type
+    source = result.result_metadata.source(run_id)
+    source_str = _SOURCE_MAP[source]
+    if environment:
+        preposition = "in" if source == ProcessResultMetadata.Source.RAN else "for"
+        source_desc = (
+            f" ({source_str} {preposition} {environment_type} environment `{environment}`)"
+        )
+    elif source == ProcessResultMetadata.Source.RAN:
+        source_desc = ""
+    else:
+        source_desc = f" ({source_str})"
 
     elapsed_print = ""
     total_elapsed_ms = result.result_metadata.total_elapsed_ms
@@ -961,7 +972,7 @@ def _format_test_summary(result: TestResult, run_id: RunId, console: Console) ->
         elapsed_secs = total_elapsed_ms / 1000
         elapsed_print = f"in {elapsed_secs:.2f}s"
 
-    suffix = f" {elapsed_print}{source_print}"
+    suffix = f" {elapsed_print}{source_desc}"
     return f"{sigil} {result.description} {status}{suffix}."
 
 
diff --git a/src/python/pants/core/goals/test_test.py b/src/python/pants/core/goals/test_test.py
--- a/src/python/pants/core/goals/test_test.py
+++ b/src/python/pants/core/goals/test_test.py
@@ -57,7 +57,13 @@
     Workspace,
 )
 from pants.engine.internals.session import RunId
-from pants.engine.process import InteractiveProcess, InteractiveProcessResult, ProcessResultMetadata
+from pants.engine.platform import Platform
+from pants.engine.process import (
+    InteractiveProcess,
+    InteractiveProcessResult,
+    ProcessExecutionEnvironment,
+    ProcessResultMetadata,
+)
 from pants.engine.target import (
     BoolField,
     MultipleSourcesField,
@@ -80,6 +86,30 @@
 from pants.util.logging import LogLevel
 
 
+def make_process_result_metadata(
+    source: str,
+    *,
+    environment_name: str | None = None,
+    docker_image: str | None = None,
+    remote_execution: bool = False,
+    total_elapsed_ms: int = 999,
+    source_run_id: int = 0,
+) -> ProcessResultMetadata:
+    return ProcessResultMetadata(
+        total_elapsed_ms,
+        ProcessExecutionEnvironment(
+            environment_name=environment_name,
+            # TODO: None of the following are currently consumed in these tests.
+            platform=Platform.create_for_localhost().value,
+            docker_image=docker_image,
+            remote_execution=remote_execution,
+            remote_execution_extra_platform_properties=[],
+        ),
+        source,
+        source_run_id,
+    )
+
+
 class MockMultipleSourcesField(MultipleSourcesField):
     pass
 
@@ -148,9 +178,7 @@ def test_result(cls, field_sets: Iterable[MockTestFieldSet]) -> TestResult:
             addresses=tuple(addresses),
             coverage_data=MockCoverageData(addresses),
             output_setting=ShowOutput.ALL,
-            result_metadata=None
-            if cls.skipped(addresses)
-            else ProcessResultMetadata(999, "ran_locally", 0),
+            result_metadata=None if cls.skipped(addresses) else make_process_result_metadata("ran"),
         )
 
 
@@ -416,10 +444,12 @@ def _assert_test_summary(
 
 def test_format_summary_remote(rule_runner: RuleRunner) -> None:
     _assert_test_summary(
-        "✓ //:dummy_address succeeded in 0.05s (ran remotely).",
+        "✓ //:dummy_address succeeded in 0.05s (ran in remote environment `ubuntu`).",
         exit_code=0,
         run_id=0,
-        result_metadata=ProcessResultMetadata(50, "ran_remotely", 0),
+        result_metadata=make_process_result_metadata(
+            "ran", environment_name="ubuntu", remote_execution=True, total_elapsed_ms=50
+        ),
     )
 
 
@@ -428,7 +458,9 @@ def test_format_summary_local(rule_runner: RuleRunner) -> None:
         "✓ //:dummy_address succeeded in 0.05s.",
         exit_code=0,
         run_id=0,
-        result_metadata=ProcessResultMetadata(50, "ran_locally", 0),
+        result_metadata=make_process_result_metadata(
+            "ran", environment_name=None, total_elapsed_ms=50
+        ),
     )
 
 
@@ -437,7 +469,18 @@ def test_format_summary_memoized(rule_runner: RuleRunner) -> None:
         "✓ //:dummy_address succeeded in 0.05s (memoized).",
         exit_code=0,
         run_id=1234,
-        result_metadata=ProcessResultMetadata(50, "ran_locally", 0),
+        result_metadata=make_process_result_metadata("ran", total_elapsed_ms=50),
+    )
+
+
+def test_format_summary_memoized_remote(rule_runner: RuleRunner) -> None:
+    _assert_test_summary(
+        "✓ //:dummy_address succeeded in 0.05s (memoized for remote environment `ubuntu`).",
+        exit_code=0,
+        run_id=1234,
+        result_metadata=make_process_result_metadata(
+            "ran", environment_name="ubuntu", remote_execution=True, total_elapsed_ms=50
+        ),
     )
 
 
@@ -543,7 +586,7 @@ def assert_streaming_output(
     output_setting: ShowOutput = ShowOutput.ALL,
     expected_level: LogLevel,
     expected_message: str,
-    result_metadata: ProcessResultMetadata = ProcessResultMetadata(999, "dummy", 0),
+    result_metadata: ProcessResultMetadata = make_process_result_metadata("dummy"),
 ) -> None:
     result = TestResult(
         exit_code=exit_code,
diff --git a/src/python/pants/core/subsystems/python_bootstrap_test.py b/src/python/pants/core/subsystems/python_bootstrap_test.py
--- a/src/python/pants/core/subsystems/python_bootstrap_test.py
+++ b/src/python/pants/core/subsystems/python_bootstrap_test.py
@@ -128,7 +128,8 @@ def test_get_pyenv_paths(rule_runner: RuleRunner) -> None:
         rule_runner.set_session_values(
             {CompleteEnvironmentVars: CompleteEnvironmentVars({"PYENV_ROOT": pyenv_root})}
         )
-        tgt = EnvironmentTarget(LocalEnvironmentTarget({}, Address("flem")))
+        env_name = "name"
+        tgt = EnvironmentTarget(env_name, LocalEnvironmentTarget({}, Address("flem")))
         paths = rule_runner.request(
             _SearchPaths,
             [_PyEnvPathsRequest(tgt, False)],
@@ -162,6 +163,7 @@ def test_expand_interpreter_search_paths(rule_runner: RuleRunner) -> None:
             ),
         }
     )
+    env_name = "name"
     with setup_pexrc_with_pex_python_path(["/pexrc/path1:/pexrc/path2"]):
         with fake_asdf_root(
             all_python_versions, asdf_home_versions, asdf_local_versions, tool_name="python"
@@ -208,7 +210,7 @@ def test_expand_interpreter_search_paths(rule_runner: RuleRunner) -> None:
                 [
                     _ExpandInterpreterSearchPathsRequest(
                         paths,
-                        EnvironmentTarget(LocalEnvironmentTarget({}, Address("flem"))),
+                        EnvironmentTarget(env_name, LocalEnvironmentTarget({}, Address("flem"))),
                     )
                 ],
             )
@@ -295,7 +297,8 @@ def test_preprocessed_interpreter_search_paths(
             DockerImageField.alias: "my_img",
         }
 
-    env_tgt = EnvironmentTarget(env_tgt_type(extra_kwargs, address=Address("flem")))
+    env_name = "name"
+    env_tgt = EnvironmentTarget(env_name, env_tgt_type(extra_kwargs, address=Address("flem")))
     if expected is not ValueError:
         assert expected == _preprocessed_interpreter_search_paths(env_tgt, search_paths, is_default)
     else:
diff --git a/src/python/pants/core/util_rules/asdf_test.py b/src/python/pants/core/util_rules/asdf_test.py
--- a/src/python/pants/core/util_rules/asdf_test.py
+++ b/src/python/pants/core/util_rules/asdf_test.py
@@ -171,8 +171,10 @@ def test_get_asdf_paths(
             extra_kwargs = {
                 DockerImageField.alias: "my_img",
             }
+        env_name = "name"
         env_tgt = EnvironmentTarget(
-            env_tgt_type(extra_kwargs, Address("flem")) if env_tgt_type is not None else None
+            env_name,
+            env_tgt_type(extra_kwargs, Address("flem")) if env_tgt_type is not None else None,
         )
 
         # Check the "all installed" fallback
diff --git a/src/python/pants/core/util_rules/environments_test.py b/src/python/pants/core/util_rules/environments_test.py
--- a/src/python/pants/core/util_rules/environments_test.py
+++ b/src/python/pants/core/util_rules/environments_test.py
@@ -81,14 +81,15 @@ def assert_config(
             remote_execution=enable_remote_execution,
             remote_execution_extra_platform_properties=["global_k=v"],
         )
+        name = "name"
         env_subsystem = create_subsystem(
             EnvironmentsSubsystem,
-            names={"name": "addr"} if envs_enabled else {},
+            names={name: "addr"} if envs_enabled else {},
         )
         result = run_rule_with_mocks(
             extract_process_config_from_environment,
             rule_args=[
-                EnvironmentTarget(env_tgt),
+                EnvironmentTarget(name, env_tgt),
                 Platform.linux_arm64,
                 global_options,
                 env_subsystem,
@@ -343,7 +344,7 @@ def get_env_name(
                 MockGet(
                     output_type=EnvironmentTarget,
                     input_types=(EnvironmentName,),
-                    mock=lambda _: EnvironmentTarget(env_tgt),
+                    mock=lambda name: EnvironmentTarget(name.val or "__local__", env_tgt),
                 ),
                 MockGet(
                     output_type=EnvironmentName,
@@ -487,7 +488,9 @@ def assert_cache(
         tgt: Target | None, *, cache_failures: bool, expected: ProcessCacheScope
     ) -> None:
         assert (
-            EnvironmentTarget(tgt).executable_search_path_cache_scope(cache_failures=cache_failures)
+            EnvironmentTarget("unused", tgt).executable_search_path_cache_scope(
+                cache_failures=cache_failures
+            )
             == expected
         )
 
diff --git a/src/python/pants/engine/internals/platform_rules_test.py b/src/python/pants/engine/internals/platform_rules_test.py
--- a/src/python/pants/engine/internals/platform_rules_test.py
+++ b/src/python/pants/engine/internals/platform_rules_test.py
@@ -40,12 +40,14 @@ def assert_platform(
         expected: Platform,
     ) -> None:
         global_options = create_subsystem(GlobalOptions, remote_execution=remote_execution)
+        name = "name"
         env_subsystem = create_subsystem(
             EnvironmentsSubsystem,
-            names={"name": "addr"} if envs_enabled else {},
+            names={name: "addr"} if envs_enabled else {},
         )
         result = run_rule_with_mocks(
-            current_platform, rule_args=[EnvironmentTarget(env_tgt), global_options, env_subsystem]
+            current_platform,
+            rule_args=[EnvironmentTarget(name, env_tgt), global_options, env_subsystem],
         )
         assert result == expected
 
@@ -100,9 +102,10 @@ def assert_env_vars(
         expected_env: str,
     ) -> None:
         global_options = create_subsystem(GlobalOptions, remote_execution=remote_execution)
+        name = "name"
         env_subsystem = create_subsystem(
             EnvironmentsSubsystem,
-            names={"name": "addr"} if envs_enabled else {},
+            names={name: "addr"} if envs_enabled else {},
         )
 
         def mock_env_process(process: Process) -> ProcessResult:
@@ -116,7 +119,7 @@ def mock_env_process(process: Process) -> ProcessResult:
                 SessionValues(
                     {CompleteEnvironmentVars: CompleteEnvironmentVars({"LOCAL": "true"})}
                 ),
-                EnvironmentTarget(env_tgt),
+                EnvironmentTarget(name, env_tgt),
                 global_options,
                 env_subsystem,
             ],
diff --git a/src/rust/engine/process_execution/docker/src/docker_tests.rs b/src/rust/engine/process_execution/docker/src/docker_tests.rs
--- a/src/rust/engine/process_execution/docker/src/docker_tests.rs
+++ b/src/rust/engine/process_execution/docker/src/docker_tests.rs
@@ -164,7 +164,6 @@ async fn capture_exit_code_signal() {
   // assert_eq!(result.original.exit_code, -15);
   assert_eq!(result.original.exit_code, 143);
   assert_eq!(result.original.output_directory, *EMPTY_DIRECTORY_DIGEST);
-  assert_eq!(result.original.platform, platform_for_tests().unwrap());
 }
 
 fn extract_env(
@@ -286,7 +285,6 @@ async fn output_files_one() {
     result.original.output_directory,
     TestDirectory::containing_roland().directory_digest()
   );
-  assert_eq!(result.original.platform, platform_for_tests().unwrap());
 }
 
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
@@ -317,7 +315,6 @@ async fn output_dirs() {
     result.original.output_directory,
     TestDirectory::recursive().directory_digest()
   );
-  assert_eq!(result.original.platform, platform_for_tests().unwrap());
 }
 
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
@@ -347,7 +344,6 @@ async fn output_files_many() {
     result.original.output_directory,
     TestDirectory::recursive().directory_digest()
   );
-  assert_eq!(result.original.platform, platform_for_tests().unwrap());
 }
 
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
@@ -376,7 +372,6 @@ async fn output_files_execution_failure() {
     result.original.output_directory,
     TestDirectory::containing_roland().directory_digest()
   );
-  assert_eq!(result.original.platform, platform_for_tests().unwrap());
 }
 
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
@@ -406,7 +401,6 @@ async fn output_files_partial_output() {
     result.original.output_directory,
     TestDirectory::containing_roland().directory_digest()
   );
-  // assert_eq!(result.original.platform, platform_for_tests().unwrap());
 }
 
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
@@ -433,7 +427,6 @@ async fn output_overlapping_file_and_dir() {
     result.original.output_directory,
     TestDirectory::nested().directory_digest()
   );
-  assert_eq!(result.original.platform, platform_for_tests().unwrap());
 }
 
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
@@ -456,7 +449,6 @@ async fn append_only_cache_created() {
   assert_eq!(result.stderr_bytes, "".as_bytes());
   assert_eq!(result.original.exit_code, 0);
   assert_eq!(result.original.output_directory, *EMPTY_DIRECTORY_DIGEST);
-  assert_eq!(result.original.platform, platform_for_tests().unwrap());
 }
 
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
@@ -536,7 +528,6 @@ async fn all_containing_directories_for_outputs_are_created() {
     result.original.output_directory,
     TestDirectory::nested_dir_and_file().directory_digest()
   );
-  assert_eq!(result.original.platform, platform_for_tests().unwrap());
 }
 
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
@@ -568,7 +559,6 @@ async fn outputs_readable_only_by_container_user_are_captured() {
     result.original.output_directory,
     TestDirectory::nested_dir_and_file().directory_digest()
   );
-  assert_eq!(result.original.platform, platform_for_tests().unwrap());
 }
 
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
@@ -594,7 +584,6 @@ async fn output_empty_dir() {
     result.original.output_directory,
     TestDirectory::containing_falcons_dir().directory_digest()
   );
-  assert_eq!(result.original.platform, platform_for_tests().unwrap());
 }
 
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
@@ -678,7 +667,6 @@ async fn working_directory() {
     result.original.output_directory,
     TestDirectory::containing_roland().directory_digest()
   );
-  assert_eq!(result.original.platform, platform_for_tests().unwrap());
 }
 
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
@@ -757,7 +745,7 @@ async fn run_command_via_docker_in_dir(
   store: Option<Store>,
   executor: Option<task_executor::Executor>,
 ) -> Result<LocalTestResult, ProcessError> {
-  req.platform = platform_for_tests().map_err(ProcessError::Unclassified)?;
+  req.execution_environment.platform = platform_for_tests().map_err(ProcessError::Unclassified)?;
   let store_dir = TempDir::new().unwrap();
   let executor = executor.unwrap_or_else(task_executor::Executor::new);
   let store =
diff --git a/src/rust/engine/process_execution/remote/src/remote_cache_tests.rs b/src/rust/engine/process_execution/remote/src/remote_cache_tests.rs
--- a/src/rust/engine/process_execution/remote/src/remote_cache_tests.rs
+++ b/src/rust/engine/process_execution/remote/src/remote_cache_tests.rs
@@ -25,7 +25,8 @@ use crate::remote_cache::RemoteCacheWarningsBehavior;
 use process_execution::{
   make_execute_request, CacheContentBehavior, CommandRunner as CommandRunnerTrait, Context,
   EntireExecuteRequest, FallibleProcessResultWithPlatform, Platform, Process, ProcessCacheScope,
-  ProcessError, ProcessResultMetadata, ProcessResultSource,
+  ProcessError, ProcessExecutionEnvironment, ProcessExecutionStrategy, ProcessResultMetadata,
+  ProcessResultSource,
 };
 
 const CACHE_READ_TIMEOUT: Duration = Duration::from_secs(5);
@@ -50,8 +51,16 @@ impl MockLocalCommandRunner {
         stderr_digest: EMPTY_DIGEST,
         exit_code,
         output_directory: EMPTY_DIRECTORY_DIGEST.clone(),
-        platform: Platform::current().unwrap(),
-        metadata: ProcessResultMetadata::new(None, ProcessResultSource::RanLocally, RunId(0)),
+        metadata: ProcessResultMetadata::new(
+          None,
+          ProcessResultSource::Ran,
+          ProcessExecutionEnvironment {
+            name: None,
+            platform: Platform::current().unwrap(),
+            strategy: ProcessExecutionStrategy::Local,
+          },
+          RunId(0),
+        ),
       }),
       call_counter,
       delay: Duration::from_millis(delay_ms),
@@ -753,8 +762,16 @@ async fn make_action_result_basic() {
     stderr_digest: TestData::robin().digest(),
     output_directory: DirectoryDigest::from_persisted_digest(directory_digest),
     exit_code: 102,
-    platform: Platform::Linux_x86_64,
-    metadata: ProcessResultMetadata::new(None, ProcessResultSource::RanLocally, RunId(0)),
+    metadata: ProcessResultMetadata::new(
+      None,
+      ProcessResultSource::Ran,
+      ProcessExecutionEnvironment {
+        name: None,
+        platform: Platform::Linux_x86_64,
+        strategy: ProcessExecutionStrategy::Local,
+      },
+      RunId(0),
+    ),
   };
 
   let (action_result, digests) = runner
diff --git a/src/rust/engine/process_execution/remote/src/remote_tests.rs b/src/rust/engine/process_execution/remote/src/remote_tests.rs
--- a/src/rust/engine/process_execution/remote/src/remote_tests.rs
+++ b/src/rust/engine/process_execution/remote/src/remote_tests.rs
@@ -29,7 +29,7 @@ use fs::{DirectoryDigest, RelativePath, SymlinkBehavior, EMPTY_DIRECTORY_DIGEST}
 use process_execution::{
   CacheName, CommandRunner as CommandRunnerTrait, Context, EntireExecuteRequest,
   FallibleProcessResultWithPlatform, InputDigests, Platform, Process, ProcessCacheScope,
-  ProcessError, ProcessExecutionStrategy,
+  ProcessError, ProcessExecutionEnvironment, ProcessExecutionStrategy,
 };
 use std::any::type_name;
 use std::io::Cursor;
@@ -71,6 +71,14 @@ enum StderrType {
   Digest(Digest),
 }
 
+fn make_environment(platform: Platform) -> ProcessExecutionEnvironment {
+  ProcessExecutionEnvironment {
+    name: None,
+    platform,
+    strategy: ProcessExecutionStrategy::RemoteExecution(vec![]),
+  }
+}
+
 #[tokio::test]
 async fn make_execute_request() {
   let executor = task_executor::Executor::new();
@@ -93,11 +101,10 @@ async fn make_execute_request() {
     level: log::Level::Info,
     append_only_caches: BTreeMap::new(),
     jdk_home: None,
-    platform: Platform::Linux_x86_64,
     execution_slot_variable: None,
     concurrency_available: 0,
     cache_scope: ProcessCacheScope::Always,
-    execution_strategy: ProcessExecutionStrategy::RemoteExecution(vec![]),
+    execution_environment: make_environment(Platform::Linux_x86_64),
     remote_cache_speculation_delay: std::time::Duration::from_millis(0),
   };
 
@@ -186,14 +193,17 @@ async fn make_execute_request_with_instance_name() {
     level: log::Level::Info,
     append_only_caches: BTreeMap::new(),
     jdk_home: None,
-    platform: Platform::Linux_x86_64,
     execution_slot_variable: None,
     concurrency_available: 0,
     cache_scope: ProcessCacheScope::Always,
-    execution_strategy: ProcessExecutionStrategy::RemoteExecution(vec![(
-      "target_platform".to_owned(),
-      "apple-2e".to_owned(),
-    )]),
+    execution_environment: ProcessExecutionEnvironment {
+      name: None,
+      platform: Platform::Linux_x86_64,
+      strategy: ProcessExecutionStrategy::RemoteExecution(vec![(
+        "target_platform".to_owned(),
+        "apple-2e".to_owned(),
+      )]),
+    },
     remote_cache_speculation_delay: std::time::Duration::from_millis(0),
   };
 
@@ -295,11 +305,10 @@ async fn make_execute_request_with_cache_key_gen_version() {
     level: log::Level::Info,
     append_only_caches: BTreeMap::new(),
     jdk_home: None,
-    platform: Platform::Linux_x86_64,
     execution_slot_variable: None,
     concurrency_available: 0,
     cache_scope: ProcessCacheScope::Always,
-    execution_strategy: ProcessExecutionStrategy::RemoteExecution(vec![]),
+    execution_environment: make_environment(Platform::Linux_x86_64),
     remote_cache_speculation_delay: std::time::Duration::from_millis(0),
   };
 
@@ -382,7 +391,7 @@ async fn make_execute_request_with_jdk() {
 
   let input_directory = TestDirectory::containing_roland();
   let mut req = Process::new(owned_string_vec(&["/bin/echo", "yo"]));
-  req.platform = Platform::Linux_x86_64;
+  req.execution_environment.platform = Platform::Linux_x86_64;
   req.jdk_home = Some(PathBuf::from("/tmp"));
   req.description = "some description".to_owned();
   req.input_digests = InputDigests::with_input_files(input_directory.directory_digest());
@@ -456,14 +465,13 @@ async fn make_execute_request_with_jdk_and_extra_platform_properties() {
   let store = Store::local_only(executor, store_dir).unwrap();
 
   let input_directory = TestDirectory::containing_roland();
-  let mut req = Process::new(owned_string_vec(&["/bin/echo", "yo"]))
-    .remote_execution_platform_properties(vec![
-      ("FIRST".to_owned(), "foo".to_owned()),
-      ("Multi".to_owned(), "uno".to_owned()),
-      ("last".to_owned(), "bar".to_owned()),
-      ("Multi".to_owned(), "dos".to_owned()),
-    ]);
-  req.platform = Platform::Linux_x86_64;
+  let mut req = Process::new(owned_string_vec(&["/bin/echo", "yo"])).remote_execution(vec![
+    ("FIRST".to_owned(), "foo".to_owned()),
+    ("Multi".to_owned(), "uno".to_owned()),
+    ("last".to_owned(), "bar".to_owned()),
+    ("Multi".to_owned(), "dos".to_owned()),
+  ]);
+  req.execution_environment.platform = Platform::Linux_x86_64;
   req.input_digests = InputDigests::with_input_files(input_directory.directory_digest());
   req.description = "some description".to_owned();
   req.jdk_home = Some(PathBuf::from("/tmp"));
@@ -570,11 +578,10 @@ async fn make_execute_request_with_timeout() {
     level: log::Level::Info,
     append_only_caches: BTreeMap::new(),
     jdk_home: None,
-    platform: Platform::Linux_x86_64,
     execution_slot_variable: None,
     concurrency_available: 0,
     cache_scope: ProcessCacheScope::Always,
-    execution_strategy: ProcessExecutionStrategy::RemoteExecution(vec![]),
+    execution_environment: make_environment(Platform::Linux_x86_64),
     remote_cache_speculation_delay: std::time::Duration::from_millis(0),
   };
 
@@ -670,11 +677,10 @@ async fn make_execute_request_with_append_only_caches() {
       CacheName::new(String::from("xyzzy")).unwrap() => RelativePath::new(Path::new(".cache/xyzzy")).unwrap(),
     },
     jdk_home: None,
-    platform: Platform::Linux_x86_64,
     execution_slot_variable: None,
     concurrency_available: 0,
     cache_scope: ProcessCacheScope::Always,
-    execution_strategy: ProcessExecutionStrategy::RemoteExecution(vec![]),
+    execution_environment: make_environment(Platform::Linux_x86_64),
     remote_cache_speculation_delay: std::time::Duration::from_millis(0),
   };
 
@@ -825,11 +831,10 @@ async fn make_execute_request_using_immutable_inputs() {
     level: log::Level::Info,
     append_only_caches: BTreeMap::new(),
     jdk_home: None,
-    platform: Platform::Linux_x86_64,
     execution_slot_variable: None,
     concurrency_available: 0,
     cache_scope: ProcessCacheScope::Always,
-    execution_strategy: ProcessExecutionStrategy::RemoteExecution(vec![]),
+    execution_environment: make_environment(Platform::Linux_x86_64),
     remote_cache_speculation_delay: std::time::Duration::from_millis(0),
   };
 
@@ -1424,7 +1429,6 @@ async fn extract_response_with_digest_stdout() {
   assert_eq!(result.stderr_bytes, testdata_empty.bytes());
   assert_eq!(result.original.exit_code, 0);
   assert_eq!(result.original.output_directory, *EMPTY_DIRECTORY_DIGEST);
-  assert_eq!(result.original.platform, Platform::Linux_x86_64);
 }
 
 #[tokio::test]
@@ -1452,35 +1456,6 @@ async fn extract_response_with_digest_stderr() {
   assert_eq!(result.stderr_bytes, testdata.bytes());
   assert_eq!(result.original.exit_code, 0);
   assert_eq!(result.original.output_directory, *EMPTY_DIRECTORY_DIGEST);
-  assert_eq!(result.original.platform, Platform::Linux_x86_64);
-}
-
-#[tokio::test]
-async fn extract_response_with_digest_stdout_osx_remote() {
-  let _ = WorkunitStore::setup_for_tests();
-  let op_name = "gimme-foo".to_string();
-  let testdata = TestData::roland();
-  let testdata_empty = TestData::empty();
-  let result = extract_execute_response(
-    make_successful_operation(
-      &op_name,
-      StdoutType::Digest(testdata.digest()),
-      StderrType::Raw(testdata_empty.string()),
-      0,
-    )
-    .op
-    .unwrap()
-    .unwrap(),
-    Platform::Macos_x86_64,
-  )
-  .await
-  .unwrap();
-
-  assert_eq!(result.stdout_bytes, testdata.bytes());
-  assert_eq!(result.stderr_bytes, testdata_empty.bytes());
-  assert_eq!(result.original.exit_code, 0);
-  assert_eq!(result.original.output_directory, *EMPTY_DIRECTORY_DIGEST);
-  assert_eq!(result.original.platform, Platform::Macos_x86_64);
 }
 
 #[tokio::test]
@@ -1566,7 +1541,6 @@ async fn ensure_inline_stdio_is_stored() {
   assert_eq!(result.stdout_bytes, test_stdout.bytes());
   assert_eq!(result.stderr_bytes, test_stderr.bytes());
   assert_eq!(result.original.exit_code, 0);
-  assert_eq!(result.original.platform, Platform::Linux_x86_64);
 
   let local_store =
     Store::local_only(runtime.clone(), store_dir_path).expect("Error creating local store");
@@ -1977,7 +1951,6 @@ async fn execute_missing_file_uploads_if_known() {
   assert_eq!(result.stdout_bytes, roland.bytes());
   assert_eq!(result.stderr_bytes, "".as_bytes());
   assert_eq!(result.original.exit_code, 0);
-  assert_eq!(result.original.platform, Platform::Linux_x86_64);
 
   {
     let blobs = cas.blobs.lock();
@@ -2082,7 +2055,6 @@ async fn extract_execute_response_success() {
     result.original.output_directory,
     TestDirectory::nested().directory_digest()
   );
-  assert_eq!(result.original.platform, Platform::Linux_x86_64);
 }
 
 #[tokio::test]
@@ -2240,7 +2212,7 @@ async fn remote_workunits_are_stored() {
   command_runner
     .extract_execute_response(
       RunId(0),
-      Platform::Linux_x86_64,
+      make_environment(Platform::Linux_x86_64),
       OperationOrStatus::Operation(operation),
     )
     .await
@@ -2794,7 +2766,7 @@ fn make_store(store_dir: &Path, cas: &mock::StubCAS, executor: task_executor::Ex
 
 async fn extract_execute_response(
   operation: Operation,
-  remote_platform: Platform,
+  platform: Platform,
 ) -> Result<RemoteTestResult, ExecutionError> {
   let cas = mock::StubCAS::builder()
     .file(&TestData::roland())
@@ -2807,7 +2779,7 @@ async fn extract_execute_response(
   let original = command_runner
     .extract_execute_response(
       RunId(0),
-      remote_platform,
+      make_environment(platform),
       OperationOrStatus::Operation(operation),
     )
     .await?;
@@ -2886,7 +2858,7 @@ fn assert_contains(haystack: &str, needle: &str) {
 fn cat_roland_request() -> Process {
   let argv = owned_string_vec(&["/bin/cat", "roland.ext"]);
   let mut process = Process::new(argv);
-  process.platform = Platform::Linux_x86_64;
+  process.execution_environment.platform = Platform::Linux_x86_64;
   process.input_digests =
     InputDigests::with_input_files(TestDirectory::containing_roland().directory_digest());
   process.timeout = one_second();
@@ -2896,7 +2868,7 @@ fn cat_roland_request() -> Process {
 
 fn echo_roland_request() -> Process {
   let mut req = Process::new(owned_string_vec(&["/bin/echo", "meoooow"]));
-  req.platform = Platform::Linux_x86_64;
+  req.execution_environment.platform = Platform::Linux_x86_64;
   req.timeout = one_second();
   req.description = "unleash a roaring meow".to_string();
   req
diff --git a/src/rust/engine/process_execution/src/local_tests.rs b/src/rust/engine/process_execution/src/local_tests.rs
--- a/src/rust/engine/process_execution/src/local_tests.rs
+++ b/src/rust/engine/process_execution/src/local_tests.rs
@@ -19,7 +19,7 @@ use workunit_store::{RunningWorkunit, WorkunitStore};
 
 use crate::{
   local, local::KeepSandboxes, CacheName, CommandRunner as CommandRunnerTrait, Context,
-  FallibleProcessResultWithPlatform, InputDigests, NamedCaches, Platform, Process, ProcessError,
+  FallibleProcessResultWithPlatform, InputDigests, NamedCaches, Process, ProcessError,
   RelativePath,
 };
 
@@ -76,7 +76,6 @@ async fn capture_exit_code_signal() {
   assert_eq!(result.stderr_bytes, "".as_bytes());
   assert_eq!(result.original.exit_code, -15);
   assert_eq!(result.original.output_directory, *EMPTY_DIRECTORY_DIGEST);
-  assert_eq!(result.original.platform, Platform::current().unwrap());
 }
 
 #[tokio::test]
@@ -169,7 +168,6 @@ async fn output_files_one() {
     result.original.output_directory,
     TestDirectory::containing_roland().directory_digest()
   );
-  assert_eq!(result.original.platform, Platform::current().unwrap());
 }
 
 #[tokio::test]
@@ -197,7 +195,6 @@ async fn output_dirs() {
     result.original.output_directory,
     TestDirectory::recursive().directory_digest()
   );
-  assert_eq!(result.original.platform, Platform::current().unwrap());
 }
 
 #[tokio::test]
@@ -224,7 +221,6 @@ async fn output_files_many() {
     result.original.output_directory,
     TestDirectory::recursive().directory_digest()
   );
-  assert_eq!(result.original.platform, Platform::current().unwrap());
 }
 
 #[tokio::test]
@@ -250,7 +246,6 @@ async fn output_files_execution_failure() {
     result.original.output_directory,
     TestDirectory::containing_roland().directory_digest()
   );
-  assert_eq!(result.original.platform, Platform::current().unwrap());
 }
 
 #[tokio::test]
@@ -277,7 +272,6 @@ async fn output_files_partial_output() {
     result.original.output_directory,
     TestDirectory::containing_roland().directory_digest()
   );
-  assert_eq!(result.original.platform, Platform::current().unwrap());
 }
 
 #[tokio::test]
@@ -301,7 +295,6 @@ async fn output_overlapping_file_and_dir() {
     result.original.output_directory,
     TestDirectory::nested().directory_digest()
   );
-  assert_eq!(result.original.platform, Platform::current().unwrap());
 }
 
 #[tokio::test]
@@ -321,7 +314,6 @@ async fn append_only_cache_created() {
   assert_eq!(result.stderr_bytes, "".as_bytes());
   assert_eq!(result.original.exit_code, 0);
   assert_eq!(result.original.output_directory, *EMPTY_DIRECTORY_DIGEST);
-  assert_eq!(result.original.platform, Platform::current().unwrap());
 }
 
 #[tokio::test]
@@ -345,7 +337,6 @@ async fn jdk_symlink() {
   assert_eq!(result.stderr_bytes, "".as_bytes());
   assert_eq!(result.original.exit_code, 0);
   assert_eq!(result.original.output_directory, *EMPTY_DIRECTORY_DIGEST);
-  assert_eq!(result.original.platform, Platform::current().unwrap());
 }
 
 #[tokio::test]
@@ -536,7 +527,6 @@ async fn all_containing_directories_for_outputs_are_created() {
     result.original.output_directory,
     TestDirectory::nested_dir_and_file().directory_digest()
   );
-  assert_eq!(result.original.platform, Platform::current().unwrap());
 }
 
 #[tokio::test]
@@ -559,7 +549,6 @@ async fn output_empty_dir() {
     result.original.output_directory,
     TestDirectory::containing_falcons_dir().directory_digest()
   );
-  assert_eq!(result.original.platform, Platform::current().unwrap());
 }
 
 #[tokio::test]
@@ -635,7 +624,6 @@ async fn working_directory() {
     result.original.output_directory,
     TestDirectory::containing_roland().directory_digest()
   );
-  assert_eq!(result.original.platform, Platform::current().unwrap());
 }
 
 #[tokio::test]
diff --git a/src/rust/engine/process_execution/src/tests.rs b/src/rust/engine/process_execution/src/tests.rs
--- a/src/rust/engine/process_execution/src/tests.rs
+++ b/src/rust/engine/process_execution/src/tests.rs
@@ -5,7 +5,10 @@ use std::collections::hash_map::DefaultHasher;
 use std::hash::{Hash, Hasher};
 use std::time::Duration;
 
-use crate::{Process, ProcessResultMetadata, ProcessResultSource};
+use crate::{
+  Platform, Process, ProcessExecutionEnvironment, ProcessExecutionStrategy, ProcessResultMetadata,
+  ProcessResultSource,
+};
 use prost_types::Timestamp;
 use protos::gen::build::bazel::remote::execution::v2 as remexec;
 use remexec::ExecutedActionMetadata;
@@ -48,6 +51,11 @@ fn process_equality() {
 
 #[test]
 fn process_result_metadata_to_and_from_executed_action_metadata() {
+  let env = ProcessExecutionEnvironment {
+    name: None,
+    platform: Platform::Linux_x86_64,
+    strategy: ProcessExecutionStrategy::Local,
+  };
   let action_metadata = ExecutedActionMetadata {
     worker_start_timestamp: Some(Timestamp {
       seconds: 100,
@@ -62,14 +70,16 @@ fn process_result_metadata_to_and_from_executed_action_metadata() {
 
   let converted_process_result: ProcessResultMetadata = ProcessResultMetadata::new_from_metadata(
     action_metadata,
-    ProcessResultSource::RanLocally,
+    ProcessResultSource::Ran,
+    env.clone(),
     RunId(0),
   );
   assert_eq!(
     converted_process_result,
     ProcessResultMetadata::new(
       Some(concrete_time::Duration::new(20, 30)),
-      ProcessResultSource::RanLocally,
+      ProcessResultSource::Ran,
+      env.clone(),
       RunId(0),
     )
   );
@@ -92,10 +102,11 @@ fn process_result_metadata_to_and_from_executed_action_metadata() {
   );
 
   // The relevant metadata may be missing from either type.
-  let empty = ProcessResultMetadata::new(None, ProcessResultSource::RanLocally, RunId(0));
+  let empty = ProcessResultMetadata::new(None, ProcessResultSource::Ran, env.clone(), RunId(0));
   let action_metadata_missing: ProcessResultMetadata = ProcessResultMetadata::new_from_metadata(
     ExecutedActionMetadata::default(),
-    ProcessResultSource::RanLocally,
+    ProcessResultSource::Ran,
+    env,
     RunId(0),
   );
   assert_eq!(action_metadata_missing, empty);
@@ -105,27 +116,38 @@ fn process_result_metadata_to_and_from_executed_action_metadata() {
 
 #[test]
 fn process_result_metadata_time_saved_from_cache() {
-  let metadata = ProcessResultMetadata::new(
+  let env = ProcessExecutionEnvironment {
+    name: None,
+    platform: Platform::Linux_x86_64,
+    strategy: ProcessExecutionStrategy::Local,
+  };
+  let mut metadata = ProcessResultMetadata::new(
     Some(concrete_time::Duration::new(5, 150)),
-    ProcessResultSource::RanLocally,
+    ProcessResultSource::Ran,
+    env.clone(),
     RunId(0),
   );
-  let time_saved = metadata.time_saved_from_cache(Duration::new(1, 100));
-  assert_eq!(time_saved, Some(Duration::new(4, 50)));
+  metadata.update_cache_hit_elapsed(Duration::new(1, 100));
+  assert_eq!(
+    Duration::from(metadata.saved_by_cache.unwrap()),
+    Duration::new(4, 50)
+  );
 
   // If the cache lookup took more time than the process, we return 0.
-  let metadata = ProcessResultMetadata::new(
+  let mut metadata = ProcessResultMetadata::new(
     Some(concrete_time::Duration::new(1, 0)),
-    ProcessResultSource::RanLocally,
+    ProcessResultSource::Ran,
+    env.clone(),
     RunId(0),
   );
-  let time_saved = metadata.time_saved_from_cache(Duration::new(5, 0));
-  assert_eq!(time_saved, Some(Duration::new(0, 0)));
-
-  // If the original process time wasn't recorded, we can't compute the time saved.
+  metadata.update_cache_hit_elapsed(Duration::new(5, 0));
   assert_eq!(
-    ProcessResultMetadata::new(None, ProcessResultSource::RanLocally, RunId(0))
-      .time_saved_from_cache(Duration::new(1, 100)),
-    None
+    Duration::from(metadata.saved_by_cache.unwrap()),
+    Duration::new(0, 0)
   );
+
+  // If the original process time wasn't recorded, we can't compute the time saved.
+  let mut metadata = ProcessResultMetadata::new(None, ProcessResultSource::Ran, env, RunId(0));
+  metadata.update_cache_hit_elapsed(Duration::new(1, 100));
+  assert_eq!(metadata.saved_by_cache, None);
 }