120 lines
6.2 KiB
Python
120 lines
6.2 KiB
Python
"""Keep constrained CLI prose separate from commands and protocol replies."""
|
|
from pathlib import Path
|
|
import configparser
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
FIXTURE = ROOT / 'test/fixtures/cli_reply_text.json'
|
|
SELECTION = re.compile(
|
|
r'MESH_CLI_REPLY_TEXT\(("(?:\\.|[^"\\])*"),\s*("(?:\\.|[^"\\])*")\)')
|
|
TOKENS = re.compile(r'"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\'|'
|
|
r'//[^\n]*|/\*.*?\*/|[A-Za-z_][A-Za-z_0-9]*|[^\s]', re.S)
|
|
|
|
|
|
def original_tokens(source):
|
|
source = re.sub(r'^#include [<"](?:helpers/)?CLIReplyText\.h[>"]\s*\n',
|
|
'', source, flags=re.M)
|
|
source = SELECTION.sub(lambda match: match.group(1), source)
|
|
return '\n'.join(token.group() for token in TOKENS.finditer(source)
|
|
if not token.group().startswith(('//', '/*'))).encode()
|
|
|
|
|
|
class CliReplyTextTests(unittest.TestCase):
|
|
def test_only_reviewed_human_literals_change_and_control_flow_stays_exact(self):
|
|
fixture = json.loads(FIXTURE.read_text())
|
|
self.assertEqual(fixture['schema'], 1)
|
|
expected = {}
|
|
for row in fixture['messages']:
|
|
expected.setdefault(row['file'], set()).add((row['verbose'], row['concise']))
|
|
old, new = row['verbose'], row['concise']
|
|
self.assertLess(len(new), len(old))
|
|
formats = r'%(?:[-+ #0]*[0-9.*]*[a-zA-Z]+|%)'
|
|
self.assertEqual(re.findall(formats, old), re.findall(formats, new))
|
|
category = r'^(Error|ERROR|ERR|Err|OK)(?=[: ,\-])'
|
|
self.assertEqual(re.findall(category, old), re.findall(category, new))
|
|
self.assertEqual(re.findall(r'\d+(?:\.\d+)?', old),
|
|
re.findall(r'\d+(?:\.\d+)?', new))
|
|
self.assertFalse(old.startswith(('> ', '{', 'KEYBACKUP ', 'ACK')))
|
|
self.assertNotRegex(old.lower().replace('[reboot]', ''),
|
|
r'host|replay|password|login|save unconfirmed|reboot|'
|
|
r'retained|warning|applying in|restore scheduled')
|
|
for protected in ('[reboot]', 'default'):
|
|
if protected in old:
|
|
self.assertIn(protected, new)
|
|
if 'unchanged' in old:
|
|
self.assertIn('sleep policy unchanged', old)
|
|
self.assertIn('sleep policy unchanged', new)
|
|
for file, messages in expected.items():
|
|
source = (ROOT / file).read_text()
|
|
actual = {(json.loads(match.group(1)), json.loads(match.group(2)))
|
|
for match in SELECTION.finditer(source)}
|
|
self.assertEqual(actual, messages, file)
|
|
# Substituting each original literal recovers every original C++
|
|
# token, including command tests, branches, calls and arguments.
|
|
self.assertEqual(hashlib.sha256(original_tokens(source)).hexdigest(),
|
|
fixture['noncompact_tokens_sha256'][file], file)
|
|
|
|
def test_profile_activation_is_explicit_and_default_is_off(self):
|
|
fixture = json.loads(FIXTURE.read_text())
|
|
enabled = []
|
|
for path in [ROOT / 'platformio.ini', *sorted((ROOT / 'variants').rglob('*.ini'))]:
|
|
config = configparser.ConfigParser(interpolation=None, strict=False)
|
|
config.read(path)
|
|
for section in config.sections():
|
|
if re.search(r'-D\s*MESH_CLI_COMPACT_REPLIES_DEFAULT=1\b',
|
|
config[section].get('build_flags', '')):
|
|
self.assertTrue(section.startswith('env:'), str(path))
|
|
enabled.append(section[4:])
|
|
self.assertEqual(sorted(enabled), sorted(fixture['profiles']))
|
|
header = (ROOT / 'src/helpers/CLIReplyText.h').read_text()
|
|
self.assertIn('#ifndef MESH_CLI_COMPACT_REPLIES\n', header)
|
|
self.assertIn('#define MESH_CLI_COMPACT_REPLIES 0', header)
|
|
|
|
def test_actual_header_selects_exact_text_without_a_runtime_lookup(self):
|
|
compiler = os.environ.get('CXX') or shutil.which('g++') or shutil.which('clang++')
|
|
if not compiler:
|
|
self.skipTest('host C++ compiler required')
|
|
messages = json.loads(FIXTURE.read_text())['messages']
|
|
selections = ',\n'.join('MESH_CLI_REPLY_TEXT(%s, %s)' %
|
|
(json.dumps(row['verbose']), json.dumps(row['concise']))
|
|
for row in messages)
|
|
source = ('#include <CLIReplyText.h>\n#include <cstdio>\n'
|
|
'int main(){const char* text[]={'+selections+'};'
|
|
'for(const char* line:text)puts(line);}\n')
|
|
with tempfile.TemporaryDirectory(prefix='cli-reply-text-') as folder:
|
|
work = Path(folder)
|
|
(work / 'test.cpp').write_text(source)
|
|
for flags, key in (([], 'verbose'),
|
|
(['-DMESH_CLI_COMPACT_REPLIES=0'], 'verbose'),
|
|
(['-DMESH_CLI_COMPACT_REPLIES=1'], 'concise'),
|
|
(['-DMESH_CLI_COMPACT_REPLIES_DEFAULT=1'], 'concise'),
|
|
(['-DMESH_CLI_COMPACT_REPLIES_DEFAULT=1',
|
|
'-DMESH_CLI_COMPACT_REPLIES=0'], 'verbose'),
|
|
(['-DMESH_CLI_COMPACT_REPLIES_DEFAULT=0',
|
|
'-DMESH_CLI_COMPACT_REPLIES=1'], 'concise')):
|
|
built = subprocess.run([compiler, '-std=c++11', '-Os', '-Wall', '-Wextra',
|
|
'-Werror', *flags, '-I'+str(ROOT / 'src/helpers'),
|
|
str(work / 'test.cpp'), '-o', str(work / 'test')], capture_output=True)
|
|
self.assertEqual(built.returncode, 0, built.stderr.decode(errors='replace'))
|
|
result = subprocess.run([str(work / 'test')], capture_output=True)
|
|
self.assertEqual(result.returncode, 0, result.stderr.decode(errors='replace'))
|
|
self.assertEqual(result.stdout.decode().splitlines(),
|
|
[row[key] for row in messages])
|
|
if key == 'concise':
|
|
image = (work / 'test').read_bytes()
|
|
selected = {row['concise'] for row in messages}
|
|
for row in messages:
|
|
if row['verbose'] not in selected:
|
|
self.assertNotIn(row['verbose'].encode()+b'\0', image)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|