mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-29 18:16:43 +00:00
fix(oci): clarify Rclone dry runs and preserve HedgeDoc description
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
"""Inert, extracted consumers for Rclone guidance and HedgeDoc metadata."""
|
||||
import ast
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest import TestCase
|
||||
from unittest.mock import Mock
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
INSTALLER = ROOT / 'oci/src/proxmenux_oci/installer.py'
|
||||
RCLONE = ('Configure the remote in the Rclone Web UI first. Checking its name may start a '
|
||||
'stopped CT, even in a dry run. Applying the mount restarts the CT and attempts '
|
||||
'to publish two FUSE views on the host; a dry run does not publish them.')
|
||||
|
||||
|
||||
def extract_function(path, name, **dependencies):
|
||||
node = next(n for n in ast.parse(path.read_text()).body
|
||||
if isinstance(n, ast.FunctionDef) and n.name == name)
|
||||
scope = dict(dependencies)
|
||||
future = ast.ImportFrom(module='__future__', names=[ast.alias(name='annotations')], level=0)
|
||||
exec(compile(ast.fix_missing_locations(ast.Module(body=[future, node], type_ignores=[])),
|
||||
str(path), 'exec'), scope)
|
||||
return scope[name]
|
||||
|
||||
|
||||
class RcloneHedgeDocWording(TestCase):
|
||||
def test_i18n_workflow_runs_for_remote_shell_edits(self):
|
||||
workflow = (ROOT / '.github/workflows/test-i18n.yml').read_text()
|
||||
self.assertEqual(workflow.count(" - 'oci/remote/**/*.sh'"), 2)
|
||||
|
||||
def test_remote_dry_run_messages_are_extracted_and_translate_with_fallback(self):
|
||||
import runpy
|
||||
extractor = runpy.run_path(str(ROOT / '.github/scripts/build_translation_cache.py'))
|
||||
shell = ROOT / 'oci/remote/configure_rclone_mount.sh'
|
||||
keys = [
|
||||
'Dry run completed; mount configuration was not applied. The CT was already running.',
|
||||
'Dry run completed; mount configuration was not applied. The CT was started for the remote check.',
|
||||
]
|
||||
extracted = extractor['extract_translate_texts'](ROOT / 'oci/remote')
|
||||
self.assertTrue(set(keys).issubset(extracted))
|
||||
ui = (ROOT / 'oci/remote/oci_ui.sh').read_text()
|
||||
lookup = 'translate() {' + ui.split('translate() {', 1)[1].split('\n}', 1)[0] + '\n}\n'
|
||||
gate = shell.read_text().split('if [[ $DRY_RUN == 1 ]]; then', 1)[1].split('\nfi\n\nRC_USER=', 1)[0]
|
||||
gate = 'if [[ $DRY_RUN == 1 ]]; then' + gate + '\nfi\n'
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
cache = Path(directory) / 'locale.json'
|
||||
for status, expected_key in [('running', keys[0]), ('stopped', keys[1])]:
|
||||
for translations, expected in [({}, expected_key),
|
||||
({expected_key: 'MESSAGGIO DI TEST'}, 'MESSAGGIO DI TEST')]:
|
||||
with self.subTest(status=status, translations=translations):
|
||||
cache.write_text(json.dumps(translations))
|
||||
setup = (f'_OCI_LANGUAGE=it\n_OCI_LANG_FILE={cache}\n'
|
||||
f'STATUS={status}\nDRY_RUN=1\n' + lookup +
|
||||
'msg_ok() { printf "%s\\n" "$1"; }\n')
|
||||
result = subprocess.run(['bash', '-c', setup + gate],
|
||||
text=True, capture_output=True, timeout=5)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(result.stdout.strip(), expected)
|
||||
|
||||
def test_remote_dry_run_reports_stopped_ct_start_and_no_mount_publication(self):
|
||||
script = (ROOT / 'oci/remote/configure_rclone_mount.sh').read_text()
|
||||
seam = script.split('msg_info "$(translate "Checking the remote...")"', 1)[1].split('\nfi\n\nRC_USER=', 1)[0]
|
||||
# This is the real status/listremotes/dry-run gate, not the root-guarded script.
|
||||
seam = 'msg_info "$(translate "Checking the remote...")"' + seam + '\nfi\n'
|
||||
for status, dry_run, starts, post_gate in (
|
||||
('stopped', '1', 1, False), ('running', '1', 0, False),
|
||||
('stopped', '0', 1, True), ('running', '0', 0, True)):
|
||||
with self.subTest(status=status, dry_run=dry_run):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
trace = Path(directory) / 'pct.log'
|
||||
harness = '''
|
||||
set -Eeuo pipefail
|
||||
VMID=101 REMOTE_NAME=notes DRY_RUN=%s
|
||||
TRACE_FILE=%s
|
||||
pct() {
|
||||
printf 'pct:%%s\\n' "$*" >>"$TRACE_FILE"
|
||||
case "$1" in
|
||||
status) printf 'status: %s\\n' ;;
|
||||
exec) if [[ "${5:-}" == version ]]; then return 0; else printf 'notes:\\n'; fi ;;
|
||||
start) return 0 ;;
|
||||
*) return 97 ;;
|
||||
esac
|
||||
}
|
||||
oci_quiet() { "$@"; }
|
||||
oci_log() { :; }
|
||||
sleep() { :; }
|
||||
translate() { printf '%%s' "$1"; }
|
||||
msg_info() { printf 'info:%%s\\n' "$1"; }
|
||||
msg_ok() { printf 'ok:%%s\\n' "$1"; }
|
||||
die() { printf 'error:%%s\\n' "$1"; exit 1; }
|
||||
''' % (dry_run, str(trace), status)
|
||||
result = subprocess.run(['bash', '-c', harness + seam + '\nprintf "POST_DRY_GATE\\n"\n'],
|
||||
text=True, capture_output=True, timeout=5)
|
||||
calls = trace.read_text()
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(calls.count('pct:start 101'), starts)
|
||||
self.assertIn('pct:exec 101 -- /usr/local/bin/rclone listremotes', calls)
|
||||
self.assertEqual('POST_DRY_GATE' in result.stdout, post_gate)
|
||||
if dry_run == '1':
|
||||
self.assertIn('mount configuration was not applied', result.stdout)
|
||||
if starts:
|
||||
self.assertIn('CT was started for the remote check', result.stdout)
|
||||
else:
|
||||
self.assertIn('CT was already running', result.stdout)
|
||||
self.assertNotIn('pct:stop', calls)
|
||||
|
||||
def test_hedgedoc_overlay_survives_index_enrichment(self):
|
||||
sys.path.insert(0, str(ROOT / 'oci/src'))
|
||||
try:
|
||||
from proxmenux_oci.catalog import Catalog
|
||||
finally:
|
||||
sys.path.pop(0)
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
catalog = Catalog(root, source=Mock())
|
||||
catalog.apps_dir.mkdir(parents=True)
|
||||
catalog.overlays_dir.mkdir(parents=True)
|
||||
template = json.loads((ROOT / 'oci/catalog/apps/hedgedoc.json').read_text())
|
||||
upstream = 'HedgeDoc gives you access to all your files wherever you are.'
|
||||
corrected = 'HedgeDoc is a self-hosted collaborative Markdown editor for notes.'
|
||||
template['catalog_ui']['description']['en_US'] = upstream
|
||||
template['catalog_ui']['tagline']['en_US'] = upstream
|
||||
overlay = json.loads((ROOT / 'oci/catalog/overlays/hedgedoc.json').read_text())
|
||||
self.assertTrue(overlay['catalog_ui']['hidden'])
|
||||
self.assertEqual(overlay['catalog_ui']['description']['en_US'], corrected)
|
||||
self.assertEqual(overlay['catalog_ui']['tagline']['en_US'], corrected)
|
||||
(catalog.overlays_dir / 'hedgedoc.json').write_text(json.dumps(overlay))
|
||||
catalog._apply_overlay('hedgedoc', template)
|
||||
self.assertEqual(template['catalog_ui']['description']['en_US'], corrected)
|
||||
self.assertEqual(template['catalog_ui']['tagline']['en_US'], corrected)
|
||||
(catalog.apps_dir / 'hedgedoc.json').write_text(json.dumps(template))
|
||||
payload = {'applications': [{'id': 'hedgedoc', 'description': upstream},
|
||||
{'id': 'unmodified', 'description': 'Original summary'}]}
|
||||
catalog._enrich_index_from_templates(payload)
|
||||
self.assertEqual(payload['applications'][0]['description'], corrected)
|
||||
self.assertEqual(payload['applications'][1]['description'], 'Original summary')
|
||||
|
||||
def test_rclone_guidance_precedes_inputs_and_confirmation(self):
|
||||
events = []
|
||||
class UI:
|
||||
def message(self, text): events.append(('message', text))
|
||||
def ask(self, label, default=None, required=True):
|
||||
events.append(('ask', label))
|
||||
return {'VMID of the Rclone OCI container': '101',
|
||||
'Exact name of the remote': 'notes',
|
||||
'Mount name': 'notes'}.get(label, default or '')
|
||||
def choose(self, label, options, default):
|
||||
events.append(('choose', label))
|
||||
return default
|
||||
def confirm(self, label, default):
|
||||
events.append(('confirm', label))
|
||||
return True
|
||||
schema = {name: {'default': value} for name, value in (
|
||||
('vfs_cache_mode', 'writes'), ('shared_mount_root_parent', '/mnt/rclone'),
|
||||
('shared_mount_root', '/mnt/rclone/rw'),
|
||||
('shared_mount_read_only_root', '/mnt/rclone/ro'))}
|
||||
schema['vfs_cache_mode']['options'] = ['writes']
|
||||
template = {'proxmox': {'laboratory_contract': {'configuration_schema': schema}}}
|
||||
fn = extract_function(INSTALLER, 'build_rclone_mount_deployment',
|
||||
TerminalUI=UI, DialogUI=UI, translate=lambda text: text,
|
||||
re=re, InstallError=ValueError, UserCancelled=ValueError)
|
||||
deployment = fn(template, UI())
|
||||
self.assertEqual(events[0], ('message', RCLONE))
|
||||
self.assertEqual(events[1][0], 'ask')
|
||||
self.assertEqual(events[-1][0], 'confirm')
|
||||
self.assertEqual(deployment['remote_name'], 'notes')
|
||||
|
||||
def test_hedgedoc_detail_uses_corrected_template_and_index(self):
|
||||
key = 'HedgeDoc is a self-hosted collaborative Markdown editor for notes.'
|
||||
template = json.loads((ROOT / 'oci/catalog/apps/hedgedoc.json').read_text())
|
||||
item = next(item for item in json.loads((ROOT / 'oci/catalog/index.json').read_text())['applications']
|
||||
if item['id'] == 'hedgedoc')
|
||||
self.assertEqual(template['catalog_ui']['tagline']['en_US'], key)
|
||||
self.assertEqual(template['catalog_ui']['description']['en_US'], key)
|
||||
self.assertEqual(item['description'], key)
|
||||
source = extract_function(ROOT / 'oci/src/proxmenux_oci/i18n.py', 'source_text',
|
||||
Any=object)
|
||||
fn = extract_function(ROOT / 'oci/src/proxmenux_oci/cli.py', '_app_detail_text',
|
||||
source_text=source, translate=lambda text: text,
|
||||
textwrap=__import__('textwrap'), DETAIL_WIDTH=92,
|
||||
publisher=lambda _: 'LinuxServer.io',
|
||||
is_tested=lambda _: False,
|
||||
_display_architectures=lambda _: 'amd64',
|
||||
_image_label=lambda text: text,
|
||||
_recommended_storage=lambda _: 'Container volume')
|
||||
result = fn(SimpleNamespace(), item, template)
|
||||
self.assertIn(key, result)
|
||||
self.assertNotIn('gives you access to all your files', result)
|
||||
|
||||
def test_missing_empty_and_synthetic_translation_on_real_consumers(self):
|
||||
from unittest.mock import Mock
|
||||
key = 'HedgeDoc is a self-hosted collaborative Markdown editor for notes.'
|
||||
scope = {'language': lambda: 'it', '_cache': {}}
|
||||
lookup = extract_function(ROOT / 'oci/src/proxmenux_oci/i18n.py', 'translate', **scope)
|
||||
cache = lookup.__globals__
|
||||
template = json.loads((ROOT / 'oci/catalog/apps/hedgedoc.json').read_text())
|
||||
item = next(x for x in json.loads((ROOT / 'oci/catalog/index.json').read_text())['applications']
|
||||
if x['id'] == 'hedgedoc')
|
||||
source = extract_function(ROOT / 'oci/src/proxmenux_oci/i18n.py', 'source_text')
|
||||
detail = extract_function(ROOT / 'oci/src/proxmenux_oci/cli.py', '_app_detail_text',
|
||||
source_text=source, translate=lookup,
|
||||
textwrap=__import__('textwrap'), DETAIL_WIDTH=92,
|
||||
publisher=lambda _: 'LinuxServer.io', is_tested=lambda _: False,
|
||||
_display_architectures=lambda _: 'amd64',
|
||||
_image_label=lambda text: text,
|
||||
_recommended_storage=lambda _: 'Container volume')
|
||||
for values, expected_rclone, expected_hedgedoc in (
|
||||
({}, RCLONE, key),
|
||||
({RCLONE: '', key: ''}, RCLONE, key),
|
||||
({RCLONE: 'GUIDA RCLONE', key: 'EDITOR COLLABORATIVO'},
|
||||
'GUIDA RCLONE', 'EDITOR COLLABORATIVO')):
|
||||
with self.subTest(values=values):
|
||||
cache['_cache'] = values
|
||||
ui = SimpleNamespace(message=Mock(), ask=lambda *_args, **_kw: (_ for _ in ()).throw(StopIteration()))
|
||||
rclone = extract_function(INSTALLER, 'build_rclone_mount_deployment',
|
||||
TerminalUI=object, DialogUI=object, translate=lookup,
|
||||
re=re, InstallError=ValueError,
|
||||
UserCancelled=ValueError)
|
||||
# Stop at the first prompt: no operational path is entered.
|
||||
with self.assertRaises(StopIteration):
|
||||
rclone(json.loads((ROOT / 'oci/catalog/apps/rclone.json').read_text()), ui)
|
||||
ui.message.assert_called_once_with(expected_rclone)
|
||||
self.assertIn(expected_hedgedoc, detail(SimpleNamespace(), item, template))
|
||||
|
||||
def test_extractor_sees_both_new_sources(self):
|
||||
import runpy
|
||||
extractor = runpy.run_path(str(ROOT / '.github/scripts/build_translation_cache.py'))
|
||||
keys = extractor['extract_python_texts']([ROOT / 'oci/src'])
|
||||
self.assertIn(RCLONE, keys)
|
||||
keys = extractor['extract_catalog_texts']([ROOT / 'oci/catalog'])
|
||||
self.assertIn('HedgeDoc is a self-hosted collaborative Markdown editor for notes.', keys)
|
||||
Reference in New Issue
Block a user