diff --git a/tests/test_pydenticon.py b/tests/test_pydenticon.py new file mode 100644 index 0000000000000000000000000000000000000000..8ed898f6c54e21058e363283147c42e50ddf043c --- /dev/null +++ b/tests/test_pydenticon.py @@ -0,0 +1,338 @@ +# Standard library imports. +import hashlib +import unittest +from io import BytesIO + +# Third-party Python library imports. +import mock +import PIL + +# Library imports. +from pydenticon import Generator + + +class GeneratorTest(unittest.TestCase): + """ + Implements tests for pydenticon.Generator class. + """ + + def test_init_entropy(self): + """ + Tests if the constructor properly checks for entropy provided by a + digest algorithm. + """ + + # Set-up the mock instance. + hexdigest_method = mock.MagicMock(return_value="aabb") + digest_instance = mock.MagicMock() + digest_instance.hexdigest = hexdigest_method + + # Set-up digest function that will always return the same digest + # instance. + digest_method = mock.MagicMock(return_value=digest_instance) + + # This should require 23 bits of entropy, while the digest we defined + # provided 2*8 bits of entropy (2 bytes). + self.assertRaises(ValueError, Generator, 5, 5, digest=digest_method) + + def test_init_parameters(self): + """ + Verifies that the constructor sets-up the instance properties correctly. + """ + + generator = Generator(5, 5, digest=hashlib.sha1, foreground=["#111111", "#222222"], background="#aabbcc") + + # sha1 provides 160 bits of entropy - 20 bytes. + self.assertEqual(generator.digest_entropy, 20 * 8) + self.assertEqual(generator.digest, hashlib.sha1) + self.assertEqual(generator.rows, 5) + self.assertEqual(generator.columns, 5) + self.assertEqual(generator.foreground, ["#111111", "#222222"]) + self.assertEqual(generator.background, "#aabbcc") + + def test_get_bit(self): + """ + Tests if the check whether bit is 1 or 0 is performed correctly. + """ + + generator = Generator(5, 5) + hash_bytes = [0b10010001, 0b10001000, 0b00111001] + + # Check a couple of bits from the above hash bytes. + self.assertEqual(True, generator._get_bit(0, hash_bytes)) + self.assertEqual(True, generator._get_bit(7, hash_bytes)) + self.assertEqual(False, generator._get_bit(22, hash_bytes)) + self.assertEqual(True, generator._get_bit(23, hash_bytes)) + + def test_generate_matrix(self): + """ + Verifies that the matrix is generated correctly based on passed hashed + bytes. + """ + + # The resulting half-matrix should be as follows (first byte is for + # ignored in matrix generation): + # + # 100 + # 011 + # 100 + # 001 + # 110 + hash_bytes = [0b11111111, 0b10101010, 0b01010101] + + expected_matrix = [ + [True, False, False, False, True], + [False, True, True, True, False], + [True, False, False, False, True], + [False, False, True, False, False], + [True, True, False, True, True], + ] + + generator = Generator(5, 5) + + matrix = generator._generate_matrix(hash_bytes) + + self.assertEqual(matrix, expected_matrix) + + def test_data_to_digest_byte_list_raw(self): + """ + Test if correct digest byte list is returned for raw (non-hex-digest) + data passed to the method. + """ + + # Set-up some raw data, and set-up the expected result. + data = "this is a test\n" + expected_digest_byte_list = [225, 156, 18, 131, 201, 37, 179, 32, 102, 133, 255, 82, 42, 207, 227, 230] + + # Instantiate a generator. + generator = Generator(5, 5, digest=hashlib.md5) + + # Call the method and get the results. + digest_byte_list = generator._data_to_digest_byte_list(data) + + # Verify the expected and actual result are identical. + self.assertEqual(expected_digest_byte_list, digest_byte_list) + + def test_data_to_digest_byte_list_hex(self): + """ + Test if correct digest byte list is returned for passed hex digest + string. + """ + + # Set-up some test hex digest (md5), and expected result. + hex_digest = "e19c1283c925b3206685ff522acfe3e6" + expected_digest_byte_list = [225, 156, 18, 131, 201, 37, 179, 32, 102, 133, 255, 82, 42, 207, 227, 230] + + # Instantiate a generator. + generator = Generator(5, 5, digest=hashlib.md5) + + # Call the method and get the results. + digest_byte_list = generator._data_to_digest_byte_list(hex_digest) + + # Verify the expected and actual result are identical. + self.assertEqual(expected_digest_byte_list, digest_byte_list) + + def test_data_to_digest_byte_list_hex_lookalike(self): + """ + Test if correct digest byte list is returned for passed raw data that + has same length as hex digest string. + """ + + # Set-up some test hex digest (md5), and expected result. + data = "qqwweerrttyyuuiiooppaassddffgghh" + expected_digest_byte_list = [25, 182, 52, 218, 118, 220, 26, 145, 164, 222, 33, 221, 183, 140, 98, 246] + + # Instantiate a generator. + generator = Generator(5, 5, digest=hashlib.md5) + + # Call the method and get the results. + digest_byte_list = generator._data_to_digest_byte_list(data) + + # Verify the expected and actual result are identical. + self.assertEqual(expected_digest_byte_list, digest_byte_list) + + def test_generate_png_basics(self): + """ + Tests some basics about generated PNG identicon image. This includes: + + - Dimensions of generated image. + - Format of generated image. + - Mode of generated image. + """ + + # Set-up parameters that will be used for generating the image. + width = 200 + height = 200 + padding = [20, 20, 20, 20] + foreground = "#ffffff" + background = "#000000" + matrix = [ + [0, 0, 1, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 1, 0, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + ] + + # Set-up a generator. + generator = Generator(5, 5) + + # Generate the raw image. + raw_image = generator._generate_png(matrix, width, height, padding, foreground, background) + + # Try to load the raw image. + image_stream = BytesIO(raw_image) + image = PIL.Image.open(image_stream) + + # Verify image size, format, and mode. + self.assertEqual(image.size[0], 240) + self.assertEqual(image.size[1], 240) + self.assertEqual(image.format, "PNG") + self.assertEqual(image.mode, "RGB") + + def test_generate_ascii(self): + """ + Tests the generated identicon in ASCII format. + """ + + # Set-up parameters that will be used for generating the image. + foreground = "1" + background = "0" + matrix = [ + [0, 0, 1, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 1, 0, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + ] + + # Set-up a generator. + generator = Generator(5, 5) + + # Generate the ASCII image. + ascii_image = generator._generate_ascii(matrix, foreground, background) + + # Verify that the result is as expected. + expected_result = """00100 +00100 +00100 +01110 +01110""" + self.assertEqual(ascii_image, expected_result) + + def test_generate_format(self): + """ + Tests if identicons are generated in requested format. + """ + + # Set-up a generator. + generator = Generator(5, 5) + + # Set-up some test data. + data = "some test data" + + # Verify that PNG image is returned when requested. + raw_image = generator.generate(data, 200, 200, output_format="png") + image_stream = BytesIO(raw_image) + image = PIL.Image.open(image_stream) + self.assertEqual(image.format, "PNG") + + # Verify that ASCII "image" is returned when requested. + raw_image = generator.generate(data, 200, 200, output_format="ascii") + self.assertIsInstance(raw_image, str) + + def test_generate_format_invalid(self): + """ + Tests if an exception is raised in case an unsupported format is + requested when generating the identicon. + """ + + # Set-up a generator. + generator = Generator(5, 5) + + # Set-up some test data. + data = "some test data" + + # Verify that an exception is raised in case of unsupported format. + self.assertRaises(ValueError, generator.generate, data, 200, 200, output_format="invalid") + + @mock.patch.object(Generator, '_generate_png') + def test_generate_inverted_png(self, generate_png_mock): + """ + Tests if the foreground and background are properly inverted when + generating PNG images. + """ + + # Set-up some test data. + data = "Some test data" + + # Set-up one foreground and background colour. + foreground = "#ffffff" + background = "#000000" + + # Set-up the generator. + generator = Generator(5, 5, foreground=[foreground], background=background) + + # Verify that colours are picked correctly when no inverstion is requsted. + generator.generate(data, 200, 200, inverted=False, output_format="png") + generate_png_mock.assert_called_with(mock.ANY, mock.ANY, mock.ANY, mock.ANY, foreground, background) + + # Verify that colours are picked correctly when inversion is requsted. + generator.generate(data, 200, 200, inverted=True, output_format="png") + generate_png_mock.assert_called_with(mock.ANY, mock.ANY, mock.ANY, mock.ANY, background, foreground) + + @mock.patch.object(Generator, '_generate_ascii') + def test_generate_inverted_ascii(self, generate_ascii_mock): + """ + Tests if the foreground and background are properly inverted when + generating ASCII "images". + """ + + # Set-up some test data. + data = "Some test data" + + # Set-up one foreground and background colour. These are not used for + # ASCII itself (instead a plus/minus sign is used). + foreground = "#ffffff" + background = "#000000" + + # Set-up the generator. + generator = Generator(5, 5, foreground=[foreground], background=background) + + # Verify that foreground/background is picked correctly when no + # inverstion is requsted. + generator.generate(data, 200, 200, inverted=False, output_format="ascii") + generate_ascii_mock.assert_called_with(mock.ANY, "+", "-") + + # Verify that foreground/background is picked correctly when inversion + # is requsted. + generator.generate(data, 200, 200, inverted=True, output_format="ascii") + generate_ascii_mock.assert_called_with(mock.ANY, "-", "+") + + @mock.patch.object(Generator, '_generate_png') + def test_generate_foreground(self, generate_png_mock): + """ + Tests if the foreground colour is picked correctly. + """ + + # Set-up some foreground colours and a single background colour. + foreground = ["#000000", "#111111", "#222222", "#333333", "#444444", "#555555"] + background = "#ffffff" + + # Set-up the generator. + generator = Generator(5, 5, foreground=foreground, background=background) + + # The first byte of hex digest should be 121 for this data, which should + # result in foreground colour of index '1'. + data = "some test data" + generator.generate(data, 200, 200) + generate_png_mock.assert_called_with(mock.ANY, mock.ANY, mock.ANY, mock.ANY, foreground[1], background) + + # The first byte of hex digest should be 149 for this data, which should + # result in foreground colour of index '5'. + data = "some other test data" + generator.generate(data, 200, 200) + generate_png_mock.assert_called_with(mock.ANY, mock.ANY, mock.ANY, mock.ANY, foreground[5], background) + +if __name__ == '__main__': + unittest.main()