Skip to content

Commit df112e3

Browse files
pb8oJonathanWoollett-Light
authored andcommitted
test: simplify Firecracker binary build
Move all Rust compilation outside the testrun Signed-off-by: Pablo Barbáchano <[email protected]>
1 parent daa3dcc commit df112e3

File tree

5 files changed

+65
-163
lines changed

5 files changed

+65
-163
lines changed

tests/framework/ab_test.py

Lines changed: 30 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,17 @@
2323
"""
2424
import contextlib
2525
import os
26-
import shutil
2726
import statistics
2827
from pathlib import Path
29-
from tempfile import TemporaryDirectory
3028
from typing import Callable, List, Optional, TypeVar
3129

3230
import scipy
3331

3432
from framework import utils
35-
from framework.defs import FC_WORKSPACE_DIR
3633
from framework.microvm import Microvm
3734
from framework.utils import CommandReturn
3835
from framework.with_filelock import with_filelock
39-
from host_tools.cargo_build import get_firecracker_binaries
36+
from host_tools.cargo_build import get_binary, get_firecracker_binaries
4037

4138
# Locally, this will always compare against main, even if we try to merge into, say, a feature branch.
4239
# We might want to do a more sophisticated way to determine a "parent" branch here.
@@ -85,25 +82,19 @@ def git_ab_test(
8582
(alternatively, your comparator can perform any required assertions and not return anything).
8683
"""
8784

88-
# We can't just checkout random branches in the current working directory. Locally, this might not work because of
89-
# uncommitted changes. In the CI this will not work because multiple tests will run in parallel, and thus switching
90-
# branches will cause random failures in other tests.
91-
with temporary_checkout(a_revision) as a_tmp:
92-
result_a = test_runner(a_tmp, True)
85+
dir_a = git_clone(Path("build") / a_revision, a_revision)
86+
result_a = test_runner(dir_a, True)
9387

94-
if b_revision:
95-
with temporary_checkout(b_revision) as b_tmp:
96-
result_b = test_runner(b_tmp, False)
97-
# Have to call comparator here to make sure both temporary directories exist (as the comparator
98-
# might rely on some files that were created during test running, see the benchmark test)
99-
comparison = comparator(result_a, result_b)
100-
else:
101-
# By default, pytest execution happens inside the `tests` subdirectory. Pass the repository root, as
102-
# documented.
103-
result_b = test_runner(Path.cwd().parent, False)
104-
comparison = comparator(result_a, result_b)
88+
if b_revision:
89+
dir_b = git_clone(Path("build") / b_revision, b_revision)
90+
else:
91+
# By default, pytest execution happens inside the `tests` subdirectory. Pass the repository root, as
92+
# documented.
93+
dir_b = Path.cwd().parent
94+
result_b = test_runner(dir_b, False)
10595

106-
return result_a, result_b, comparison
96+
comparison = comparator(result_a, result_b)
97+
return result_a, result_b, comparison
10798

10899

109100
def is_pr() -> bool:
@@ -162,45 +153,6 @@ def set_did_not_grow_comparator(
162153
)
163154

164155

