1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
|
# Copyright 2013-2023 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
"""
This module handles transmission of Spack state to child processes started
using the 'spawn' start method. Notably, installations are performed in a
subprocess and require transmitting the Package object (in such a way
that the repository is available for importing when it is deserialized);
installations performed in Spack unit tests may include additional
modifications to global state in memory that must be replicated in the
child process.
"""
import io
import multiprocessing
import pickle
import pydoc
import sys
from types import ModuleType
import spack.config
import spack.environment
import spack.main
import spack.platforms
import spack.repo
import spack.store
_SERIALIZE = sys.platform == "win32" or (sys.version_info >= (3, 8) and sys.platform == "darwin")
patches = None
def append_patch(patch):
global patches
if not patches:
patches = list()
patches.append(patch)
def serialize(obj):
serialized_obj = io.BytesIO()
pickle.dump(obj, serialized_obj)
serialized_obj.seek(0)
return serialized_obj
class SpackTestProcess:
def __init__(self, fn):
self.fn = fn
def _restore_and_run(self, fn, test_state):
test_state.restore()
fn()
def create(self):
test_state = TestState()
return multiprocessing.Process(target=self._restore_and_run, args=(self.fn, test_state))
class PackageInstallContext:
"""Captures the in-memory process state of a package installation that
needs to be transmitted to a child process.
"""
def __init__(self, pkg):
if _SERIALIZE:
self.serialized_pkg = serialize(pkg)
self.serialized_env = serialize(spack.environment.active_environment())
else:
self.pkg = pkg
self.env = spack.environment.active_environment()
self.spack_working_dir = spack.main.spack_working_dir
self.test_state = TestState()
def restore(self):
self.test_state.restore()
spack.main.spack_working_dir = self.spack_working_dir
env = pickle.load(self.serialized_env) if _SERIALIZE else self.env
pkg = pickle.load(self.serialized_pkg) if _SERIALIZE else self.pkg
if env:
spack.environment.activate(env)
return pkg
class TestState:
"""Spack tests may modify state that is normally read from disk in memory;
this object is responsible for properly serializing that state to be
applied to a subprocess. This isn't needed outside of a testing environment
but this logic is designed to behave the same inside or outside of tests.
"""
def __init__(self):
if _SERIALIZE:
self.config = spack.config.CONFIG
self.platform = spack.platforms.host
self.test_patches = store_patches()
self.store = spack.store.STORE
def restore(self):
if _SERIALIZE:
spack.config.CONFIG = self.config
spack.repo.PATH = spack.repo.create(self.config)
spack.platforms.host = self.platform
spack.store.STORE = self.store
self.test_patches.restore()
class TestPatches:
def __init__(self, module_patches, class_patches):
self.module_patches = list((x, y, serialize(z)) for (x, y, z) in module_patches)
self.class_patches = list((x, y, serialize(z)) for (x, y, z) in class_patches)
def restore(self):
for module_name, attr_name, value in self.module_patches:
value = pickle.load(value)
module = __import__(module_name)
setattr(module, attr_name, value)
for class_fqn, attr_name, value in self.class_patches:
value = pickle.load(value)
cls = pydoc.locate(class_fqn)
setattr(cls, attr_name, value)
def store_patches():
global patches
module_patches = list()
class_patches = list()
if not patches:
return TestPatches(list(), list())
for target, name, _ in patches:
if isinstance(target, ModuleType):
new_val = getattr(target, name)
module_name = target.__name__
module_patches.append((module_name, name, new_val))
elif isinstance(target, type):
new_val = getattr(target, name)
class_fqn = ".".join([target.__module__, target.__name__])
class_patches.append((class_fqn, name, new_val))
return TestPatches(module_patches, class_patches)
def clear_patches():
global patches
patches = None
|