~billy-olsen/maas/lp1484696-proxypass-websockets-1.9

« back to all changes in this revision

Viewing changes to src/provisioningserver/tests/test_config.py

[r=allenap][bug=1524007][author=allenap] Backport of r4551 from lp:maas: By default, open configuration files read-only.

This eliminates the need for locking when using the YAML-based back-end.

Show diffs side-by-side

added added

removed removed

Lines of Context:
15
15
__all__ = []
16
16
 
17
17
import contextlib
18
 
from operator import methodcaller
 
18
from operator import (
 
19
    delitem,
 
20
    methodcaller,
 
21
    setitem,
 
22
)
19
23
import os.path
20
24
import random
21
25
import re
39
43
    Configuration,
40
44
    ConfigurationDatabase,
41
45
    ConfigurationFile,
 
46
    ConfigurationImmutable,
42
47
    ConfigurationMeta,
43
48
    ConfigurationOption,
44
49
    Directory,
51
56
from provisioningserver.utils.fs import RunLock
52
57
from testtools import ExpectedException
53
58
from testtools.matchers import (
 
59
    Equals,
54
60
    FileContains,
55
61
    FileExists,
56
62
    Is,
311
317
        with expected_exception:
312
318
            config.foo = "bar"
313
319
 
314
 
    def test_opens_using_backend(self):
 
320
    def test_open_uses_backend_as_context_manager(self):
315
321
        config_file = self.make_file()
316
322
        backend = self.patch(ExampleConfiguration, "backend")
317
 
        with backend.open(config_file) as config:
318
 
            backend_ctx = backend.open.return_value
 
323
        with ExampleConfiguration.open(config_file) as config:
 
324
            # The backend was opened using open() too.
 
325
            self.assertThat(backend.open, MockCalledOnceWith(config_file))
319
326
            # The object returned from backend.open() has been used as the
320
327
            # context manager, providing `config`.
321
 
            self.assertThat(config, Is(backend_ctx.__enter__.return_value))
322
 
            # We're within the context, as expected.
323
 
            self.assertThat(backend_ctx.__exit__, MockNotCalled())
324
 
        # The context has been exited.
 
328
            backend_ctx = backend.open.return_value
 
329
            self.assertThat(config.store, Is(
 
330
                backend_ctx.__enter__.return_value))
 
331
            # We're within the context, as expected.
 
332
            self.assertThat(backend_ctx.__exit__, MockNotCalled())
 
333
        # The backend context has also been exited.
 
334
        self.assertThat(
 
335
            backend_ctx.__exit__,
 
336
            MockCalledOnceWith(None, None, None))
 
337
 
 
338
    def test_open_for_update_uses_backend_as_context_manager(self):
 
339
        config_file = self.make_file()
 
340
        backend = self.patch(ExampleConfiguration, "backend")
 
341
        with ExampleConfiguration.open_for_update(config_file) as config:
 
342
            # The backend was opened using open_for_update() too.
 
343
            self.assertThat(
 
344
                backend.open_for_update, MockCalledOnceWith(config_file))
 
345
            # The object returned from backend.open_for_update() has been used
 
346
            # as the context manager, providing `config`.
 
347
            backend_ctx = backend.open_for_update.return_value
 
348
            self.assertThat(config.store, Is(
 
349
                backend_ctx.__enter__.return_value))
 
350
            # We're within the context, as expected.
 
351
            self.assertThat(backend_ctx.__exit__, MockNotCalled())
 
352
        # The backend context has also been exited.
325
353
        self.assertThat(
326
354
            backend_ctx.__exit__,
327
355
            MockCalledOnceWith(None, None, None))
338
366
    def make_database_store(self):
339
367
        database = sqlite3.connect(":memory:")
340
368
        self.addCleanup(database.close)
341
 
        return ConfigurationDatabase(database)
 
369
        return ConfigurationDatabase(database, mutable=True)
342
370
 
343
371
    def make_file_store(self):
344
 
        return ConfigurationFile(self.make_file())
 
372
        return ConfigurationFile(self.make_file(), mutable=True)
345
373
 
346
374
    def make_config(self):
347
375
        store = self.make_store(self)
399
427
 
400
428
    def test_adding_configuration_option(self):
401
429
        database = sqlite3.connect(":memory:")
402
 
        config = ConfigurationDatabase(database)
 
430
        config = ConfigurationDatabase(database, mutable=True)
403
431
        config["alice"] = {"abc": 123}
404
432
        self.assertEqual({"alice"}, set(config))
405
433
        self.assertEqual({"abc": 123}, config["alice"])
406
434
 
407
435
    def test_replacing_configuration_option(self):
408
436
        database = sqlite3.connect(":memory:")
409
 
        config = ConfigurationDatabase(database)
 
437
        config = ConfigurationDatabase(database, mutable=True)
410
438
        config["alice"] = {"abc": 123}
411
439
        config["alice"] = {"def": 456}
412
440
        self.assertEqual({"alice"}, set(config))
414
442
 
415
443
    def test_getting_configuration_option(self):
416
444
        database = sqlite3.connect(":memory:")
417
 
        config = ConfigurationDatabase(database)
 
445
        config = ConfigurationDatabase(database, mutable=True)
418
446
        config["alice"] = {"abc": 123}
419
447
        self.assertEqual({"abc": 123}, config["alice"])
420
448
 
425
453
 
426
454
    def test_removing_configuration_option(self):
427
455
        database = sqlite3.connect(":memory:")
428
 
        config = ConfigurationDatabase(database)
 
456
        config = ConfigurationDatabase(database, mutable=True)
429
457
        config["alice"] = {"abc": 123}
430
458
        del config["alice"]
431
459
        self.assertEqual(set(), set(config))
434
462
        # ConfigurationDatabase.open() returns a context manager that closes
435
463
        # the database on exit.
436
464
        config_file = os.path.join(self.make_dir(), "config")
437
 
        config = ConfigurationDatabase.open(config_file)
 
465
        config = ConfigurationDatabase.open_for_update(config_file)
438
466
        self.assertIsInstance(config, contextlib.GeneratorContextManager)
439
467
        with config as config:
440
468
            self.assertIsInstance(config, ConfigurationDatabase)
465
493
        config_file = os.path.join(self.make_dir(), "config")
466
494
        config_key = factory.make_name("key")
467
495
        config_value = factory.make_name("value")
468
 
        with ConfigurationDatabase.open(config_file) as config:
 
496
        with ConfigurationDatabase.open_for_update(config_file) as config:
469
497
            config[config_key] = config_value
470
498
        with ConfigurationDatabase.open(config_file) as config:
471
499
            self.assertEqual(config_value, config[config_key])
477
505
        exception_type = factory.make_exception_type()
478
506
        # Set a configuration option, then crash.
479
507
        with ExpectedException(exception_type):
480
 
            with ConfigurationDatabase.open(config_file) as config:
 
508
            with ConfigurationDatabase.open_for_update(config_file) as config:
481
509
                config[config_key] = config_value
482
510
                raise exception_type()
483
511
        # No value has been saved for `config_key`.
484
512
        with ConfigurationDatabase.open(config_file) as config:
485
513
            self.assertRaises(KeyError, lambda: config[config_key])
486
514
 
 
515
    def test_as_string(self):
 
516
        database = sqlite3.connect(":memory:")
 
517
        config = ConfigurationDatabase(database)
 
518
        self.assertThat(unicode(config), Equals(
 
519
            "ConfigurationDatabase(main=:memory:)"))
 
520
 
 
521
 
 
522
class TestConfigurationDatabaseMutability(MAASTestCase):
 
523
    """Tests for `ConfigurationDatabase` mutability."""
 
524
 
 
525
    def test_immutable(self):
 
526
        database = sqlite3.connect(":memory:")
 
527
        config = ConfigurationDatabase(database, mutable=False)
 
528
        self.assertRaises(ConfigurationImmutable, setitem, config, "alice", 1)
 
529
        self.assertRaises(ConfigurationImmutable, delitem, config, "alice")
 
530
 
 
531
    def test_mutable(self):
 
532
        database = sqlite3.connect(":memory:")
 
533
        config = ConfigurationDatabase(database, mutable=True)
 
534
        config["alice"] = 1234
 
535
        del config["alice"]
 
536
 
 
537
    def test_open_yields_immutable_backend(self):
 
538
        config_file = os.path.join(self.make_dir(), "config")
 
539
        config_key = factory.make_name("key")
 
540
        with ConfigurationDatabase.open(config_file) as config:
 
541
            with ExpectedException(ConfigurationImmutable):
 
542
                config[config_key] = factory.make_name("value")
 
543
            with ExpectedException(ConfigurationImmutable):
 
544
                del config[config_key]
 
545
 
 
546
    def test_open_for_update_yields_mutable_backend(self):
 
547
        config_file = os.path.join(self.make_dir(), "config")
 
548
        config_key = factory.make_name("key")
 
549
        with ConfigurationDatabase.open_for_update(config_file) as config:
 
550
            config[config_key] = factory.make_name("value")
 
551
            del config[config_key]
 
552
 
487
553
 
488
554
class TestConfigurationFile(MAASTestCase):
489
555
    """Tests for `ConfigurationFile`."""
496
562
                config={}, dirty=False, path=sentinel.filename))
