Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 37 additions & 87 deletions tests/test_core.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
# coding: utf-8
#

import hashlib
import json
import socket
import struct
import threading
import time
from datetime import datetime
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from unittest.mock import Mock, mock_open, patch
from unittest.mock import Mock, patch

import pytest
from adbutils import FileInfo

from uiautomator2.core import DEFAULT_SERVER_PORT, BasicUiautomatorServer, _http_request
from uiautomator2.exceptions import HTTPError, HTTPTimeoutError
Expand All @@ -27,94 +28,43 @@ def mock_server():
yield server, mock_dev


class TestCheckDeviceFileHash:
"""Test the _check_device_file_hash method with toybox fallback"""

def test_toybox_md5sum_success(self, mock_server):
"""Test when toybox md5sum command works correctly"""
server, mock_dev = mock_server

# Create a temporary file with known content
test_content = b"test content for md5"
local_md5 = hashlib.md5(test_content).hexdigest()

# Mock the shell command to return toybox md5sum output
# Format: "md5hash filename"
mock_dev.shell.return_value = f"{local_md5} /data/local/tmp/u2.jar"

# Mock the file read to return our test content
with patch("builtins.open", mock_open(read_data=test_content)):
result = server._check_device_file_hash("test.jar", "/data/local/tmp/u2.jar")

# Verify the result is True (hash matches)
assert result is True
# Verify toybox md5sum was called
mock_dev.shell.assert_called_once_with(["toybox", "md5sum", "/data/local/tmp/u2.jar"])

def test_toybox_not_found_fallback_to_md5(self, mock_server):
"""Test fallback to md5 command when toybox is not found"""
class TestCheckDeviceFileSize:
"""Test the _check_device_file_size method"""

def test_same_size(self, mock_server, tmp_path):
"""Test when the remote file has the same size as the local one"""
server, mock_dev = mock_server

# Create a temporary file with known content
test_content = b"test content for md5"
local_md5 = hashlib.md5(test_content).hexdigest()

# Mock the shell command to return different outputs
# First call: toybox not found
# Second call: md5 command output (format: "MD5 (filename) = md5hash")
mock_dev.shell.side_effect = [
"toybox: not found",
f"MD5 (/data/local/tmp/u2.jar) = {local_md5}"
]

# Mock the file read to return our test content
with patch("builtins.open", mock_open(read_data=test_content)):
result = server._check_device_file_hash("test.jar", "/data/local/tmp/u2.jar")

# Verify the result is True (hash matches)
assert result is True
# Verify both commands were called
assert mock_dev.shell.call_count == 2
assert mock_dev.shell.call_args_list[0][0][0] == ["toybox", "md5sum", "/data/local/tmp/u2.jar"]
assert mock_dev.shell.call_args_list[1][0][0] == ["md5", "/data/local/tmp/u2.jar"]

def test_hash_mismatch(self, mock_server):
"""Test when the hash doesn't match"""
local_file = tmp_path / "u2.jar"
local_file.write_bytes(b"test content")
mock_dev.sync.stat.return_value = FileInfo(
mode=0o100644, size=len(b"test content"), mtime=datetime.now(), path="/data/local/tmp/u2.jar")

assert server._check_device_file_size(local_file, "/data/local/tmp/u2.jar") is True
mock_dev.sync.stat.assert_called_once_with("/data/local/tmp/u2.jar")
# no shell command should be used anymore
mock_dev.shell.assert_not_called()

def test_size_mismatch(self, mock_server, tmp_path):
"""Test when the remote file has a different size"""
server, mock_dev = mock_server

# Create a temporary file with known content
test_content = b"test content for md5"
different_md5 = hashlib.md5(b"different content").hexdigest()

# Mock the shell command to return a different hash
mock_dev.shell.return_value = f"{different_md5} /data/local/tmp/u2.jar"

# Mock the file read to return our test content
with patch("builtins.open", mock_open(read_data=test_content)):
result = server._check_device_file_hash("test.jar", "/data/local/tmp/u2.jar")

# Verify the result is False (hash doesn't match)
assert result is False

def test_md5_command_also_fails(self, mock_server):
"""Test when both toybox and md5 commands fail to find the file"""
local_file = tmp_path / "u2.jar"
local_file.write_bytes(b"test content")
mock_dev.sync.stat.return_value = FileInfo(
mode=0o100644, size=1, mtime=datetime.now(), path="/data/local/tmp/u2.jar")

assert server._check_device_file_size(local_file, "/data/local/tmp/u2.jar") is False
mock_dev.shell.assert_not_called()

def test_file_not_exists(self, mock_server, tmp_path):
"""Test when the remote file does not exist (adbutils reports mtime None)"""
server, mock_dev = mock_server

# Create a temporary file with known content
test_content = b"test content for md5"

# Mock the shell command to return errors for both commands
mock_dev.shell.side_effect = [
"toybox: not found",
"md5: /data/local/tmp/u2.jar: No such file or directory"
]

# Mock the file read to return our test content
with patch("builtins.open", mock_open(read_data=test_content)):
result = server._check_device_file_hash("test.jar", "/data/local/tmp/u2.jar")

# Verify the result is False (file not found on device)
assert result is False
local_file = tmp_path / "u2.jar"
local_file.write_bytes(b"test content")
mock_dev.sync.stat.return_value = FileInfo(
mode=0, size=0, mtime=None, path="/data/local/tmp/u2.jar")

assert server._check_device_file_size(local_file, "/data/local/tmp/u2.jar") is False
mock_dev.shell.assert_not_called()


class FakeU2Handler(BaseHTTPRequestHandler):
Expand Down
23 changes: 10 additions & 13 deletions uiautomator2/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import atexit
import datetime
import hashlib
import json
import logging
import os
Expand Down Expand Up @@ -258,23 +257,21 @@ def start_uiautomator(self):
def _setup_jar(self):
with with_package_resource("assets/u2.jar") as jar_path:
target_path = "/data/local/tmp/u2.jar"
if self._check_device_file_hash(jar_path, target_path):
if self._check_device_file_size(jar_path, target_path):
logger.debug("file u2.jar already pushed")
else:
logger.debug("push %s -> %s", jar_path, target_path)
self._dev.sync.push(jar_path, target_path, check=True)

def _check_device_file_hash(self, local_file: Union[str, Path], remote_file: str) -> bool:
""" check if remote file hash is correct """
md5 = hashlib.md5()
with open(local_file, "rb") as f:
md5.update(f.read())
local_md5 = md5.hexdigest()
logger.debug("file %s md5: %s", os.path.basename(local_file), local_md5)
output = self._dev.shell(["toybox", "md5sum", remote_file])
if "toybox" in output and "not found" in output:
output = self._dev.shell(["md5", remote_file])
return local_md5 in output
def _check_device_file_size(self, local_file: Union[str, Path], remote_file: str) -> bool:
""" check whether remote file exists and has the same size as the local one """
local_size = os.path.getsize(local_file)
remote_info = self._dev.sync.stat(remote_file)
# adbutils returns mtime None when the file does not exist
if remote_info.mtime is None:
return False
logger.debug("file %s size: local=%d remote=%d", os.path.basename(local_file), local_size, remote_info.size)
return local_size == remote_info.size

def _wait_ready(self, launch_timeout=30):
"""Wait until uiautomator2 server is ready"""
Expand Down
Loading