diff --git a/dav_events/choices.py b/dav_events/choices.py index 6635a86..91ae1ea 100644 --- a/dav_events/choices.py +++ b/dav_events/choices.py @@ -10,6 +10,8 @@ class ChoiceSet(object): self._codes = list() self._labels = dict() for code, label in choices: + if code in self._codes: + raise ValueError(u'Code not unique: {}'.format(code)) self._codes.append(code) self._labels[code] = label diff --git a/dav_events/tests/test_choiceset.py b/dav_events/tests/test_choiceset.py new file mode 100644 index 0000000..207c2b0 --- /dev/null +++ b/dav_events/tests/test_choiceset.py @@ -0,0 +1,191 @@ +# -*- coding: utf-8 -*- +from django.test import SimpleTestCase +from django.utils.translation import gettext_lazy as _ + +from ..choices import ChoiceSet + + +class ChoiceSetTestCase(SimpleTestCase): + def setUp(self): + # We need at least three tuples with code and label. + # Codes (i.e. first field of the tuples) must not be in sorted order! + self.tuples = [ + ('4', 'A single digit number'), + ('b', 'A lowercase bee'), + ('B', 'A capital bee'), + ('W', _('Wanderung')), + ('bee', 'A B'), + ('CO2', 'Carbon dioxide'), + ('H2o', 'Water with mixed case'), + ('h2O', 'Water with mixed case'), + ('H-O-H', 'Water molecule'), + ('O=C=O', 'Carbon dioxide'), + ('.dot', 'Label with a dot'), + ('_underscore', 'Label with underscore'), + ('miXed.ALL_to-gether-98_76.543210', 'Label with mixed characters'), + ('üÄö', 'Mixed case Umlaute'), + (' ', 'single blank space as code'), + ('', 'Empty code'), + ('empty', ''), + ('A long code without umlaute for a short label', 'ÜäÖ'), + ('True', 'Yes'), + ('False', 'No'), + ('None', 'Unknown'), + ('NONE', 'Often used'), + ('OTHER', 'Often used'), + ] + self.choiceset = ChoiceSet(self.tuples) + self.code_not_in_choiceset = 'A' + + def test_testdata(self): + codes = [c for c, l in self.tuples] + self.assertNotIn(self.code_not_in_choiceset, codes, "Improper test data") + + sorted_codes = sorted(codes) + self.assertNotEqual(codes, sorted_codes, "Improper test data:" + " codes of test tuples must not be in sorted order") + + def test_code_not_unique(self): + with self.assertRaises(ValueError) as cm: + _ = ChoiceSet([('A', 'First Label'), ('A', 'Second Label')]) + self.assertEqual(str(cm.exception), 'Code not unique: A') + + def test_len(self): + cs = ChoiceSet([]) + self.assertEqual(len(cs), 0) + cs = ChoiceSet([('X', 'Single')]) + self.assertEqual(len(cs), 1) + cs = self.choiceset + self.assertEqual(len(cs), len(self.tuples)) + + def test_getitem(self): + cs = ChoiceSet([]) + with self.assertRaises(IndexError): + _ = cs[0] + + cs = self.choiceset + code, label = cs[0] + self.assertEqual(code, self.tuples[0][0]) + self.assertEqual(label, self.tuples[0][1]) + code, label = cs[1] + self.assertEqual(code, self.tuples[1][0]) + self.assertEqual(label, self.tuples[1][1]) + code, label = cs[-1] + self.assertEqual(code, self.tuples[-1][0]) + self.assertEqual(label, self.tuples[-1][1]) + code, label = cs[-2] + self.assertEqual(code, self.tuples[-2][0]) + self.assertEqual(label, self.tuples[-2][1]) + + with self.assertRaises(IndexError): + _ = cs[len(cs) + 1] + with self.assertRaises(IndexError): + _ = cs[-len(cs) - 1] + + def test_iter_returns_tuples(self): + items = list(self.choiceset) + self.assertEqual(items, self.tuples) + + def test_iter_empty_choiceset(self): + items = list(ChoiceSet([])) + self.assertEqual(items, []) + + def test_iter_with_for_loop(self): + expected_codes = [c for c, l in self.tuples] + expected_labels = [l for c, l in self.tuples] + + codes = [] + labels = [] + for code, label in self.choiceset: + codes.append(code) + labels.append(label) + + self.assertEqual(codes, expected_codes) + self.assertEqual(labels, expected_labels) + + def test_iter_multiple_times(self): + first_iteration = list(self.choiceset) + second_iteration = list(self.choiceset) + self.assertEqual(first_iteration, second_iteration) + + def test_contains(self): + for c, l in self.tuples: + self.assertIn((c, l), self.choiceset) + self.assertIn((c, l[::-1]), self.choiceset) + + self.assertNotIn((self.code_not_in_choiceset, self.tuples[0][1]), self.choiceset) + + def test_contains_empty_choiceset(self): + cs = ChoiceSet([]) + self.assertNotIn(self.tuples[0], cs) + + def test_codes(self): + expected_codes = [] + codes = ChoiceSet([]).codes + self.assertEqual(codes, expected_codes) + self.assertIsInstance(codes, list) + + expected_codes = [c for c, l in self.tuples] + codes = self.choiceset.codes + self.assertEqual(codes, expected_codes) + self.assertIsInstance(codes, list) + + def test_get_label_valid_code(self): + for c, l in self.tuples: + self.assertEqual(self.choiceset.get_label(c), l) + + def test_get_label_invalid_code_raises_keyerror(self): + with self.assertRaises(KeyError): + self.choiceset.get_label(self.code_not_in_choiceset) + + def test_get_label_case_sensitive(self): + cs = ChoiceSet([('UPPER', 'Label')]) + with self.assertRaises(KeyError): + cs.get_label('upper') + + def test_sort_numeric_codes(self): + cs = ChoiceSet([('3', 'Three'), ('1', 'One'), ('2', 'Two'), ('10', 'Ten')]) + cs.sort() + self.assertEqual(cs.codes, ['1', '10', '2', '3']) + + def test_sort_alphanumeric_codes(self): + cs = ChoiceSet([ + ('B1', 'Three'), + ('A2', 'Two'), + ('B2', 'Four'), + ('A1', 'One'), + ('a20', 'Five'), + ('a3', 'Six'), + ('b1', 'Seven'), + ]) + cs.sort() + self.assertEqual(cs.codes, ['A1', 'A2', 'B1', 'B2', 'a20', 'a3', 'b1']) + + def test_sort_unsortable(self): + cs = ChoiceSet([]) + cs.sort() + self.assertEqual(cs.codes, []) + + cs = ChoiceSet([('X', 'Single')]) + cs.sort() + self.assertEqual(cs.codes, ['X']) + + def test_sort_already_sorted(self): + cs = ChoiceSet([('1', 'One'), ('2', 'Two'), ('3', 'Three')]) + cs.sort() + self.assertEqual(cs.codes, ['1', '2', '3']) + + def test_sort_preserves_labels(self): + cs = ChoiceSet([('B', 'Two'), ('A', 'One'), ('C', 'Three')]) + cs.sort() + + self.assertEqual(cs.get_label('A'), 'One') + self.assertEqual(cs.get_label('B'), 'Two') + self.assertEqual(cs.get_label('C'), 'Three') + + def test_sort_test_data(self): + expected_codes = [c for c, l in self.tuples] + expected_codes.sort() + cs = ChoiceSet(self.tuples) + cs.sort() + self.assertEqual(cs.codes, expected_codes)