Marco Ricci commited on 2024-12-12 12:03:30
Zeige 2 geänderte Dateien mit 205 Einfügungen und 209 Löschungen.
Redesign the state machine for testing vault configuration imports, and
the `hypothesis` generation strategies for vault configurations, to
overcome the following problems with the old design:
1. Data generation of vault configurations needs many redraws, because
we express dependencies by rejecting the whole tuple of properties
if it doesn't match. We could instead draw the independent
properties, and conditional on their values, draw the dependent
ones; cf. Gibbs sampling vs. rejection sampling.
2. `hypothesis` implements state machine rule selection as drawing from
a list of eligible rules. This list must remain static during
redrawing and backtracking, lest `hypothesis` complains the test is
flaky. While it isn't expressly forbidden to store state manually
in the state machine, the old design made the list of eligible
rules dependent on the implicit machine state and on the generated
values, in a way that led to flakiness.
Fix these issues by doing the following:
- Rename the class from `ConfigMergingStateMachine` to
`ConfigManagementStateMachine`, because it's no longer just about
merging configurations.
- Implement strategies to build a full vault configuration from
multiple full settings configurations, and a partial settings
configuration from a full settings configuration. Draw
interactively, first the independent properties, then the dependent
ones. This tackles issue (1).
- Overhaul the state machine, effectively rewriting it from scratch,
by managing all state in `hypothesis` bundles. Ensure the bundles
are filled at initialization, and that each (non-bookkeeping) rule
begins with storing the configuration and ends with checking and
returning the transformed configuration. Filter the values drawn
from the bundles, instead of filtering the rule set based on the
machine's current state. This tackles issue (2) above.
The resulting state machine should have an acceptable drawing success
rate (without needing to override the `filter_too_much` `hypothesis`
health check), at the cost of slow(ish) drawing speed and slow(er)
execution speed because the configuration must be replayed onto disk
during each step. On the other hand, the single steps now are more
repeatable and more independent of each other.
This implementation makes use of several top-level functions and
constants, outside of the state machine class. Attempting to use static
and class methods instead leads to messy workarounds because of the name
resolution rules and because of mismatches in the calling conventions of
static and class methods during class definition vs. during execution
time. In the end, the "top-level functions and constants" version is
the most straight-forward to read.
| ... | ... |
@@ -333,13 +333,13 @@ def _test_config_ids(val: VaultTestConfig) -> Any: # pragma: no cover |
| 333 | 333 |
|
| 334 | 334 |
@strategies.composite |
| 335 | 335 |
def vault_full_service_config(draw: strategies.DrawFn) -> dict[str, int]: |
| 336 |
+ repeat = draw(strategies.integers(min_value=0, max_value=10)) |
|
| 336 | 337 |
lower = draw(strategies.integers(min_value=0, max_value=10)) |
| 337 | 338 |
upper = draw(strategies.integers(min_value=0, max_value=10)) |
| 338 | 339 |
number = draw(strategies.integers(min_value=0, max_value=10)) |
| 339 |
- space = draw(strategies.integers(min_value=0, max_value=10)) |
|
| 340 |
+ space = draw(strategies.integers(min_value=0, max_value=repeat)) |
|
| 340 | 341 |
dash = draw(strategies.integers(min_value=0, max_value=10)) |
| 341 | 342 |
symbol = draw(strategies.integers(min_value=0, max_value=10)) |
| 342 |
- repeat = draw(strategies.integers(min_value=0, max_value=10)) |
|
| 343 | 343 |
length = draw( |
| 344 | 344 |
strategies.integers( |
| 345 | 345 |
min_value=max(1, lower + upper + number + space + dash + symbol), |
| ... | ... |
@@ -13,7 +13,7 @@ import os |
| 13 | 13 |
import shutil |
| 14 | 14 |
import socket |
| 15 | 15 |
import warnings |
| 16 |
-from typing import TYPE_CHECKING, cast |
|
| 16 |
+from typing import TYPE_CHECKING |
|
| 17 | 17 |
|
| 18 | 18 |
import click.testing |
| 19 | 19 |
import hypothesis |
| ... | ... |
@@ -2327,7 +2327,79 @@ class TestCLITransition: |
| 2327 | 2327 |
), 'expected known warning message in stderr' |
| 2328 | 2328 |
|
| 2329 | 2329 |
|
| 2330 |
-class ConfigMergingStateMachine(stateful.RuleBasedStateMachine): |
|
| 2330 |
+_known_services = (DUMMY_SERVICE, 'email', 'bank', 'work') |
|
| 2331 |
+_valid_properties = ( |
|
| 2332 |
+ 'length', |
|
| 2333 |
+ 'repeat', |
|
| 2334 |
+ 'upper', |
|
| 2335 |
+ 'lower', |
|
| 2336 |
+ 'number', |
|
| 2337 |
+ 'space', |
|
| 2338 |
+ 'dash', |
|
| 2339 |
+ 'symbol', |
|
| 2340 |
+) |
|
| 2341 |
+ |
|
| 2342 |
+ |
|
| 2343 |
+def _build_reduced_vault_config_settings( |
|
| 2344 |
+ config: _types.VaultConfigServicesSettings, |
|
| 2345 |
+ keys_to_purge: frozenset[str], |
|
| 2346 |
+) -> _types.VaultConfigServicesSettings: |
|
| 2347 |
+ config2 = copy.deepcopy(config) |
|
| 2348 |
+ for key in keys_to_purge: |
|
| 2349 |
+ config2.pop(key, None) # type: ignore[misc] |
|
| 2350 |
+ return config2 |
|
| 2351 |
+ |
|
| 2352 |
+ |
|
| 2353 |
+_services_strategy = strategies.builds( |
|
| 2354 |
+ _build_reduced_vault_config_settings, |
|
| 2355 |
+ tests.vault_full_service_config(), |
|
| 2356 |
+ strategies.sets( |
|
| 2357 |
+ strategies.sampled_from(_valid_properties), |
|
| 2358 |
+ max_size=7, |
|
| 2359 |
+ ), |
|
| 2360 |
+) |
|
| 2361 |
+ |
|
| 2362 |
+ |
|
| 2363 |
+def _assemble_config( |
|
| 2364 |
+ global_data: _types.VaultConfigGlobalSettings, |
|
| 2365 |
+ service_data: list[tuple[str, _types.VaultConfigServicesSettings]], |
|
| 2366 |
+) -> _types.VaultConfig: |
|
| 2367 |
+ services_dict = dict(service_data) |
|
| 2368 |
+ return ( |
|
| 2369 |
+ {'global': global_data, 'services': services_dict}
|
|
| 2370 |
+ if global_data |
|
| 2371 |
+ else {'services': services_dict}
|
|
| 2372 |
+ ) |
|
| 2373 |
+ |
|
| 2374 |
+ |
|
| 2375 |
+@strategies.composite |
|
| 2376 |
+def _draw_service_name_and_data( |
|
| 2377 |
+ draw: hypothesis.strategies.DrawFn, |
|
| 2378 |
+ num_entries: int, |
|
| 2379 |
+) -> tuple[tuple[str, _types.VaultConfigServicesSettings], ...]: |
|
| 2380 |
+ possible_services = list(_known_services) |
|
| 2381 |
+ selected_services: list[str] = [] |
|
| 2382 |
+ for _ in range(num_entries): |
|
| 2383 |
+ selected_services.append( |
|
| 2384 |
+ draw(strategies.sampled_from(possible_services)) |
|
| 2385 |
+ ) |
|
| 2386 |
+ possible_services.remove(selected_services[-1]) |
|
| 2387 |
+ return tuple( |
|
| 2388 |
+ (service, draw(_services_strategy)) for service in selected_services |
|
| 2389 |
+ ) |
|
| 2390 |
+ |
|
| 2391 |
+ |
|
| 2392 |
+_vault_full_config = strategies.builds( |
|
| 2393 |
+ _assemble_config, |
|
| 2394 |
+ _services_strategy, |
|
| 2395 |
+ strategies.integers( |
|
| 2396 |
+ min_value=2, |
|
| 2397 |
+ max_value=4, |
|
| 2398 |
+ ).flatmap(_draw_service_name_and_data), |
|
| 2399 |
+) |
|
| 2400 |
+ |
|
| 2401 |
+ |
|
| 2402 |
+class ConfigManagementStateMachine(stateful.RuleBasedStateMachine): |
|
| 2331 | 2403 |
def __init__(self) -> None: |
| 2332 | 2404 |
super().__init__() |
| 2333 | 2405 |
self.runner = click.testing.CliRunner(mix_stderr=False) |
| ... | ... |
@@ -2342,165 +2414,63 @@ class ConfigMergingStateMachine(stateful.RuleBasedStateMachine): |
| 2342 | 2414 |
config={'services': {}},
|
| 2343 | 2415 |
) |
| 2344 | 2416 |
) |
| 2345 |
- self.current_config = cli._load_config() |
|
| 2346 |
- |
|
| 2347 |
- known_services = stateful.Bundle('known_services')
|
|
| 2348 |
- settings = stateful.Bundle('settings')
|
|
| 2349 |
- configurations = stateful.Bundle('configurations')
|
|
| 2350 | 2417 |
|
| 2351 |
- @stateful.initialize(target=configurations) |
|
| 2352 |
- def init_empty_configuration(self) -> _types.VaultConfig: |
|
| 2353 |
- return copy.deepcopy(self.current_config) |
|
| 2354 |
- |
|
| 2355 |
- @stateful.initialize(target=configurations) |
|
| 2356 |
- def init_standard_testing_configuration(self) -> _types.VaultConfig: |
|
| 2357 |
- return cast( |
|
| 2358 |
- _types.VaultConfig, |
|
| 2359 |
- {
|
|
| 2360 |
- 'services': {
|
|
| 2361 |
- DUMMY_SERVICE: copy.deepcopy(DUMMY_CONFIG_SETTINGS) |
|
| 2362 |
- } |
|
| 2363 |
- }, |
|
| 2364 |
- ) |
|
| 2418 |
+ setting = stateful.Bundle('setting')
|
|
| 2419 |
+ configuration = stateful.Bundle('configuration')
|
|
| 2365 | 2420 |
|
| 2366 | 2421 |
@stateful.initialize( |
| 2367 |
- target=known_services, |
|
| 2368 |
- service_names=strategies.lists( |
|
| 2369 |
- strategies.text( |
|
| 2370 |
- strategies.characters(min_codepoint=32, max_codepoint=126), |
|
| 2371 |
- min_size=1, |
|
| 2372 |
- max_size=50, |
|
| 2373 |
- ), |
|
| 2374 |
- min_size=10, |
|
| 2375 |
- max_size=10, |
|
| 2376 |
- unique=True, |
|
| 2377 |
- ), |
|
| 2378 |
- ) |
|
| 2379 |
- def init_random_service_names( |
|
| 2380 |
- self, service_names: list[str] |
|
| 2381 |
- ) -> Iterable[str]: |
|
| 2382 |
- return stateful.multiple(*service_names) |
|
| 2383 |
- |
|
| 2384 |
- # Don't include key or phrase settings here. While easy to |
|
| 2385 |
- # implement when manipulating the stored config directly, the |
|
| 2386 |
- # command-line interface for changing the passphrase and key values |
|
| 2387 |
- # is not straight-forward, and key values require a running SSH |
|
| 2388 |
- # agent and the key to be loaded. |
|
| 2389 |
- @stateful.initialize( |
|
| 2390 |
- target=settings, |
|
| 2391 |
- settings_list=strategies.lists( |
|
| 2392 |
- tests.vault_full_service_config(), |
|
| 2393 |
- min_size=5, |
|
| 2394 |
- max_size=5, |
|
| 2395 |
- unique_by=lambda obj: json.dumps(obj, sort_keys=True), |
|
| 2422 |
+ target=configuration, |
|
| 2423 |
+ configs=strategies.lists( |
|
| 2424 |
+ _vault_full_config, |
|
| 2425 |
+ min_size=4, |
|
| 2426 |
+ max_size=4, |
|
| 2396 | 2427 |
), |
| 2397 | 2428 |
) |
| 2398 |
- def init_random_full_settings( |
|
| 2399 |
- self, settings_list: list[_types.VaultConfigGlobalSettings] |
|
| 2400 |
- ) -> Iterable[_types.VaultConfigGlobalSettings]: |
|
| 2401 |
- return stateful.multiple(*settings_list) |
|
| 2402 |
- |
|
| 2403 |
- @staticmethod |
|
| 2404 |
- def build_reduced_vault_config_settings( |
|
| 2405 |
- config: _types.VaultConfigGlobalSettings, |
|
| 2406 |
- keys_to_purge: frozenset[str], |
|
| 2407 |
- ) -> _types.VaultConfigGlobalSettings: |
|
| 2408 |
- config2 = copy.deepcopy(config) |
|
| 2409 |
- for key in keys_to_purge: |
|
| 2410 |
- config2.pop(key, None) # type: ignore[misc] |
|
| 2411 |
- return config2 |
|
| 2429 |
+ def declare_initial_configs( |
|
| 2430 |
+ self, |
|
| 2431 |
+ configs: Iterable[_types.VaultConfig], |
|
| 2432 |
+ ) -> Iterable[_types.VaultConfig]: |
|
| 2433 |
+ return stateful.multiple(*configs) |
|
| 2412 | 2434 |
|
| 2413 |
- # See comment on `init_random_full_settings`. |
|
| 2414 | 2435 |
@stateful.initialize( |
| 2415 |
- target=settings, |
|
| 2416 |
- settings_list=strategies.lists( |
|
| 2417 |
- strategies.builds( |
|
| 2418 |
- build_reduced_vault_config_settings, |
|
| 2419 |
- tests.vault_full_service_config(), |
|
| 2420 |
- strategies.sets( |
|
| 2421 |
- strategies.sampled_from([ |
|
| 2422 |
- 'length', |
|
| 2423 |
- 'repeat', |
|
| 2424 |
- 'upper', |
|
| 2425 |
- 'lower', |
|
| 2426 |
- 'number', |
|
| 2427 |
- 'space', |
|
| 2428 |
- 'dash', |
|
| 2429 |
- 'symbol', |
|
| 2430 |
- ]), |
|
| 2431 |
- max_size=7, |
|
| 2432 |
- ), |
|
| 2433 |
- ), |
|
| 2434 |
- min_size=5, |
|
| 2435 |
- max_size=5, |
|
| 2436 |
- unique_by=lambda obj: json.dumps(obj, sort_keys=True), |
|
| 2437 |
- ).filter(bool), |
|
| 2436 |
+ target=setting, |
|
| 2437 |
+ config=_vault_full_config, |
|
| 2438 | 2438 |
) |
| 2439 |
- def init_random_partial_settings( |
|
| 2440 |
- self, settings_list: list[_types.VaultConfigGlobalSettings] |
|
| 2441 |
- ) -> Iterable[_types.VaultConfigGlobalSettings]: |
|
| 2442 |
- return stateful.multiple(*settings_list) |
|
| 2443 |
- |
|
| 2444 |
- @stateful.invariant() |
|
| 2445 |
- def check_consistency_of_configs(self) -> None: |
|
| 2446 |
- _types.clean_up_falsy_vault_config_values(self.current_config) |
|
| 2447 |
- assert self.current_config == cli._load_config() |
|
| 2448 |
- |
|
| 2449 |
- @stateful.rule( |
|
| 2450 |
- target=settings, |
|
| 2451 |
- settings_obj=tests.vault_full_service_config(), |
|
| 2452 |
- ) |
|
| 2453 |
- def prepare_settings( |
|
| 2454 |
- self, settings_obj: dict[str, int] |
|
| 2455 |
- ) -> _types.VaultConfigGlobalSettings: |
|
| 2456 |
- return cast(_types.VaultConfigGlobalSettings, settings_obj.copy()) |
|
| 2457 |
- |
|
| 2458 |
- @stateful.rule( |
|
| 2459 |
- target=known_services, |
|
| 2460 |
- name=strategies.text( |
|
| 2461 |
- strategies.characters(min_codepoint=32, max_codepoint=126), |
|
| 2462 |
- min_size=1, |
|
| 2463 |
- max_size=50, |
|
| 2464 |
- ), |
|
| 2439 |
+ def extract_initial_settings( |
|
| 2440 |
+ self, |
|
| 2441 |
+ config: _types.VaultConfig, |
|
| 2442 |
+ ) -> Iterable[_types.VaultConfigServicesSettings]: |
|
| 2443 |
+ return stateful.multiple( |
|
| 2444 |
+ *map(copy.deepcopy, config['services'].values()) |
|
| 2465 | 2445 |
) |
| 2466 |
- def prepare_service_name(self, name: str) -> str: |
|
| 2467 |
- return name |
|
| 2468 | 2446 |
|
| 2469 | 2447 |
@stateful.rule( |
| 2470 |
- target=configurations, |
|
| 2471 |
- settings_obj=stateful.consumes(settings), |
|
| 2448 |
+ target=configuration, |
|
| 2449 |
+ config=_vault_full_config, |
|
| 2472 | 2450 |
) |
| 2473 |
- def prepare_global_config( |
|
| 2451 |
+ def declare_config( |
|
| 2474 | 2452 |
self, |
| 2475 |
- settings_obj: dict[str, int], |
|
| 2453 |
+ config: _types.VaultConfig, |
|
| 2476 | 2454 |
) -> _types.VaultConfig: |
| 2477 |
- return {
|
|
| 2478 |
- 'global': cast(_types.VaultConfigGlobalSettings, settings_obj), |
|
| 2479 |
- 'services': {},
|
|
| 2480 |
- } |
|
| 2455 |
+ return config |
|
| 2481 | 2456 |
|
| 2482 | 2457 |
@stateful.rule( |
| 2483 |
- target=configurations, |
|
| 2484 |
- service=known_services, |
|
| 2485 |
- settings_obj=stateful.consumes(settings), |
|
| 2458 |
+ target=setting, |
|
| 2459 |
+ config=_vault_full_config, |
|
| 2486 | 2460 |
) |
| 2487 |
- def prepare_service_config( |
|
| 2461 |
+ def extract_settings( |
|
| 2488 | 2462 |
self, |
| 2489 |
- service: str, |
|
| 2490 |
- settings_obj: dict[str, int], |
|
| 2491 |
- ) -> _types.VaultConfig: |
|
| 2492 |
- return {
|
|
| 2493 |
- 'services': {
|
|
| 2494 |
- service: cast( |
|
| 2495 |
- _types.VaultConfigServicesSettings, settings_obj |
|
| 2496 |
- ), |
|
| 2497 |
- }, |
|
| 2498 |
- } |
|
| 2463 |
+ config: _types.VaultConfig, |
|
| 2464 |
+ ) -> Iterable[_types.VaultConfigServicesSettings]: |
|
| 2465 |
+ return stateful.multiple( |
|
| 2466 |
+ *map(copy.deepcopy, config['services'].values()) |
|
| 2467 |
+ ) |
|
| 2499 | 2468 |
|
| 2500 | 2469 |
@staticmethod |
| 2501 | 2470 |
def fold_configs( |
| 2502 | 2471 |
c1: _types.VaultConfig, c2: _types.VaultConfig |
| 2503 | 2472 |
) -> _types.VaultConfig: |
| 2473 |
+ """Fold `c1` into `c2`, overriding the latter.""" |
|
| 2504 | 2474 |
new_global_dict = c1.get('global', c2.get('global'))
|
| 2505 | 2475 |
if new_global_dict is not None: |
| 2506 | 2476 |
return {
|
| ... | ... |
@@ -2512,30 +2482,22 @@ class ConfigMergingStateMachine(stateful.RuleBasedStateMachine): |
| 2512 | 2482 |
} |
| 2513 | 2483 |
|
| 2514 | 2484 |
@stateful.rule( |
| 2515 |
- target=configurations, |
|
| 2516 |
- config_base=stateful.consumes(configurations), |
|
| 2517 |
- config_folded=stateful.consumes(configurations), |
|
| 2518 |
- ) |
|
| 2519 |
- def fold_configuration_into( |
|
| 2520 |
- self, |
|
| 2521 |
- config_base: _types.VaultConfig, |
|
| 2522 |
- config_folded: _types.VaultConfig, |
|
| 2523 |
- ) -> _types.VaultConfig: |
|
| 2524 |
- return self.fold_configs(config_folded, config_base) |
|
| 2525 |
- |
|
| 2526 |
- @stateful.rule( |
|
| 2527 |
- settings_obj=stateful.consumes(settings), |
|
| 2485 |
+ target=configuration, |
|
| 2486 |
+ config=configuration, |
|
| 2487 |
+ setting=setting.filter(bool), |
|
| 2528 | 2488 |
overwrite=strategies.booleans(), |
| 2529 | 2489 |
) |
| 2530 | 2490 |
def set_globals( |
| 2531 | 2491 |
self, |
| 2532 |
- settings_obj: _types.VaultConfigGlobalSettings, |
|
| 2492 |
+ config: _types.VaultConfig, |
|
| 2493 |
+ setting: _types.VaultConfigGlobalSettings, |
|
| 2533 | 2494 |
overwrite: bool, |
| 2534 |
- ) -> None: |
|
| 2495 |
+ ) -> _types.VaultConfig: |
|
| 2496 |
+ cli._save_config(config) |
|
| 2535 | 2497 |
if overwrite: |
| 2536 |
- self.current_config['global'] = {}
|
|
| 2537 |
- self.current_config.setdefault('global', {}).update(settings_obj)
|
|
| 2538 |
- assert _types.is_vault_config(self.current_config) |
|
| 2498 |
+ config['global'] = {}
|
|
| 2499 |
+ config.setdefault('global', {}).update(setting)
|
|
| 2500 |
+ assert _types.is_vault_config(config) |
|
| 2539 | 2501 |
# NOTE: This relies on settings_obj containing only the keys |
| 2540 | 2502 |
# "length", "repeat", "upper", "lower", "number", "space", |
| 2541 | 2503 |
# "dash" and "symbol". |
| ... | ... |
@@ -2545,38 +2507,37 @@ class ConfigMergingStateMachine(stateful.RuleBasedStateMachine): |
| 2545 | 2507 |
'--config', |
| 2546 | 2508 |
'--overwrite-existing' if overwrite else '--merge-existing', |
| 2547 | 2509 |
] |
| 2548 |
- + [f'--{key}={value}' for key, value in settings_obj.items()],
|
|
| 2510 |
+ + [ |
|
| 2511 |
+ f'--{key}={value}'
|
|
| 2512 |
+ for key, value in setting.items() |
|
| 2513 |
+ if key in _valid_properties |
|
| 2514 |
+ ], |
|
| 2549 | 2515 |
catch_exceptions=False, |
| 2550 | 2516 |
) |
| 2551 | 2517 |
result = tests.ReadableResult.parse(_result) |
| 2552 | 2518 |
assert result.clean_exit(empty_stderr=False) |
| 2519 |
+ assert cli._load_config() == config |
|
| 2520 |
+ return config |
|
| 2553 | 2521 |
|
| 2554 |
- # No check for whether the service settings currently exist. This |
|
| 2555 |
- # may therefore actually "create" the settings, not merely "modify" |
|
| 2556 |
- # them. |
|
| 2557 |
- # |
|
| 2558 |
- # (There is no check because this appears to be hard or impossible |
|
| 2559 |
- # to express as a hypothesis strategy: it would depend on the |
|
| 2560 |
- # current state of the state machine instance. This could be |
|
| 2561 |
- # circumvented with `hypothesis.assume`, but that may likely trigger |
|
| 2562 |
- # health check errors.) |
|
| 2563 | 2522 |
@stateful.rule( |
| 2564 |
- service=known_services, |
|
| 2565 |
- settings_obj=stateful.consumes(settings), |
|
| 2523 |
+ target=configuration, |
|
| 2524 |
+ config=configuration, |
|
| 2525 |
+ service=strategies.sampled_from(_known_services), |
|
| 2526 |
+ setting=setting.filter(bool), |
|
| 2566 | 2527 |
overwrite=strategies.booleans(), |
| 2567 | 2528 |
) |
| 2568 | 2529 |
def set_service( |
| 2569 | 2530 |
self, |
| 2531 |
+ config: _types.VaultConfig, |
|
| 2570 | 2532 |
service: str, |
| 2571 |
- settings_obj: _types.VaultConfigServicesSettings, |
|
| 2533 |
+ setting: _types.VaultConfigServicesSettings, |
|
| 2572 | 2534 |
overwrite: bool, |
| 2573 |
- ) -> None: |
|
| 2535 |
+ ) -> _types.VaultConfig: |
|
| 2536 |
+ cli._save_config(config) |
|
| 2574 | 2537 |
if overwrite: |
| 2575 |
- self.current_config['services'][service] = {}
|
|
| 2576 |
- self.current_config['services'].setdefault(service, {}).update(
|
|
| 2577 |
- settings_obj |
|
| 2578 |
- ) |
|
| 2579 |
- assert _types.is_vault_config(self.current_config) |
|
| 2538 |
+ config['services'][service] = {}
|
|
| 2539 |
+ config['services'].setdefault(service, {}).update(setting)
|
|
| 2540 |
+ assert _types.is_vault_config(config) |
|
| 2580 | 2541 |
# NOTE: This relies on settings_obj containing only the keys |
| 2581 | 2542 |
# "length", "repeat", "upper", "lower", "number", "space", |
| 2582 | 2543 |
# "dash" and "symbol". |
| ... | ... |
@@ -2586,17 +2547,29 @@ class ConfigMergingStateMachine(stateful.RuleBasedStateMachine): |
| 2586 | 2547 |
'--config', |
| 2587 | 2548 |
'--overwrite-existing' if overwrite else '--merge-existing', |
| 2588 | 2549 |
] |
| 2589 |
- + [f'--{key}={value}' for key, value in settings_obj.items()]
|
|
| 2550 |
+ + [ |
|
| 2551 |
+ f'--{key}={value}'
|
|
| 2552 |
+ for key, value in setting.items() |
|
| 2553 |
+ if key in _valid_properties |
|
| 2554 |
+ ] |
|
| 2590 | 2555 |
+ ['--', service], |
| 2591 | 2556 |
catch_exceptions=False, |
| 2592 | 2557 |
) |
| 2593 | 2558 |
result = tests.ReadableResult.parse(_result) |
| 2594 | 2559 |
assert result.clean_exit(empty_stderr=False) |
| 2560 |
+ assert cli._load_config() == config |
|
| 2561 |
+ return config |
|
| 2595 | 2562 |
|
| 2596 |
- @stateful.precondition(lambda self: 'global' in self.current_config) |
|
| 2597 |
- @stateful.rule() |
|
| 2598 |
- def purge_global(self) -> None: |
|
| 2599 |
- self.current_config.pop('global')
|
|
| 2563 |
+ @stateful.rule( |
|
| 2564 |
+ target=configuration, |
|
| 2565 |
+ config=configuration.filter(lambda c: 'global' in c), |
|
| 2566 |
+ ) |
|
| 2567 |
+ def purge_global( |
|
| 2568 |
+ self, |
|
| 2569 |
+ config: _types.VaultConfig, |
|
| 2570 |
+ ) -> _types.VaultConfig: |
|
| 2571 |
+ cli._save_config(config) |
|
| 2572 |
+ config.pop('global')
|
|
| 2600 | 2573 |
_result = self.runner.invoke( |
| 2601 | 2574 |
cli.derivepassphrase_vault, |
| 2602 | 2575 |
['--delete-globals'], |
| ... | ... |
@@ -2605,21 +2578,27 @@ class ConfigMergingStateMachine(stateful.RuleBasedStateMachine): |
| 2605 | 2578 |
) |
| 2606 | 2579 |
result = tests.ReadableResult.parse(_result) |
| 2607 | 2580 |
assert result.clean_exit(empty_stderr=False) |
| 2581 |
+ assert cli._load_config() == config |
|
| 2582 |
+ return config |
|
| 2608 | 2583 |
|
| 2609 |
- # No check for whether the service settings currently exist. This |
|
| 2610 |
- # may therefore actually be almost a no-op, purging settings that |
|
| 2611 |
- # aren't set in the first place. |
|
| 2612 |
- # |
|
| 2613 |
- # (There is no check because this appears to be hard or impossible |
|
| 2614 |
- # to express as a hypothesis strategy: it would depend on the |
|
| 2615 |
- # current state of the state machine instance. This could be |
|
| 2616 |
- # circumvented with `hypothesis.assume`, but that may likely trigger |
|
| 2617 |
- # health check errors.) |
|
| 2618 |
- @stateful.precondition(lambda self: bool(self.current_config['services'])) |
|
| 2619 |
- @stateful.rule(service=stateful.consumes(known_services)) |
|
| 2620 |
- def purge_service(self, service: str) -> None: |
|
| 2621 |
- ret = self.current_config['services'].pop(service, None) |
|
| 2622 |
- if ret is not None: |
|
| 2584 |
+ @stateful.rule( |
|
| 2585 |
+ target=configuration, |
|
| 2586 |
+ config_and_service=configuration.filter( |
|
| 2587 |
+ lambda c: len(c['services']) > 1 |
|
| 2588 |
+ ).flatmap( |
|
| 2589 |
+ lambda c: strategies.tuples( |
|
| 2590 |
+ strategies.just(c), |
|
| 2591 |
+ strategies.sampled_from(tuple(c['services'].keys())), |
|
| 2592 |
+ ) |
|
| 2593 |
+ ), |
|
| 2594 |
+ ) |
|
| 2595 |
+ def purge_service( |
|
| 2596 |
+ self, |
|
| 2597 |
+ config_and_service: tuple[_types.VaultConfig, str], |
|
| 2598 |
+ ) -> _types.VaultConfig: |
|
| 2599 |
+ config, service = config_and_service |
|
| 2600 |
+ cli._save_config(config) |
|
| 2601 |
+ config['services'].pop(service, None) |
|
| 2623 | 2602 |
_result = self.runner.invoke( |
| 2624 | 2603 |
cli.derivepassphrase_vault, |
| 2625 | 2604 |
['--delete', '--', service], |
| ... | ... |
@@ -2628,10 +2607,19 @@ class ConfigMergingStateMachine(stateful.RuleBasedStateMachine): |
| 2628 | 2607 |
) |
| 2629 | 2608 |
result = tests.ReadableResult.parse(_result) |
| 2630 | 2609 |
assert result.clean_exit(empty_stderr=False) |
| 2610 |
+ assert cli._load_config() == config |
|
| 2611 |
+ return config |
|
| 2631 | 2612 |
|
| 2632 |
- @stateful.rule() |
|
| 2633 |
- def purge_all(self) -> None: |
|
| 2634 |
- self.current_config = {'services': {}}
|
|
| 2613 |
+ @stateful.rule( |
|
| 2614 |
+ target=configuration, |
|
| 2615 |
+ config=configuration.filter(lambda c: 0 < len(c['services']) < 5), |
|
| 2616 |
+ ) |
|
| 2617 |
+ def purge_all( |
|
| 2618 |
+ self, |
|
| 2619 |
+ config: _types.VaultConfig, |
|
| 2620 |
+ ) -> _types.VaultConfig: |
|
| 2621 |
+ cli._save_config(config) |
|
| 2622 |
+ config = {'services': {}}
|
|
| 2635 | 2623 |
_result = self.runner.invoke( |
| 2636 | 2624 |
cli.derivepassphrase_vault, |
| 2637 | 2625 |
['--clear'], |
| ... | ... |
@@ -2640,35 +2628,43 @@ class ConfigMergingStateMachine(stateful.RuleBasedStateMachine): |
| 2640 | 2628 |
) |
| 2641 | 2629 |
result = tests.ReadableResult.parse(_result) |
| 2642 | 2630 |
assert result.clean_exit(empty_stderr=False) |
| 2631 |
+ assert cli._load_config() == config |
|
| 2632 |
+ return config |
|
| 2643 | 2633 |
|
| 2644 | 2634 |
@stateful.rule( |
| 2645 |
- config=stateful.consumes(configurations), |
|
| 2635 |
+ target=configuration, |
|
| 2636 |
+ base_config=configuration, |
|
| 2637 |
+ config_to_import=configuration, |
|
| 2646 | 2638 |
overwrite=strategies.booleans(), |
| 2647 | 2639 |
) |
| 2648 |
- def import_configuraton( |
|
| 2640 |
+ def import_configuration( |
|
| 2649 | 2641 |
self, |
| 2650 |
- config: _types.VaultConfig, |
|
| 2642 |
+ base_config: _types.VaultConfig, |
|
| 2643 |
+ config_to_import: _types.VaultConfig, |
|
| 2651 | 2644 |
overwrite: bool, |
| 2652 |
- ) -> None: |
|
| 2653 |
- self.current_config = ( |
|
| 2654 |
- self.fold_configs(config, self.current_config) |
|
| 2645 |
+ ) -> _types.VaultConfig: |
|
| 2646 |
+ cli._save_config(base_config) |
|
| 2647 |
+ config = ( |
|
| 2648 |
+ self.fold_configs(config_to_import, base_config) |
|
| 2655 | 2649 |
if not overwrite |
| 2656 |
- else config |
|
| 2650 |
+ else config_to_import |
|
| 2657 | 2651 |
) |
| 2658 |
- assert _types.is_vault_config(self.current_config) |
|
| 2652 |
+ assert _types.is_vault_config(config) |
|
| 2659 | 2653 |
_result = self.runner.invoke( |
| 2660 | 2654 |
cli.derivepassphrase_vault, |
| 2661 | 2655 |
['--import', '-'] |
| 2662 | 2656 |
+ (['--overwrite-existing'] if overwrite else []), |
| 2663 |
- input=json.dumps(config), |
|
| 2657 |
+ input=json.dumps(config_to_import), |
|
| 2664 | 2658 |
catch_exceptions=False, |
| 2665 | 2659 |
) |
| 2666 | 2660 |
assert tests.ReadableResult.parse(_result).clean_exit( |
| 2667 | 2661 |
empty_stderr=False |
| 2668 | 2662 |
) |
| 2663 |
+ assert cli._load_config() == config |
|
| 2664 |
+ return config |
|
| 2669 | 2665 |
|
| 2670 | 2666 |
def teardown(self) -> None: |
| 2671 | 2667 |
self.exit_stack.close() |
| 2672 | 2668 |
|
| 2673 | 2669 |
|
| 2674 |
-TestConfigMerging = ConfigMergingStateMachine.TestCase |
|
| 2670 |
+TestConfigManagement = ConfigManagementStateMachine.TestCase |
|
| 2675 | 2671 |