feat: load optional XDG TOML configuration with session overrides
This commit is contained in:
+44
-11
@@ -21,10 +21,14 @@ def check_cli_instances(root, app, fixtures):
|
||||
runtime = shutil.which("deno") or shutil.which("node")
|
||||
canonical = "https://www.youtube.com/watch?v=aqz-KE-bpKQ"
|
||||
|
||||
def invoke(arguments):
|
||||
result = subprocess.run([str(cli), *arguments], capture_output=True, text=True, timeout=20)
|
||||
if result.returncode:
|
||||
raise RuntimeError(f"CLI {arguments} failed: {result.returncode}; {result.stdout}{result.stderr}")
|
||||
config = root / "xdg/terebi/config.toml"
|
||||
environment = dict(os.environ, XDG_CONFIG_HOME=str(config.parent.parent))
|
||||
|
||||
def invoke(arguments, expected_code=0):
|
||||
result = subprocess.run([str(cli), *arguments], capture_output=True, text=True, timeout=20,
|
||||
env=environment)
|
||||
if result.returncode != expected_code:
|
||||
raise RuntimeError(f"CLI {arguments} returned {result.returncode}, expected {expected_code}; {result.stdout}{result.stderr}")
|
||||
return result
|
||||
|
||||
def wait_states(count, ready, message):
|
||||
@@ -41,6 +45,8 @@ def check_cli_instances(root, app, fixtures):
|
||||
return states
|
||||
time.sleep(0.02) # Infrastructure deadline; assertions use native events/PTS, not wall dates.
|
||||
raise RuntimeError(f"{message}: native state deadline: {states}")
|
||||
def settings(state):
|
||||
return (state["showBufferInfo"], state["automaticQuality"], state["snapToEdges"])
|
||||
|
||||
with StreamingServer(fixtures) as server:
|
||||
if extractor and runtime:
|
||||
@@ -58,8 +64,8 @@ def check_cli_instances(root, app, fixtures):
|
||||
+ " --load-info-json " + shlex.quote(str(metadata))
|
||||
+ ' "${args[@]:0:${#args[@]}-2}"\n')
|
||||
wrapper.chmod(0o755)
|
||||
settings = {"directory": str(root), "path": str(root) + os.pathsep + os.environ.get("PATH", "")}
|
||||
(app / "Contents/Resources/cli-check.json").write_text(json.dumps(settings))
|
||||
runtime_settings = {"directory": str(root), "path": str(root) + os.pathsep + os.environ.get("PATH", "")}
|
||||
(app / "Contents/Resources/cli-check.json").write_text(json.dumps(runtime_settings))
|
||||
movie = fixtures / "cache.mp4"
|
||||
audio = root / "audio with spaces.wav"
|
||||
shutil.copy2(fixtures / "audio.wav", audio)
|
||||
@@ -69,15 +75,23 @@ def check_cli_instances(root, app, fixtures):
|
||||
and state["position"] > 0 and state["hasVideo"] for state in states.values()),
|
||||
"cold default CLI opens one actual player window")
|
||||
first_pid = next(iter(first))
|
||||
if settings(first[first_pid]) != (False, False, True) or config.parent.exists():
|
||||
raise RuntimeError(f"missing config must use defaults without creating a file: {first}")
|
||||
invoke([])
|
||||
wait_states(1, lambda states: states[first_pid]["media"] == movie.name,
|
||||
"warm default CLI reuses the existing process without replacing media")
|
||||
|
||||
config.parent.mkdir(parents=True)
|
||||
config_text = '"show_buffer_info" = true # optional display\n automatic_quality = true\nsnap_to_edges = false\n'
|
||||
config.write_text(config_text)
|
||||
invoke(["--new-instance"])
|
||||
second = wait_states(2, lambda states: any(state["media"] == "" and state["status"] == 0
|
||||
and settings(state) == (True, True, False)
|
||||
for pid, state in states.items() if pid != first_pid),
|
||||
"empty independent CLI creates exactly one extra player")
|
||||
empty_pid = next(pid for pid in second if pid != first_pid)
|
||||
if settings(second[first_pid]) != (False, False, True) or config.read_text() != config_text:
|
||||
raise RuntimeError(f"config loading changed an existing session or wrote the file: {second}")
|
||||
invoke(["--new-instance", str(audio)])
|
||||
third = wait_states(3, lambda states: any(state["media"] == audio.name and state["status"] == 3
|
||||
and state["hasAudio"] and not state["hasVideo"]
|
||||
@@ -85,17 +99,34 @@ def check_cli_instances(root, app, fixtures):
|
||||
"independent local media reaches only the freshly launched process")
|
||||
if third[first_pid]["media"] != movie.name or third[first_pid]["status"] != 3 or third[empty_pid]["media"]:
|
||||
raise RuntimeError(f"independent launch disturbed an existing player: {third}")
|
||||
count = 3
|
||||
config.write_text("show_buffer_info = [\n")
|
||||
failure = invoke(["--new-instance"], expected_code=78)
|
||||
if str(config) not in failure.stderr:
|
||||
raise RuntimeError(f"configuration failure omitted its path: {failure.stderr}")
|
||||
invoke(["--no-config"])
|
||||
unchanged = wait_states(3, lambda _: True, "bypass reuses an existing process without resetting its settings")
|
||||
if any(settings(unchanged[pid]) != settings(third[pid]) for pid in third):
|
||||
raise RuntimeError(f"warm bypass changed an existing session: {third}; {unchanged}")
|
||||
invoke(["--no-config", "--new-instance", str(movie)])
|
||||
bypass = wait_states(4, lambda states: any(state["media"] == movie.name and state["status"] == 3
|
||||
and settings(state) == (False, False, True)
|
||||
for pid, state in states.items() if pid not in third),
|
||||
"fresh bypass skips invalid config and plays with built-in settings")
|
||||
invoke(["--no-config", "--new-instance", "--help"])
|
||||
wait_states(4, lambda _: True, "help skips invalid config without starting another process")
|
||||
config.write_text(config_text)
|
||||
count = 4
|
||||
if extractor and runtime:
|
||||
invoke(["--new-instance", "--url", canonical])
|
||||
fourth = wait_states(4, lambda states: any(
|
||||
fourth = wait_states(5, lambda states: any(
|
||||
state["media"] == "Independent YouTube fixture" and state["status"] == 3
|
||||
and state["position"] > 0 and state["hasAudio"] and state["hasVideo"]
|
||||
for pid, state in states.items() if pid not in third),
|
||||
and settings(state) == (True, True, False)
|
||||
for pid, state in states.items() if pid not in bypass),
|
||||
"independent YouTube playback reaches only one new registered process")
|
||||
if fourth[first_pid]["media"] != movie.name or fourth[first_pid]["status"] != 3 or fourth[empty_pid]["media"]:
|
||||
raise RuntimeError(f"independent YouTube launch disturbed existing playback: {fourth}")
|
||||
count = 4
|
||||
count = 5
|
||||
else:
|
||||
print("SKIP independent YouTube playback: yt-dlp and a supported JS runtime are required")
|
||||
|
||||
@@ -111,7 +142,9 @@ def check_cli_instances(root, app, fixtures):
|
||||
changed = [pid for pid in before if before[pid]["media"] != after[pid]["media"]]
|
||||
if len(changed) != 1:
|
||||
raise RuntimeError(f"default media open changed multiple players: {before}; {after}")
|
||||
print("PASS actual CLI instances: cold/default reuse, independent empty/local/YouTube, one window per PID, targeted media, original playback retained, help, warm replacement")
|
||||
if config.read_text() != config_text:
|
||||
raise RuntimeError("native app startup modified config.toml")
|
||||
print("PASS actual CLI instances: native startup config/defaults/invalid-file error/bypass/read-only settings, cold/default reuse, independent empty/local/YouTube, one window per PID, targeted media, original playback retained, help, warm replacement")
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -71,7 +71,10 @@ private enum CLIInstanceCheck {
|
||||
let directory = URL(fileURLWithPath: settings["directory"]!, isDirectory: true)
|
||||
setenv("PATH", settings["path"]!, 1)
|
||||
_ = NSApplication.shared
|
||||
PlayerModel.shared.setVolume(0)
|
||||
_ = NotificationCenter.default.addObserver(forName: NSApplication.willFinishLaunchingNotification,
|
||||
object: nil, queue: .main) { _ in
|
||||
MainActor.assumeIsolated { PlayerModel.shared.setVolume(0) }
|
||||
}
|
||||
// Exercise the real SwiftUI scene and delegate without displaying test windows.
|
||||
_ = NotificationCenter.default.addObserver(forName: NSWindow.didUpdateNotification, object: nil, queue: .main) { _ in
|
||||
MainActor.assumeIsolated { for window in NSApp.windows { window.alphaValue = 0 } }
|
||||
@@ -95,6 +98,9 @@ private enum CLIInstanceCheck {
|
||||
"hasAudio": model.state.has_audio,
|
||||
"hasVideo": model.state.has_video,
|
||||
"error": model.errorText ?? "",
|
||||
"showBufferInfo": model.showsBufferInfo,
|
||||
"automaticQuality": model.automaticYouTubeQuality,
|
||||
"snapToEdges": model.compactWindow.snappingEnabled,
|
||||
]
|
||||
do {
|
||||
let data = try JSONSerialization.data(withJSONObject: snapshot)
|
||||
@@ -102,6 +108,6 @@ private enum CLIInstanceCheck {
|
||||
} catch { fatalError("Could not publish CLI instance check: \(error)") }
|
||||
}
|
||||
}
|
||||
TerebiApp.main()
|
||||
terebiAppMain()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import AppKit
|
||||
import TerebiConfiguration
|
||||
|
||||
@MainActor
|
||||
private final class FixedQualityClock { var now = 1000.0 }
|
||||
@@ -65,15 +66,16 @@ struct AutomaticQualityPlaybackChecks {
|
||||
"reselecting the current manual resolution disables auto without disrupting playback")
|
||||
model.setAutomaticYouTubeQuality(true)
|
||||
let reopened = PlayerModel(defaults: defaults, qualityClock: { clock.now })
|
||||
require(reopened.automaticYouTubeQuality, "explicit opt-in survives player restart")
|
||||
require(!reopened.automaticYouTubeQuality, "session opt-in does not survive player restart")
|
||||
reopened.shutdown()
|
||||
model.setYouTubeQuality(nil)
|
||||
require(!model.automaticYouTubeQuality && model.youtubeQuality == nil,
|
||||
"default quality is also a manual override")
|
||||
let manual = PlayerModel(defaults: defaults, qualityClock: { clock.now })
|
||||
require(!manual.automaticYouTubeQuality, "manual override persists after restart")
|
||||
let manual = PlayerModel(defaults: defaults, configuration: PlayerConfiguration(automaticQuality: true),
|
||||
qualityClock: { clock.now })
|
||||
require(manual.automaticYouTubeQuality, "manual session override does not replace startup configuration")
|
||||
manual.shutdown()
|
||||
model.closeFile()
|
||||
print("PASS native adaptive playback: default-off/stalled downshift/healthy upshift/progress preservation/current+default manual override/opt-in persistence")
|
||||
print("PASS native adaptive playback: default-off/stalled downshift/healthy upshift/progress preservation/current+default manual override/session-only settings")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ struct CompactWindowChecks {
|
||||
"target width shrinks to usable bounds without distorting ordinary aspect")
|
||||
}
|
||||
|
||||
static func windowBehavior(defaults: UserDefaults) throws {
|
||||
static func windowBehavior() throws {
|
||||
_ = NSApplication.shared
|
||||
guard let screen = NSScreen.main else {
|
||||
throw Failure(description: "FAIL: native window checks require a macOS GUI session with a screen")
|
||||
@@ -127,7 +127,7 @@ struct CompactWindowChecks {
|
||||
let originalHides = window.hidesOnDeactivate
|
||||
let originalDrag = window.isMovableByWindowBackground
|
||||
let identity = ObjectIdentifier(window)
|
||||
let controller = CompactWindowController(defaults: defaults)
|
||||
let controller = CompactWindowController()
|
||||
defer { controller.restore(resumeFullScreen: false) }
|
||||
controller.attach(window)
|
||||
try require(!controller.isCompact && !controller.isTransitioning, "attached window starts ordinary")
|
||||
@@ -182,14 +182,14 @@ struct CompactWindowChecks {
|
||||
try require(close(window.frame.minX, visible.minX + 16) && close(window.frame.minY, visible.minY + 16)
|
||||
&& window.frame.size == nearEdge.size, "enabled preference snaps the actual moved window")
|
||||
controller.snappingEnabled = false
|
||||
let second = CompactWindowController(defaults: defaults)
|
||||
try require(!second.snappingEnabled, "disabled preference persists across controller instances")
|
||||
let second = CompactWindowController(snappingEnabled: false)
|
||||
try require(!second.snappingEnabled, "configuration can disable snapping for a new controller")
|
||||
window.setFrame(nearEdge, display: false)
|
||||
controller.snapAfterDrag()
|
||||
try require(sameFrame(window.frame, nearEdge), "disabled preference leaves an actual moved window untouched")
|
||||
second.snappingEnabled = true
|
||||
let third = CompactWindowController(defaults: defaults)
|
||||
try require(third.snappingEnabled, "re-enabled preference persists across controller instances")
|
||||
let third = CompactWindowController(snappingEnabled: false)
|
||||
try require(!third.snappingEnabled, "a session override does not change the next controller's settings")
|
||||
controller.snappingEnabled = true
|
||||
let resized = NSRect(x: nearEdge.minX, y: nearEdge.minY, width: 240, height: 135)
|
||||
window.setFrame(resized, display: false)
|
||||
@@ -218,13 +218,8 @@ struct CompactWindowChecks {
|
||||
}
|
||||
|
||||
static func main() throws {
|
||||
let suite = "terebi.compact-window-checks.\(UUID().uuidString)"
|
||||
guard let defaults = UserDefaults(suiteName: suite) else {
|
||||
throw Failure(description: "FAIL: cannot create isolated preference suite")
|
||||
}
|
||||
defer { defaults.removePersistentDomain(forName: suite) }
|
||||
try geometry()
|
||||
try windowBehavior(defaults: defaults)
|
||||
try windowBehavior()
|
||||
print("Compact window checks passed")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -16,11 +16,12 @@ struct PlayerCommandChecks {
|
||||
let instanceFile = directory.appendingPathComponent("--new-instance")
|
||||
for file in [movie, optionFile, colonFile, instanceFile] { try Data().write(to: file) }
|
||||
|
||||
func opened(_ arguments: [String], newInstance: Bool = false) throws -> URL? {
|
||||
guard case .launch(let url, let independent) = try PlayerCommand.parse(arguments, directory: directory) else {
|
||||
func opened(_ arguments: [String], newInstance: Bool = false, noConfig: Bool = false) throws -> URL? {
|
||||
guard case .launch(let url, let independent, let bypass) = try PlayerCommand.parse(arguments, directory: directory) else {
|
||||
fatalError("FAIL: expected a launch command")
|
||||
}
|
||||
require(independent == newInstance, "launch mode preserves default reuse or explicit independence")
|
||||
require(bypass == noConfig, "configuration loading defaults on and can be explicitly bypassed")
|
||||
return url
|
||||
}
|
||||
require(try opened([movie.lastPathComponent]) == movie, "relative paths resolve against invocation directory, preserving spaces")
|
||||
@@ -40,6 +41,17 @@ struct PlayerCommandChecks {
|
||||
"independent launch preserves the filename terminator")
|
||||
require(try opened(["--", "--new-instance"]) == instanceFile,
|
||||
"an escaped instance flag is a filename, not a launch option")
|
||||
require(try opened(["--no-config"], noConfig: true) == nil, "configuration bypass can omit media")
|
||||
require(try opened(["--no-config", movie.path], noConfig: true) == movie,
|
||||
"configuration bypass retains local media delivery")
|
||||
require(try opened(["--new-instance", "--no-config", "--url", canonical.absoluteString],
|
||||
newInstance: true, noConfig: true) == canonical,
|
||||
"independent bypass retains explicit YouTube identity")
|
||||
require(try opened(["--no-config", "--new-instance", "--", "--help"],
|
||||
newInstance: true, noConfig: true) == optionFile, "launch options work in either order")
|
||||
guard case .help = try PlayerCommand.parse(["--no-config", "--new-instance", "--help"]) else {
|
||||
fatalError("FAIL: help with launch options must not launch a player")
|
||||
}
|
||||
guard case .help = try PlayerCommand.parse(["--new-instance", "--help"]) else {
|
||||
fatalError("FAIL: help must not launch an independent player")
|
||||
}
|
||||
@@ -49,7 +61,8 @@ struct PlayerCommandChecks {
|
||||
["file://remote-host/video.mp4"], ["https://example.com/video.mp4"],
|
||||
["--new-instance", "--new-instance"], ["--new-instance", "--url"],
|
||||
["--new-instance", "--"], ["--new-instance", movie.path, movie.path],
|
||||
["--new-instance", "--unknown"]] {
|
||||
["--new-instance", "--unknown"], ["--no-config", "--no-config"],
|
||||
["--no-config", "--new-instance", "--no-config"]] {
|
||||
do {
|
||||
_ = try PlayerCommand.parse(arguments, directory: directory)
|
||||
fatalError("FAIL: accepted an invalid command")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import AppKit
|
||||
import MediaPlayer
|
||||
import TerebiConfiguration
|
||||
|
||||
@main
|
||||
@MainActor
|
||||
@@ -29,13 +30,20 @@ struct SystemPlaybackChecks {
|
||||
"buffer readout can be enabled before playback and reveals controls")
|
||||
initialModel.shutdown()
|
||||
let restoredModel = PlayerModel(defaults: defaults)
|
||||
require(restoredModel.showsBufferInfo, "enabled buffer readout survives model recreation")
|
||||
restoredModel.setShowsBufferInfo(false)
|
||||
require(!restoredModel.showsBufferInfo, "buffer readout can be explicitly disabled")
|
||||
require(!restoredModel.showsBufferInfo, "session opt-in does not survive model recreation")
|
||||
restoredModel.shutdown()
|
||||
let configuredModel = PlayerModel(defaults: defaults, configuration:
|
||||
PlayerConfiguration(showBufferInfo: true, automaticQuality: true, snapToEdges: false))
|
||||
require(configuredModel.showsBufferInfo && configuredModel.automaticYouTubeQuality
|
||||
&& !configuredModel.compactWindow.snappingEnabled, "configuration initializes all three controls")
|
||||
configuredModel.setShowsBufferInfo(false)
|
||||
configuredModel.setAutomaticYouTubeQuality(false)
|
||||
configuredModel.compactWindow.snappingEnabled = true
|
||||
configuredModel.shutdown()
|
||||
let model = PlayerModel(defaults: defaults)
|
||||
defer { model.shutdown() }
|
||||
require(!model.showsBufferInfo, "explicitly disabled buffer readout survives model recreation")
|
||||
require(!model.showsBufferInfo && !model.automaticYouTubeQuality && model.compactWindow.snappingEnabled,
|
||||
"session overrides never replace built-in settings")
|
||||
model.setVolume(0)
|
||||
let directory = URL(fileURLWithPath: CommandLine.arguments[1], isDirectory: true)
|
||||
let center = MPNowPlayingInfoCenter.default()
|
||||
|
||||
Reference in New Issue
Block a user