Exemple #1
0
 def test_value_characters (self):
     feature = BaseFeature(self.bfm, 'voiced')
     self.assertEqual(feature.get_value_characters(HAS_FEATURE), set())
     self.assertEqual(feature.get_value_characters(NOT_HAS_FEATURE), set())
     character1 = BaseCharacter(self.bfm, 'a')
     character1.set_feature_value(feature, HAS_FEATURE)
     self.assertEqual(feature.get_value_characters(HAS_FEATURE),
                      set([character1]))
     self.assertEqual(feature.get_value_characters(NOT_HAS_FEATURE), set())
     character1.set_feature_value(feature, NOT_HAS_FEATURE)
     self.assertEqual(feature.get_value_characters(HAS_FEATURE), set())
     self.assertEqual(feature.get_value_characters(NOT_HAS_FEATURE),
                      set([character1]))
     character2 = DiacriticCharacter(self.bfm, 'b')
     character2.set_feature_value(feature, HAS_FEATURE)
     self.assertEqual(feature.get_value_characters(HAS_FEATURE),
                      set([character2]))
     self.assertEqual(feature.get_value_characters(NOT_HAS_FEATURE),
                      set([character1]))
     character2.set_feature_value(feature, INAPPLICABLE_FEATURE)
     self.assertEqual(feature.get_value_characters(HAS_FEATURE), set())
     self.assertEqual(feature.get_value_characters(NOT_HAS_FEATURE),
                      set([character1]))
     character3 = SpacingCharacter(self.bfm, 'c')
     character3.set_feature_value(feature, NOT_HAS_FEATURE)
     self.assertEqual(feature.get_value_characters(HAS_FEATURE), set())
     self.assertEqual(feature.get_value_characters(NOT_HAS_FEATURE),
                      set([character1, character3]))
Exemple #2
0
 def test_normalised_form (self):
     feature1 = BaseFeature(self.bfm, 'anterior')
     character1 = DiacriticCharacter(self.bfm, 'a')
     character1.set_feature_value(feature1, HAS_FEATURE)
     self.assertEqual(character1.normalised_form, NormalisedForm(
             '{0}{1}'.format(BNFM, HAS_FEATURE)))
     # Adding a feature changes the normalised form.
     feature2 = BaseFeature(self.bfm, 'dental')
     character1.set_feature_value(feature2, INAPPLICABLE_FEATURE)
     self.assertEqual(character1.normalised_form, NormalisedForm(
             '{0}{1}{2}'.format(BNFM, HAS_FEATURE, INAPPLICABLE_FEATURE)))
     # The order of the normalised form feature values is
     # alphabetical by feature name.
     feature3 = BaseFeature(self.bfm, 'consonantal')
     character1.set_feature_value(feature3, NOT_HAS_FEATURE)
     self.assertEqual(character1.normalised_form, NormalisedForm(
             '{0}{1}{2}{3}'.format(BNFM, HAS_FEATURE, NOT_HAS_FEATURE,
                                   INAPPLICABLE_FEATURE)))
     # Renaming a feature may change the normalised form.
     feature1.name = 'vocalic'
     self.assertEqual(character1.normalised_form, NormalisedForm(
             '{0}{1}{2}{3}'.format(BNFM, NOT_HAS_FEATURE,
                                   INAPPLICABLE_FEATURE, HAS_FEATURE)))
     # Changing a feature value changes the normalised form.
     character1.set_feature_value(feature1, NOT_HAS_FEATURE)
     self.assertEqual(character1.normalised_form, NormalisedForm(
             '{0}{1}{2}{1}'.format(BNFM, NOT_HAS_FEATURE,
                                   INAPPLICABLE_FEATURE)))
     # Removing a feature value changes the normalised form.
     feature2.delete()
     self.assertEqual(character1.normalised_form, NormalisedForm(
             '{0}{1}{1}'.format(BNFM, NOT_HAS_FEATURE)))
