Initial server source import
This commit is contained in:
@@ -0,0 +1,566 @@
|
||||
import copy
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
APP_DIR = ROOT / 'app'
|
||||
sys.path.insert(0, str(APP_DIR))
|
||||
|
||||
import console_runner
|
||||
import docker_depth_experiment as depth_config
|
||||
import scanner
|
||||
import supervisor
|
||||
|
||||
|
||||
TEST_QUERIES = [f'query-{index:02d}' for index in range(61)]
|
||||
|
||||
|
||||
def valid_config(enabled=False):
|
||||
experiment = {
|
||||
'experiment_key': 'docker-depth-test-v1',
|
||||
'enabled': enabled,
|
||||
'queries': list(TEST_QUERIES),
|
||||
'repositories_per_query': 10,
|
||||
'shallow_images_per_repository': 1,
|
||||
'deep_repositories_per_query': 1,
|
||||
'deep_images_per_repository': 10,
|
||||
'target_limit': 1200,
|
||||
'selector_version': depth_config.DOCKER_DEPTH_SELECTOR_VERSION,
|
||||
}
|
||||
return {
|
||||
'global': {
|
||||
'database_url': 'postgresql://example.invalid/truf',
|
||||
'sync_file_queues': False,
|
||||
},
|
||||
'sources': {
|
||||
'dockerhub': {
|
||||
'mode': 'search',
|
||||
'require_digest': True,
|
||||
'queries': list(TEST_QUERIES),
|
||||
'pages': 30,
|
||||
'per_page': 100,
|
||||
'docker_platform_filter_enabled': True,
|
||||
'docker_platform_os': 'linux',
|
||||
'docker_platform_arch': 'amd64',
|
||||
'docker_platform_candidate_tags': 20,
|
||||
'docker_images_per_repository': 3,
|
||||
'docker_depth_experiment': experiment,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def layer(value):
|
||||
return 'sha256:' + f'{value:064x}'
|
||||
|
||||
|
||||
def graph_candidate(target, updated_at, source_index, layers):
|
||||
return {
|
||||
'target': target,
|
||||
'updated_at': updated_at,
|
||||
'source_index': source_index,
|
||||
'layers': tuple(layer(value) for value in layers),
|
||||
}
|
||||
|
||||
|
||||
class DockerDepthConfigurationTests(unittest.TestCase):
|
||||
def test_image_depth_accepts_only_strict_integers_from_one_through_ten(self):
|
||||
self.assertEqual(depth_config.validate_docker_images_per_repository(1), 1)
|
||||
self.assertEqual(depth_config.validate_docker_images_per_repository(10), 10)
|
||||
for value in (
|
||||
False, True, None, '1', '10', 1.0, 10.0, [], {}, 0, -1, 11,
|
||||
):
|
||||
with self.subTest(value=value), self.assertRaises(ValueError):
|
||||
depth_config.validate_docker_images_per_repository(value)
|
||||
with self.subTest(scanner_value=value), self.assertRaises(ValueError):
|
||||
scanner.docker_images_per_repository_limit(value)
|
||||
|
||||
def test_experiment_mapping_rejects_unknown_missing_and_malformed_ids(self):
|
||||
config = valid_config()
|
||||
config['sources']['dockerhub']['docker_depth_experiment']['unexpected'] = 1
|
||||
with self.assertRaisesRegex(ValueError, 'unsupported keys: unexpected'):
|
||||
depth_config.validate_docker_depth_config(config)
|
||||
|
||||
config = valid_config()
|
||||
del config['sources']['dockerhub']['docker_depth_experiment']['target_limit']
|
||||
with self.assertRaisesRegex(ValueError, 'missing required keys: target_limit'):
|
||||
depth_config.validate_docker_depth_config(config)
|
||||
|
||||
for identifier in ('', 'Docker Depth', '../depth', 'depth/', '-depth'):
|
||||
config = valid_config()
|
||||
config['sources']['dockerhub']['docker_depth_experiment']['experiment_key'] = identifier
|
||||
with self.subTest(identifier=identifier), self.assertRaisesRegex(
|
||||
ValueError, 'stable identifier',
|
||||
):
|
||||
depth_config.validate_docker_depth_config(config)
|
||||
|
||||
def test_experiment_queries_candidate_depth_cap_and_fixed_limits_fail_closed(self):
|
||||
config = valid_config()
|
||||
config['sources']['dockerhub']['docker_depth_experiment']['queries'][-1] = TEST_QUERIES[0]
|
||||
config['sources']['dockerhub']['queries'][-1] = TEST_QUERIES[0]
|
||||
with self.assertRaisesRegex(ValueError, 'must be unique'):
|
||||
depth_config.validate_docker_depth_config(config)
|
||||
|
||||
config = valid_config()
|
||||
config['sources']['dockerhub']['queries'] = list(reversed(TEST_QUERIES))
|
||||
with self.assertRaisesRegex(ValueError, 'exactly match ordered'):
|
||||
depth_config.validate_docker_depth_config(config)
|
||||
|
||||
config = valid_config()
|
||||
config['sources']['dockerhub']['docker_platform_candidate_tags'] = 9
|
||||
with self.assertRaisesRegex(ValueError, 'at least the configured deep image depth'):
|
||||
depth_config.validate_docker_depth_config(config)
|
||||
|
||||
config = valid_config(enabled=True)
|
||||
config['sources']['dockerhub']['docker_repository_refresh_max_per_cycle'] = 1
|
||||
with self.assertRaisesRegex(
|
||||
ValueError, 'docker_repository_refresh_max_per_cycle=0',
|
||||
):
|
||||
depth_config.validate_docker_depth_config(config)
|
||||
|
||||
config = valid_config(enabled=False)
|
||||
config['sources']['dockerhub']['docker_repository_refresh_max_per_cycle'] = 1
|
||||
self.assertIsNotNone(
|
||||
depth_config.validate_docker_depth_config(config).experiment
|
||||
)
|
||||
|
||||
config = valid_config()
|
||||
config['sources']['dockerhub']['docker_depth_experiment']['target_limit'] = 1158
|
||||
with self.assertRaisesRegex(ValueError, 'theoretical target maximum'):
|
||||
depth_config.validate_docker_depth_config(config)
|
||||
|
||||
for key, value in (
|
||||
('repositories_per_query', 9),
|
||||
('shallow_images_per_repository', 2),
|
||||
('deep_repositories_per_query', 2),
|
||||
('deep_images_per_repository', 9),
|
||||
('target_limit', 1201),
|
||||
):
|
||||
config = valid_config()
|
||||
config['sources']['dockerhub']['docker_depth_experiment'][key] = value
|
||||
with self.subTest(key=key), self.assertRaises(ValueError):
|
||||
depth_config.validate_docker_depth_config(config)
|
||||
|
||||
def test_disabled_collection_keeps_ordinary_fifo_resolution_at_three(self):
|
||||
config = valid_config(enabled=False)
|
||||
validated = depth_config.validate_docker_depth_config(config)
|
||||
source = validated.normalized_config['sources']['dockerhub']
|
||||
args = console_runner.build_args_from_source_config(
|
||||
'dockerhub', source, validated.normalized_config['global'], TEST_QUERIES[0],
|
||||
)
|
||||
self.assertEqual(args.docker_images_per_repository, 3)
|
||||
self.assertEqual(validated.experiment.deep_images_per_repository, 10)
|
||||
|
||||
invalid = valid_config(enabled=False)
|
||||
invalid['sources']['dockerhub']['docker_images_per_repository'] = 10
|
||||
with self.assertRaisesRegex(ValueError, 'ordinary resolver depth 3'):
|
||||
depth_config.validate_docker_depth_config(invalid)
|
||||
|
||||
class DB:
|
||||
last_error = ''
|
||||
|
||||
def __init__(self):
|
||||
self.claimed = False
|
||||
self.finished = []
|
||||
|
||||
def claim_docker_resolutions(self, *_args, **_kwargs):
|
||||
if self.claimed:
|
||||
return []
|
||||
self.claimed = True
|
||||
return [{'id': 1, 'target': 'owner/repository', 'resolver_token': 'token'}]
|
||||
|
||||
def finish_docker_resolution(self, *values, **kwargs):
|
||||
self.finished.append((values, kwargs))
|
||||
return True
|
||||
|
||||
selected = []
|
||||
|
||||
def fetch(_repository, _since, limit, *_args, **_kwargs):
|
||||
candidates = [
|
||||
graph_candidate(f'image-{index}', 100 - index, index, (index + 1,))
|
||||
for index in range(10)
|
||||
]
|
||||
selected.extend(scanner.select_docker_layer_graphs(candidates, limit))
|
||||
return SimpleNamespace(
|
||||
tags=tuple(item['target'] for item in selected), status='ok',
|
||||
remote_attempted=True, retry_at=None, error='',
|
||||
)
|
||||
|
||||
database = DB()
|
||||
with mock.patch.object(console_runner, 'fetch_dockerhub_tags', side_effect=fetch), \
|
||||
mock.patch('builtins.print'):
|
||||
self.assertEqual(
|
||||
console_runner.resolve_due_docker_queue_targets(
|
||||
database, 'dockerhub', args,
|
||||
),
|
||||
1,
|
||||
)
|
||||
self.assertEqual([item['rank'] for item in selected], [1, 2, 3])
|
||||
self.assertEqual(len(database.finished[0][0][3]), 3)
|
||||
|
||||
def test_enabled_and_disabled_collection_require_all_static_authorities(self):
|
||||
for enabled in (False, True):
|
||||
config = valid_config(enabled=enabled)
|
||||
validated = depth_config.validate_docker_depth_config(config)
|
||||
self.assertIs(validated.experiment.enabled, enabled)
|
||||
|
||||
with self.subTest(enabled=enabled), self.assertRaisesRegex(
|
||||
ValueError, 'managed PostgreSQL',
|
||||
):
|
||||
depth_config.validate_docker_depth_config(
|
||||
config, managed_postgres=False, final_cutover=True,
|
||||
)
|
||||
with self.subTest(enabled=enabled), self.assertRaisesRegex(
|
||||
ValueError, 'final cutover',
|
||||
):
|
||||
depth_config.validate_docker_depth_config(
|
||||
config, managed_postgres=True, final_cutover=False,
|
||||
)
|
||||
|
||||
for key, value, message in (
|
||||
('mode', 'recent', 'search mode'),
|
||||
('require_digest', False, 'require_digest=true'),
|
||||
('sync_file_queues', True, 'final cutover'),
|
||||
):
|
||||
invalid = valid_config(enabled=enabled)
|
||||
invalid['sources']['dockerhub'][key] = value
|
||||
with self.subTest(enabled=enabled, key=key), self.assertRaisesRegex(
|
||||
ValueError, message,
|
||||
):
|
||||
depth_config.validate_docker_depth_config(invalid)
|
||||
|
||||
invalid = valid_config(enabled=enabled)
|
||||
invalid['global']['database_url'] = 'sqlite:///scanner.db'
|
||||
with self.subTest(enabled=enabled), self.assertRaisesRegex(
|
||||
ValueError, 'PostgreSQL database_url',
|
||||
):
|
||||
depth_config.validate_docker_depth_config(
|
||||
invalid, managed_postgres=True,
|
||||
)
|
||||
|
||||
def test_disabled_collection_rejects_late_policy_and_platform_coercions(self):
|
||||
for key, value, message in (
|
||||
('pages', '30', 'pages must be an integer'),
|
||||
('per_page', True, 'per_page must be an integer'),
|
||||
('docker_platform_candidate_tags', '20', 'must be an integer'),
|
||||
('docker_platform_candidate_tags', 101, 'must be from 1 through 100'),
|
||||
('docker_platform_filter_enabled', 'true', 'must be a boolean'),
|
||||
('docker_platform_os', 'windows', 'supports only linux/amd64'),
|
||||
('docker_platform_arch', 'arm64', 'supports only linux/amd64'),
|
||||
):
|
||||
invalid = valid_config(enabled=False)
|
||||
invalid['sources']['dockerhub'][key] = value
|
||||
with self.subTest(key=key, value=value), self.assertRaisesRegex(
|
||||
ValueError, message,
|
||||
):
|
||||
depth_config.validate_docker_depth_config(invalid)
|
||||
|
||||
invalid = valid_config(enabled=False)
|
||||
invalid['sources']['dockerhub']['query_overrides'] = []
|
||||
with self.assertRaisesRegex(ValueError, 'query_overrides must be a mapping'):
|
||||
depth_config.validate_docker_depth_config(invalid)
|
||||
|
||||
invalid = valid_config(enabled=False)
|
||||
invalid['sources']['dockerhub']['query_overrides'] = {
|
||||
'not-configured': {'pages': 30},
|
||||
}
|
||||
with self.assertRaisesRegex(ValueError, 'unconfigured queries'):
|
||||
depth_config.validate_docker_depth_config(invalid)
|
||||
|
||||
invalid = valid_config(enabled=False)
|
||||
invalid['sources']['dockerhub']['query_overrides'] = {
|
||||
TEST_QUERIES[0]: {'pages': '30'},
|
||||
}
|
||||
with self.assertRaisesRegex(ValueError, 'pages.*must be an integer'):
|
||||
depth_config.validate_docker_depth_config(invalid)
|
||||
|
||||
def test_mixed_experiment_discovery_policies_are_rejected(self):
|
||||
config = valid_config(enabled=False)
|
||||
config['sources']['dockerhub']['query_overrides'] = {
|
||||
TEST_QUERIES[0]: {'pages': 29},
|
||||
}
|
||||
with self.assertRaisesRegex(ValueError, 'one consistent pages/per_page'):
|
||||
depth_config.validate_docker_depth_config(config)
|
||||
|
||||
def test_validation_is_side_effect_free_and_hashes_are_canonical(self):
|
||||
config = valid_config()
|
||||
original = copy.deepcopy(config)
|
||||
first = depth_config.validate_docker_depth_config(config)
|
||||
self.assertEqual(config, original)
|
||||
self.assertIsNot(first.normalized_config, config)
|
||||
self.assertEqual(first.experiment.theoretical_max_targets, 1159)
|
||||
|
||||
reordered = copy.deepcopy(config)
|
||||
mapping = reordered['sources']['dockerhub']['docker_depth_experiment']
|
||||
reordered['sources']['dockerhub']['docker_depth_experiment'] = dict(
|
||||
reversed(list(mapping.items()))
|
||||
)
|
||||
second = depth_config.validate_docker_depth_config(reordered)
|
||||
self.assertEqual(
|
||||
(first.ordered_query_hash, first.config_hash, first.selector_hash),
|
||||
(second.ordered_query_hash, second.config_hash, second.selector_hash),
|
||||
)
|
||||
|
||||
changed = copy.deepcopy(config)
|
||||
changed_queries = list(reversed(TEST_QUERIES))
|
||||
changed['sources']['dockerhub']['queries'] = changed_queries
|
||||
changed['sources']['dockerhub']['docker_depth_experiment']['queries'] = changed_queries
|
||||
third = depth_config.validate_docker_depth_config(changed)
|
||||
self.assertNotEqual(first.ordered_query_hash, third.ordered_query_hash)
|
||||
self.assertNotEqual(first.config_hash, third.config_hash)
|
||||
|
||||
changed_policy = copy.deepcopy(config)
|
||||
changed_policy['sources']['dockerhub']['pages'] = 29
|
||||
policy_hash = depth_config.validate_docker_depth_config(changed_policy)
|
||||
self.assertNotEqual(first.config_hash, policy_hash.config_hash)
|
||||
|
||||
without_filter = copy.deepcopy(config)
|
||||
without_filter['sources']['dockerhub']['docker_platform_filter_enabled'] = False
|
||||
fourth = depth_config.validate_docker_depth_config(without_filter)
|
||||
self.assertNotEqual(first.config_hash, fourth.config_hash)
|
||||
|
||||
arm64 = copy.deepcopy(without_filter)
|
||||
arm64['sources']['dockerhub']['docker_platform_arch'] = 'arm64'
|
||||
fifth = depth_config.validate_docker_depth_config(arm64)
|
||||
self.assertNotEqual(fourth.config_hash, fifth.config_hash)
|
||||
|
||||
windows = copy.deepcopy(without_filter)
|
||||
windows['sources']['dockerhub']['docker_platform_os'] = 'windows'
|
||||
sixth = depth_config.validate_docker_depth_config(windows)
|
||||
self.assertNotEqual(fourth.config_hash, sixth.config_hash)
|
||||
|
||||
enabled = valid_config(enabled=True)
|
||||
activated = depth_config.validate_docker_depth_config(enabled)
|
||||
self.assertFalse(first.experiment.enabled)
|
||||
self.assertTrue(activated.experiment.enabled)
|
||||
self.assertEqual(first.config_hash, activated.config_hash)
|
||||
|
||||
unauthorized = copy.deepcopy(enabled)
|
||||
unauthorized['sources']['dockerhub']['pages'] = 29
|
||||
self.assertNotEqual(
|
||||
activated.config_hash,
|
||||
depth_config.validate_docker_depth_config(unauthorized).config_hash,
|
||||
)
|
||||
|
||||
def test_production_returns_to_default_docker_depth(self):
|
||||
with open(APP_DIR / 'config.yaml', 'r', encoding='utf-8') as handle:
|
||||
configured = yaml.safe_load(handle)
|
||||
validated = depth_config.validate_docker_depth_config(configured)
|
||||
source = configured['sources']['dockerhub']
|
||||
|
||||
self.assertEqual(source['docker_images_per_repository'], 3)
|
||||
self.assertNotIn('docker_depth_experiment', source)
|
||||
self.assertIsNone(validated.experiment)
|
||||
|
||||
|
||||
class DockerDepthStartupOrderingTests(unittest.TestCase):
|
||||
def invalid_config_path(self, directory):
|
||||
path = Path(directory) / 'invalid-config.yaml'
|
||||
config = valid_config(enabled=False)
|
||||
config['sources']['dockerhub']['pages'] = '30'
|
||||
path.write_text(yaml.safe_dump(config), encoding='utf-8')
|
||||
return str(path)
|
||||
|
||||
def test_supervisor_validation_precedes_lifecycle_workers_secrets_state_and_database(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = self.invalid_config_path(directory)
|
||||
args = SimpleNamespace(
|
||||
config=path, sources=None, dry_run=False, background_status=False,
|
||||
cmd=None, stop_background=False, attach=False, background=True,
|
||||
with_postgres=True, background_child=False,
|
||||
)
|
||||
with mock.patch.object(supervisor, 'parse_args', return_value=args), \
|
||||
mock.patch.dict(os.environ, {
|
||||
supervisor.RUNTIME_BOOTSTRAP_ENV: supervisor.RUNTIME_BOOTSTRAP_VALUE,
|
||||
}, clear=False), \
|
||||
mock.patch.object(supervisor, 'preflight_lifecycle_paths') as lifecycle, \
|
||||
mock.patch.object(supervisor, 'start_background') as background, \
|
||||
mock.patch.object(supervisor, 'load_postgres_env') as secrets, \
|
||||
mock.patch.object(supervisor, 'ManagedSource') as worker, \
|
||||
mock.patch.object(supervisor, 'connect_postgres') as database:
|
||||
with self.assertRaisesRegex(ValueError, 'pages must be an integer'):
|
||||
supervisor.main()
|
||||
for side_effect in (lifecycle, background, secrets, worker, database):
|
||||
side_effect.assert_not_called()
|
||||
|
||||
def test_scanner_child_validation_precedes_lifecycle_state_network_secrets_and_database(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = self.invalid_config_path(directory)
|
||||
args = SimpleNamespace(show_state=False, config=path)
|
||||
with mock.patch.object(console_runner, 'parse_args', return_value=args), \
|
||||
mock.patch.dict(os.environ, {
|
||||
'TRUF_MANAGED_POSTGRES_DSN': 'postgresql://managed.invalid/truf',
|
||||
}, clear=False), \
|
||||
mock.patch.object(console_runner, 'require_active_supervisor_child') as lifecycle, \
|
||||
mock.patch.object(console_runner, 'load_state') as state, \
|
||||
mock.patch.object(console_runner, 'initialize_scanner_runtime') as network, \
|
||||
mock.patch.object(console_runner, 'load_secrets') as secrets, \
|
||||
mock.patch.object(console_runner, 'ScannerDB') as database:
|
||||
with self.assertRaisesRegex(ValueError, 'pages must be an integer'):
|
||||
console_runner.main()
|
||||
for side_effect in (lifecycle, state, network, secrets, database):
|
||||
side_effect.assert_not_called()
|
||||
|
||||
|
||||
class DockerLayerGraphSelectionTests(unittest.TestCase):
|
||||
def test_selector_hash_binds_executable_source_decisions_and_version(self):
|
||||
version = depth_config.DOCKER_DEPTH_SELECTOR_VERSION
|
||||
baseline = depth_config.canonical_selector_hash(version)
|
||||
self.assertIs(scanner.select_docker_layer_graphs, depth_config.select_docker_layer_graphs)
|
||||
|
||||
with mock.patch.object(
|
||||
depth_config,
|
||||
'_SELECTOR_LATER_SCORE',
|
||||
'changed_marginal_layer_rule',
|
||||
):
|
||||
self.assertNotEqual(
|
||||
baseline, depth_config.canonical_selector_hash(version),
|
||||
)
|
||||
|
||||
getsource = depth_config.inspect.getsource
|
||||
|
||||
def changed_selector_source(function):
|
||||
source = getsource(function)
|
||||
if function is depth_config.select_docker_layer_graphs:
|
||||
original = 'while len(selected) < limit and distinct:'
|
||||
self.assertIn(original, source)
|
||||
return source.replace(
|
||||
original,
|
||||
'while len(selected) <= limit and distinct:',
|
||||
1,
|
||||
)
|
||||
return source
|
||||
|
||||
with mock.patch.object(
|
||||
depth_config.inspect,
|
||||
'getsource',
|
||||
side_effect=changed_selector_source,
|
||||
):
|
||||
self.assertNotEqual(
|
||||
baseline, depth_config.canonical_selector_hash(version),
|
||||
)
|
||||
self.assertNotEqual(
|
||||
baseline, depth_config.canonical_selector_hash(version + '-changed'),
|
||||
)
|
||||
|
||||
def first_three(self):
|
||||
return [
|
||||
graph_candidate('newest', 100, 0, (1, 2)),
|
||||
graph_candidate('maximum-novelty', 80, 1, (3, 4, 5)),
|
||||
graph_candidate('oldest', 0, 2, (6,)),
|
||||
]
|
||||
|
||||
def test_first_three_semantics_and_metadata_are_preserved(self):
|
||||
selected = scanner.select_docker_layer_graphs(self.first_three(), 3)
|
||||
self.assertEqual(
|
||||
[record['target'] for record in selected],
|
||||
['newest', 'maximum-novelty', 'oldest'],
|
||||
)
|
||||
self.assertEqual(
|
||||
[record['reason'] for record in selected],
|
||||
[
|
||||
'newest_distinct_graph',
|
||||
'maximum_marginal_layer_novelty',
|
||||
'oldest_distinct_graph',
|
||||
],
|
||||
)
|
||||
self.assertEqual([record['rank'] for record in selected], [1, 2, 3])
|
||||
self.assertEqual(selected[0]['image_rank'], 1)
|
||||
self.assertEqual(selected[0]['graph'], selected[0]['layers'])
|
||||
self.assertEqual(selected[0]['graph_hash'], selected[0]['graph_sha256'])
|
||||
self.assertEqual(selected[0]['layer_count'], 2)
|
||||
self.assertEqual(
|
||||
selected[0]['layer_metadata'],
|
||||
(
|
||||
{'digest': layer(1), 'position_from_base': 1, 'position_from_top': 2},
|
||||
{'digest': layer(2), 'position_from_base': 2, 'position_from_top': 1},
|
||||
),
|
||||
)
|
||||
|
||||
def test_ranks_four_through_ten_are_deterministic_and_skip_bad_or_duplicate_graphs(self):
|
||||
candidates = self.first_three() + [
|
||||
graph_candidate(f'candidate-{index}', 75 - index, index, (index + 10,))
|
||||
for index in range(3, 14)
|
||||
]
|
||||
candidates.extend([
|
||||
graph_candidate('newest-alias', 99, 99, (1, 2)),
|
||||
{'target': 'empty', 'updated_at': 200, 'source_index': 100, 'layers': ()},
|
||||
{'target': 'malformed', 'updated_at': 201, 'source_index': 101,
|
||||
'layers': ('not-a-digest',)},
|
||||
])
|
||||
selected = scanner.select_docker_layer_graphs(candidates, 10)
|
||||
reversed_selected = scanner.select_docker_layer_graphs(list(reversed(candidates)), 10)
|
||||
|
||||
self.assertEqual(len(selected), 10)
|
||||
self.assertEqual([item['rank'] for item in selected], list(range(1, 11)))
|
||||
self.assertEqual(
|
||||
[item['target'] for item in selected],
|
||||
[item['target'] for item in reversed_selected],
|
||||
)
|
||||
self.assertEqual(len({item['graph_hash'] for item in selected}), 10)
|
||||
self.assertNotIn('newest-alias', [item['target'] for item in selected])
|
||||
self.assertNotIn('empty', [item['target'] for item in selected])
|
||||
self.assertNotIn('malformed', [item['target'] for item in selected])
|
||||
|
||||
def test_later_rank_ties_use_temporal_recency_target_and_graph_hash_order(self):
|
||||
cases = [
|
||||
(
|
||||
[
|
||||
graph_candidate('temporally-far', 40, 10, (20,)),
|
||||
graph_candidate('temporally-near', 60, 11, (21,)),
|
||||
],
|
||||
'temporally-far',
|
||||
),
|
||||
(
|
||||
[
|
||||
graph_candidate('older', 30, 10, (22,)),
|
||||
graph_candidate('newer', 50, 11, (23,)),
|
||||
],
|
||||
'newer',
|
||||
),
|
||||
(
|
||||
[
|
||||
graph_candidate('repo/b', 50, 10, (24,)),
|
||||
graph_candidate('repo/a', 50, 11, (25,)),
|
||||
],
|
||||
'repo/a',
|
||||
),
|
||||
]
|
||||
for candidates, expected_target in cases:
|
||||
with self.subTest(expected_target=expected_target):
|
||||
selected = scanner.select_docker_layer_graphs(
|
||||
self.first_three() + candidates, 4,
|
||||
)
|
||||
self.assertEqual(selected[3]['target'], expected_target)
|
||||
|
||||
graph_tie = [
|
||||
graph_candidate('same-target', 50, 10, (26,)),
|
||||
graph_candidate('same-target', 50, 11, (27,)),
|
||||
]
|
||||
expected_hash = min(
|
||||
depth_config.canonical_docker_layer_graph_hash((layer(26),)),
|
||||
depth_config.canonical_docker_layer_graph_hash((layer(27),)),
|
||||
)
|
||||
selected = scanner.select_docker_layer_graphs(self.first_three() + graph_tie, 4)
|
||||
self.assertEqual(selected[3]['graph_hash'], expected_hash)
|
||||
|
||||
def test_resolution_outcome_metadata_fields_default_for_existing_callers(self):
|
||||
outcome = scanner.DockerTagResolutionOutcome(
|
||||
tags=(), status='empty', remote_attempted=False,
|
||||
)
|
||||
self.assertEqual(outcome.selection_records, ())
|
||||
self.assertEqual(outcome.selections, ())
|
||||
self.assertEqual(outcome.selector_version, '')
|
||||
self.assertEqual(outcome.selector_hash, '')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user