Skip to content

Commit 649164d

Browse files
authored
Enhanced test fixtures for integration tests. (#1834)
- Part of #1833. - Adds `ContainerFixture` with scoped resource lifecycle and cleanup in place of implementation inheritance for test support functions. The fixture also handles resource prefixing and uses a more ergonomic `CommandResult` in place of a tuple for return values. - `ImageWarmup` suite pre-pulls well-known images, and `copyWarmupImage()` tags test-local refs, keeping the canonical image store untouched. - Three-phase `integration-new`: warmup, followed by concurrent tests (managed by the swift test `--experimental-maximum-parallelization-width` flag), followed by serialized tests. - `coverage-new` merges unit + integration-new profraw, replacing `coverage` in CI as a migration progress indicator. - Updates GH workflow so non-coverage invokes both the `integration` and `integration-new` Makefile targets, while coverage runs invoke the `coverage-new` target.
1 parent aa7fef3 commit 649164d

8 files changed

Lines changed: 537 additions & 4 deletions

File tree

.github/workflows/common.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,11 @@ jobs:
103103
104104
- name: Test the container project
105105
if: ${{ !inputs.coverage }}
106-
run: make APP_ROOT="${APP_ROOT}" LOG_ROOT="${LOG_ROOT}" test install-kernel integration
106+
run: make APP_ROOT="${APP_ROOT}" LOG_ROOT="${LOG_ROOT}" test install-kernel integration integration-new
107107

108108
- name: Test the container project with coverage
109109
if: ${{ inputs.coverage }}
110-
run: make APP_ROOT="${APP_ROOT}" LOG_ROOT="${LOG_ROOT}" install-kernel coverage
110+
run: make APP_ROOT="${APP_ROOT}" LOG_ROOT="${LOG_ROOT}" install-kernel coverage-new
111111

112112
- name: Extract coverage percentages
113113
if: ${{ inputs.coverage }}

Makefile

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ dsym:
151151

152152
.PHONY: test
153153
test:
154-
@$(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --skip TestCLI
154+
@$(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --skip TestCLI --skip IntegrationTests
155155

156156
.PHONY: install-kernel
157157
install-kernel:
@@ -194,6 +194,56 @@ define GENERATE_COV_REPORTS
194194
@cat $(COVERAGE_OUTPUT_DIR)/$(2)/coverage-percent.txt
195195
endef
196196

197+
# New integration test infrastructure.
198+
# PARALLEL_WIDTH controls --experimental-maximum-parallelization-width for the
199+
# concurrent pass. WARMUP_FILTER, CONCURRENT_FILTER, and GLOBAL_FILTER select
200+
# the three phases. Expand the filter lists as suites are migrated from CLITests.
201+
PARALLEL_WIDTH ?= 2
202+
WARMUP_FILTER = ImageWarmup
203+
CONCURRENT_FILTER = DemoConcurrentTests
204+
GLOBAL_FILTER = DemoGlobalTests
205+
206+
INTEGRATION_SWIFT_EXTRA ?=
207+
INTEGRATION_POST_TEST ?=
208+
209+
define RUN_INTEGRATION
210+
@echo Ensuring apiserver stopped before the CLI integration tests...
211+
@bin/container system stop && sleep 3 && scripts/ensure-container-stopped.sh
212+
@if [ -n "$(APP_ROOT)" ]; then \
213+
echo "Clearing application data under $(APP_ROOT) (preserving kernels)..." ; \
214+
mkdir -p $(APP_ROOT) ; \
215+
find "$(APP_ROOT)" -mindepth 1 -maxdepth 1 ! -name kernels -exec rm -rf {} + ; \
216+
fi
217+
@echo Running the integration tests...
218+
@bin/container --debug system start --timeout 60 --enable-kernel-install $(SYSTEM_START_OPTS) && \
219+
{ \
220+
CLITEST_LOG_ROOT=$(LOG_ROOT) && export CLITEST_LOG_ROOT ; \
221+
CONTAINER_CLI_PATH=$(ROOT_DIR)/bin/container && export CONTAINER_CLI_PATH ; \
222+
echo "==> Warmup pass" && \
223+
$(SWIFT) test $(INTEGRATION_SWIFT_EXTRA) -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter "$(WARMUP_FILTER)" && \
224+
echo "==> Concurrent pass (width=$(PARALLEL_WIDTH))" && \
225+
$(SWIFT) test $(INTEGRATION_SWIFT_EXTRA) -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --experimental-maximum-parallelization-width $(PARALLEL_WIDTH) --filter "$(CONCURRENT_FILTER)" && \
226+
echo "==> Global pass (serial)" && \
227+
$(SWIFT) test $(INTEGRATION_SWIFT_EXTRA) -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter "$(GLOBAL_FILTER)" ; \
228+
exit_code=$$? ; \
229+
$(INTEGRATION_POST_TEST) \
230+
echo Ensuring apiserver stopped after the CLI integration tests ; \
231+
scripts/ensure-container-stopped.sh ; \
232+
exit $${exit_code} ; \
233+
}
234+
endef
235+
236+
.PHONY: integration-new
237+
integration-new: init-block
238+
$(RUN_INTEGRATION)
239+
240+
.PHONY: coverage-integration-new
241+
coverage-integration-new: INTEGRATION_SWIFT_EXTRA = --skip-build --enable-code-coverage
242+
coverage-integration-new: INTEGRATION_POST_TEST = cp $(COV_DATA_DIR)/*.profraw $(COVERAGE_OUTPUT_DIR)/integration/ ;
243+
coverage-integration-new: all
244+
@mkdir -p $(COVERAGE_OUTPUT_DIR)/integration
245+
$(RUN_INTEGRATION)
246+
197247
INTEGRATION_TEST_SUITES ?= \
198248
TestCLIHelp \
199249
TestCLIStatus \
@@ -228,6 +278,21 @@ empty :=
228278
space := $(empty) $(empty)
229279
INTEGRATION_FILTER := $(subst $(space),|,$(strip $(INTEGRATION_TEST_SUITES)))
230280

281+
.PHONY: coverage-new
282+
# Merges unit coverage with integration-new coverage. Use this during migration;
283+
# replace coverage with coverage-new in CI until all legacy tests are removed.
284+
coverage-new: coverage-build coverage-unit coverage-integration-new
285+
@echo Merging integration coverage profdata...
286+
@xcrun llvm-profdata merge -sparse $(COVERAGE_OUTPUT_DIR)/integration/*.profraw -o $(COVERAGE_OUTPUT_DIR)/integration/default.profdata
287+
$(call GENERATE_COV_REPORTS,$(COVERAGE_OUTPUT_DIR)/integration/default.profdata,integration)
288+
@echo Merging combined coverage profdata...
289+
@mkdir -p $(COVERAGE_OUTPUT_DIR)/combined
290+
@xcrun llvm-profdata merge -sparse \
291+
$(COVERAGE_OUTPUT_DIR)/unit/default.profdata \
292+
$(COVERAGE_OUTPUT_DIR)/integration/default.profdata \
293+
-o $(COVERAGE_OUTPUT_DIR)/combined/default.profdata
294+
$(call GENERATE_COV_REPORTS,$(COVERAGE_OUTPUT_DIR)/combined/default.profdata,combined)
295+
231296
.PHONY: coverage-build
232297
coverage-build:
233298
@echo Building tests with coverage instrumentation...
@@ -249,7 +314,7 @@ coverage-unit:
249314
@echo Running unit test coverage...
250315
@rm -f $(COV_DATA_DIR)/*.profraw
251316
@mkdir -p $(COVERAGE_OUTPUT_DIR)/unit
252-
@$(SWIFT) test --skip-build --enable-code-coverage -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --skip TestCLI
317+
@$(SWIFT) test --skip-build --enable-code-coverage -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --skip TestCLI --skip IntegrationTests
253318
@echo Merging unit coverage profdata...
254319
@xcrun llvm-profdata merge -sparse $(COV_DATA_DIR)/*.profraw -o $(COVERAGE_OUTPUT_DIR)/unit/default.profdata
255320
$(call GENERATE_COV_REPORTS,$(COVERAGE_OUTPUT_DIR)/unit/default.profdata,unit)

Package.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,16 @@ let package = Package(
8080
],
8181
path: "Sources/CLI"
8282
),
83+
.testTarget(
84+
name: "IntegrationTests",
85+
dependencies: [
86+
.product(name: "Logging", package: "swift-log"),
87+
.product(name: "SystemPackage", package: "swift-system"),
88+
"ContainerLog",
89+
"Yams",
90+
],
91+
path: "Tests/IntegrationTests"
92+
),
8393
.testTarget(
8494
name: "CLITests",
8595
dependencies: [
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2026 Apple Inc. and the container project authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
import Testing
18+
19+
/// Demonstration suite for the concurrent test pass.
20+
///
21+
/// These eight tests run under ``--experimental-maximum-parallelization-width``
22+
/// to show bounded parallelism. Each test starts an isolated container (name
23+
/// scoped to its ``ContainerFixture/testID``) and sleeps for a random interval,
24+
/// so the total wall-clock time should be roughly max(individual durations)
25+
/// rather than their sum.
26+
///
27+
/// Delete this suite once real tests have been migrated to ``IntegrationTests``.
28+
@Suite
29+
struct DemoConcurrentTests {
30+
@Test func test1() async throws { try await runDemo() }
31+
@Test func test2() async throws { try await runDemo() }
32+
@Test func test3() async throws { try await runDemo() }
33+
@Test func test4() async throws { try await runDemo() }
34+
@Test func test5() async throws { try await runDemo() }
35+
@Test func test6() async throws { try await runDemo() }
36+
@Test func test7() async throws { try await runDemo() }
37+
@Test func test8() async throws { try await runDemo() }
38+
39+
private func runDemo() async throws {
40+
try await ContainerFixture.with { f in
41+
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
42+
try await f.withContainer(image: image) { _ in
43+
try await Task.sleep(for: .seconds(Int.random(in: 2...4)))
44+
}
45+
}
46+
}
47+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2026 Apple Inc. and the container project authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
import Testing
18+
19+
/// Demonstration suite for the serial global test pass.
20+
///
21+
/// These two tests are structurally identical to ``DemoConcurrentTests`` but
22+
/// run under ``--experimental-maximum-parallelization-width 1`` in the Makefile
23+
/// to show serial execution. Total wall-clock time should be approximately the
24+
/// sum of the individual durations rather than the maximum.
25+
///
26+
/// Real global tests (image prune, system df, kernel set, etc.) will live here
27+
/// once migrated. Delete this suite at that point.
28+
@Suite
29+
struct DemoGlobalTests {
30+
@Test func globalTest1() async throws { try await runDemo() }
31+
@Test func globalTest2() async throws { try await runDemo() }
32+
33+
private func runDemo() async throws {
34+
try await ContainerFixture.with { f in
35+
let image = try f.copyWarmupImage(ContainerFixture.warmupImages[0])
36+
try await f.withContainer(image: image) { _ in
37+
try await Task.sleep(for: .seconds(Int.random(in: 2...4)))
38+
}
39+
}
40+
}
41+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2026 Apple Inc. and the container project authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
import Foundation
18+
19+
struct CommandResult: Sendable {
20+
let outputData: Data
21+
let errorData: Data
22+
let status: Int32
23+
24+
var output: String {
25+
String(data: outputData, encoding: .utf8) ?? ""
26+
}
27+
28+
var error: String {
29+
String(data: errorData, encoding: .utf8) ?? ""
30+
}
31+
32+
@discardableResult
33+
func check(_ message: String? = nil) throws -> CommandResult {
34+
guard status == 0 else {
35+
let detail = message ?? error.trimmingCharacters(in: .whitespacesAndNewlines)
36+
throw CommandError.nonZeroExit(status, detail)
37+
}
38+
return self
39+
}
40+
}
41+
42+
enum CommandError: Error {
43+
case binaryNotFound
44+
case executionFailed(String)
45+
case nonZeroExit(Int32, String)
46+
}

0 commit comments

Comments
 (0)