Exemple #3
0
 def test_object_caching (self):
     # Creating a Character with the same IPA form as an existing
     # Character must return the existing Character, if both are
     # associated with the same BinaryFeaturesModel.
     character1 = BaseCharacter(self.bfm, 'a')
     character2 = BaseCharacter(self.bfm, 'a')
     self.assertEqual(character1, character2)
     character3 = BaseCharacter(self.bfm, 'b')
     self.assertNotEqual(character1, character3)
     bfm2 = BinaryFeaturesModel()
     character4 = BaseCharacter(bfm2, 'a')
     self.assertNotEqual(character1, character4)
     # It is an error to create a Character with the same IPA form
     # but of a different type (subclass).
     self.assertRaises(InvalidCharacterError, DiacriticCharacter,
                       self.bfm, 'a')
     # Initialising of the Character should happen only once.
     feature = BaseFeature(self.bfm, 'voiced')
     character1.set_feature_value(feature, HAS_FEATURE)
     self.assertEqual(character1.get_feature_value(feature), HAS_FEATURE)
     character5 = BaseCharacter(self.bfm, 'a')
     self.assertEqual(character1.get_feature_value(feature), HAS_FEATURE)
     character6 = DiacriticCharacter(self.bfm, 'c')
     character6.set_feature_value(feature, HAS_FEATURE)
     self.assertEqual(character6.get_feature_value(feature), HAS_FEATURE)
     character7 = DiacriticCharacter(self.bfm, 'c')
     self.assertEqual(character6.get_feature_value(feature), HAS_FEATURE)
     character8 = SpacingCharacter(self.bfm, 'd')
     character8.set_feature_value(feature, HAS_FEATURE)
     self.assertEqual(character8.get_feature_value(feature), HAS_FEATURE)
     character9 = SpacingCharacter(self.bfm, 'd')
     self.assertEqual(character8.get_feature_value(feature), HAS_FEATURE)
     character10 = SuprasegmentalCharacter(self.bfm, 'e')
     feature2 = SuprasegmentalFeature(self.bfm, 'syllabic')
     character10.set_feature_value(feature2, HAS_FEATURE)
     self.assertEqual(character10.get_feature_value(feature2), HAS_FEATURE)
     character11 = SuprasegmentalCharacter(self.bfm, 'e')
     self.assertEqual(character10.get_feature_value(feature2), HAS_FEATURE)
Exemple #4
0
 def test_object_caching(self):
     # Creating a Character with the same IPA form as an existing
     # Character must return the existing Character, if both are
     # associated with the same BinaryFeaturesModel.
     character1 = BaseCharacter(self.bfm, 'a')
     character2 = BaseCharacter(self.bfm, 'a')
     self.assertEqual(character1, character2)
     character3 = BaseCharacter(self.bfm, 'b')
     self.assertNotEqual(character1, character3)
     bfm2 = BinaryFeaturesModel()
     character4 = BaseCharacter(bfm2, 'a')
     self.assertNotEqual(character1, character4)
     # It is an error to create a Character with the same IPA form
     # but of a different type (subclass).
     self.assertRaises(InvalidCharacterError, DiacriticCharacter, self.bfm,
                       'a')
     # Initialising of the Character should happen only once.
     feature = BaseFeature(self.bfm, 'voiced')
     character1.set_feature_value(feature, HAS_FEATURE)
     self.assertEqual(character1.get_feature_value(feature), HAS_FEATURE)
     character5 = BaseCharacter(self.bfm, 'a')
     self.assertEqual(character1.get_feature_value(feature), HAS_FEATURE)
     character6 = DiacriticCharacter(self.bfm, 'c')
     character6.set_feature_value(feature, HAS_FEATURE)
     self.assertEqual(character6.get_feature_value(feature), HAS_FEATURE)
     character7 = DiacriticCharacter(self.bfm, 'c')
     self.assertEqual(character6.get_feature_value(feature), HAS_FEATURE)
     character8 = SpacingCharacter(self.bfm, 'd')
     character8.set_feature_value(feature, HAS_FEATURE)
     self.assertEqual(character8.get_feature_value(feature), HAS_FEATURE)
     character9 = SpacingCharacter(self.bfm, 'd')
     self.assertEqual(character8.get_feature_value(feature), HAS_FEATURE)
     character10 = SuprasegmentalCharacter(self.bfm, 'e')
     feature2 = SuprasegmentalFeature(self.bfm, 'syllabic')
     character10.set_feature_value(feature2, HAS_FEATURE)
     self.assertEqual(character10.get_feature_value(feature2), HAS_FEATURE)
     character11 = SuprasegmentalCharacter(self.bfm, 'e')
     self.assertEqual(character10.get_feature_value(feature2), HAS_FEATURE)
