Skip to content

Commit 235517a

Browse files
committed
update encoding
1 parent e381d71 commit 235517a

File tree

4 files changed

+126
-88
lines changed

4 files changed

+126
-88
lines changed

Sources/Commands/SwiftTestCommand.swift

Lines changed: 50 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ public struct CoverageOptions: ParsableArguments {
265265
.customLong("show-code-coverage-path"),
266266
.customLong("show-coverage-path"),
267267
],
268-
help: "Print the path of the exported code coverage JSON file.",
268+
help: "Print the path of the exported code coverage files.",
269269
)
270270
var shouldPrintPath: Bool = false
271271

@@ -399,11 +399,40 @@ package struct CoverageFormatOutput: Encodable {
399399
package init() {
400400
self._underlying = [CoverageFormat : AbsolutePath]()
401401
}
402-
402+
403403
package init(data: [CoverageFormat : AbsolutePath]) {
404404
self._underlying = data
405405
}
406406

407+
// Custom encoding to ensure the dictionary is encoded as a JSON object, not an array
408+
public func encode(to encoder: Encoder) throws {
409+
// Use keyed container to encode each format and its path
410+
// This will create proper JSON objects and proper plain text "key: value" format
411+
var container = encoder.container(keyedBy: DynamicCodingKey.self)
412+
413+
// Sort entries for consistent output
414+
let sortedEntries = _underlying.sorted { $0.key.rawValue < $1.key.rawValue }
415+
416+
for (format, path) in sortedEntries {
417+
let key = DynamicCodingKey(stringValue: format.rawValue)!
418+
try container.encode(path.pathString, forKey: key)
419+
}
420+
}
421+
422+
// Dynamic coding keys for the formats
423+
private struct DynamicCodingKey: CodingKey {
424+
var stringValue: String
425+
var intValue: Int? { nil }
426+
427+
init?(stringValue: String) {
428+
self.stringValue = stringValue
429+
}
430+
431+
init?(intValue: Int) {
432+
return nil
433+
}
434+
}
435+
407436
/// Adds a key/value pair to the underlying dictionary.
408437
/// - Parameters:
409438
/// - format: The coverage format key
@@ -420,7 +449,7 @@ package struct CoverageFormatOutput: Encodable {
420449
package subscript(format: CoverageFormat) -> AbsolutePath? {
421450
return _underlying[format]
422451
}
423-
452+
424453
/// Gets the path for a format, throwing an error if it doesn't exist.
425454
/// - Parameter format: The coverage format
426455
/// - Returns: The absolute path for the format
@@ -431,48 +460,17 @@ package struct CoverageFormatOutput: Encodable {
431460
}
432461
return path
433462
}
434-
463+
435464
/// Returns all formats currently stored
436465
package var formats: [CoverageFormat] {
437466
return Array(_underlying.keys).sorted()
438467
}
439-
468+
440469
/// Iterate over format/path pairs
441470
package func forEach(_ body: (CoverageFormat, AbsolutePath) throws -> Void) rethrows {
442471
try _underlying.forEach(body)
443472
}
444-
445-
/// Encodes the coverage format output as JSON string
446-
/// - Returns: JSON string representation of the format/path mapping
447-
/// - Throws: `StringError` if JSON encoding fails
448-
package func encodeAsJSON() throws -> String {
449-
let sortedData = _underlying.sorted { $0.key.rawValue < $1.key.rawValue }
450-
let jsonObject: [String: String] = Dictionary(uniqueKeysWithValues: sortedData.map { ($0.key.rawValue, $0.value.pathString) })
451-
452-
do {
453-
let jsonData = try JSONSerialization.data(withJSONObject: jsonObject, options: [.prettyPrinted, .sortedKeys])
454-
guard let jsonString = String(data: jsonData, encoding: .utf8) else {
455-
throw StringError("Failed to convert JSON data to string")
456-
}
457-
return jsonString
458-
} catch {
459-
throw StringError("Failed to encode coverage format output as JSON: \(error)")
460-
}
461-
}
462-
463-
/// Encodes the coverage format output as plain text
464-
/// - Returns: Text string with format/path pairs, one per line
465-
package func encodeAsText() -> String {
466-
let sortedFormats = _underlying.keys.sorted()
467-
return sortedFormats.map { format in
468-
let value = _underlying[format]!.pathString
469-
if _underlying.count == 1 {
470-
return value
471-
} else {
472-
return "\(format.rawValue.uppercased()): \(value)"
473-
}
474-
}.joined(separator: "\n")
475-
}
473+
476474
}
477475

