Skip to content

Commit 9a3e7af

Browse files
authored
test: Add tests for the tar writer (#50)
Motivation ---------- The `tar` writer is tested by the end to end tests, but unit tests are more helpful for refactoring and extending it. Modifications ------------- * Extract `tar` into its own module * Extract tar header construction into a separate function * Add initial unit tests for the helper methods used to build `tar` headers * Stop using `Swift(format:)` to convert integers into octal strings because of swiftlang/swift-corelibs-foundation#5152 Result ------ The basic helper functions underpinning the tar writer will have unit tests, making it easier to test, refactor and extend the tar writer in future. Test Plan --------- This pull request adds new tests. All existing tests continue to pass.
1 parent ec08871 commit 9a3e7af

File tree

5 files changed

+118
-9
lines changed

5 files changed

+118
-9
lines changed

.swift-format

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"lineLength" : 120,
1515
"maximumBlankLines" : 1,
1616
"prioritizeKeepingFunctionOutputTogether" : false,
17-
"respectsExistingLineBreaks" : false,
17+
"respectsExistingLineBreaks" : true,
1818
"rules" : {
1919
"AllPublicDeclarationsHaveDocumentation" : true,
2020
"AlwaysUseLowerCamelCase" : false,

Package.swift

+3-2
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ let package = Package(
4040
.executableTarget(
4141
name: "containertool",
4242
dependencies: [
43-
.target(name: "ContainerRegistry"), .target(name: "VendorCNIOExtrasZlib"),
43+
.target(name: "ContainerRegistry"), .target(name: "VendorCNIOExtrasZlib"), .target(name: "Tar"),
4444
.product(name: "ArgumentParser", package: "swift-argument-parser"),
4545
],
4646
swiftSettings: [.swiftLanguageMode(.v5)]
@@ -51,7 +51,7 @@ let package = Package(
5151
dependencies: [],
5252
path: "Vendor/github.com/apple/swift-nio-extras/Sources/CNIOExtrasZlib",
5353
linkerSettings: [.linkedLibrary("z")]
54-
),
54+
), .target(name: "Tar"),
5555
.target(
5656
// Vendored from https://github.com/apple/swift-package-manager with modifications
5757
name: "Basics",
@@ -87,6 +87,7 @@ let package = Package(
8787
dependencies: [.target(name: "ContainerRegistry")],
8888
resources: [.process("Resources")]
8989
), .testTarget(name: "containertoolTests", dependencies: [.target(name: "containertool")]),
90+
.testTarget(name: "TarTests", dependencies: [.target(name: "Tar")]),
9091
],
9192
swiftLanguageModes: [.v6]
9293
)

Sources/containertool/tar.swift renamed to Sources/Tar/tar.swift

+35-6
Original file line numberDiff line numberDiff line change
@@ -72,16 +72,24 @@ extension [UInt8] {
7272
func octal6(_ value: Int) -> String {
7373
precondition(value >= 0)
7474
precondition(value < 0o777777)
75-
return String(format: "%06o", value)
75+
// String(format: "%06o", value) cannot be used because of a race in Foundation
76+
// which causes it to return an empty string from time to time when running the tests
77+
// in parallel using swift-testing: https://github.com/swiftlang/swift-corelibs-foundation/issues/5152
78+
let str = String(value, radix: 8)
79+
return String(repeating: "0", count: 6 - str.count).appending(str)
7680
}
7781

78-
/// Serializes an integer to a 11 character octal representation.
82+
/// Serializes an integer to an 11 character octal representation.
7983
/// - Parameter value: The integer to serialize.
8084
/// - Returns: The serialized form of `value`.
8185
func octal11(_ value: Int) -> String {
8286
precondition(value >= 0)
8387
precondition(value < 0o777_7777_7777)
84-
return String(format: "%011o", value)
88+
// String(format: "%011o", value) cannot be used because of a race in Foundation
89+
// which causes it to return an empty string from time to time when running the tests
90+
// in parallel using swift-testing: https://github.com/swiftlang/swift-corelibs-foundation/issues/5152
91+
let str = String(value, radix: 8)
92+
return String(repeating: "0", count: 11 - str.count).appending(str)
8593
}
8694

8795
// These ranges define the offsets of the standard fields in a Tar header.
@@ -143,7 +151,12 @@ let CONTTYPE = "7" // reserved
143151
let XHDTYPE = "x" // Extended header referring to the next file in the archive
144152
let XGLTYPE = "g" // Global extended header
145153

146-
func tar(_ bytes: [UInt8], filename: String = "app") -> [UInt8] {
154+
/// Creates a tar header for a single file
155+
/// - Parameters:
156+
/// - filesize: The size of the file
157+
/// - filename: The file's name in the archive
158+
/// - Returns: A tar header representing the file
159+
public func tarHeader(filesize: Int, filename: String = "app") -> [UInt8] {
147160
// A file entry consists of a file header followed by the
148161
// contents of the file. The header includes information such as
149162
// the file name, size and permissions. Different versions of
@@ -158,7 +171,7 @@ func tar(_ bytes: [UInt8], filename: String = "app") -> [UInt8] {
158171
hdr.writeString(octal6(0o555), inField: mode, withTermination: .spaceAndNull)
159172
hdr.writeString(octal6(0o000000), inField: uid, withTermination: .spaceAndNull)
160173
hdr.writeString(octal6(0o000000), inField: gid, withTermination: .spaceAndNull)
161-
hdr.writeString(octal11(bytes.count), inField: size, withTermination: .space)
174+
hdr.writeString(octal11(filesize), inField: size, withTermination: .space)
162175
hdr.writeString(octal11(0), inField: mtime, withTermination: .space)
163176
hdr.writeString(INIT_CHECKSUM, inField: chksum, withTermination: .none)
164177
hdr.writeString(REGTYPE, inField: typeflag, withTermination: .none)
@@ -174,6 +187,17 @@ func tar(_ bytes: [UInt8], filename: String = "app") -> [UInt8] {
174187
// Fill in the checksum.
175188
hdr.writeString(octal6(checksum(header: hdr)), inField: chksum, withTermination: .nullAndSpace)
176189

190+
return hdr
191+
}
192+
193+
/// Creates a tar archive containing a single file
194+
/// - Parameters:
195+
/// - bytes: The file's body data
196+
/// - filename: The file's name in the archive
197+
/// - Returns: A tar archive containing the file
198+
public func tar(_ bytes: [UInt8], filename: String = "app") -> [UInt8] {
199+
var hdr = tarHeader(filesize: bytes.count, filename: filename)
200+
177201
// Append the file data to the header
178202
hdr.append(contentsOf: bytes)
179203

@@ -187,4 +211,9 @@ func tar(_ bytes: [UInt8], filename: String = "app") -> [UInt8] {
187211
return hdr
188212
}
189213

190-
func tar(_ data: Data, filename: String) -> [UInt8] { tar([UInt8](data), filename: filename) }
214+
/// Creates a tar archive containing a single file
215+
/// - Parameters:
216+
/// - data: The file's body data
217+
/// - filename: The file's name in the archive
218+
/// - Returns: A tar archive containing the file
219+
public func tar(_ data: Data, filename: String) -> [UInt8] { tar([UInt8](data), filename: filename) }

Sources/containertool/containertool.swift

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import ArgumentParser
1616
import Foundation
1717
import ContainerRegistry
18+
import Tar
1819
import Basics
1920

2021
extension Swift.String: Swift.Error {}

Tests/TarTests/TarUnitTests.swift

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftContainerPlugin open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the SwiftContainerPlugin project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Foundation
16+
import Testing
17+
18+
@testable import Tar
19+
20+
let blocksize = 512
21+
let headerLen = blocksize
22+
let trailerLen = 2 * blocksize
23+
24+
@Suite struct TarUnitTests {
25+
@Test(arguments: [
26+
(input: 0o000, expected: "000000"),
27+
(input: 0o555, expected: "000555"),
28+
(input: 0o750, expected: "000750"),
29+
(input: 0o777, expected: "000777"),
30+
(input: 0o1777, expected: "001777"),
31+
])
32+
func testOctal6(input: Int, expected: String) async throws {
33+
#expect(octal6(input) == expected)
34+
}
35+
36+
@Test(arguments: [
37+
(input: 0, expected: "00000000000"),
38+
(input: 1024, expected: "00000002000"),
39+
(input: 0o2000, expected: "00000002000"),
40+
(input: 1024 * 1024, expected: "00004000000"),
41+
])
42+
func testOctal11(input: Int, expected: String) async throws {
43+
#expect(octal11(input) == expected)
44+
}
45+
46+
@Test func testUInt8writeString() async throws {
47+
// Fill the buffer with 0xFF to show null termination
48+
var hdr = [UInt8](repeating: 255, count: 21)
49+
50+
// The typechecker timed out when these test cases were passed as arguments, in the style of the octal tests
51+
hdr.writeString("abc", inField: 0..<5, withTermination: .none)
52+
#expect(
53+
hdr == [
54+
97, 98, 99, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
55+
]
56+
)
57+
58+
hdr.writeString("def", inField: 3..<7, withTermination: .null)
59+
#expect(
60+
hdr == [97, 98, 99, 100, 101, 102, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255]
61+
)
62+
63+
hdr.writeString("ghi", inField: 7..<11, withTermination: .space)
64+
#expect(
65+
hdr == [97, 98, 99, 100, 101, 102, 0, 103, 104, 105, 32, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255]
66+
)
67+
68+
hdr.writeString("jkl", inField: 11..<16, withTermination: .nullAndSpace)
69+
#expect(
70+
hdr == [97, 98, 99, 100, 101, 102, 0, 103, 104, 105, 32, 106, 107, 108, 0, 32, 255, 255, 255, 255, 255]
71+
)
72+
73+
hdr.writeString("mno", inField: 16..<21, withTermination: .spaceAndNull)
74+
#expect(
75+
hdr == [97, 98, 99, 100, 101, 102, 0, 103, 104, 105, 32, 106, 107, 108, 0, 32, 109, 110, 111, 32, 0]
76+
)
77+
}
78+
}

0 commit comments

Comments
 (0)