| #!/usr/bin/env fuchsia-vendored-python |
| # Copyright 2023 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import os |
| import sys |
| import unittest |
| from pathlib import Path |
| from unittest import mock |
| from typing import Iterable, Sequence |
| |
| import textpb |
| |
| |
| def unlex(tokens: Iterable[textpb.Token]) -> str: |
| """Concatenates tokens' text.""" |
| return ''.join(token.text for token in tokens) |
| |
| |
| class LexTests(unittest.TestCase): |
| |
| def _test_tokens(self, tokens: Sequence[textpb.Token]): |
| text = unlex(tokens) |
| lexed = list(textpb._lex_line(text)) |
| self.assertEqual(lexed, tokens) |
| |
| def test_start_block(self): |
| self._test_tokens( |
| [textpb.Token(text='<', type=textpb.TokenType.START_BLOCK)]) |
| self._test_tokens( |
| [textpb.Token(text='{', type=textpb.TokenType.START_BLOCK)]) |
| |
| def test_end_block(self): |
| self._test_tokens( |
| [textpb.Token(text='>', type=textpb.TokenType.END_BLOCK)]) |
| self._test_tokens( |
| [textpb.Token(text='}', type=textpb.TokenType.END_BLOCK)]) |
| |
| def test_empty_block(self): |
| self._test_tokens( |
| [ |
| textpb.Token(text='<', type=textpb.TokenType.START_BLOCK), |
| textpb.Token(text='>', type=textpb.TokenType.END_BLOCK), |
| ]) |
| self._test_tokens( |
| [ |
| textpb.Token(text='{', type=textpb.TokenType.START_BLOCK), |
| textpb.Token(text='}', type=textpb.TokenType.END_BLOCK), |
| ]) |
| |
| def test_spaces(self): |
| self._test_tokens([textpb.Token(text=' ', type=textpb.TokenType.SPACE)]) |
| self._test_tokens( |
| [textpb.Token(text=' ', type=textpb.TokenType.SPACE)]) |
| self._test_tokens( |
| [textpb.Token(text='\t', type=textpb.TokenType.SPACE)]) |
| self._test_tokens( |
| [textpb.Token(text='\t\t', type=textpb.TokenType.SPACE)]) |
| |
| def test_newlines(self): |
| self._test_tokens( |
| [textpb.Token(text='\n', type=textpb.TokenType.NEWLINE)]) |
| self._test_tokens( |
| [textpb.Token(text='\r\n', type=textpb.TokenType.NEWLINE)]) |
| |
| def test_field_name(self): |
| self._test_tokens( |
| [textpb.Token(text='field:', type=textpb.TokenType.FIELD_NAME)]) |
| self._test_tokens( |
| [textpb.Token(text='key:', type=textpb.TokenType.FIELD_NAME)]) |
| self._test_tokens( |
| [textpb.Token(text='value:', type=textpb.TokenType.FIELD_NAME)]) |
| |
| def test_strings(self): |
| self._test_tokens( |
| [textpb.Token(text='""', type=textpb.TokenType.STRING_VALUE)]) |
| self._test_tokens( |
| [textpb.Token(text='"string"', type=textpb.TokenType.STRING_VALUE)]) |
| self._test_tokens( |
| [ |
| textpb.Token( |
| text='"with space"', type=textpb.TokenType.STRING_VALUE) |
| ]) |
| self._test_tokens( |
| [ |
| textpb.Token( |
| text='"\\"escaped\\""', type=textpb.TokenType.STRING_VALUE) |
| ]) |
| |
| def test_nonstring_values(self): |
| self._test_tokens( |
| [textpb.Token(text='ENUM', type=textpb.TokenType.OTHER_VALUE)]) |
| self._test_tokens( |
| [ |
| textpb.Token( |
| text='OTHER_ENUM', type=textpb.TokenType.OTHER_VALUE) |
| ]) |
| self._test_tokens( |
| [textpb.Token(text='123', type=textpb.TokenType.OTHER_VALUE)]) |
| |
| def test_combination(self): |
| self._test_tokens( |
| [ |
| textpb.Token( |
| text='field_name:', type=textpb.TokenType.FIELD_NAME), |
| textpb.Token(text=' ', type=textpb.TokenType.SPACE), |
| textpb.Token(text='{', type=textpb.TokenType.START_BLOCK), |
| textpb.Token(text='\n', type=textpb.TokenType.NEWLINE), |
| textpb.Token(text='key:', type=textpb.TokenType.FIELD_NAME), |
| textpb.Token(text=' ', type=textpb.TokenType.SPACE), |
| textpb.Token( |
| text='"path/to/something"', |
| type=textpb.TokenType.STRING_VALUE), |
| textpb.Token(text='\n', type=textpb.TokenType.NEWLINE), |
| textpb.Token(text='value:', type=textpb.TokenType.FIELD_NAME), |
| textpb.Token(text=' ', type=textpb.TokenType.SPACE), |
| textpb.Token(text='9827364', type=textpb.TokenType.OTHER_VALUE), |
| textpb.Token(text='\n', type=textpb.TokenType.NEWLINE), |
| textpb.Token(text='}', type=textpb.TokenType.END_BLOCK), |
| textpb.Token(text='\n', type=textpb.TokenType.NEWLINE), |
| ]) |
| |
| |
| class AutoDictTests(unittest.TestCase): |
| |
| def test_empty(self): |
| self.assertEqual(textpb._auto_dict([]), []) |
| |
| def test_not_dict(self): |
| v = ['VAL1', 'VAL2'] |
| self.assertEqual(textpb._auto_dict(v), v) |
| |
| def test_single(self): |
| self.assertEqual( |
| textpb._auto_dict( |
| [ |
| { |
| 'key': |
| [ |
| textpb.Token( |
| text='"foo"', |
| type=textpb.TokenType.STRING_VALUE) |
| ], |
| 'value': ['BAR'] |
| } |
| ]), |
| {'foo': 'BAR'}, |
| ) |
| |
| def test_multiple(self): |
| self.assertEqual( |
| textpb._auto_dict( |
| [ |
| { |
| 'key': |
| [ |
| textpb.Token( |
| text='"foo"', |
| type=textpb.TokenType.STRING_VALUE) |
| ], |
| 'value': ['BAR'], |
| }, |
| { |
| 'key': |
| [ |
| textpb.Token( |
| text='"moo"', |
| type=textpb.TokenType.STRING_VALUE) |
| ], |
| 'value': ['COW'], |
| }, |
| ]), |
| { |
| 'foo': 'BAR', |
| 'moo': 'COW', |
| }, |
| ) |
| |
| |
| class ParseTests(unittest.TestCase): |
| |
| def test_empty(self): |
| self.assertEqual( |
| textpb.parse([]), |
| dict(), |
| ) |
| |
| def test_one_field(self): |
| self.assertEqual( |
| textpb.parse(['cost: 24']), |
| { |
| 'cost': |
| [ |
| textpb.Token( |
| text='24', type=textpb.TokenType.OTHER_VALUE) |
| ] |
| }, |
| ) |
| |
| def test_one_field_empty_struct(self): |
| self.assertEqual( |
| textpb.parse(['tools: {}']), |
| {'tools': [dict()]}, |
| ) |
| |
| def test_repeated_field(self): |
| self.assertEqual( |
| textpb.parse([ |
| 'alias: "ab"', |
| 'alias: "cd"', |
| ]), |
| { |
| 'alias': |
| [ |
| textpb.Token( |
| text='"ab"', type=textpb.TokenType.STRING_VALUE), |
| textpb.Token( |
| text='"cd"', type=textpb.TokenType.STRING_VALUE), |
| ] |
| }, |
| ) |
| |
| def test_deeply_nested(self): |
| self.assertEqual( |
| textpb.parse( |
| [ |
| 'outer: {' |
| ' inner: {', |
| ' status: GOOD', |
| ' }', |
| '}', |
| ]), |
| { |
| 'outer': |
| [ |
| { |
| 'inner': |
| [ |
| { |
| 'status': |
| [ |
| textpb.Token( |
| text='GOOD', |
| type=textpb.TokenType. |
| OTHER_VALUE) |
| ] |
| } |
| ] |
| } |
| ] |
| }, |
| ) |
| |
| def test_mixed_levels(self): |
| self.assertEqual( |
| textpb.parse( |
| [ |
| 'outer: {' |
| ' inner: {', |
| ' status: GOOD', |
| ' }', |
| ' middle: 77', |
| ' middle: 55', |
| '}', |
| 'magic: "wand"', |
| ]), |
| { |
| 'outer': |
| [ |
| { |
| 'inner': |
| [ |
| { |
| 'status': |
| [ |
| textpb.Token( |
| text='GOOD', |
| type=textpb.TokenType. |
| OTHER_VALUE) |
| ] |
| } |
| ], |
| 'middle': |
| [ |
| textpb.Token( |
| text='77', |
| type=textpb.TokenType.OTHER_VALUE), |
| textpb.Token( |
| text='55', |
| type=textpb.TokenType.OTHER_VALUE) |
| ], |
| } |
| ], |
| 'magic': |
| [ |
| textpb.Token( |
| text='"wand"', type=textpb.TokenType.STRING_VALUE) |
| ], |
| }, |
| ) |
| |
| def test_nested_dictionaries(self): |
| self.assertEqual( |
| textpb.parse( |
| [ |
| 'outer: {' |
| ' inner: {', |
| ' key: "aaa"', |
| ' value: {', |
| ' size: 44', |
| ' status: BAD', |
| ' attributes: {', |
| ' key: "secret"', |
| ' value: "de4db33f"', |
| ' }', |
| ' attributes: {', |
| ' key: "output"', |
| ' value: "ff/gg"', |
| ' }', |
| ' }', |
| ' }', |
| ' inner: {', |
| ' key: "bbb"', |
| ' value: {', |
| ' size: 33', |
| ' status: GOOD', |
| ' attributes: {', |
| ' key: "secret"', |
| ' value: "0000aaaa"', |
| ' }', |
| ' attributes: {', |
| ' key: "output"', |
| ' value: "pp/qq"', |
| ' }', |
| ' }', |
| ' }', |
| '}', |
| ]), |
| { |
| 'outer': |
| [ |
| { |
| 'inner': |
| { |
| 'aaa': |
| { |
| 'size': |
| [ |
| textpb.Token( |
| text='44', |
| type=textpb.TokenType. |
| OTHER_VALUE) |
| ], |
| 'status': |
| [ |
| textpb.Token( |
| text='BAD', |
| type=textpb.TokenType. |
| OTHER_VALUE) |
| ], |
| 'attributes': |
| { |
| 'secret': |
| textpb.Token( |
| text='"de4db33f"', |
| type=textpb. |
| TokenType. |
| STRING_VALUE), |
| 'output': |
| textpb.Token( |
| text='"ff/gg"', |
| type=textpb. |
| TokenType. |
| STRING_VALUE), |
| } |
| }, |
| 'bbb': |
| { |
| 'size': |
| [ |
| textpb.Token( |
| text='33', |
| type=textpb.TokenType. |
| OTHER_VALUE) |
| ], |
| 'status': |
| [ |
| textpb.Token( |
| text='GOOD', |
| type=textpb.TokenType. |
| OTHER_VALUE) |
| ], |
| 'attributes': |
| { |
| 'secret': |
| textpb.Token( |
| text='"0000aaaa"', |
| type=textpb. |
| TokenType. |
| STRING_VALUE), |
| 'output': |
| textpb.Token( |
| text='"pp/qq"', |
| type=textpb. |
| TokenType. |
| STRING_VALUE), |
| } |
| }, |
| } |
| } |
| ] |
| }, |
| ) |
| |
| def test_unbalanced_close_block(self): |
| with self.assertRaisesRegex(textpb.ParseError, |
| 'Unexpected end-of-block'): |
| textpb.parse(['}']) |
| |
| def test_unbalanced_open_block(self): |
| with self.assertRaisesRegex(textpb.ParseError, 'Unexpected EOF'): |
| textpb.parse(['foo: {']) |
| |
| def test_missing_value(self): |
| with self.assertRaisesRegex(textpb.ParseError, 'Unexpected EOF'): |
| textpb.parse(['foo: ']) |
| |
| def test_expected_field(self): |
| with self.assertRaisesRegex(textpb.ParseError, 'Expected a field name'): |
| textpb.parse(['"value"']) |
| |
| def test_expected_value(self): |
| with self.assertRaisesRegex(textpb.ParseError, |
| 'Unexpected token: .*END_BLOCK'): |
| textpb.parse(['foo: }']) |
| |
| |
| if __name__ == '__main__': |
| unittest.main() |