mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-25 18:40:06 -04:00
* domains.yaml imports by diffing domains.yaml declares one section, its domain Collections keyed by name, comparing description, directory, Tag and file format. An unchanged import writes nothing. A removed domain without Archives is deleted; one with Archives is reset to its default settings (decision 7c: a refresh of its Archives would re-create it), and the config is saved with it. An invalid entry (name or directory) is skipped, counted, and its domain kept. Once tags.yaml is imported, a domain's Tag that no longer exists is dropped and the config saved without it (`unknown_tag_names`). Linking Downloads to domains reads first (the domains once, not one query per RSS Download) and opens a short write only when there is something to link; the config is then saved with the links. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test_bookmarks_config_import starts the app like its siblings It imports a config without `async_client`, so it fails (no shared_ctx.configs_imported) whenever it is the first test on its worker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * domains.yaml: converge without a save; no links while entries are skipped A save after the import is refused while an entry is skipped, so the import must converge without one: - a removed domain with Archives that is already reset is left as it is (prepare reads `import_db_rows`); before, every import reset it again; - after prepare, the framework drops changed rows that now match the database (a dropped unknown Tag), so they are not written again. Linking Downloads changes domains.yaml after its version is recorded; while entries are skipped the save is refused, the database stays newer, and the next startup would save over the skipped entries. Linking now waits for an import without skipped entries. An entry that is not a mapping is skipped and counted, instead of failing the whole file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
233 lines
9.8 KiB
Python
233 lines
9.8 KiB
Python
"""domains.yaml imports by diffing its domain Collections against the database, and writing only the differences
|
|
(see wrolpi.config_import). A domain removed from the file is deleted, unless it has Archives: then it is reset to
|
|
its default settings (a refresh of its Archives would re-create it)."""
|
|
import pytest
|
|
import yaml
|
|
|
|
from modules.archive.lib import get_domains_config, import_domains_config
|
|
from modules.archive.models import Archive
|
|
from wrolpi.collections import Collection
|
|
from wrolpi.config_version import get_config_version
|
|
from wrolpi.downloader import Download
|
|
from wrolpi.test.test_config_import import edit_config, statements, writes
|
|
|
|
|
|
@pytest.fixture
|
|
async def saved_domains(test_session, test_directory, async_client, archive_factory, tag_factory, await_switches):
|
|
"""`example.com` (tagged, in a custom directory), `other.org`, and `archived.net` (which has an Archive and a
|
|
description), saved to the config (which records its version)."""
|
|
tag = await tag_factory('news')
|
|
archive_factory(domain='archived.net', url='https://archived.net/a')
|
|
test_session.commit()
|
|
archived = test_session.query(Collection).filter_by(name='archived.net', kind='domain').one()
|
|
archived.description = 'Has archives'
|
|
test_session.add_all([
|
|
Collection(name='example.com', kind='domain', description='Example', tag_id=tag.id,
|
|
directory=test_directory / 'archive/news/example.com'),
|
|
Collection(name='other.org', kind='domain'),
|
|
])
|
|
test_session.commit()
|
|
get_domains_config().dump_config()
|
|
await await_switches()
|
|
assert get_config_version(test_session, 'domains.yaml') is not None
|
|
return get_domains_config().get_file()
|
|
|
|
|
|
def domains(test_session) -> dict:
|
|
test_session.expire_all()
|
|
return {i.name: i for i in test_session.query(Collection).filter_by(kind='domain')}
|
|
|
|
|
|
def edit_domain(path, name, **values):
|
|
def change(data):
|
|
next(i for i in data['collections'] if i['name'] == name).update(values)
|
|
|
|
edit_config(path, change)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unchanged_import_writes_nothing(test_session, saved_domains):
|
|
"""Importing a config the database already matches opens no write transaction, nor does linking Downloads when
|
|
there are none to link."""
|
|
config = get_domains_config()
|
|
with statements() as collected:
|
|
import_domains_config()
|
|
|
|
assert config.successful_import is True
|
|
assert writes(collected) == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_import_writes_only_the_differences(test_session, test_directory, saved_domains):
|
|
"""A changed domain is updated, a removed one (without Archives) is deleted, an added one is created; the next
|
|
import writes nothing."""
|
|
def change(data):
|
|
data['collections'] = [i for i in data['collections'] if i['name'] != 'other.org']
|
|
data['collections'].append(dict(name='new.com', kind='domain', description='New'))
|
|
example = next(i for i in data['collections'] if i['name'] == 'example.com')
|
|
example.update(description='Changed', tag_name=None, directory='archive/example.com')
|
|
|
|
edit_config(saved_domains, change)
|
|
config = get_domains_config()
|
|
config.import_config()
|
|
|
|
assert config.successful_import is True
|
|
found = domains(test_session)
|
|
assert set(found) == {'example.com', 'archived.net', 'new.com'}
|
|
example = found['example.com']
|
|
assert (example.description, example.tag_id, example.directory) == \
|
|
('Changed', None, test_directory / 'archive/example.com')
|
|
assert found['new.com'].description == 'New'
|
|
with statements() as collected:
|
|
config.import_config()
|
|
assert writes(collected) == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_removed_domain_with_archives_is_reset(test_session, saved_domains, await_switches):
|
|
"""A domain with Archives that is removed from the config keeps its Archives, and is reset to its default
|
|
settings; the config is saved with it, so the next import writes nothing."""
|
|
edit_config(saved_domains, lambda data: data.update(
|
|
collections=[i for i in data['collections'] if i['name'] != 'archived.net']))
|
|
|
|
config = get_domains_config()
|
|
config.import_config()
|
|
await await_switches()
|
|
|
|
archived = domains(test_session)['archived.net']
|
|
assert archived.description is None
|
|
assert test_session.query(Archive).filter_by(collection_id=archived.id).count() == 1
|
|
assert 'archived.net' in saved_domains.read_text()
|
|
with statements() as collected:
|
|
config.import_config()
|
|
assert writes(collected) == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_invalid_entries_are_kept(test_session, saved_domains):
|
|
"""An entry with an invalid name or directory is skipped (counted), and its domain is not deleted."""
|
|
edit_domain(saved_domains, 'other.org', directory='../outside')
|
|
edit_config(saved_domains, lambda data: data['collections'].append(dict(name='nodot', kind='domain')))
|
|
|
|
config = get_domains_config()
|
|
config.import_config()
|
|
|
|
assert config.import_skipped == 2
|
|
assert set(domains(test_session)) == {'example.com', 'other.org', 'archived.net'}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unknown_tag_is_dropped(test_session, saved_domains, test_tags_config, await_switches):
|
|
"""Once tags.yaml is imported, a domain's Tag that no longer exists is dropped and the config is saved without
|
|
it: the next import writes nothing."""
|
|
from wrolpi.tags import get_tags_config
|
|
get_tags_config().dump_config()
|
|
get_tags_config().import_config()
|
|
edit_domain(saved_domains, 'other.org', tag_name='gone')
|
|
|
|
config = get_domains_config()
|
|
config.import_config()
|
|
await await_switches()
|
|
|
|
assert domains(test_session)['other.org'].tag_id is None
|
|
assert 'gone' not in saved_domains.read_text()
|
|
with statements() as collected:
|
|
config.import_config()
|
|
assert writes(collected) == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_downloads_are_linked(test_session, saved_domains, await_switches):
|
|
"""After the import, an RSS archive Download of a domain is linked to it (the domains are read once, not once
|
|
per Download), and the config is saved with it."""
|
|
test_session.add_all([
|
|
Download(url='https://other.org/feed.xml', downloader='rss', sub_downloader='archive', frequency=86400),
|
|
Download(url='https://example.com/feed.xml', downloader='rss', sub_downloader='archive', frequency=86400),
|
|
])
|
|
test_session.commit()
|
|
|
|
with statements() as collected:
|
|
import_domains_config()
|
|
await await_switches()
|
|
|
|
found = domains(test_session)
|
|
assert {i.url: i.collection_id for i in test_session.query(Download)} == {
|
|
'https://other.org/feed.xml': found['other.org'].id,
|
|
'https://example.com/feed.xml': found['example.com'].id,
|
|
}
|
|
assert len([i for i in collected if 'FROM collection' in i and 'WHERE collection.name' in i]) == 0
|
|
assert 'https://other.org/feed.xml' in saved_domains.read_text()
|
|
|
|
|
|
def add_invalid_entry(path):
|
|
edit_config(path, lambda data: data['collections'].append(dict(name='nodot', kind='domain')))
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_reset_with_a_skipped_entry_converges(test_session, saved_domains, await_switches):
|
|
"""A skipped entry stops the config from being saved; a domain reset by this import is not reset again by the
|
|
next one (which writes nothing)."""
|
|
edit_config(saved_domains, lambda data: data.update(
|
|
collections=[i for i in data['collections'] if i['name'] != 'archived.net']))
|
|
add_invalid_entry(saved_domains)
|
|
|
|
config = get_domains_config()
|
|
config.import_config()
|
|
await await_switches()
|
|
assert domains(test_session)['archived.net'].description is None
|
|
|
|
with statements() as collected:
|
|
config.import_config()
|
|
assert config.import_skipped == 1
|
|
assert writes(collected) == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unknown_tag_with_a_skipped_entry_converges(test_session, saved_domains, test_tags_config,
|
|
await_switches):
|
|
"""A dropped Tag the config cannot be saved without (an entry was skipped) is not dropped again."""
|
|
from wrolpi.tags import get_tags_config
|
|
get_tags_config().dump_config()
|
|
get_tags_config().import_config()
|
|
edit_domain(saved_domains, 'other.org', tag_name='gone')
|
|
add_invalid_entry(saved_domains)
|
|
|
|
config = get_domains_config()
|
|
config.import_config()
|
|
await await_switches()
|
|
|
|
with statements() as collected:
|
|
config.import_config()
|
|
assert writes(collected) == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_no_links_while_entries_are_skipped(test_session, saved_domains, await_switches):
|
|
"""Linking changes domains.yaml; while an entry is skipped the config cannot be saved, so Downloads are not
|
|
linked (the database would be newer than the file, and the next startup would save over the skipped entry)."""
|
|
test_session.add(Download(url='https://other.org/feed.xml', downloader='rss', sub_downloader='archive',
|
|
frequency=86400))
|
|
test_session.commit()
|
|
add_invalid_entry(saved_domains)
|
|
|
|
import_domains_config()
|
|
await await_switches()
|
|
|
|
test_session.expire_all()
|
|
assert test_session.query(Download).one().collection_id is None
|
|
assert get_config_version(test_session, 'domains.yaml') == yaml.safe_load(saved_domains.read_text())['version']
|
|
assert 'nodot' in saved_domains.read_text()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_entry_that_is_not_a_mapping_is_skipped(test_session, saved_domains):
|
|
"""An entry that is not a mapping is skipped (counted); the rest of the file is imported."""
|
|
edit_config(saved_domains, lambda data: data['collections'].extend(['example.net', None]))
|
|
edit_domain(saved_domains, 'other.org', description='Changed')
|
|
|
|
config = get_domains_config()
|
|
config.import_config()
|
|
|
|
assert config.successful_import is True
|
|
assert config.import_skipped == 2
|
|
assert domains(test_session)['other.org'].description == 'Changed'
|