165-
def git_ab_test_with_binaries(
166-
test_runner: Callable[[Path, Path], T],
167-
comparator: Callable[[T, T], U] = default_comparator,
168-
*,
169-
a_revision: str = DEFAULT_A_REVISION,
170-
b_revision: Optional[str] = None,
171-
) -> (T, T, U):
172-
"""Similar to `git_ab_test`, with the only difference being that this function compiles firecracker at the specified
173-
revisions and only passes the firecracker binaries to the test_runner. Maintains a cache of previously compiled
174-
revisions, to prevent excessive recompilation across different tests of the same revision
175-
"""
176-
177-
@with_filelock
178-
def grab_binaries(checkout: Path):
179-
with chdir(checkout):
180-
revision = utils.run_cmd("git rev-parse HEAD").stdout.strip()
181-
182-
revision_store = FC_WORKSPACE_DIR / "build" / revision
183-
if not revision_store.exists():
184-
with chdir(checkout):
185-
firecracker, jailer = get_firecracker_binaries(workspace_dir=checkout)
186-
187-
revision_store.mkdir(parents=True, exist_ok=True)
188-
shutil.copy(firecracker, revision_store / "firecracker")
189-
shutil.copy(jailer, revision_store / "jailer")
190-
191-
return (
192-
revision_store / "firecracker",
193-
revision_store / "jailer",
194-
)
195-
196-
return git_ab_test(
197-
lambda checkout, _is_a: test_runner(*grab_binaries(checkout)),
198-
comparator,
199-
a_revision=a_revision,
200-
b_revision=b_revision,
201-
)
202-
203-
204156
def git_ab_test_guest_command(
205157
microvm_factory: Callable[[Path, Path], Microvm],
206158
command: str,
@@ -212,11 +164,16 @@ def git_ab_test_guest_command(
212164
"""The same as git_ab_test_command, but via SSH. The closure argument should setup a microvm using the passed
213165
paths to firecracker and jailer binaries."""
214166

215-
def test_runner(firecracker, jailer):
167+
def test_runner(workspace_dir, _is_a: bool):
168+
utils.run_cmd("./tools/release.sh --profile release", cwd=workspace_dir)
169+
bin_dir = get_binary(
170+
"firecracker", workspace_dir=workspace_dir
171+
).parent.resolve()
172+
firecracker, jailer = bin_dir / "firecracker", bin_dir / "jailer"
216173
microvm = microvm_factory(firecracker, jailer)
217174
return microvm.ssh.run(command)
218175

219-
(_, old_out, old_err), (_, new_out, new_err), the_same = git_ab_test_with_binaries(
176+
(_, old_out, old_err), (_, new_out, new_err), the_same = git_ab_test(
220177
test_runner, comparator, a_revision=a_revision, b_revision=b_revision
221178
)
222179

@@ -270,35 +227,25 @@ def check_regression(
270227
)
271228

272229

273-
@contextlib.contextmanager
274-
def temporary_checkout(revision: str):
275-
"""
276-
Context manager that checks out firecracker in a temporary directory and `chdir`s into it
230+
@with_filelock
231+
def git_clone(clone_path, commitish):
232+
"""Clone the repository at `commit`.
277233
278-
Will change back to whatever was the current directory when the context manager was entered, even if exceptions
279-
happen along the way.
234+
:return: the working copy directory.
280235
"""
281-
with TemporaryDirectory() as tmp_dir:
282-
basename = Path(tmp_dir).name
236+
if not clone_path.exists():
283237
ret, _, _ = utils.run_cmd(
284-
f"git cat-file -t {revision}", ignore_return_code=True
238+
f"git cat-file -t {commitish}", ignore_return_code=True
285239
)
286240
if ret != 0:
287-
# git didn't recognize this object, so maybe it is a branch; qualify it
288-
revision = f"origin/{revision}"
241+
# git didn't recognize this object; qualify it if it is a branch
242+
commitish = f"origin/{commitish}"
289243
# make a temp branch for that commit so we can directly check it out
290-
utils.run_cmd(f"git branch {basename} {revision}")
291-
# `git clone` can take a path instead of an URL, which causes it to create a copy of the
292-
# repository at the given path. However, that path needs to point to the root of a repository,
293-
# it cannot be some arbitrary subdirectory. Therefore:
244+
utils.run_cmd(f"git branch {clone_path} {commitish}")
294245
_, git_root, _ = utils.run_cmd("git rev-parse --show-toplevel")
295246
# split off the '\n' at the end of the stdout
296-
utils.run_cmd(f"git clone -b {basename} {git_root.strip()} {tmp_dir}")
297-
yield Path(tmp_dir)
298-
299-
# If we compiled firecracker inside the checkout, python's recursive shutil.rmdir will
300-
# run incredibly long. Thus, remove manually.
301-
utils.run_cmd(f"rm -rf {tmp_dir}")
247+
utils.run_cmd(f"git clone -b {clone_path} {git_root.strip()} {clone_path}")
248+
return clone_path
302249

303250

304251
# Once we upgrade to python 3.11, this will be in contextlib:

tests/framework/defs.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@
1111
# The Firecracker sources workspace dir
1212
FC_WORKSPACE_DIR = Path(__file__).parent.parent.parent.resolve()
1313

14-
# Cargo build directory for seccompiler
15-
SECCOMPILER_TARGET_DIR = FC_WORKSPACE_DIR / "build/seccompiler"
16-
1714
# Folder containing JSON seccomp filters
1815
SECCOMP_JSON_DIR = FC_WORKSPACE_DIR / "resources/seccomp"
1916

tests/host_tools/cargo_build.py

Lines changed: 17 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,8 @@
1010
from framework.defs import FC_WORKSPACE_DIR
1111
from framework.with_filelock import with_filelock
1212

13-
CARGO_BUILD_REL_PATH = "firecracker_binaries"
14-
"""Keep a single build path across all build tests."""
15-
16-
CARGO_RELEASE_REL_PATH = os.path.join(CARGO_BUILD_REL_PATH, "release")
17-
"""Keep a single Firecracker release binary path across all test types."""
18-
19-
20-
DEFAULT_BUILD_TARGET = "{}-unknown-linux-musl".format(platform.machine())
21-
RELEASE_BINARIES_REL_PATH = "{}/release/".format(DEFAULT_BUILD_TARGET)
22-
23-
CARGO_UNITTEST_REL_PATH = os.path.join(CARGO_BUILD_REL_PATH, "test")
13+
DEFAULT_TARGET = f"{platform.machine()}-unknown-linux-musl"
14+
DEFAULT_TARGET_DIR = f"{DEFAULT_TARGET}/release/"
2415

2516

2617
def cargo(
@@ -33,11 +24,8 @@ def cargo(
3324
):
3425
"""Executes the specified cargo subcommand"""
3526
env = env or {}
36-
3727
env_string = " ".join(f'{key}="{str(value)}"' for key, value in env.items())
38-
3928
cmd = f"{env_string} cargo {subcommand} {cargo_args} -- {subcommand_args}"
40-
4129
return utils.run_cmd(cmd, cwd=cwd)
4230

4331

@@ -48,46 +36,27 @@ def get_rustflags():
4836
return ""
4937

5038

51-
@with_filelock
52-
def cargo_build(path, extra_args="", src_dir=None):
53-
"""Trigger build depending on flags provided."""
54-
cargo("build", extra_args, env={"CARGO_TARGET_DIR": path}, cwd=src_dir)
55-
56-
5739
def cargo_test(path, extra_args=""):
5840
"""Trigger unit tests depending on flags provided."""
5941
env = {
60-
"CARGO_TARGET_DIR": os.path.join(path, CARGO_UNITTEST_REL_PATH),
42+
"CARGO_TARGET_DIR": os.path.join(path, "unit-tests"),
6143
"RUST_TEST_THREADS": 1,
6244
"RUST_BACKTRACE": 1,
6345
"RUSTFLAGS": get_rustflags(),
6446
}
6547
cargo("test", extra_args + " --all --no-fail-fast", env=env)
6648

6749

68-
@with_filelock
6950
def get_binary(name, *, workspace_dir=FC_WORKSPACE_DIR, example=None):
70-
"""Build a binary"""
71-
target = DEFAULT_BUILD_TARGET
72-
target_dir = workspace_dir / "build" / "cargo_target"
73-
bin_path = target_dir / target / "release" / name
74-
cmd = f"-p {name}"
51+
"""Get a binary. The binaries are built before starting a testrun."""
52+
target_dir = workspace_dir / "build" / "cargo_target" / DEFAULT_TARGET_DIR
53+
bin_path = target_dir / name
7554
if example:
76-
bin_path = target_dir / target / "release" / "examples" / example
77-
cmd += f" --example {example}"
78-
if not bin_path.exists():
79-
env = {"RUSTFLAGS": get_rustflags()}
80-
cargo(
81-
"build",
82-
f"--release --target {target} {cmd}",
83-
env=env,
84-
cwd=workspace_dir,
85-
)
86-
utils.run_cmd(f"strip --strip-debug {bin_path}")
55+
bin_path = target_dir / "examples" / example
56+
assert bin_path.exists()
8757
return bin_path
8858

8959

90-
@with_filelock
9160
def get_firecracker_binaries(*, workspace_dir=FC_WORKSPACE_DIR):
9261
"""Build the Firecracker and Jailer binaries if they don't exist.
9362
@@ -104,69 +73,53 @@ def get_example(name, *args, package="firecracker", **kwargs):
10473
return get_binary(package, *args, **kwargs, example=name)
10574

10675

107-
@with_filelock
10876
def run_seccompiler_bin(bpf_path, json_path=defs.SECCOMP_JSON_DIR, basic=False):
10977
"""
11078
Run seccompiler-bin.
11179
11280
:param bpf_path: path to the output file
11381
:param json_path: optional path to json file
11482
"""
115-
cargo_target = "{}-unknown-linux-musl".format(platform.machine())
116-
11783
# If no custom json filter, use the default one for the current target.
11884
if json_path == defs.SECCOMP_JSON_DIR:
119-
json_path = json_path / "{}.json".format(cargo_target)
85+
json_path = json_path / f"{DEFAULT_TARGET}.json"
12086

12187
seccompiler_args = f"--input-file {json_path} --target-arch {platform.machine()} --output-file {bpf_path}"
12288

12389
if basic:
12490
seccompiler_args += " --basic"
12591

126-
rc, _, _ = cargo(
127-
"run",
128-
f"-p seccompiler --target-dir {defs.SECCOMPILER_TARGET_DIR} --target {cargo_target}",
129-
seccompiler_args,
130-
)
131-
92+
seccompiler = get_binary("seccompiler-bin")
93+
rc, _, _ = utils.run_cmd(f"{seccompiler} {seccompiler_args}")
13294
assert rc == 0
13395

13496

135-
@with_filelock
13697
def run_snap_editor_rebase(base_snap, diff_snap):
13798
"""
13899
Run apply_diff_snap.
139100
140101
:param base_snap: path to the base snapshot mem file
141102
:param diff_snap: path to diff snapshot mem file
142103
"""
143-
cargo_target = "{}-unknown-linux-musl".format(platform.machine())
144104

145-
rc, _, _ = cargo(
146-
"run",
147-
f"-p snapshot-editor --target {cargo_target}",
148-
f"edit-memory rebase --memory-path {base_snap} --diff-path {diff_snap}",
105+
snap_ed = get_binary("snapshot-editor")
106+
rc, _, _ = utils.run_cmd(
107+
f"{snap_ed} edit-memory rebase --memory-path {base_snap} --diff-path {diff_snap}"
149108
)
150-
151109
assert rc == 0
152110

153111

154-
@with_filelock
155112
def run_rebase_snap_bin(base_snap, diff_snap):
156113
"""
157114
Run apply_diff_snap.
158115
159116
:param base_snap: path to the base snapshot mem file
160117
:param diff_snap: path to diff snapshot mem file
161118
"""
162-
cargo_target = "{}-unknown-linux-musl".format(platform.machine())
163-
164-
rc, _, _ = cargo(
165-
"run",
166-
f"-p rebase-snap --target {cargo_target}",
167-
f"--base-file {base_snap} --diff-file {diff_snap}",
119+
rebase_snap = get_binary("rebase-snap")
120+
rc, _, _ = utils.run_cmd(
121+
f"{rebase_snap} --base-file {base_snap} --diff-file {diff_snap}"
168122
)
169-
170123
assert rc == 0
171124

172125

tools/ab_test.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@
3131

3232
# pylint:disable=wrong-import-position
3333
from framework import utils
34-
from framework.ab_test import check_regression, git_ab_test_with_binaries
34+
from framework.ab_test import check_regression, git_ab_test
3535
from framework.properties import global_props
36+
from host_tools.cargo_build import get_binary
3637
from host_tools.metrics import (
3738
emit_raw_emf,
3839
format_with_reduced_unit,
@@ -152,17 +153,13 @@ def load_data_series(report_path: Path, revision: str = None, *, reemit: bool =
152153
return processed_emf
153154

154155

155-
def collect_data(firecracker_binary: Path, jailer_binary: Path, test: str):
156+
def collect_data(binary_dir: Path, test: str):
156157
"""Executes the specified test using the provided firecracker binaries"""
157-
# Ensure the binaries are in the same directory. Will always be the case if used with git_ab_test_with_binaries
158-
assert jailer_binary.parent == firecracker_binary.parent
159-
160-
binary_dir = firecracker_binary.parent
161158
revision = binary_dir.name
162159

163160
print("Collecting samples")
164161
_, stdout, _ = utils.run_cmd(
165-
f"AWS_EMF_ENVIRONMENT=local AWS_EMF_NAMESPACE=local ./tools/test.sh --binary-dir=/firecracker/build/{revision} {test} -m ''"
162+
f"AWS_EMF_ENVIRONMENT=local AWS_EMF_NAMESPACE=local ./tools/test.sh --binary-dir={binary_dir} {test} -m ''"
166163
)
167164
print(stdout.strip())
168165

@@ -318,10 +315,13 @@ def ab_performance_test(
318315
)
319316
print(commit_list.strip())
320317

321-
git_ab_test_with_binaries(
322-
lambda firecracker_binary, jailer_binary: collect_data(
323-
firecracker_binary, jailer_binary, test
324-
),
318+
def test_runner(workspace, _is_ab: bool):
319+
utils.run_cmd("./tools/release.sh --profile release", cwd=workspace)
320+
bin_dir = ".." / get_binary("firecracker", workspace_dir=workspace).parent
321+
return collect_data(bin_dir, test)
322+
323+
return git_ab_test(
324+
test_runner,
325325
lambda ah, be: analyze_data(
326326
ah,
327327
be,

0 commit comments

Comments
 (0)