Skip to content
📔 阅读量:

断言

YouQu 自带多种断言语句,几乎满足了所有的断言场景;

用例中使用方法

APP 工程自有断言模块:my_app_assert.py,它继承了 YouQu 框架的断言库:

python
# my_app_assert.py

from src.assert_common import AssertCommon


class MyAppAssert(AssertCommon):
    """MyAppAssert"""

用例基类 BaseCase 继承 MyAppAssert

python
# base_case.py

from apps.autotest_my_app.my_app_assert import MyAppAssert
from src.webui import WebAssert


class BaseCase(MyAppAssert):
    """用例基类"""

用例中通过 self 调用所有的断言语句:

python
class TestMyCase(BaseCase):

    def test_mycase_001(self):
        """my case 001"""
        self.assert_xxx

方法明细

python
#!/usr/bin/env python3
# _*_ coding:utf-8 _*_
# SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd.
# SPDX-License-Identifier: GPL-2.0-only
import os
from time import sleep
from typing import Union

try:
    import pyscreenshot
except ModuleNotFoundError:
    pass

from src.ocr_utils import OCRUtils as OCR
from src.image_utils import ImageUtils
from src.mouse_key import MouseKey
from src.filectl import FileCtl
from src.dogtail_utils import DogtailUtils
from src.custom_exception import TemplateElementNotFound
from src.custom_exception import TemplateElementFound
from src.custom_exception import ElementNotFound
from src.custom_exception import ElementExpressionError
from src.custom_exception import AssertOptionError
from src.cmdctl import CmdCtl
from src.button_center import ButtonCenter
from src import logger, log
from setting.globalconfig import GlobalConfig


