feat: load optional XDG TOML configuration with session overrides
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
import Foundation
|
||||
import TerebiConfiguration
|
||||
|
||||
@main
|
||||
struct ConfigurationChecks {
|
||||
struct Failure: Error, CustomStringConvertible {
|
||||
let description: String
|
||||
}
|
||||
|
||||
static func require(_ condition: Bool, _ message: String) throws {
|
||||
if !condition { throw Failure(description: "FAIL: " + message) }
|
||||
}
|
||||
|
||||
static func requireLoadError(at url: URL, _ message: String) throws {
|
||||
do {
|
||||
_ = try PlayerConfiguration.load(from: url)
|
||||
} catch let caught {
|
||||
let diagnostic = caught.localizedDescription + " " + String(describing: caught)
|
||||
try require(diagnostic.contains(url.path), message + " identifies the actual config path")
|
||||
return
|
||||
}
|
||||
throw Failure(description: "FAIL: " + message + " was accepted")
|
||||
}
|
||||
|
||||
static func main() throws {
|
||||
let files = FileManager.default
|
||||
let root = files.temporaryDirectory.appendingPathComponent("terebi.configuration-check.\(UUID().uuidString)")
|
||||
try files.createDirectory(at: root, withIntermediateDirectories: true)
|
||||
defer { try? files.removeItem(at: root) }
|
||||
let home = root.appendingPathComponent("home")
|
||||
let xdg = root.appendingPathComponent("xdg")
|
||||
let fallback = home.appendingPathComponent(".config/terebi/config.toml")
|
||||
let config = xdg.appendingPathComponent("terebi/config.toml")
|
||||
let defaults = PlayerConfiguration()
|
||||
try require(!defaults.showBufferInfo && !defaults.automaticQuality && defaults.snapToEdges,
|
||||
"built-in defaults hide buffer info, disable automatic quality, and enable snapping")
|
||||
try require(PlayerConfiguration.fileURL(environment: ["XDG_CONFIG_HOME": xdg.path], homeDirectory: home) == config,
|
||||
"absolute XDG_CONFIG_HOME selects terebi/config.toml")
|
||||
for environment in [[:], ["XDG_CONFIG_HOME": ""], ["XDG_CONFIG_HOME": "relative/config"]] {
|
||||
try require(PlayerConfiguration.fileURL(environment: environment, homeDirectory: home) == fallback,
|
||||
"unset, empty, and relative XDG_CONFIG_HOME fall back to ~/.config")
|
||||
}
|
||||
try require(try PlayerConfiguration.load(from: fallback) == defaults, "missing files use built-in defaults")
|
||||
try require(try PlayerConfiguration.loadForApplication(arguments: [], environment: [:], homeDirectory: home) == defaults,
|
||||
"application startup uses defaults when its resolved config is missing")
|
||||
try require(!files.fileExists(atPath: home.path), "missing-file loading creates neither home, config directory, nor file")
|
||||
|
||||
try files.createDirectory(at: config.deletingLastPathComponent(), withIntermediateDirectories: true)
|
||||
let original = Data("""
|
||||
# Root keys can be quoted, with comments and whitespace.
|
||||
"show_buffer_info" = true # session display default
|
||||
'automatic_quality' = true
|
||||
snap_to_edges = false
|
||||
""".utf8)
|
||||
try original.write(to: config)
|
||||
let configured = PlayerConfiguration(showBufferInfo: true, automaticQuality: true, snapToEdges: false)
|
||||
let environment = ["XDG_CONFIG_HOME": xdg.path]
|
||||
try require(try PlayerConfiguration.load(from: config) == configured,
|
||||
"TOML comments, whitespace, quoted root keys, and both boolean values load")
|
||||
var session = try PlayerConfiguration.loadForApplication(arguments: [], environment: environment, homeDirectory: home)
|
||||
try require(session == configured, "application startup automatically loads the XDG config")
|
||||
session.showBufferInfo = false
|
||||
session.automaticQuality = false
|
||||
session.snapToEdges = true
|
||||
try require(session == defaults, "controls can override all settings within the session")
|
||||
try require(try Data(contentsOf: config) == original, "loading and session overrides do not change config bytes")
|
||||
try require(try PlayerConfiguration.load(from: config) == configured, "a fresh load resets session overrides to file values")
|
||||
|
||||
let partial = root.appendingPathComponent("partial.toml")
|
||||
try Data("automatic_quality = true\n".utf8).write(to: partial)
|
||||
try require(try PlayerConfiguration.load(from: partial) == PlayerConfiguration(automaticQuality: true),
|
||||
"partial configuration retains defaults for omitted settings")
|
||||
let empty = root.appendingPathComponent("empty.toml")
|
||||
try Data("# Defaults only\n\n".utf8).write(to: empty)
|
||||
try require(try PlayerConfiguration.load(from: empty) == defaults, "empty/comment-only TOML uses defaults")
|
||||
try require(try PlayerConfiguration.loadForApplication(arguments: ["--config-file", partial.absoluteString],
|
||||
environment: environment, homeDirectory: home)
|
||||
== PlayerConfiguration(automaticQuality: true), "explicit file URL overrides the resolved XDG config")
|
||||
|
||||
let invalid: [(String, String)] = [
|
||||
("syntax", "show_buffer_info = [\n"),
|
||||
("duplicate", "show_buffer_info = true\n\"show_buffer_info\" = false\n"),
|
||||
("unknown", "unknown_setting = true\n"),
|
||||
("string", "show_buffer_info = \"true\"\n"),
|
||||
("integer", "automatic_quality = 1\n"),
|
||||
("array", "snap_to_edges = [true]\n"),
|
||||
("table", "[show_buffer_info]\nenabled = true\n")
|
||||
]
|
||||
for (name, text) in invalid {
|
||||
let url = root.appendingPathComponent(name + ".toml")
|
||||
let bytes = Data(text.utf8)
|
||||
try bytes.write(to: url)
|
||||
try requireLoadError(at: url, "invalid \(name) configuration")
|
||||
try require(try Data(contentsOf: url) == bytes, "rejected \(name) config remains unchanged")
|
||||
}
|
||||
let invalidUTF8 = root.appendingPathComponent("invalid-utf8.toml")
|
||||
let invalidBytes = Data([0xff, 0xfe, 0x80])
|
||||
try invalidBytes.write(to: invalidUTF8)
|
||||
try requireLoadError(at: invalidUTF8, "invalid UTF-8 configuration")
|
||||
try require(try Data(contentsOf: invalidUTF8) == invalidBytes, "invalid UTF-8 config remains unchanged")
|
||||
let directory = root.appendingPathComponent("directory.toml")
|
||||
try files.createDirectory(at: directory, withIntermediateDirectories: false)
|
||||
try requireLoadError(at: directory, "directory instead of config file")
|
||||
|
||||
let malformed = root.appendingPathComponent("syntax.toml")
|
||||
let inaccessible = partial.appendingPathComponent("config.toml") // A regular file cannot be traversed as a directory.
|
||||
for url in [malformed, inaccessible, directory] {
|
||||
try require(try PlayerConfiguration.loadForApplication(arguments: ["--config-file", url.absoluteString, "--no-config"],
|
||||
environment: environment, homeDirectory: home) == defaults,
|
||||
"--no-config bypasses malformed, inaccessible, and directory config paths")
|
||||
}
|
||||
let badXDG = root.appendingPathComponent("bad-xdg")
|
||||
let badConfig = badXDG.appendingPathComponent("terebi/config.toml")
|
||||
try files.createDirectory(at: badConfig.deletingLastPathComponent(), withIntermediateDirectories: true)
|
||||
let malformedBytes = Data("snap_to_edges =\n".utf8)
|
||||
try malformedBytes.write(to: badConfig)
|
||||
try require(try PlayerConfiguration.loadForApplication(arguments: ["--no-config"],
|
||||
environment: ["XDG_CONFIG_HOME": badXDG.path], homeDirectory: home) == defaults,
|
||||
"--no-config also bypasses automatic XDG loading")
|
||||
try require(try Data(contentsOf: badConfig) == malformedBytes, "bypassing config never repairs or rewrites it")
|
||||
try require(!files.fileExists(atPath: home.path), "all checks leave the isolated fallback home uncreated")
|
||||
print("Configuration checks passed")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user