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 |