Exemple #5
0
 def test_normalised_form(self):
     feature1 = BaseFeature(self.bfm, 'anterior')
     character1 = DiacriticCharacter(self.bfm, 'a')
     character1.set_feature_value(feature1, HAS_FEATURE)
     self.assertEqual(character1.normalised_form,
                      NormalisedForm('{0}{1}'.format(BNFM, HAS_FEATURE)))
     # Adding a feature changes the normalised form.
     feature2 = BaseFeature(self.bfm, 'dental')
     character1.set_feature_value(feature2, INAPPLICABLE_FEATURE)
     self.assertEqual(
         character1.normalised_form,
         NormalisedForm('{0}{1}{2}'.format(BNFM, HAS_FEATURE,
                                           INAPPLICABLE_FEATURE)))
     # The order of the normalised form feature values is
     # alphabetical by feature name.
     feature3 = BaseFeature(self.bfm, 'consonantal')
     character1.set_feature_value(feature3, NOT_HAS_FEATURE)
     self.assertEqual(
         character1.normalised_form,
         NormalisedForm('{0}{1}{2}{3}'.format(BNFM, HAS_FEATURE,
                                              NOT_HAS_FEATURE,
                                              INAPPLICABLE_FEATURE)))
     # Renaming a feature may change the normalised form.
     feature1.name = 'vocalic'
     self.assertEqual(
         character1.normalised_form,
         NormalisedForm('{0}{1}{2}{3}'.format(BNFM, NOT_HAS_FEATURE,
                                              INAPPLICABLE_FEATURE,
                                              HAS_FEATURE)))
     # Changing a feature value changes the normalised form.
     character1.set_feature_value(feature1, NOT_HAS_FEATURE)
     self.assertEqual(
         character1.normalised_form,
         NormalisedForm('{0}{1}{2}{1}'.format(BNFM, NOT_HAS_FEATURE,
                                              INAPPLICABLE_FEATURE)))
     # Removing a feature value changes the normalised form.
     feature2.delete()
     self.assertEqual(
         character1.normalised_form,
         NormalisedForm('{0}{1}{1}'.format(BNFM, NOT_HAS_FEATURE)))