497
563
 
498
564
    def test_adding_configuration_option(self):
499
 
        config = ConfigurationFile(sentinel.filename)
 
565
        config = ConfigurationFile(sentinel.filename, mutable=True)
500
566
        config["alice"] = {"abc": 123}
501
567
        self.assertEqual({"alice"}, set(config))
502
568
        self.assertEqual({"abc": 123}, config["alice"])
503
569
        self.assertTrue(config.dirty)
504
570
 
505
571
    def test_replacing_configuration_option(self):
506
 
        config = ConfigurationFile(sentinel.filename)
 
572
        config = ConfigurationFile(sentinel.filename, mutable=True)
507
573
        config["alice"] = {"abc": 123}
508
574
        config["alice"] = {"def": 456}
509
575
        self.assertEqual({"alice"}, set(config))
511
577
        self.assertTrue(config.dirty)
512
578
 
513
579
    def test_getting_configuration_option(self):
514
 
        config = ConfigurationFile(sentinel.filename)
 
580
        config = ConfigurationFile(sentinel.filename, mutable=True)
515
581
        config["alice"] = {"abc": 123}
516
582
        self.assertEqual({"abc": 123}, config["alice"])
517
583
 
520
586
        self.assertRaises(KeyError, lambda: config["alice"])
521
587
 
