Skip to content

Commit d3e4b2d

Browse files
committed
update encoding
1 parent e381d71 commit d3e4b2d

File tree

3 files changed

+106
-81
lines changed

3 files changed

+106
-81
lines changed

Sources/Commands/SwiftTestCommand.swift

Lines changed: 45 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,24 @@ 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+
data = Data("\(singlePath.pathString)".utf8)
1135+
} else {
1136+
let coverageOutput = CoverageFormatOutput(data: coverageData)
1137+
var encoder = PlainTextEncoder()
1138+
encoder.formattingOptions = [.prettyPrinted]
1139+
data = try encoder.encode(coverageOutput)
1140+
}
11481141
}
11491142
print(String(decoding: data, as: UTF8.self))
11501143
}

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/TestCommandTests+Helpers.swift

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@ import func Commands.getOutputDir
1818
import enum Commands.CoverageFormat
1919
import struct Commands.CoverageFormatOutput
2020
import typealias Basics.StringError
21+
import struct Commands.PlainTextEncoder
2122

2223
@Suite(
2324
.tags(
2425
.TestSize.small,
2526
)
2627
)
27-
struct TestCommmandHelperTests {
28+
struct TestCommmandHelpersTests {
2829

2930
@Suite
3031
struct getOutputDirTests {
@@ -404,8 +405,10 @@ struct TestCommmandHelperTests {
404405
var output = CoverageFormatOutput()
405406
try output.addFormat(.json, path: path)
406407

407-
let jsonString = try output.encodeAsJSON()
408-
let jsonData = jsonString.data(using: .utf8)!
408+
let encoder = JSONEncoder()
409+
encoder.keyEncodingStrategy = .convertToSnakeCase
410+
let jsonData = try encoder.encode(output)
411+
let jsonString = String(decoding: jsonData, as: UTF8.self)
409412
let decoded = try JSONSerialization.jsonObject(with: jsonData) as! [String: String]
410413

411414
#expect(decoded["json"] == "/path/to/coverage.json")
@@ -430,8 +433,11 @@ struct TestCommmandHelperTests {
430433
try output.addFormat(.json, path: jsonPath)
431434
try output.addFormat(.html, path: htmlPath)
432435

433-
let jsonString = try output.encodeAsJSON()
434-
let jsonData = jsonString.data(using: .utf8)!
436+
let encoder = JSONEncoder()
437+
encoder.keyEncodingStrategy = .convertToSnakeCase
438+
encoder.outputFormatting = [.prettyPrinted]
439+
let jsonData = try encoder.encode(output)
440+
let jsonString = String(decoding: jsonData, as: UTF8.self)
435441
let decoded = try JSONSerialization.jsonObject(with: jsonData) as! [String: String]
436442

437443
#expect(decoded["json"] == "/path/to/coverage.json")
@@ -447,8 +453,11 @@ struct TestCommmandHelperTests {
447453
func encodeAsJSONEmpty() throws {
448454
let output = CoverageFormatOutput()
449455

450-
let jsonString = try output.encodeAsJSON()
451-
let jsonData = jsonString.data(using: .utf8)!
456+
let encoder = JSONEncoder()
457+
encoder.keyEncodingStrategy = .convertToSnakeCase
458+
encoder.outputFormatting = [.prettyPrinted]
459+
let jsonData = try encoder.encode(output)
460+
let jsonString = String(decoding: jsonData, as: UTF8.self)
452461
let decoded = try JSONSerialization.jsonObject(with: jsonData) as! [String: String]
453462

454463
#expect(decoded.isEmpty)
@@ -469,9 +478,14 @@ struct TestCommmandHelperTests {
469478
var output = CoverageFormatOutput()
470479
try output.addFormat(format, path: path)
471480

472-
let textString = output.encodeAsText()
481+
var encoder = PlainTextEncoder()
482+
encoder.formattingOptions = [.prettyPrinted]
483+
let textData = try encoder.encode(output)
484+
let textString = String(decoding: textData, as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines)
473485

474-
#expect(textString == "/path/to/coverage.json")
486+
// PlainTextEncoder capitalizes first letter of keys
487+
let expectedFormat = format.rawValue.prefix(1).uppercased() + format.rawValue.dropFirst()
488+
#expect(textString == "\(expectedFormat): /path/to/coverage.json")
475489
}
476490

477491
@Test("Encode as text with multiple formats")
@@ -483,17 +497,24 @@ struct TestCommmandHelperTests {
483497
try output.addFormat(.json, path: jsonPath)
484498
try output.addFormat(.html, path: htmlPath)
485499

486-
let textString = output.encodeAsText()
500+
var encoder = PlainTextEncoder()
501+
encoder.formattingOptions = [.prettyPrinted]
502+
let textData = try encoder.encode(output)
503+
let textString = String(decoding: textData, as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines)
487504

488505
// Should be sorted by format name (html comes before json alphabetically)
489-
#expect(textString == "HTML: /path/to/coverage-html\nJSON: /path/to/coverage.json")
506+
// PlainTextEncoder capitalizes first letter of keys
507+
#expect(textString == "Html: /path/to/coverage-html\nJson: /path/to/coverage.json")
490508
}
491509

492510
@Test("Encode as text with empty data")
493511
func encodeAsTextEmpty() throws {
494512
let output = CoverageFormatOutput()
495513

496-
let textString = output.encodeAsText()
514+
var encoder = PlainTextEncoder()
515+
encoder.formattingOptions = [.prettyPrinted]
516+
let textData = try encoder.encode(output)
517+
let textString = String(decoding: textData, as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines)
497518

498519
#expect(textString.isEmpty)
499520
}
@@ -510,13 +531,17 @@ struct TestCommmandHelperTests {
510531
try output.addFormat(.html, path: htmlPath) // Add html second
511532

512533
// Text encoding should show html first (alphabetically)
513-
let textString = output.encodeAsText()
514-
#expect(textString.hasPrefix("HTML:"))
515-
#expect(textString.hasSuffix("JSON: /json/path"))
534+
var textEncoder = PlainTextEncoder()
535+
textEncoder.formattingOptions = [.prettyPrinted]
536+
let textData = try textEncoder.encode(output)
537+
let textString = String(decoding: textData, as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines)
538+
#expect(textString.hasPrefix("Html:"))
539+
#expect(textString.hasSuffix("Json: /json/path"))
516540

517541
// JSON encoding should also maintain consistent ordering
518-
let jsonString = try output.encodeAsJSON()
519-
let jsonData = jsonString.data(using: .utf8)!
542+
let jsonEncoder = JSONEncoder()
543+
jsonEncoder.keyEncodingStrategy = .convertToSnakeCase
544+
let jsonData = try jsonEncoder.encode(output)
520545
let decoded = try JSONSerialization.jsonObject(with: jsonData) as! [String: String]
521546

522547
#expect(decoded["html"] == "/html/path")
@@ -529,9 +554,12 @@ struct TestCommmandHelperTests {
529554
var output = CoverageFormatOutput()
530555
try output.addFormat(.json, path: specialPath)
531556

532-
let textString = output.encodeAsText()
557+
var encoder = PlainTextEncoder()
558+
encoder.formattingOptions = [.prettyPrinted]
559+
let textData = try encoder.encode(output)
560+
let textString = String(decoding: textData, as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines)
533561

534-
#expect(textString == "/path with/spaces & symbols/coverage.json")
562+
#expect(textString == "Json: /path with/spaces & symbols/coverage.json")
535563
}
536564

537565
@Test("JSON encoding handles special characters in paths")
@@ -540,8 +568,9 @@ struct TestCommmandHelperTests {
540568
var output = CoverageFormatOutput()
541569
try output.addFormat(.json, path: specialPath)
542570

543-
let jsonString = try output.encodeAsJSON()
544-
let jsonData = jsonString.data(using: .utf8)!
571+
let encoder = JSONEncoder()
572+
encoder.keyEncodingStrategy = .convertToSnakeCase
573+
let jsonData = try encoder.encode(output)
545574
let decoded = try JSONSerialization.jsonObject(with: jsonData) as! [String: String]
546575

547576
#expect(decoded["json"] == "/path with/spaces & symbols/coverage.json")

0 commit comments

Comments
 (0)