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()