522
588
    def test_removing_configuration_option(self):
523
 
        config = ConfigurationFile(sentinel.filename)
 
589
        config = ConfigurationFile(sentinel.filename, mutable=True)
524
590
        config["alice"] = {"abc": 123}
525
591
        del config["alice"]
526
592
        self.assertEqual(set(), set(config))
575
641
        config_file = os.path.join(self.make_dir(), "config")
576
642
        open(config_file, "wb").close()  # touch.
577
643
        os.chmod(config_file, 0o644)  # u=rw,go=r
578
 
        with ConfigurationFile.open(config_file):
 
644
        with ConfigurationFile.open_for_update(config_file):
579
645
            perms = FilePath(config_file).getPermissions()
580
646
            self.assertEqual("rw-r--r--", perms.shorthand())
581
647
        perms = FilePath(config_file).getPermissions()
587
653
        config_file = os.path.join(self.make_dir(), "config")
588
654
        open(config_file, "wb").close()  # touch.
589
655
        os.chmod(config_file, 0o644)  # u=rw,go=r
590
 
        with ConfigurationFile.open(config_file) as config:
 
656
        with ConfigurationFile.open_for_update(config_file) as config:
591
657
            perms = FilePath(config_file).getPermissions()
592
658
            self.assertEqual("rw-r--r--", perms.shorthand())
