Skip to content

Commit c66ab74

Browse files
committed
feat: add info command
Shows catalog metadata and local status for any game, installed or not. - Displays name, ID, year, status, alias (if set), archive/install path, and saved default executable - Status is one of: Not downloaded / Downloaded / Installed - Alias and Command lines are omitted when not applicable - Accepts GAME_ID or ALIAS as argument
1 parent 9b748ac commit c66ab74

2 files changed

Lines changed: 244 additions & 0 deletions

File tree

src/dosctl/commands/info.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""Info command — show catalog metadata and local status for a game."""
2+
import click
3+
from dosctl.config import DOWNLOADS_DIR, INSTALLED_DIR
4+
from dosctl.lib.decorators import ensure_cache
5+
from dosctl.lib.aliases import resolve_game_id, list_aliases
6+
from dosctl.lib.config_store import get_game_command
7+
8+
9+
@click.command()
10+
@click.argument("game_id", metavar="GAME_ID|ALIAS")
11+
@ensure_cache
12+
def info(collection, game_id):
13+
"""Show information about a game."""
14+
game_id = resolve_game_id(game_id)
15+
16+
game = collection.find_game(game_id)
17+
if not game:
18+
click.echo(f"Error: No game with ID '{game_id}' found in the collection.", err=True)
19+
return
20+
21+
# Resolve alias (invert the alias map to find any alias for this game)
22+
alias = next(
23+
(name for name, gid in list_aliases().items() if gid == game_id),
24+
None,
25+
)
26+
27+
# Determine status and relevant paths
28+
install_path = INSTALLED_DIR / game_id
29+
archive_path = DOWNLOADS_DIR / f"{game['name']}.zip"
30+
31+
if install_path.exists():
32+
status = "Installed"
33+
elif archive_path.exists():
34+
status = "Downloaded"
35+
else:
36+
status = "Not downloaded"
37+
38+
# Saved default executable
39+
command = get_game_command(game_id)
40+
41+
# Display
42+
click.echo(f"Name: {game['name']}")
43+
click.echo(f"ID: {game_id}")
44+
click.echo(f"Year: {game.get('year') or '----'}")
45+
if alias:
46+
click.echo(f"Alias: {alias}")
47+
click.echo(f"Status: {status}")
48+
if status == "Downloaded":
49+
click.echo(f"Archive: {archive_path}")
50+
if status == "Installed":
51+
click.echo(f"Path: {install_path}")
52+
if command:
53+
click.echo(f"Command: {command}")

tests/commands/test_info.py

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
"""Tests for the info command."""
2+
import json
3+
from unittest.mock import patch, MagicMock
4+
from click.testing import CliRunner
5+
from dosctl.main import cli
6+
7+
8+
GAME = {"id": "abc12345", "name": "Doom (1993)", "year": "1993", "full_path": "Doom (1993).zip"}
9+
10+
11+
def _make_collection(game=GAME):
12+
mock = MagicMock()
13+
mock.find_game.side_effect = lambda gid: game if gid == game["id"] else None
14+
return mock
15+
16+
17+
def _patch_aliases(tmp_path, aliases=None):
18+
f = tmp_path / "aliases.json"
19+
f.write_text(json.dumps(aliases or {}))
20+
return patch("dosctl.lib.aliases.ALIASES_FILE", f)
21+
22+
23+
# ---------------------------------------------------------------------------
24+
# Basic output
25+
# ---------------------------------------------------------------------------
26+
27+
class TestInfoCommand:
28+
def test_shows_name_id_year(self, tmp_path):
29+
runner = CliRunner()
30+
with _patch_aliases(tmp_path):
31+
with patch("dosctl.lib.decorators.create_collection") as mock_col:
32+
with patch("dosctl.commands.info.INSTALLED_DIR", tmp_path / "installed"):
33+
with patch("dosctl.commands.info.DOWNLOADS_DIR", tmp_path / "downloads"):
34+
with patch("dosctl.commands.info.get_game_command", return_value=None):
35+
mock_col.return_value = _make_collection()
36+
result = runner.invoke(cli, ["info", "abc12345"])
37+
38+
assert result.exit_code == 0
39+
assert "Doom (1993)" in result.output
40+
assert "abc12345" in result.output
41+
assert "1993" in result.output
42+
43+
def test_unknown_game_id_shows_error(self, tmp_path):
44+
runner = CliRunner()
45+
with _patch_aliases(tmp_path):
46+
with patch("dosctl.lib.decorators.create_collection") as mock_col:
47+
mock_col.return_value = _make_collection()
48+
result = runner.invoke(cli, ["info", "notfound"])
49+
50+
assert result.exit_code == 0
51+
assert "Error" in result.output
52+
53+
def test_game_with_no_year_shows_placeholder(self, tmp_path):
54+
game = {**GAME, "year": None}
55+
runner = CliRunner()
56+
with _patch_aliases(tmp_path):
57+
with patch("dosctl.lib.decorators.create_collection") as mock_col:
58+
with patch("dosctl.commands.info.INSTALLED_DIR", tmp_path / "installed"):
59+
with patch("dosctl.commands.info.DOWNLOADS_DIR", tmp_path / "downloads"):
60+
with patch("dosctl.commands.info.get_game_command", return_value=None):
61+
mock_col.return_value = _make_collection(game)
62+
result = runner.invoke(cli, ["info", "abc12345"])
63+
64+
assert result.exit_code == 0
65+
assert "----" in result.output
66+
67+
68+
# ---------------------------------------------------------------------------
69+
# Status
70+
# ---------------------------------------------------------------------------
71+
72+
class TestInfoStatus:
73+
def test_status_not_downloaded(self, tmp_path):
74+
runner = CliRunner()
75+
with _patch_aliases(tmp_path):
76+
with patch("dosctl.lib.decorators.create_collection") as mock_col:
77+
with patch("dosctl.commands.info.INSTALLED_DIR", tmp_path / "installed"):
78+
with patch("dosctl.commands.info.DOWNLOADS_DIR", tmp_path / "downloads"):
79+
with patch("dosctl.commands.info.get_game_command", return_value=None):
80+
mock_col.return_value = _make_collection()
81+
result = runner.invoke(cli, ["info", "abc12345"])
82+
83+
assert "Not downloaded" in result.output
84+
assert "Archive:" not in result.output
85+
assert "Path:" not in result.output
86+
87+
def test_status_downloaded(self, tmp_path):
88+
downloads = tmp_path / "downloads"
89+
downloads.mkdir()
90+
(downloads / "Doom (1993).zip").touch()
91+
92+
runner = CliRunner()
93+
with _patch_aliases(tmp_path):
94+
with patch("dosctl.lib.decorators.create_collection") as mock_col:
95+
with patch("dosctl.commands.info.INSTALLED_DIR", tmp_path / "installed"):
96+
with patch("dosctl.commands.info.DOWNLOADS_DIR", downloads):
97+
with patch("dosctl.commands.info.get_game_command", return_value=None):
98+
mock_col.return_value = _make_collection()
99+
result = runner.invoke(cli, ["info", "abc12345"])
100+
101+
assert "Downloaded" in result.output
102+
assert "Archive:" in result.output
103+
assert "Path:" not in result.output
104+
105+
def test_status_installed(self, tmp_path):
106+
installed = tmp_path / "installed"
107+
(installed / "abc12345").mkdir(parents=True)
108+
109+
runner = CliRunner()
110+
with _patch_aliases(tmp_path):
111+
with patch("dosctl.lib.decorators.create_collection") as mock_col:
112+
with patch("dosctl.commands.info.INSTALLED_DIR", installed):
113+
with patch("dosctl.commands.info.DOWNLOADS_DIR", tmp_path / "downloads"):
114+
with patch("dosctl.commands.info.get_game_command", return_value=None):
115+
mock_col.return_value = _make_collection()
116+
result = runner.invoke(cli, ["info", "abc12345"])
117+
118+
assert "Installed" in result.output
119+
assert "Path:" in result.output
120+
assert "Archive:" not in result.output
121+
122+
123+
# ---------------------------------------------------------------------------
124+
# Alias and command
125+
# ---------------------------------------------------------------------------
126+
127+
class TestInfoAliasAndCommand:
128+
def test_shows_alias_when_set(self, tmp_path):
129+
runner = CliRunner()
130+
with _patch_aliases(tmp_path, {"doom": "abc12345"}):
131+
with patch("dosctl.lib.decorators.create_collection") as mock_col:
132+
with patch("dosctl.commands.info.INSTALLED_DIR", tmp_path / "installed"):
133+
with patch("dosctl.commands.info.DOWNLOADS_DIR", tmp_path / "downloads"):
134+
with patch("dosctl.commands.info.get_game_command", return_value=None):
135+
mock_col.return_value = _make_collection()
136+
result = runner.invoke(cli, ["info", "abc12345"])
137+
138+
assert "Alias:" in result.output
139+
assert "doom" in result.output
140+
141+
def test_omits_alias_line_when_none(self, tmp_path):
142+
runner = CliRunner()
143+
with _patch_aliases(tmp_path):
144+
with patch("dosctl.lib.decorators.create_collection") as mock_col:
145+
with patch("dosctl.commands.info.INSTALLED_DIR", tmp_path / "installed"):
146+
with patch("dosctl.commands.info.DOWNLOADS_DIR", tmp_path / "downloads"):
147+
with patch("dosctl.commands.info.get_game_command", return_value=None):
148+
mock_col.return_value = _make_collection()
149+
result = runner.invoke(cli, ["info", "abc12345"])
150+
151+
assert "Alias:" not in result.output
152+
153+
def test_shows_saved_command(self, tmp_path):
154+
runner = CliRunner()
155+
with _patch_aliases(tmp_path):
156+
with patch("dosctl.lib.decorators.create_collection") as mock_col:
157+
with patch("dosctl.commands.info.INSTALLED_DIR", tmp_path / "installed"):
158+
with patch("dosctl.commands.info.DOWNLOADS_DIR", tmp_path / "downloads"):
159+
with patch("dosctl.commands.info.get_game_command", return_value="DOOM.EXE"):
160+
mock_col.return_value = _make_collection()
161+
result = runner.invoke(cli, ["info", "abc12345"])
162+
163+
assert "Command:" in result.output
164+
assert "DOOM.EXE" in result.output
165+
166+
def test_omits_command_line_when_none(self, tmp_path):
167+
runner = CliRunner()
168+
with _patch_aliases(tmp_path):
169+
with patch("dosctl.lib.decorators.create_collection") as mock_col:
170+
with patch("dosctl.commands.info.INSTALLED_DIR", tmp_path / "installed"):
171+
with patch("dosctl.commands.info.DOWNLOADS_DIR", tmp_path / "downloads"):
172+
with patch("dosctl.commands.info.get_game_command", return_value=None):
173+
mock_col.return_value = _make_collection()
174+
result = runner.invoke(cli, ["info", "abc12345"])
175+
176+
assert "Command:" not in result.output
177+
178+
def test_resolves_alias_as_argument(self, tmp_path):
179+
"""Passing an alias instead of a raw ID should work."""
180+
runner = CliRunner()
181+
with _patch_aliases(tmp_path, {"doom": "abc12345"}):
182+
with patch("dosctl.lib.decorators.create_collection") as mock_col:
183+
with patch("dosctl.commands.info.INSTALLED_DIR", tmp_path / "installed"):
184+
with patch("dosctl.commands.info.DOWNLOADS_DIR", tmp_path / "downloads"):
185+
with patch("dosctl.commands.info.get_game_command", return_value=None):
186+
mock_col.return_value = _make_collection()
187+
result = runner.invoke(cli, ["info", "doom"])
188+
189+
assert result.exit_code == 0
190+
assert "Doom (1993)" in result.output
191+
assert "abc12345" in result.output

0 commit comments

Comments
 (0)