@log
class AssertCommon:
    """
    自定义断言类
    """

    @staticmethod
    def assert_image_exist(
        widget: str,
        rate: float = None,
        multiple: bool = False,
        picture_abspath: str = None,
        network_retry: int = None,
        pause: [int, float] = None,
        timeout: [int, float] = None,
        match_number: int = None,
    ):
        """
         期望界面存在模板图片
        :param widget: 图片路径 例:apps/autotest_app/assert_res/1.png
        :param rate: 匹配相似度
        """
        logger.info(f"屏幕上匹配图片< {f'***{widget[-40:]}' if len(widget) >= 40 else widget} >")

        try:
            ImageUtils.find_image(
                widget,
                rate=rate,
                multiple=multiple,
                picture_abspath=picture_abspath,
                network_retry=network_retry,
                pause=pause,
                timeout=timeout,
                max_match_number=match_number,
            )
        except TemplateElementNotFound as exc:
            raise AssertionError(exc) from TemplateElementNotFound
        except Exception as exc:
            raise AssertOptionError(exc) from Exception

    @classmethod
    def assert_image_exist_during_time(
        cls,
        widget: str,
        screen_time: Union[float, int],
        rate: float = None,
        pause: Union[int, float] = None,
    ):
        """
        在一段时间内截图多张图片进行识别,其中有一张图片识别成功即返回结果;
        适用于气泡类的断言,比如气泡在1秒内消失,如果用常规的图像识别则有可能无法识别到;
        :param image_path: 要识别的模板图片;
        :param screen_time: 截取屏幕图片的时间,单位秒;
        :param rate: 识别率;
        :param pause: 截取屏幕图片的间隔时间,默认不间隔;
        """
        logger.info(f"屏幕上匹配图片< {f'***{widget[-40:]}' if len(widget) >= 40 else widget} >")
        try:
            ImageUtils.get_during(widget, screen_time, rate, pause)
        except TemplateElementNotFound as exc:
            raise AssertionError(exc) from TemplateElementNotFound
        except Exception as exc:
            raise AssertOptionError(exc) from Exception

    @staticmethod
    def assert_image_not_exist(
        widget: str,
        rate: float = None,
        multiple: bool = False,
        picture_abspath: str = None,
        network_retry: int = None,
        pause: [int, float] = None,
        timeout: [int, float] = None,
        match_number: int = None,
    ):
        """
         期望界面不存在模板图片
        :param widget: 图片路径 apps/autotest_app/assert_res/1.png
        :param rate: 匹配相似度
        """
        logger.info(
            f"屏幕上匹配不存在图片< {f'***{widget[-40:]}' if len(widget) >= 40 else widget} >"
        )
        try:
            sleep(1)
            ImageUtils.find_image(
                widget,
                rate=rate,
                multiple=multiple,
                picture_abspath=picture_abspath,
                network_retry=network_retry,
                pause=pause,
                timeout=timeout,
                max_match_number=match_number,
            )
            raise TemplateElementFound(widget)
        except TemplateElementNotFound:
            pass
        except TemplateElementFound as exc:
            raise AssertionError(exc) from TemplateElementFound
        except Exception as exc:
            raise AssertOptionError(exc) from Exception

    @staticmethod
    def assert_file_exist(widget, file=None, recursive=False):
        """
         期望存在文件路径
        :param widget: 文件全路径或目录 例:~/Desktop/1.txt
        :param file: 文件名
        :param recursive: 是否递归查找
        """
        sleep(1)
        if recursive:
            if file:
                for _, _, files in os.walk(widget):
                    for filename in files:
                        if file == filename:
                            return True
                raise AssertionError(f"目录 {widget}及子目录无 {file} 文件")
            if not os.path.exists(os.path.expanduser(widget)):
                raise AssertionError(f"文件不存在! 路径 {widget}")
            return True
        if file:
            _path = f"{widget}/{file}"
        else:
            _path = widget
        logger.info(f"断言文件存在<{_path}>")
        if not os.path.exists(os.path.expanduser(_path)):
            raise AssertionError(f"文件不存在! 路径 {_path}")
        return True

    @staticmethod
    def assert_file_not_exist(widget, file=None, recursive=False):
        """
         期望不存在文件路径
        :param widget: 文件全路径 例:~/Desktop/1.txt
        :param file: 文件名
        :param recursive: 是否递归查找
        """
        sleep(1)
        logger.info(f"断言文件不存在<{widget}>")
        if recursive:
            if file:
                for _, _, files in os.walk(widget):
                    for filename in files:
                        if file == filename:
                            raise AssertionError(f"目录 {widget}或子目录存在 {file} 文件")
            else:
                if os.path.exists(os.path.expanduser(widget)):
                    raise AssertionError(f"文件存在! 路径 {widget}")
        else:
            if file:
                widget = f"{widget}/{file}"
            if os.path.exists(os.path.expanduser(widget)):
                raise AssertionError(f"文件存在! 路径 {widget}")

    @staticmethod
    def assert_element_exist(expr):
        """
         期望元素存在
        :param expr: 匹配元素的格式, 例如: $/dde-file-manager//1.txt
        """
        sleep(0.5)
        logger.info(f"断言元素存在<{expr}>")
        if not DogtailUtils().find_element_by_attr(expr):
            raise AssertionError(f"元素不存在!!!expr= <{expr}>")

    @staticmethod
    def assert_element_not_exist(expr):
        """
         期望元素不存在
        :param expr: 匹配元素的格式
        """
        logger.info(f"断言元素不存在<{expr}>")
        try:
            DogtailUtils().find_element_by_attr(expr)
            raise AssertionError(f"元素不应存在!!!expr= <{expr}>")
        except ElementNotFound:
            pass

    @staticmethod
    def assert_element_numbers(expr, number):
        """
         查找元素的个数与期望一致
        :param expr: 匹配元素的格式
        :param number: 匹配元素个数
        """
        logger.info(f"断言元素出现的个数 <{expr}, {number}>")
        result = DogtailUtils().find_elements_by_attr(expr)
        if isinstance(result, bool):
            raise ElementExpressionError(expr)
        if len(result) != number:
            raise AssertionError(f"元素个数{len(result)} 与期望个数 {number} 不符!")

    @staticmethod
    def assert_window_size(expect, real):
        """
         断言窗口大小与期望一致
        :param expect: 窗口的期望大小 (1920, 400)
        :param real: 窗口的实际大小(1920, 400)
        """
        logger.info(f"断言实际窗口大小{real}与期望{expect}是否相同")
        if expect != real:
            raise AssertionError(f"实际窗口大小{real}与期望{expect}不相同")

    @staticmethod
    def assert_process_status(expect, app):
        """
         断言应用进程是否存在
        :param expect: 进程期望结果 True /False
        :param app: 应用名字
        """
        logger.info(f"断言应用进程状态{app}与期望{expect}是否相同")
        if expect != CmdCtl.get_process_status(app):
            raise AssertionError(f"断言应用进程状态{app}与期望{expect}不相同")

    @staticmethod
    def assert_process_num(num, app):
        """
         断言应用进程的数量
        :param num: 期望的进程数量
        :param app: 应用名字
        """
        logger.info(f"断言 {app} 应用进程数量是否为 {num}")
        if num != CmdCtl.get_daemon_process_num(app):
            raise AssertionError(f"断言 {app} 应用进程数量与期望{num}不相同")

    @staticmethod
    def assert_window_amount(app, expect):
        """
         断言应用窗口数量
        :param expect: 应用窗口数量
        :param app: 应用名字
        """
        logger.info(f"断言应用窗口数量{app}与期望{expect}是否相同")
        number = ButtonCenter(app_name=app, config_path="xxx").get_windows_number(app)
        if expect != number:
            raise AssertionError(f"断言应用窗口数量{app}{number}与期望{expect}不相同")

    @staticmethod
    def assert_share_folder(filename):
        """
         断言存在共享文件夹 filename
        :param filename: 共享文件夹名称
        """
        share_folder = CmdCtl.run_cmd(
            "net usershare list",
            interrupt=False,
            out_debug_flag=False,
            command_log=False,
        )
        if share_folder:
            share_folder = share_folder.split("\n")
        logger.info(f"断言共享目录中是否存在{filename}文件夹")
        if filename not in share_folder:
            raise AssertionError(f"断言共享目录中不存在{filename}文件夹")

    @staticmethod
    def assert_not_share_folder(filename):
        """
         断言不存在共享文件夹 filename
        :param filename: 共享文件夹名称
        """
        share_folder = CmdCtl.run_cmd(
            "net usershare list",
            interrupt=False,
            out_debug_flag=False,
            command_log=False,
        )
        if share_folder:
            share_folder = share_folder.split("\n")
        logger.info(f"断言共享目录中是否存在{filename}文件夹")
        if filename in share_folder:
            raise AssertionError(f"断言共享目录中存在{filename}文件夹")

    @staticmethod
    def assert_theme(expect):
        """
         断言主题, 图片中颜色大于50%, 为断言主题准确性,建议最大化窗口
        :param expect: 期望的主题 浅色/深色
        """
        logger.info(f"断言主题是否为<{expect}>主题")
        config = {"浅色": (248, 248, 248), "深色": (37, 37, 37)}
        exp_color = config[expect]
        # 在data/pic_res这个目录下生成一张临时的图,每次生成会被覆盖
        if GlobalConfig.IS_X11:
            pyscreenshot.grab().save(GlobalConfig.SCREEN_CACHE)
        else:
            GlobalConfig.SCREEN_CACHE = (
                os.popen("qdbus org.kde.KWin /Screenshot screenshotFullscreen").read().strip("\n")
            )
        color_list = ImageUtils.find_image_color(GlobalConfig.SCREEN_CACHE)
        proportion = round(color_list.count(exp_color) / len(color_list), 2)
        if proportion < 0.6:
            raise AssertionError(
                f"{expect}主题颜色占屏幕总体颜色占比低于60%,实际占比{proportion * 100}%,初步断言主题失败"
            )

    @staticmethod
    def assert_equal(expect, actual):
        """
         断言相等
        :param expect: 期望结果
        :param actual: 实际结果
        """
        logger.info(f"预期值<{expect}>与实际值<{actual}>是否相等")
        if not bool(expect == actual):
            raise AssertionError(f"预期值<{expect}>与实际值<{actual}>不相等")

    @staticmethod
    def assert_not_equal(expect, actual):
        """
         断言不相等
        :param expect: 期望结果
        :param actual: 实际结果
        """
        logger.info(f"预期值<{expect}>与实际值<{actual}>是否相等")
        if bool(expect == actual):
            raise AssertionError(f"预期值<{expect}>与实际值<{actual}>不相等")

    @staticmethod
    def assert_true(expect):
        """
         断言结果为真
        :param expect: 结果
        """
        if not expect:
            raise AssertionError(f"<{expect}>不为真")

    @staticmethod
    def assert_false(expect):
        """
         断言结果为假
        :param expect: 结果
        """
        if expect:
            raise AssertionError(f"<{expect}>不为假")

    @classmethod
    def assert_pic_px(cls, file, size=(0, 0)):
        """
         断言图片尺寸
        :param file: 结果
        :param size: 期望尺寸 例如(120, 400)
        """
        really = ImageUtils.get_pic_px(file)
        if size != really:
            raise AssertionError(f"实际尺寸<{really}>与期望尺寸<{size}>不符")

    @classmethod
    def assert_file_endwith_exist(cls, path, endwith):
        """
         断言路径下是否存在以 endwith 结果的文件
        :param path: 路径
        :param endwith: 文件后缀, txt,rar 等
        """
        if not FileCtl.find_files(path, endwith=endwith):
            raise AssertionError(f"路径 {path} 下,不存在以 {endwith} 结尾的文件")

    @staticmethod
    def assert_ocr_exist(
        *args,
        picture_abspath=None,
        similarity=0.6,
        return_first=False,
        lang="ch",
        network_retry: int = None,
        pause: [int, float] = None,
        timeout: [int, float] = None,
        max_match_number: int = None,
        mode: str = "all",
        bbox: dict = None,
    ):
        """
        断言文案存在
        :param args: 目标字符,识别一个字符串或多个字符串。
        :param picture_abspath: 要识别的图片路径,如果不传默认截取全屏识别。
        :param similarity: 匹配度。
        :param return_first: 只返回第一个,默认为 False,返回识别到的所有数据。
        :param lang: `ch`, `en`, `fr`, `german`, `korean`, `japan`
        :param network_retry: 连接服务器重试次数
        :param pause: 重试间隔时间,单位秒
        :param timeout: 最大匹配超时,单位秒
        :param max_match_number: 最大匹配次数
        :param mode: "all" or "any",all 表示识别所有目标字符,any 表示识别任意一个目标字符,默认值为 all
        :param bbox:
            接收一个字典,包含一个区域,在区域内进行识别,用于干扰较大时提升OCR识别精准度
            字典字段:
                start_x: 开始 x 坐标(左上角)
                start_y: 开始 y 坐标(左上角)
                w: 宽度
                h: 高度
                end_x: 结束 x 坐标(右下角)
                end_y: 结束 y 坐标(右下角)
                注意 : end_x + end_y 与 w + h 为互斥关系, 必须且只能传入其中一组
            示例:
                {start_x=0, start_y=0, w=100, h=100}
                {start_x=0, start_y=0, end_x=100, end_y=100}
        """

        if len(args) == 0:
            raise ValueError("缺少 ocr 断言关键字")

        pic = None
        if picture_abspath is not None:
            pic = picture_abspath + ".png"

        resolution = MouseKey.screen_size()
        if bbox is not None:
            start_x = bbox.get("start_x") if bbox.get("start_x") is not None else None
            start_y = bbox.get("start_y") if bbox.get("start_y") is not None else None
            w = bbox.get("w") if bbox.get("w") is not None else None
            h = bbox.get("h") if bbox.get("h") is not None else None
            end_x = bbox.get("end_x") if bbox.get("end_x") is not None else None
            end_y = bbox.get("end_y") if bbox.get("end_y") is not None else None

            if start_x is None or start_y is None:
                raise ValueError("缺失 start_x 或 start_y 坐标")

            wh_provided = w is not None and h is not None
            end_xy_provided = end_x is not None and end_y is not None

            if not (wh_provided ^ end_xy_provided):
                raise ValueError("end_x + end_y 与 w + h 为互斥关系, 必须且只能传入其中一组")

            if end_xy_provided:
                w = end_x - start_x
                h = end_y - start_y
            picture_abspath = ImageUtils.save_temporary_picture(start_x, start_y, w, h)
            pic = picture_abspath + ".png"

            resolution = f"{start_x, start_y} -> {w, h}"

        res = OCR.ocr(
            *args,
            picture_abspath=pic,
            similarity=similarity,
            return_first=return_first,
            lang=lang,
            network_retry=network_retry,
            pause=pause,
            timeout=timeout,
            max_match_number=max_match_number,
        )
        if res is False:
            raise AssertionError(
                (
                    f"通过OCR在范围[{resolution}]未识别到:{args}",
                    f"{pic if pic else GlobalConfig.SCREEN_CACHE}",
                )
            )
        if isinstance(res, tuple):
            pass
        elif isinstance(res, dict):
            mode = mode.lower()
            if mode == "all" and False in res.values():
                res = filter(lambda x: x[1] is False, res.items())
                raise AssertionError(
                    (
                        f"通过OCR在范围[{resolution}]未识别到:{dict(res)}",
                        f"{pic if pic else GlobalConfig.SCREEN_CACHE}",
                    )
                )
            elif mode == "any" and len(res) == list(res.values()).count(False):
                raise AssertionError(
                    (
                        f"通过OCR在范围[{resolution}]未识别到:{args}中的任意一个",
                        f"{pic if pic else GlobalConfig.SCREEN_CACHE}",
                    )
                )

    @staticmethod
    def assert_ocr_not_exist(
        *args,
        picture_abspath=None,
        similarity=0.6,
        return_first=False,
        lang="ch",
        network_retry: int = None,
        pause: [int, float] = None,
        timeout: [int, float] = None,
        max_match_number: int = None,
        bbox: dict = None,
    ):
        """
        断言文案不存在
        :param args: 目标字符,识别一个字符串或多个字符串。
        :param picture_abspath: 要识别的图片路径,如果不传默认截取全屏识别。
        :param similarity: 匹配度。
        :param return_first: 只返回第一个,默认为 False,返回识别到的所有数据。
        :param lang: `ch`, `en`, `fr`, `german`, `korean`, `japan`
        :param network_retry: 连接服务器重试次数
        :param pause: 重试间隔时间,单位秒
        :param timeout: 最大匹配超时,单位秒
        :param max_match_number: 最大匹配次数
        :param bbox:
            接收一个字典,包含一个区域,在区域内进行识别,用于干扰较大时提升OCR识别精准度
            字典字段:
                start_x: 开始 x 坐标(左上角)
                start_y: 开始 y 坐标(左上角)
                w: 宽度
                h: 高度
                end_x: 结束 x 坐标(右下角)
                end_y: 结束 y 坐标(右下角)
                注意 : end_x + end_y 与 w + h 为互斥关系, 必须且只能传入其中一组
            示例:
                {start_x=0, start_y=0, w=100, h=100}
                {start_x=0, start_y=0, end_x=100, end_y=100}
        """

        if len(args) == 0:
            raise ValueError("缺少 ocr 断言关键字")

        pic = None
        if picture_abspath is not None:
            pic = picture_abspath + ".png"

        resolution = MouseKey.screen_size()
        if bbox is not None:
            start_x = bbox.get("start_x") if bbox.get("start_x") is not None else None
            start_y = bbox.get("start_y") if bbox.get("start_y") is not None else None
            w = bbox.get("w") if bbox.get("w") is not None else None
            h = bbox.get("h") if bbox.get("h") is not None else None
            end_x = bbox.get("end_x") if bbox.get("end_x") is not None else None
            end_y = bbox.get("end_y") if bbox.get("end_y") is not None else None

            if start_x is None or start_y is None:
                raise ValueError("缺失 start_x 或 start_y 坐标")

            wh_provided = w is not None and h is not None
            end_xy_provided = end_x is not None and end_y is not None

            if not (wh_provided ^ end_xy_provided):
                raise ValueError("end_x + end_y 与 w + h 为互斥关系, 必须且只能传入其中一组")

            if end_xy_provided:
                w = end_x - start_x
                h = end_y - start_y
            picture_abspath = ImageUtils.save_temporary_picture(start_x, start_y, w, h)
            pic = picture_abspath + ".png"

            resolution = f"{start_x, start_y} -> {w, h}"

        res = OCR.ocr(
            *args,
            picture_abspath=pic,
            similarity=similarity,
            return_first=return_first,
            lang=lang,
            network_retry=network_retry,
            pause=pause,
            timeout=timeout,
            max_match_number=max_match_number,
        )
        if res is False:
            pass
        elif isinstance(res, tuple):
            raise AssertionError(
                (
                    f"通过ocr在范围[{resolution}]识别到不应存在的文案 {res}",
                    f"{pic if pic else GlobalConfig.SCREEN_CACHE}",
                )
            )
        elif isinstance(res, dict):
            if all(value is False for value in res.values()):
                pass
            else:
                res = filter(lambda x: x[1] is not False, res.items())
                raise AssertionError(
                    (
                        f"通过OCR在范围[{resolution}]识别到不应存在的文案:{dict(res)}",
                        f"{pic if pic else GlobalConfig.SCREEN_CACHE}",
                    )
                )