Exemple #6
0
class ClusterTestCase(unittest.TestCase):
    def setUp(self):
        self._populate_binary_features_model()

    def _populate_binary_features_model(self):
        self.bfm = BinaryFeaturesModel()
        self.anterior = BaseFeature(self.bfm, 'anterior')
        self.back = BaseFeature(self.bfm, 'back')
        self.coronal = BaseFeature(self.bfm, 'coronal')
        self.long = BaseFeature(self.bfm, 'long')
        self.voiced = BaseFeature(self.bfm, 'voiced')
        self.p = BaseCharacter(self.bfm, 'p')
        self.p.set_feature_value(self.anterior, HAS_FEATURE)
        self.p.set_feature_value(self.back, NOT_HAS_FEATURE)
        self.p.set_feature_value(self.coronal, NOT_HAS_FEATURE)
        self.p.set_feature_value(self.long, NOT_HAS_FEATURE)
        self.p.set_feature_value(self.voiced, NOT_HAS_FEATURE)
        self.b = BaseCharacter(self.bfm, 'b')
        self.b.set_feature_value(self.anterior, HAS_FEATURE)
        self.b.set_feature_value(self.back, NOT_HAS_FEATURE)
        self.b.set_feature_value(self.coronal, NOT_HAS_FEATURE)
        self.b.set_feature_value(self.long, NOT_HAS_FEATURE)
        self.b.set_feature_value(self.voiced, HAS_FEATURE)
        self.t = BaseCharacter(self.bfm, 't')
        self.t.set_feature_value(self.anterior, HAS_FEATURE)
        self.t.set_feature_value(self.back, NOT_HAS_FEATURE)
        self.t.set_feature_value(self.coronal, HAS_FEATURE)
        self.t.set_feature_value(self.long, NOT_HAS_FEATURE)
        self.t.set_feature_value(self.voiced, NOT_HAS_FEATURE)
        self.d = BaseCharacter(self.bfm, 'd')
        self.d.set_feature_value(self.anterior, HAS_FEATURE)
        self.d.set_feature_value(self.back, NOT_HAS_FEATURE)
        self.d.set_feature_value(self.coronal, HAS_FEATURE)
        self.d.set_feature_value(self.long, NOT_HAS_FEATURE)
        self.d.set_feature_value(self.voiced, HAS_FEATURE)
        self.q = BaseCharacter(self.bfm, 'q')
        self.q.set_feature_value(self.anterior, NOT_HAS_FEATURE)
        self.q.set_feature_value(self.back, HAS_FEATURE)
        self.q.set_feature_value(self.coronal, NOT_HAS_FEATURE)
        self.q.set_feature_value(self.long, NOT_HAS_FEATURE)
        self.q.set_feature_value(self.voiced, NOT_HAS_FEATURE)
        self.ring = DiacriticCharacter(self.bfm, '̥')
        self.ring.set_feature_value(self.anterior, INAPPLICABLE_FEATURE)
        self.ring.set_feature_value(self.back, INAPPLICABLE_FEATURE)
        self.ring.set_feature_value(self.coronal, INAPPLICABLE_FEATURE)
        self.ring.set_feature_value(self.long, INAPPLICABLE_FEATURE)
        self.ring.set_feature_value(self.voiced, NOT_HAS_FEATURE)
        self.caret = DiacriticCharacter(self.bfm, '̬')
        self.caret.set_feature_value(self.anterior, INAPPLICABLE_FEATURE)
        self.caret.set_feature_value(self.back, INAPPLICABLE_FEATURE)
        self.caret.set_feature_value(self.coronal, INAPPLICABLE_FEATURE)
        self.caret.set_feature_value(self.long, INAPPLICABLE_FEATURE)
        self.caret.set_feature_value(self.voiced, HAS_FEATURE)
        self.ː = SpacingCharacter(self.bfm, 'ː')
        self.ː.set_feature_value(self.anterior, INAPPLICABLE_FEATURE)
        self.ː.set_feature_value(self.back, INAPPLICABLE_FEATURE)
        self.ː.set_feature_value(self.coronal, INAPPLICABLE_FEATURE)
        self.ː.set_feature_value(self.long, HAS_FEATURE)
        self.ː.set_feature_value(self.voiced, INAPPLICABLE_FEATURE)

    def test_resolve_normalised_form(self):
        nf1 = NormalisedForm('{0}{1}{2}{2}{2}{2}'.format(
            BNFM, HAS_FEATURE, NOT_HAS_FEATURE))
        cluster1 = Cluster(self.bfm, normalised_form=nf1)
        self.assertEqual(cluster1.base_character, self.p)
        self.assertEqual(cluster1.diacritic_characters, [])
        self.assertEqual(cluster1.spacing_characters, [])
        nf2 = NormalisedForm('{0}{2}{1}{2}{2}{1}'.format(
            BNFM, HAS_FEATURE, NOT_HAS_FEATURE))
        cluster2 = Cluster(self.bfm, normalised_form=nf2)
        self.assertEqual(cluster2.base_character, self.q)
        self.assertEqual(cluster2.diacritic_characters, [self.caret])
        self.assertEqual(cluster2.spacing_characters, [])
        nf3 = NormalisedForm('{0}{2}{1}{2}{1}{1}'.format(
            BNFM, HAS_FEATURE, NOT_HAS_FEATURE))
        cluster3 = Cluster(self.bfm, normalised_form=nf3)
        self.assertEqual(cluster3.base_character, self.q)
        self.assertEqual(cluster3.diacritic_characters, [self.caret])
        self.assertEqual(cluster3.spacing_characters, [self.ː])

    def test_base_cluster_creation_illegal(self):
        bfm1 = BinaryFeaturesModel()
        bfm2 = BinaryFeaturesModel()
        character1 = BaseCharacter(bfm1, 'a')
        character2 = DiacriticCharacter(bfm2, 'b')
        character3 = DiacriticCharacter(bfm1, 'd')
        character4 = SpacingCharacter(bfm1, 'e')
        nf1 = NormalisedForm('{}{}'.format(BNFM, HAS_FEATURE))
        # A BaseCluster must be initialised with either a base
        # character or a normalised form.
        self.assertRaises(IllegalArgumentError, BaseCluster, bfm1)
        # A BaseCluster must not be initialised with both a normalised
        # form and any other argument.
        self.assertRaises(IllegalArgumentError,
                          BaseCluster,
                          bfm1,
                          base_character=character1,
                          normalised_form=nf1)
        self.assertRaises(IllegalArgumentError,
                          BaseCluster,
                          bfm1,
                          diacritic_characters=[character3],
                          normalised_form=nf1)
        self.assertRaises(IllegalArgumentError,
                          BaseCluster,
                          bfm1,
                          spacing_characters=[character4],
                          normalised_form=nf1)
        # All of the characters in a BaseCluster must be associated with
        # the same binary features model.
        self.assertRaises(MismatchedModelsError,
                          BaseCluster,
                          bfm1,
                          base_character=character1,
                          diacritic_characters=[character2])
        # A BaseCluster expects specific types of characters in its
        # arguments.
        self.assertRaises(MismatchedTypesError,
                          BaseCluster,
                          bfm1,
                          base_character=character3)
        self.assertRaises(MismatchedTypesError,
                          BaseCluster,
                          bfm1,
                          base_character=character1,
                          diacritic_characters=[character4])
        self.assertRaises(MismatchedTypesError,
                          BaseCluster,
                          bfm1,
                          base_character=character1,
                          spacing_characters=[character3])

    def test_applier_form(self):
        cluster1 = BaseCluster(self.bfm, base_character=self.p)
        af1 = '{0}{1}{2}{3}{3}{3}{3}'.format(AFM, BNFM, HAS_FEATURE,
                                             NOT_HAS_FEATURE)
        self.assertEqual(cluster1.applier_form, af1)
        cluster2 = BaseCluster(self.bfm,
                               base_character=self.q,
                               diacritic_characters=[self.caret])
        af2 = '{0}{1}{3}{2}{3}{3}{2}'.format(AFM, BNFM, HAS_FEATURE,
                                             NOT_HAS_FEATURE)
        self.assertEqual(cluster2.applier_form, af2)
        cluster3 = BaseCluster(self.bfm,
                               base_character=self.q,
                               diacritic_characters=[self.caret],
                               spacing_characters=[self.ː])
        af3 = '{0}{1}{3}{2}{3}{2}{2}'.format(AFM, BNFM, HAS_FEATURE,
                                             NOT_HAS_FEATURE)
        self.assertEqual(cluster3.applier_form, af3)

    def test_normalised_form(self):
        cluster1 = BaseCluster(self.bfm, base_character=self.p)
        nf1 = NormalisedForm('{0}{1}{2}{2}{2}{2}'.format(
            BNFM, HAS_FEATURE, NOT_HAS_FEATURE))
        self.assertEqual(cluster1.normalised_form, nf1)
        cluster2 = BaseCluster(self.bfm,
                               base_character=self.q,
                               diacritic_characters=[self.caret])
        nf2 = NormalisedForm('{0}{2}{1}{2}{2}{1}'.format(
            BNFM, HAS_FEATURE, NOT_HAS_FEATURE))
        self.assertEqual(cluster2.normalised_form, nf2)
        cluster3 = BaseCluster(self.bfm,
                               base_character=self.q,
                               diacritic_characters=[self.caret],
                               spacing_characters=[self.ː])
        nf3 = NormalisedForm('{0}{2}{1}{2}{1}{1}'.format(
            BNFM, HAS_FEATURE, NOT_HAS_FEATURE))
        self.assertEqual(cluster3.normalised_form, nf3)

    def test_string_form(self):
        cluster1 = BaseCluster(self.bfm, base_character=self.p)
        self.assertEqual(str(cluster1), 'p')
        cluster2 = BaseCluster(self.bfm,
                               base_character=self.q,
                               diacritic_characters=[self.caret])
        self.assertEqual(str(cluster2), 'q̬')
        cluster3 = BaseCluster(self.bfm,
                               base_character=self.q,
                               diacritic_characters=[self.caret],
                               spacing_characters=[self.ː])
        self.assertEqual(str(cluster3), 'q̬ː')