478476
struct CodeCoverageConfiguration {
@@ -1122,29 +1120,29 @@ extension SwiftTestCommand {
11221120
let config = try await self.getCodeCoverageConfiguration(swiftCommandState, format: format)
11231121
coverageData[format] = config.outputDir
11241122
}
1125-
1126-
let coverageOutput = CoverageFormatOutput(data: coverageData)
1127-
1128-
switch printMode {
1129-
case .json:
1130-
let jsonOutput = try coverageOutput.encodeAsJSON()
1131-
print(jsonOutput)
1132-
case .text:
1133-
let textOutput = coverageOutput.encodeAsText()
1134-
print(textOutput)
1135-
}
11361123

1137-
print("-----------------------")
11381124
let data: Data
11391125
switch printMode {
11401126
case .json:
1127+
let coverageOutput = CoverageFormatOutput(data: coverageData)
11411128
let encoder = JSONEncoder.makeWithDefaults()
11421129
encoder.keyEncodingStrategy = .convertToSnakeCase
11431130
data = try encoder.encode(coverageOutput)
11441131
case .text:
1145-
var encoder = PlainTextEncoder()
1146-
encoder.formattingOptions = [.prettyPrinted]
1147-
data = try encoder.encode(coverageOutput)
1132+
// When there's only one format, don't show the key prefix
1133+
if formats.count == 1, let singlePath = coverageData.values.first {
1134+
swiftCommandState.observabilityScope.emit(
1135+
warning: """
1136+
The contents of this output are subject to change in the future. Use `--print-coverage-path-mode json` if the output is required in a script.
1137+
""",
1138+
)
1139+
data = Data("\(singlePath.pathString)".utf8)
1140+
} else {
1141+
let coverageOutput = CoverageFormatOutput(data: coverageData)
1142+
var encoder = PlainTextEncoder()
1143+
encoder.formattingOptions = [.prettyPrinted]
1144+
data = try encoder.encode(coverageOutput)
1145+
}
11481146
}
11491147
print(String(decoding: data, as: UTF8.self))
11501148
}

Sources/Commands/Utilities/PlainTextEncoder.swift

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,31 +14,34 @@ import struct Foundation.Data
1414
import class TSCBasic.BufferedOutputByteStream
1515
import protocol TSCBasic.OutputByteStream
1616

17-
struct PlainTextEncoder {
17+
package struct PlainTextEncoder {
1818
/// The formatting of the output plain-text data.
19-
struct FormattingOptions: OptionSet {
20-
let rawValue: UInt
19+
package struct FormattingOptions: OptionSet {
20+
package let rawValue: UInt
2121

22-
init(rawValue: UInt) {
22+
package init(rawValue: UInt) {
2323
self.rawValue = rawValue
2424
}
2525

2626
/// Produce plain-text format with indented output.
27-
static let prettyPrinted = FormattingOptions(rawValue: 1 << 0)
27+
package static let prettyPrinted = FormattingOptions(rawValue: 1 << 0)
2828
}
2929

3030
/// The output format to produce. Defaults to `[]`.
31-
var formattingOptions: FormattingOptions = []
31+
package var formattingOptions: FormattingOptions = []
3232

3333
/// Contextual user-provided information for use during encoding.
34-
var userInfo: [CodingUserInfoKey: Any] = [:]
34+
package var userInfo: [CodingUserInfoKey: Any] = [:]
35+
36+
/// Initializes a new PlainTextEncoder.
37+
package init() {}
3538

3639
/// Encodes the given top-level value and returns its plain text representation.
3740
///
3841
/// - parameter value: The value to encode.
3942
/// - returns: A new `Data` value containing the encoded plan-text data.
4043
/// - throws: An error if any value throws an error during encoding.
41-
func encode<T: Encodable>(_ value: T) throws -> Data {
44+
package func encode<T: Encodable>(_ value: T) throws -> Data {
4245
let outputStream = BufferedOutputByteStream()
4346
let encoder = _PlainTextEncoder(
4447
outputStream: outputStream,

Tests/CommandsTests/CoverageTests.swift

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ struct CoverageTests {
128128
}
129129

130130
struct GenerateCoverageReportTestData {
131-
let buildData: BuildData
131+
// let buildData: BuildData
132132
let fixtureName: String
133133
let coverageFormat: CoverageFormat
134134
}
@@ -138,23 +138,24 @@ struct CoverageTests {
138138
"Coverage/Simple",
139139
"Miscellaneous/TestDiscovery/Simple",
140140
].flatMap { fixturePath in
141-
getBuildData(for: SupportedBuildSystemOnAllPlatforms).flatMap { buildData in
141+
// getBuildData(for: SupportedBuildSystemOnAllPlatforms).flatMap { buildData in
142142
CoverageFormat.allCases.map { format in
143143
GenerateCoverageReportTestData(
144-
buildData: buildData,
144+
// buildData: buildData,
145145
fixtureName: fixturePath,
146146
coverageFormat: format,
147147
)
148148
}
149-
}
149+
// }
150150
},
151151
)
152152
func generateSingleCoverageReport(
153+
buildData: BuildData,
153154
testData: GenerateCoverageReportTestData,
154155
) async throws {
155156
let fixtureName = testData.fixtureName
156157
let coverageFormat = testData.coverageFormat
157-
let buildData = testData.buildData
158+
// let buildData = testData.buildData
158159
try await fixture(name: fixtureName) { path in
159160
let commonCoverageArgs = [
160161
"--coverage-format",
@@ -199,13 +200,20 @@ struct CoverageTests {
199200
@Test
200201
func htmlReportOutputDirectory() async throws {
201202
// Verify the output directory argument specified in the response file override the default location.
202-
Issue.record("Test needs to be implemented.")
203+
Issue.record("""
204+
Test needs to be implemented.
205+
- Provide output directory that is a relative path
206+
- provide output directory that is an absolute path
207+
""")
203208
}
204209

205210
@Test
206211
func htmlReportResponseFile() async throws {
207212
// Verify the arguments specified in the response file are used.
208-
Issue.record("Test needs to be implemented.")
213+
Issue.record("""
214+
Test needs to be implemented.
215+
- verify command line argument
216+
""")
209217
}
210218

211219
@Suite

0 commit comments

Comments
 (0)