taler-android

Android apps for GNU Taler (wallet, PoS, cashier)
Log | Files | Refs | README | LICENSE

test_capture_screenshots.py (6201B)


      1 """Capture selection and emulator restoration, without a connected device."""
      2 import importlib.util
      3 import io
      4 import json
      5 from pathlib import Path
      6 import subprocess
      7 import sys
      8 import tempfile
      9 from types import SimpleNamespace
     10 import unittest
     11 from unittest.mock import patch
     12 
     13 from PIL import Image, ImageDraw
     14 
     15 SCRIPT = Path(__file__).resolve().parents[1] / 'capture-screenshots.py'
     16 spec = importlib.util.spec_from_file_location('capture_screenshots', SCRIPT)
     17 flow = importlib.util.module_from_spec(spec)
     18 sys.modules[spec.name] = flow
     19 spec.loader.exec_module(flow)
     20 
     21 
     22 class CaptureFlowTest(unittest.TestCase):
     23     def setUp(self):
     24         self.temp = tempfile.TemporaryDirectory()
     25         self.addCleanup(self.temp.cleanup)
     26         self.args = SimpleNamespace(subset='tutorials', profiles=None, themes=None,
     27                                     scenarios=None, locales=['en'], from_captures=None,
     28                                     out=Path(self.temp.name), serial='emulator-5560', adb='adb', resume=False)
     29         self.cards = json.loads((flow.DESIGN / 'cards.json').read_text())
     30 
     31     def test_play_store_selects_only_published_cards_and_themes(self):
     32         self.args.subset = 'play-store'
     33         jobs = flow.plan(self.args, self.cards)
     34         self.assertEqual(len(jobs), 16)
     35         self.assertEqual(sum(j.theme == 'dark' for j in jobs), 2)
     36         self.assertEqual({j.scenario for j in jobs if j.theme == 'dark'}, {'navigation'})
     37         self.assertEqual({j.path.name for j in jobs}, {c['screenshot'] for c in self.cards.values()})
     38 
     39     def test_tutorial_filters_keep_existing_filenames(self):
     40         self.args.scenarios = ['order']
     41         self.args.themes = ['light']
     42         self.args.profiles = ['phone-portrait']
     43         self.args.locales = ['en', 'de', 'fr']
     44         jobs = flow.plan(self.args, self.cards)
     45         self.assertEqual(len(jobs), 3)
     46         self.assertTrue(all(j.path.name == '07-order.png' for j in jobs))
     47         self.assertTrue(all(j.card is None for j in jobs))
     48 
     49     def test_all_keeps_different_capture_sizes_in_separate_directories(self):
     50         self.args.subset = 'all'
     51         jobs = flow.plan(self.args, self.cards)
     52         self.assertEqual(len(jobs), len(flow.SCENARIOS) * 4 + 16)
     53         self.assertEqual(len({j.path for j in jobs}), len(jobs))
     54 
     55     def capture_with_fake_adb(self, fail_capture=False, fail_restore=False):
     56         self.args.profiles = ['phone-portrait']
     57         self.args.scenarios = ['order']
     58         self.args.themes = ['light']
     59         job = flow.plan(self.args, self.cards)[0]
     60         image = Image.new('RGB', (1080, 2340), 'white')
     61         ImageDraw.Draw(image).rectangle((0, 0, 540, 2340), fill='blue')
     62         buffer = io.BytesIO()
     63         image.save(buffer, format='PNG')
     64         png = buffer.getvalue()
     65         commands = []
     66 
     67         def adb(command, **kwargs):
     68             cmd = tuple(command[3:])
     69             commands.append(cmd)
     70             if cmd[:3] == ('shell', 'settings', 'get'):
     71                 return {'accelerometer_rotation': '1', 'user_rotation': '0', 'font_scale': 'null'}[cmd[-1]]
     72             if cmd == ('shell', 'wm', 'size'):
     73                 return 'Physical size: 1080x2340\nOverride size: 900x1600\n'
     74             if cmd == ('shell', 'wm', 'density'):
     75                 return 'Physical density: 420\n'
     76             if cmd[:2] == ('logcat', '-d'):
     77                 return 'I SCREENSHOT_READY: order\n'
     78             if cmd[:2] == ('exec-out', 'screencap'):
     79                 if fail_capture:
     80                     raise subprocess.CalledProcessError(1, command)
     81                 return png
     82             if fail_restore and cmd == ('shell', 'wm', 'size', '900x1600'):
     83                 raise subprocess.CalledProcessError(1, command)
     84             return ''
     85 
     86         with patch.object(flow.subprocess, 'check_output', side_effect=adb), patch.object(flow.time, 'sleep'):
     87             if fail_capture:
     88                 with self.assertRaises(subprocess.CalledProcessError):
     89                     flow.capture([job], self.args)
     90             else:
     91                 flow.capture([job], self.args)
     92                 self.assertEqual(job.path.read_bytes(), png)
     93                 self.assertEqual([p for p in self.args.out.rglob('*') if p.is_file()], [job.path])
     94                 launch = next(i for i, cmd in enumerate(commands) if cmd[:3] == ('shell', 'am', 'start'))
     95                 self.assertGreater(commands.index(('shell', 'wm', 'user-rotation', 'lock', '0')), launch)
     96         self.assertIn(('shell', 'wm', 'size', '900x1600'), commands)
     97         self.assertIn(('shell', 'wm', 'density', 'reset'), commands)
     98         self.assertIn(('shell', 'settings', 'put', 'system', 'accelerometer_rotation', '1'), commands)
     99         self.assertIn(('shell', 'settings', 'delete', 'system', 'font_scale'), commands)
    100 
    101     def test_capture_writes_only_original_png_and_restores_settings(self):
    102         self.capture_with_fake_adb()
    103 
    104     def test_capture_failure_still_attempts_every_restore(self):
    105         self.capture_with_fake_adb(fail_capture=True, fail_restore=True)
    106 
    107     def test_resume_rejects_wrong_size_instead_of_silently_reusing(self):
    108         self.args.profiles = ['phone-portrait']
    109         self.args.scenarios = ['order']
    110         self.args.themes = ['light']
    111         self.args.resume = True
    112         job = flow.plan(self.args, self.cards)[0]
    113         job.path.parent.mkdir(parents=True)
    114         Image.new('RGB', (100, 100), 'white').save(job.path)
    115         with patch.object(flow.subprocess, 'check_output', return_value='0'):
    116             with self.assertRaisesRegex(RuntimeError, 'Wrong screenshot size'):
    117                 flow.capture([job], self.args)
    118 
    119     def test_ready_timeout_does_not_save_a_screenshot(self):
    120         self.args.profiles = ['phone-portrait']
    121         self.args.scenarios = ['order']
    122         self.args.themes = ['light']
    123         job = flow.plan(self.args, self.cards)[0]
    124         with patch.object(flow.subprocess, 'check_output', return_value='0'), \
    125                 patch.object(flow.time, 'monotonic', side_effect=[0, 30]):
    126             with self.assertRaisesRegex(RuntimeError, 'No ready signal'):
    127                 flow.capture([job], self.args)
    128         self.assertFalse(job.path.exists())
    129 
    130 
    131 if __name__ == '__main__':
    132     unittest.main()