Exemple #7
0
class ClusterTestCase(unittest.TestCase):
    def setUp(self):
        self._populate_binary_features_model()

    def _populate_binary_features_model(self):
        self.bfm = BinaryFeaturesModel()
        self.anterior = BaseFeature(self.bfm, "anterior")
        self.back = BaseFeature(self.bfm, "back")
        self.coronal = BaseFeature(self.bfm, "coronal")
        self.long = BaseFeature(self.bfm, "long")
        self.voiced = BaseFeature(self.bfm, "voiced")
        self.p = BaseCharacter(self.bfm, "p")
        self.p.set_feature_value(self.anterior, HAS_FEATURE)
        self.p.set_feature_value(self.back, NOT_HAS_FEATURE)
        self.p.set_feature_value(self.coronal, NOT_HAS_FEATURE)
        self.p.set_feature_value(self.long, NOT_HAS_FEATURE)
        self.p.set_feature_value(self.voiced, NOT_HAS_FEATURE)
        self.b = BaseCharacter(self.bfm, "b")
        self.b.set_feature_value(self.anterior, HAS_FEATURE)
        self.b.set_feature_value(self.back, NOT_HAS_FEATURE)
        self.b.set_feature_value(self.coronal, NOT_HAS_FEATURE)
        self.b.set_feature_value(self.long, NOT_HAS_FEATURE)
        self.b.set_feature_value(self.voiced, HAS_FEATURE)
        self.t = BaseCharacter(self.bfm, "t")
        self.t.set_feature_value(self.anterior, HAS_FEATURE)
        self.t.set_feature_value(self.back, NOT_HAS_FEATURE)
        self.t.set_feature_value(self.coronal, HAS_FEATURE)
        self.t.set_feature_value(self.long, NOT_HAS_FEATURE)
        self.t.set_feature_value(self.voiced, NOT_HAS_FEATURE)
        self.d = BaseCharacter(self.bfm, "d")
        self.d.set_feature_value(self.anterior, HAS_FEATURE)
        self.d.set_feature_value(self.back, NOT_HAS_FEATURE)
        self.d.set_feature_value(self.coronal, HAS_FEATURE)
        self.d.set_feature_value(self.long, NOT_HAS_FEATURE)
        self.d.set_feature_value(self.voiced, HAS_FEATURE)
        self.q = BaseCharacter(self.bfm, "q")
        self.q.set_feature_value(self.anterior, NOT_HAS_FEATURE)
        self.q.set_feature_value(self.back, HAS_FEATURE)
        self.q.set_feature_value(self.coronal, NOT_HAS_FEATURE)
        self.q.set_feature_value(self.long, NOT_HAS_FEATURE)
        self.q.set_feature_value(self.voiced, NOT_HAS_FEATURE)
        self.ring = DiacriticCharacter(self.bfm, "̥")
        self.ring.set_feature_value(self.anterior, INAPPLICABLE_FEATURE)
        self.ring.set_feature_value(self.back, INAPPLICABLE_FEATURE)
        self.ring.set_feature_value(self.coronal, INAPPLICABLE_FEATURE)
        self.ring.set_feature_value(self.long, INAPPLICABLE_FEATURE)
        self.ring.set_feature_value(self.voiced, NOT_HAS_FEATURE)
        self.caret = DiacriticCharacter(self.bfm, "̬")
        self.caret.set_feature_value(self.anterior, INAPPLICABLE_FEATURE)
        self.caret.set_feature_value(self.back, INAPPLICABLE_FEATURE)
        self.caret.set_feature_value(self.coronal, INAPPLICABLE_FEATURE)
        self.caret.set_feature_value(self.long, INAPPLICABLE_FEATURE)
        self.caret.set_feature_value(self.voiced, HAS_FEATURE)
        self.ː = SpacingCharacter(self.bfm, "ː")
        self.ː.set_feature_value(self.anterior, INAPPLICABLE_FEATURE)
        self.ː.set_feature_value(self.back, INAPPLICABLE_FEATURE)
        self.ː.set_feature_value(self.coronal, INAPPLICABLE_FEATURE)
        self.ː.set_feature_value(self.long, HAS_FEATURE)
        self.ː.set_feature_value(self.voiced, INAPPLICABLE_FEATURE)

    def test_resolve_normalised_form(self):
        nf1 = NormalisedForm("{0}{1}{2}{2}{2}{2}".format(BNFM, HAS_FEATURE, NOT_HAS_FEATURE))
        cluster1 = Cluster(self.bfm, normalised_form=nf1)
        self.assertEqual(cluster1.base_character, self.p)
        self.assertEqual(cluster1.diacritic_characters, [])
        self.assertEqual(cluster1.spacing_characters, [])
        nf2 = NormalisedForm("{0}{2}{1}{2}{2}{1}".format(BNFM, HAS_FEATURE, NOT_HAS_FEATURE))
        cluster2 = Cluster(self.bfm, normalised_form=nf2)
        self.assertEqual(cluster2.base_character, self.q)
        self.assertEqual(cluster2.diacritic_characters, [self.caret])
        self.assertEqual(cluster2.spacing_characters, [])
        nf3 = NormalisedForm("{0}{2}{1}{2}{1}{1}".format(BNFM, HAS_FEATURE, NOT_HAS_FEATURE))
        cluster3 = Cluster(self.bfm, normalised_form=nf3)
        self.assertEqual(cluster3.base_character, self.q)
        self.assertEqual(cluster3.diacritic_characters, [self.caret])
        self.assertEqual(cluster3.spacing_characters, [self.ː])

    def test_base_cluster_creation_illegal(self):
        bfm1 = BinaryFeaturesModel()
        bfm2 = BinaryFeaturesModel()
        character1 = BaseCharacter(bfm1, "a")
        character2 = DiacriticCharacter(bfm2, "b")
        character3 = DiacriticCharacter(bfm1, "d")
        character4 = SpacingCharacter(bfm1, "e")
        nf1 = NormalisedForm("{}{}".format(BNFM, HAS_FEATURE))
        # A BaseCluster must be initialised with either a base
        # character or a normalised form.
        self.assertRaises(IllegalArgumentError, BaseCluster, bfm1)
        # A BaseCluster must not be initialised with both a normalised
        # form and any other argument.
        self.assertRaises(IllegalArgumentError, BaseCluster, bfm1, base_character=character1, normalised_form=nf1)
        self.assertRaises(
            IllegalArgumentError, BaseCluster, bfm1, diacritic_characters=[character3], normalised_form=nf1
        )
        self.assertRaises(IllegalArgumentError, BaseCluster, bfm1, spacing_characters=[character4], normalised_form=nf1)
        # All of the characters in a BaseCluster must be associated with
        # the same binary features model.
        self.assertRaises(
            MismatchedModelsError, BaseCluster, bfm1, base_character=character1, diacritic_characters=[character2]
        )
        # A BaseCluster expects specific types of characters in its
        # arguments.
        self.assertRaises(MismatchedTypesError, BaseCluster, bfm1, base_character=character3)
        self.assertRaises(
            MismatchedTypesError, BaseCluster, bfm1, base_character=character1, diacritic_characters=[character4]
        )
        self.assertRaises(
            MismatchedTypesError, BaseCluster, bfm1, base_character=character1, spacing_characters=[character3]
        )

    def test_applier_form(self):
        cluster1 = BaseCluster(self.bfm, base_character=self.p)
        af1 = "{0}{1}{2}{3}{3}{3}{3}".format(AFM, BNFM, HAS_FEATURE, NOT_HAS_FEATURE)
        self.assertEqual(cluster1.applier_form, af1)
        cluster2 = BaseCluster(self.bfm, base_character=self.q, diacritic_characters=[self.caret])
        af2 = "{0}{1}{3}{2}{3}{3}{2}".format(AFM, BNFM, HAS_FEATURE, NOT_HAS_FEATURE)
        self.assertEqual(cluster2.applier_form, af2)
        cluster3 = BaseCluster(
            self.bfm, base_character=self.q, diacritic_characters=[self.caret], spacing_characters=[self.ː]
        )
        af3 = "{0}{1}{3}{2}{3}{2}{2}".format(AFM, BNFM, HAS_FEATURE, NOT_HAS_FEATURE)
        self.assertEqual(cluster3.applier_form, af3)

    def test_normalised_form(self):
        cluster1 = BaseCluster(self.bfm, base_character=self.p)
        nf1 = NormalisedForm("{0}{1}{2}{2}{2}{2}".format(BNFM, HAS_FEATURE, NOT_HAS_FEATURE))
        self.assertEqual(cluster1.normalised_form, nf1)
        cluster2 = BaseCluster(self.bfm, base_character=self.q, diacritic_characters=[self.caret])
        nf2 = NormalisedForm("{0}{2}{1}{2}{2}{1}".format(BNFM, HAS_FEATURE, NOT_HAS_FEATURE))
        self.assertEqual(cluster2.normalised_form, nf2)
        cluster3 = BaseCluster(
            self.bfm, base_character=self.q, diacritic_characters=[self.caret], spacing_characters=[self.ː]
        )
        nf3 = NormalisedForm("{0}{2}{1}{2}{1}{1}".format(BNFM, HAS_FEATURE, NOT_HAS_FEATURE))
        self.assertEqual(cluster3.normalised_form, nf3)

    def test_string_form(self):
        cluster1 = BaseCluster(self.bfm, base_character=self.p)
        self.assertEqual(str(cluster1), "p")
        cluster2 = BaseCluster(self.bfm, base_character=self.q, diacritic_characters=[self.caret])
        self.assertEqual(str(cluster2), "q̬")
        cluster3 = BaseCluster(
            self.bfm, base_character=self.q, diacritic_characters=[self.caret], spacing_characters=[self.ː]
        )
        self.assertEqual(str(cluster3), "q̬ː")