Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
44d267c
feat: Add Swift config file reader & tests
jbelkins Dec 30, 2025
a371933
Created config file reader & test targets
jbelkins Dec 30, 2025
d94d990
Removed error extension
AntAmazonian Dec 31, 2025
df038c0
Changed file URL to use appendingPathComponent
AntAmazonian Dec 31, 2025
ca5d326
Merge branch 'main' into af/config_file_reader
AntAmazonian Jan 2, 2026
c83dccb
Merge branch 'main' into af/config_file_reader
AntAmazonian Jan 5, 2026
666f59b
Merge branch 'main' into af/config_file_reader
AntAmazonian Jan 5, 2026
edd5fd2
Refactored helper functions, simplified code logic and corrected synt…
AntAmazonian Jan 9, 2026
f1ebc24
Merge branch 'main' into af/config_file_reader
AntAmazonian Jan 9, 2026
6853fd0
Merge branch 'main' into af/config_file_reader
AntAmazonian Jan 9, 2026
2cef0d0
Refactored most of func config() and add new helpers, along with upda…
AntAmazonian Jan 16, 2026
cd381a0
Merge branch 'main' into af/config_file_reader
AntAmazonian Jan 20, 2026
983ccad
Refactored code to use a new unique keys for property megring across …
AntAmazonian Jan 22, 2026
6e4652f
Commented the ConfigFileReaderTest, and added different default file …
AntAmazonian Jan 22, 2026
6450c23
Re-organized code hierarchy, reduced configuration task to single res…
AntAmazonian Feb 3, 2026
025d0d0
Updated the struct ConfigFile to look up other section types besides …
AntAmazonian Feb 4, 2026
252a490
Changed logic to use modern logger if available but fall back to olde…
AntAmazonian Feb 4, 2026
f77c6a7
Merge branch 'main' into af/config_file_reader
AntAmazonian Feb 4, 2026
06a7119
Merge branch 'main' into af/config_file_reader
AntAmazonian Feb 5, 2026
3d57bac
Merge branch 'main' into af/config_file_reader
AntAmazonian Feb 13, 2026
9ef2707
Merge branch 'main' into af/config_file_reader
AntAmazonian Mar 17, 2026
fe932b0
Merge branch 'main' into af/config_file_reader
AntAmazonian Mar 18, 2026
8f42944
Refactored ConfigFileReader into 3 components enable a 3 stage pipeli…
AntAmazonian Mar 23, 2026
749e155
Merge branch 'main' into af/config_file_reader
AntAmazonian Mar 24, 2026
a8c4e4a
Created a unit tests that first creates temp file to tests env loc ke…
AntAmazonian Mar 31, 2026
4ba5186
Merge branch 'main' into af/config_file_reader
AntAmazonian Mar 31, 2026
bafcbb3
Merge branch 'af/config_file_reader' of github.com:awslabs/aws-sdk-sw…
AntAmazonian Mar 31, 2026
4eacd14
Added the last function to utilize JSON testing parameters.
AntAmazonian Apr 1, 2026
4d4e071
Corrected headers to align AWS standards
AntAmazonian Apr 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,13 @@ private var runtimeTargets: [Target] {
name: "AWSSDKDynamic",
path: "Sources/Core/AWSSDKDynamic/Sources/AWSSDKDynamic"
),
.target(
name: "AWSSDKConfigFileReader",
dependencies: [
.AWSSDKCommon,
],
path: "Sources/Core/AWSSDKConfigFileReader/Sources/AWSSDKConfigFileReader"
),
] + internalServiceTargets
}

Expand Down Expand Up @@ -271,6 +278,15 @@ private var runtimeTestTargets: [Target] {
path: "Sources/Core/AWSSDKIdentity/Tests/AWSSDKIdentityTests",
resources: [.process("Resources")]
),
.testTarget(
name: "AWSSDKConfigFileReaderTests",
dependencies: [
.AWSSDKCommon,
"AWSSDKConfigFileReader",
],
path: "Sources/Core/AWSSDKConfigFileReader/Tests/AWSSDKConfigFileReaderTests",
resources: [.process("Resources")]
),
]
}

Expand Down
16 changes: 16 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2345,6 +2345,13 @@ private var runtimeTargets: [Target] {
name: "AWSSDKDynamic",
path: "Sources/Core/AWSSDKDynamic/Sources/AWSSDKDynamic"
),
.target(
name: "AWSSDKConfigFileReader",
dependencies: [
.AWSSDKCommon,
],
path: "Sources/Core/AWSSDKConfigFileReader/Sources/AWSSDKConfigFileReader"
),
] + internalServiceTargets
}

