import Foundation import Testing import TOML @testable import phbar // MARK: - expand @Test func envExpandDollarBraceForm() { let env = PHEnvironment(variables: ["ACCENT": "#ff0000"]) #expect(env.expand("${ACCENT}") == "#ff0000") } @Test func envExpandDollarForm() { let env = PHEnvironment(variables: ["FOO": "bar"]) #expect(env.expand("$FOO") == "bar") } @Test func envExpandsEmbeddedReferences() { let env = PHEnvironment(variables: ["HOME": "/Users/tom", "BIN": "/opt/bin"]) #expect(env.expand("$HOME/bin:${BIN}/extra") == "/Users/tom/bin:/opt/bin/extra") } @Test func envExpandLeavesUnresolvedIntact() { // Unresolved references stay literal instead of being blanked, so a typo // remains visible. let env = PHEnvironment(variables: [:]) #expect(env.expand("$MISSING and ${ALSO_MISSING}") == "$MISSING and ${ALSO_MISSING}") } @Test func envExpandIgnoresNonIdentifierDollar() { let env = PHEnvironment(variables: ["FOO": "bar"]) // A `$` not followed by an identifier name is left untouched. #expect(env.expand("cost: $5 and $FOO") == "cost: $5 and bar") } // MARK: - merging @Test func envMergingOverridesExistingKeys() { let env = PHEnvironment(variables: ["PATH": "/usr/bin", "HOME": "/u"]) let merged = env.merging(["PATH": "/usr/bin:/extra"]) #expect(merged.variables["PATH"] == "/usr/bin:/extra") #expect(merged.variables["HOME"] == "/u") } @Test func envMergingNilReturnsSameVariables() { let env = PHEnvironment(variables: ["A": "1"]) #expect(env.merging(nil).variables == ["A": "1"]) } // MARK: - [env] value expansion (config load) private func makeConfig(env: [String: String]?) -> PHConfig { PHConfig(theme: "default", window: "default", blocks: nil, env: env, monitors: nil) } @Test func resolvingEnvExpandsValuesAgainstGivenEnvironment() { // `PATH = "$PATH:/extra"` extends an inherited variable. let config = makeConfig(env: [ "PATH": "$PATH:/opt/phbar/bin", "WORKDIR": "${TMPDIR}/phbar", "PLAIN": "literal", ]) let resolved = config.resolvingEnv(in: PHEnvironment(variables: [ "PATH": "/usr/bin", "TMPDIR": "/var/tmp", ])) #expect(resolved.env?["PATH"] == "/usr/bin:/opt/phbar/bin") #expect(resolved.env?["WORKDIR"] == "/var/tmp/phbar") #expect(resolved.env?["PLAIN"] == "literal") } @Test func resolvingEnvDefaultsToProcessEnvironment() { // `$TMPDIR` is set in phbar's launch environment, so this also exercises // real process-env expansion end to end. guard let tmpdir = ProcessInfo.processInfo.environment["TMPDIR"] else { return } let config = makeConfig(env: ["OUT": "$TMPDIR"]) let resolved = config.resolvingEnv() #expect(resolved.env?["OUT"] == tmpdir) } @Test func resolvingEnvLeavesConfigUntouchedWhenEnvAbsent() { let config = makeConfig(env: nil) #expect(config.resolvingEnv(in: .process).env == nil) } // MARK: - @EnvExpanded wrapper /// Stand-in Decodable that mirrors how a theme field opts into expansion. private struct Color: Decodable { @EnvExpanded var hex: String } @Test func envExpandedResolvesAgainstInjectedEnvironment() throws { let decoder = TOMLDecoder() decoder.userInfo[PHEnvironment.userInfoKey] = PHEnvironment(variables: ["ACCENT": "#ff0000"]) let color = try decoder.decode(Color.self, from: #"hex = "$ACCENT""#) #expect(color.hex == "#ff0000") } @Test func envExpandedFallsBackToLaunchEnvironment() throws { // Without injected userInfo the wrapper falls back to phbar's launch // environment (`ProcessInfo`), which is a snapshot taken at process start. guard let path = ProcessInfo.processInfo.environment["PATH"] else { return } let color = try TOMLDecoder().decode(Color.self, from: #"hex = "$PATH""#) #expect(color.hex == path) } @Test func envExpandedLeavesUnresolvedIntact() throws { let color = try TOMLDecoder().decode(Color.self, from: #"hex = "$NEVER_SET""#) #expect(color.hex == "$NEVER_SET") } // MARK: - Theme integration @Test func themeStyleColorExpandsEnvReference() throws { let decoder = TOMLDecoder() decoder.userInfo[PHEnvironment.userInfoKey] = PHEnvironment(variables: ["ACCENT": "#ff0000"]) let style = try decoder.decode(PHThemeStyleColor.self, from: #"color = "$ACCENT""#) #expect(style.color == "#ff0000") } @Test func themeTextFontExpandsEnvReference() throws { let decoder = TOMLDecoder() decoder.userInfo[PHEnvironment.userInfoKey] = PHEnvironment(variables: ["FONT": "Comic Code"]) let text = try decoder.decode( PHThemeText.self, from: """ name = "default" font = "$FONT" size = 14 weight = "regular" style = "normal" """ ) #expect(text.fontFamily == "Comic Code") }