593
659
            config["foobar"] = "I am a modification"
601
667
        config_file = os.path.join(self.make_dir(), "config")
602
668
        open(config_file, "wb").close()  # touch.
603
669
        os.chmod(config_file, 0o644)  # u=rw,go=r
604
 
        with ConfigurationFile.open(config_file) as config:
 
670
        with ConfigurationFile.open_for_update(config_file) as config:
605
671
            config["foobar"] = "I am a modification"
606
672
            os.unlink(config_file)
607
673
        perms = FilePath(config_file).getPermissions()
613
679
        config_file = os.path.join(self.make_dir(), "config")
614
680
        config_key = factory.make_name("key")
615
681
        config_value = factory.make_name("value")
616
 
        with ConfigurationFile.open(config_file) as config:
 
682
        with ConfigurationFile.open_for_update(config_file) as config:
617
683
            config[config_key] = config_value
618
684
            self.assertEqual({config_key: config_value}, config.config)
619
685
            self.assertTrue(config.dirty)
627
693
        exception_type = factory.make_exception_type()
628
694
        # Set a configuration option, then crash.
629
695
        with ExpectedException(exception_type):
630
 
            with ConfigurationFile.open(config_file) as config:
 
696
            with ConfigurationFile.open_for_update(config_file) as config:
631
697
                config[config_key] = config_value
632
698
                raise exception_type()
633
699
        # No value has been saved for `config_key`.
638
704
        config_file = os.path.join(self.make_dir(), "config")
639
705
        config_lock = RunLock(config_file)
640
706
        self.assertFalse(config_lock.is_locked())
641
 
        with ConfigurationFile.open(config_file):
 
707
        with ConfigurationFile.open_for_update(config_file):
642
708
            self.assertTrue(config_lock.is_locked())
643
709
        self.assertFalse(config_lock.is_locked())
644
710
 
 
711
    def test_as_string(self):
 
712
        config_file = os.path.join(self.make_dir(), "config")
 
713
        config = ConfigurationFile(config_file)
 
714
        self.assertThat(unicode(config), Equals(
 
715
            "ConfigurationFile(%r)" % config_file))
 
716
 
 
717
 
 
718
class TestConfigurationFileMutability(MAASTestCase):
 
719
    """Tests for `ConfigurationFile` mutability."""
 
720
 
 
721
    def test_immutable(self):
 
722
        config_file = os.path.join(self.make_dir(), "config")
 
723
        config = ConfigurationFile(config_file, mutable=False)
 
724
        self.assertRaises(ConfigurationImmutable, setitem, config, "alice", 1)
 
725
        self.assertRaises(ConfigurationImmutable, delitem, config, "alice")
 
726
 
 
727
    def test_mutable(self):
 
728
        config_file = os.path.join(self.make_dir(), "config")
 
729
        config = ConfigurationFile(config_file, mutable=True)
 
730
        config["alice"] = 1234
 
731
        del config["alice"]
 
732
 
 
733
    def test_open_yields_immutable_backend(self):
 
734
        config_file = os.path.join(self.make_dir(), "config")
 
735
        config_key = factory.make_name("key")
 
736
        with ConfigurationFile.open(config_file) as config:
 
737
            with ExpectedException(ConfigurationImmutable):
 
738
                config[config_key] = factory.make_name("value")
 
739
            with ExpectedException(ConfigurationImmutable):
 
740
                del config[config_key]
 
741
 
 
742
    def test_open_for_update_yields_mutable_backend(self):
 
743
        config_file = os.path.join(self.make_dir(), "config")
 
744
        config_key = factory.make_name("key")
 
745
        with ConfigurationFile.open_for_update(config_file) as config:
 
746
            config[config_key] = factory.make_name("value")
 
747
            del config[config_key]
 
748
 
645
749
 
646
750
class TestClusterConfiguration(MAASTestCase):
647
751
    """Tests for `ClusterConfiguration`."""