Expand Down Expand Up @@ -2403,6 +2410,15 @@ private var runtimeTestTargets: [Target] {
path: "Sources/Core/AWSSDKIdentity/Tests/AWSSDKIdentityTests",
resources: [.process("Resources")]
),
.testTarget(
name: "AWSSDKConfigFileReaderTests",
dependencies: [
.AWSSDKCommon,
"AWSSDKConfigFileReader",
],
path: "Sources/Core/AWSSDKConfigFileReader/Tests/AWSSDKConfigFileReaderTests",
resources: [.process("Resources")]
),
]
}

Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// File.swift
// ConfigFileReader
//
// Created by Elkins, Josh on 9/30/25.
//

// Custom error for parsing lines
public enum ParsingError: Error, Equatable {
case incompleteProfile(line: String)
case invalidFormat(line: String)
case invalidLineOrder(line: String)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import XCTest
@_spi(FileBasedConfig) import AWSSDKCommon
//@_spi(FileBasedConfig) import ConfigFileReader

struct ParserTests: Decodable {

struct Test: Decodable {

struct Input: Decodable {
let configFile: String?
let credentialsFile: String?
}

struct Output: Decodable {

enum Value: Decodable, Equatable {
case string(String)
case subproperty([String: String])

init(from decoder: any Decoder) throws {
do {
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)
self = .string(string)
} catch {
let container = try decoder.singleValueContainer()
let dict = try container.decode([String: String].self)
self = .subproperty(dict)
}
}
}

let profiles: [String: [String: Value]]?
let ssoSessions: [String: [String: Value]]?
let errorContaining: String?
}

let name: String
let input: Input
let output: Output
}

let description: String
let tests: [Test]
}

