Skip to content

Commit be05442

Browse files
committed
update encoding
1 parent 24f913d commit be05442

File tree

3 files changed

+96
-76
lines changed

3 files changed

+96
-76
lines changed

Sources/Commands/SwiftTestCommand.swift

Lines changed: 35 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -398,11 +398,40 @@ package struct CoverageFormatOutput: Encodable {
398398
package init() {
399399
self._underlying = [CoverageFormat : AbsolutePath]()
400400
}
401-
401+
402402
package init(data: [CoverageFormat : AbsolutePath]) {
403403
self._underlying = data
404404
}
405405

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

477475
struct CodeCoverageConfiguration {
@@ -1111,19 +1109,9 @@ extension SwiftTestCommand {
11111109
let config = try await self.getCodeCoverageConfiguration(swiftCommandState, format: format)
11121110
coverageData[format] = config.outputDir
11131111
}
1114-
1112+
11151113
let coverageOutput = CoverageFormatOutput(data: coverageData)
1116-
1117-
switch printMode {
1118-
case .json:
1119-
let jsonOutput = try coverageOutput.encodeAsJSON()
1120-
print(jsonOutput)
1121-
case .text:
1122-
let textOutput = coverageOutput.encodeAsText()
1123-
print(textOutput)
1124-
}
11251114

1126-
print("-----------------------")
11271115
let data: Data
11281116
switch printMode {
11291117
case .json:

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)