class ConfigFileParserTests: XCTestCase {

func test_json_runAllTestsDefinedInJSON() async throws {

// Read the JSON test definitions into memory using the Decodable types above
let testDataFileURL = Bundle.module.url(forResource: "config-file-parser-tests", withExtension: "json")!
let testData = try Data(contentsOf: testDataFileURL)
let allTests = try JSONDecoder().decode(ParserTests.self, from: testData)

// Run each test
for test in allTests.tests{

// If config file contents were given, write them to a file on disk for use during the test.
// If no config file contents were given, the file will not exist.
let configFileURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
if let configFileContents = test.input.configFile {
let configData = Data(configFileContents.utf8)
try configData.write(to: configFileURL)
}

// Do the same for credentials file as was done for config.
let credentialsFileURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
if let credentialsFileContents = test.input.credentialsFile {
let credentialsData = Data(credentialsFileContents.utf8)
try credentialsData.write(to: credentialsFileURL)
}

// Read the config/credential files, and store the result in a Swift Result for future use.
let result: Result<FileBasedConfigurationSectionProviding?, Error>
do {
let success = try await TestSubject.constructor(configFileURL.path, credentialsFileURL.path)
result = .success(success)
} catch {
result = .failure(error)
}

// If an error was expected, don't try to match the message. Just make sure something was thrown.
if let expectedErrorMessage = test.output.errorContaining {
do {
_ = try result.get()
XCTFail("Test \"\(test.name)\" should have thrown \"\(expectedErrorMessage)\" but didn't")
} catch {
// No action, this test was expected to throw
}
}

// Make sure that any expected profiles are in the actual result, and that they match the expectation.
if let expectedProfiles = test.output.profiles {
do {
let actualSections = try result.get()
for (profileName, profileValue) in expectedProfiles {
guard let actualProfile = actualSections?.section(for: profileName, type: .profile) else {
XCTFail("Test \"\(test.name)\" Expected profile \"\(profileName)\" not in actual")
continue
}
if compare(testName: test.name, profileName: profileName, isSSO: false, expectedProfile: profileValue, actualProfile: actualProfile) {
// Profiles match, no error
} else {
XCTFail("Test \"\(test.name)\" profile named \"\(profileName)\" didn't match")
}
}
} catch {
XCTFail("Test \"\(test.name)\" should have succeeded, but threw \"\(error.localizedDescription)\" instead")
}
}

// Make sure any expected SSO sessions are in the actual result, and that they match the expectation.
if let expectedSSOSessions = test.output.ssoSessions {
do {
let actualSections = try result.get()
for (ssoSessionName, ssoSessionValue) in expectedSSOSessions {
guard let actualSSOSession = actualSections?.section(for: ssoSessionName, type: .ssoSession) else {
XCTFail("Test \"\(test.name)\" Expected sso-session \"\(ssoSessionName)\" not in actual")
continue
}
if compare(testName: test.name, profileName: ssoSessionName, isSSO: true, expectedProfile: ssoSessionValue, actualProfile: actualSSOSession) {
// Profiles match, no error
} else {
XCTFail("Test \"\(test.name)\" sso-session named \"\(ssoSessionName)\" didn't match")
}
}
} catch {
XCTFail("Test \"\(test.name)\" should have succeeded, but threw \"\(error.localizedDescription)\" instead")
}
}
}
}

private func compare(
testName: String,
profileName: String,
isSSO: Bool,
expectedProfile: [String: ParserTests.Test.Output.Value],
actualProfile: any FileBasedConfigurationSection
) -> Bool {
let elementName = isSSO ? "SSO session" : "profile"
for (expectedPropertyName, expectedPropertyValue) in expectedProfile {
let key = FileBasedConfigurationKey(rawValue: expectedPropertyName)
switch expectedPropertyValue {
case .string(let expectedString):
guard let actualString = actualProfile.string(for: key) else {
XCTFail("Test \"\(testName)\" \(elementName) \"\(profileName)\": expected property \"\(expectedPropertyName)\" not present in actual")
return false
}
if actualString != expectedString {
XCTFail("Test \"\(testName)\" \(elementName) \"\(profileName)\": property \"\(expectedPropertyName)\" does not match")
return false
}
case .subproperty(let expectedSubproperty):
guard let actualSubproperty = actualProfile.subproperties(for: key) else {
XCTFail("Test \"\(testName)\" \(elementName) \"\(profileName)\": expected subproperties \"\(expectedPropertyName)\" not present in actual")
return false
}
for (expectedSubpropertyName, expectedSubpropertyValue) in expectedSubproperty {
let subpropertyKey = FileBasedConfigurationKey(rawValue: expectedSubpropertyName)
guard let actualSubpropertyValue = actualSubproperty.value(for: subpropertyKey) else {
XCTFail("Test \"\(testName)\" \(elementName) \"\(profileName)\": expected subproperties \"\(expectedPropertyName)\" not present in actual")
return false
}
if expectedSubpropertyValue != actualSubpropertyValue {
XCTFail("Test \"\(testName)\" \(elementName) \"\(profileName)\": property \"\(expectedPropertyName)\" subproperty \"\(expectedSubpropertyName)\" does not match")
return false
}
}
}
}
return true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import XCTest
import Foundation
@_spi(FileBasedConfig) import AWSSDKConfigFileReader
@_spi(FileBasedConfig) import AWSSDKCommon

final class ConfigFileReaderTests: XCTestCase {
let exampleConfigFilePath = Bundle.module.path(forResource: "config_file_reader_tests", ofType: nil)!

func test_readsConfigFileDefaultSection() async throws {
// arrange
let subject = try await TestSubject.constructor(exampleConfigFilePath, nil)

// act
let defaultSection = subject?.section(for: "default")

// assert
XCTAssertNotNil(defaultSection)
}

func test_readsConfigFileBusinessCasualSection() async throws {
// arrange
let subject = try await TestSubject.constructor(exampleConfigFilePath, nil)

// act
let businessCasualSection = subject?.section(for: "business-casual")

// assert
XCTAssertNotNil(businessCasualSection)
}

func test_configFileHasAllExpectedSections() async throws {
// arrange
let subject = try await TestSubject.constructor(exampleConfigFilePath, nil)

// act
let sections = [
subject?.section(for: "default"),
subject?.section(for: "business-casual"),
subject?.section(for: "my-session"),
subject?.section(for: "my-services-config")
].compactMap { $0 }

// assert
XCTAssertTrue(sections.count == 4)
}


func test_readsDefaultSection() async throws {
// arrange
let subject = try await TestSubject.constructor(exampleConfigFilePath, nil)

// act
let region = subject?.section(for: "default")?.string(for: "region")

// assert
XCTAssertEqual(region, "us-east-1")
}

func test_readsCustomSection() async throws {
// arrange
let subject = try await TestSubject.constructor(exampleConfigFilePath, nil)

// act
let region = subject?.section(for: "business-casual")?.string(for: "region")

// assert
XCTAssertEqual(region, "ap-southeast-2")
}

func test_readsSessionSection() async throws {
// arrange
let subject = try await TestSubject.constructor(exampleConfigFilePath, nil)

// act
let region = subject?.section(for: "my-session")?.string(for: "region")

// assert
XCTAssertEqual(region, "us-west-3")
}

func test_readsServicesSection() async throws {
// arrange
let subject = try await TestSubject.constructor(exampleConfigFilePath, nil)

// act
let region = subject?.section(for: "my-services-config")?.string(for: "region")

// assert
XCTAssertEqual(region, "ap-southwest-4")
}
}
Loading
Loading