From 6eb987fd07711cd1d47dbf837cf3bfe9254961da Mon Sep 17 00:00:00 2001 From: zy7y <13271962515@163.com> Date: Sat, 27 Apr 2024 23:48:02 +0800 Subject: [PATCH] =?UTF-8?q?feat=20#31=20ci=20build=20=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.py | 62 +++++++++++++- .github/workflows/build_client.yml | 44 ++++++++++ .github/workflows/release.yml | 100 ----------------------- README.md | 7 ++ dfs_generate/conversion.py | 4 +- dfs_generate/server.py | 9 ++- dfs_generate/tools.py | 2 +- docs/wechat.jpeg | Bin 0 -> 53581 bytes tests/test_client.py | 50 ++++++++++++ tests/test_conversion.py | 92 +++++++++++++++++++++ tests/test_tools.py | 125 +++++++++++++++++++++++++++++ 11 files changed, 386 insertions(+), 109 deletions(-) delete mode 100644 .github/workflows/release.yml create mode 100644 docs/wechat.jpeg create mode 100644 tests/test_client.py create mode 100644 tests/test_conversion.py create mode 100644 tests/test_tools.py diff --git a/.github/workflows/build.py b/.github/workflows/build.py index 45471c2..99efb21 100644 --- a/.github/workflows/build.py +++ b/.github/workflows/build.py @@ -1,19 +1,75 @@ +import platform +import subprocess + import yapf_third_party from PyInstaller import __main__ as pyi +def gen_client_py(): + code = """ +import random +import socket +import threading + +from dfs_generate.server import app + + +def get_unused_port(): + while True: + port = random.randint(1024, 65535) # 端口范围一般为1024-65535 + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + sock.bind(("localhost", port)) + sock.close() + return port + except OSError: + pass + + +import webview + + +def desktop_client(): + port = get_unused_port() + t = threading.Thread(target=app.run, kwargs={"port": port}) + t.daemon = True + t.start() + webview.create_window("DFS代码生成", f"http://127.0.0.1:{port}") + webview.start() + + +if __name__ == '__main__': + desktop_client() + """ + with open("dfs_generate/client.py", "w", encoding="utf-8") as f: + f.write(code) + + +gen_client_py() + + params = [ "--windowed", "--onefile", "--add-data", - "static:static", + "web/dist:web/dist", "--add-data", f'{yapf_third_party.__file__.replace("__init__.py", "")}:yapf_third_party', "--clean", "--noconfirm", "--name=client", - "server.py", + "dfs_generate/client.py", ] - pyi.run(params) + + +# 如果是macos,则压缩打包后的目录 +if platform.system() == "Darwin": + cmds = ["zip", "-r", "dist/client.zip", "dist/"] + subprocess.call(cmds) + rm_cmds = ["rm", "-rf", "dist/client.app"] + subprocess.call(rm_cmds) + # 删除空目录 + rm_cmds = ["rm", "-rf", "dist/client"] + subprocess.call(rm_cmds) diff --git a/.github/workflows/build_client.yml b/.github/workflows/build_client.yml index c7577d4..b72fcf5 100644 --- a/.github/workflows/build_client.yml +++ b/.github/workflows/build_client.yml @@ -1 +1,45 @@ # 构建桌面端 +name: build + +on: + push: + branches: + - 25-todo +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ windows-latest, macos-latest ] + steps: + - name: Checkout代码 + uses: actions/checkout@v3 + - name: 设置Node.js环境 + uses: actions/setup-node@v3 + with: + node-version: 18.15 + - name: 进入web目录 + run: cd web + - name: 安装依赖 + run: npm i + - name: 打包Node.js应用 + run: npm run build + - uses: actions/checkout@v3 + - name: Set up Python 3.11 + uses: actions/setup-python@v3 + with: + python-version: "3.11" + - name: Install dependencies + run: | + pip install -r requirements-build.txt + + - name: Build executable + run: | + python .github/workflows/build.py + + - name: Upload artifact + uses: actions/upload-artifact@v3 + with: + name: Setup + retention-days: 1 + path: ./dist/* \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 70f38c8..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,100 +0,0 @@ -name: release - -on: - push: - tags: - - v* - -jobs: - - # 创建发布 - release: - runs-on: ubuntu-latest - # 输出变量 - outputs: - upload_url: ${{ steps.create_release.outputs.upload_url }} - steps: - - name: Create release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.TOKEN }} - with: - tag_name: ${{ github.ref }} - release_name: Release ${{ github.ref }} - body: | - Release ${{ github.ref }} - draft: false - prerelease: false - - - # 打包上传 - build-windows-app: - needs: release - runs-on: windows-latest - steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.11 - uses: actions/setup-python@v3 - with: - python-version: "3.11" - - name: Install dependencies - run: | - pip install -r requirements.txt - pip install pyinstaller - pip install pillow - - - name: Build executable - run: | - python .github/workflows/build.py - - - name: Upload executables - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.TOKEN }} - with: - # 获取变量 - upload_url: ${{ needs.release.outputs.upload_url }} - asset_path: ./dist/client.exe - asset_name: client.exe - asset_content_type: application/octet-stream - - build-macos-linux: - needs: release - runs-on: ${{ matrix.os }} - strategy: - matrix: - # ubuntu macos 打包 - os: [ ubuntu-latest, macos-latest ] - steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: - python-version: "3.11" - - - name: Install dependencies - run: | - pip install -r requirements.txt - pip install pyinstaller - pip install pillow - - - name: Build executable - run: | - python .github/workflows/build.py - if [[ "${{ matrix.os }}" == "macos-latest" ]]; then - zip -r -X client-macos.zip ./dist/client.app - fi - - - name: Upload executables - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.TOKEN }} - with: - # 获取变量 - upload_url: ${{ needs.release.outputs.upload_url }} - asset_path: | - ${{ matrix.os == 'macos-latest' && './client-macos.zip' || './dist/client' }} - asset_name: | - ${{ matrix.os == 'macos-latest' && 'client-macos.zip' || 'client-linux' }} - asset_content_type: application/octet-stream \ No newline at end of file diff --git a/README.md b/README.md index 2ea15f4..9819e68 100644 --- a/README.md +++ b/README.md @@ -26,4 +26,11 @@ +
+WeChat打赏 + +![wechat](docs/wechat.jpeg) + +
+ diff --git a/dfs_generate/conversion.py b/dfs_generate/conversion.py index 4e257df..39084b1 100644 --- a/dfs_generate/conversion.py +++ b/dfs_generate/conversion.py @@ -1,6 +1,6 @@ from string import Template -from templates import ( +from dfs_generate.templates import ( SQLMODEL_DAO, TORTOISE_DAO, RESPONSE_SCHEMA, @@ -10,7 +10,7 @@ TORTOISE_ROUTER, SQLMODEL_DB, ) -from tools import to_pascal, tran, to_snake +from dfs_generate.tools import to_pascal, tran, to_snake def _pydantic_field(column, imports): diff --git a/dfs_generate/server.py b/dfs_generate/server.py index 0bc56d7..81aaa8c 100644 --- a/dfs_generate/server.py +++ b/dfs_generate/server.py @@ -5,15 +5,18 @@ import isort from yapf.yapflib.yapf_api import FormatCode -from conversion import SQLModelConversion, TortoiseConversion -from tools import MySQLConf, MySQLHelper +from dfs_generate.conversion import SQLModelConversion, TortoiseConversion +from dfs_generate.tools import MySQLConf, MySQLHelper app = bottle.Bottle() CACHE: Dict[str, MySQLHelper] = {} # 解决打包桌面程序static找不到的问题 -static_file_abspath = os.path.join(os.path.dirname(__file__), "../web/dist") +static_file_abspath = os.path.join( + os.path.dirname(os.path.dirname(__file__)), "web", "dist" +) +print(static_file_abspath) @app.hook("before_request") diff --git a/dfs_generate/tools.py b/dfs_generate/tools.py index 589cee4..6e94f97 100644 --- a/dfs_generate/tools.py +++ b/dfs_generate/tools.py @@ -3,7 +3,7 @@ import pymysql -from types_map import TYPES +from dfs_generate.types_map import TYPES def tran(t, mode) -> dict: diff --git a/docs/wechat.jpeg b/docs/wechat.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..b3f8243df19c78533f0d55a07dfc6e0520622df9 GIT binary patch literal 53581 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 literal 0 HcmV?d00001 diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..5e913e2 --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,50 @@ +import pytest +from dfs_generate.client import get_unused_port, desktop_client + + +# 由于get_unused_port函数依赖于随机性和系统状态,我们倾向于对socket操作进行mock +@pytest.mark.parametrize("mocked_port", [12345]) +def test_get_unused_port(mocker, mocked_port): + # Mocking the socket operations + mock_socket = mocker.patch("dfs_generate.client.socket.socket") + mock_socket.bind.return_value = None + mock_socket.close.return_value = None + + # To ensure we control the behavior of randint for predictable testing + mock_randint = mocker.patch("dfs_generate.client.random.randint") + mock_randint.return_value = mocked_port + + # Test the function + port = get_unused_port() + assert port == mocked_port + mock_socket.bind.assert_called_once_with(("localhost", mocked_port)) + mock_socket.close.assert_called_once() + + +@pytest.fixture +def mock_app_run(mocker): + """Fixture to mock app.run method.""" + mock_run = mocker.patch("dfs_generate.server.app.run") + yield mock_run + + +@pytest.fixture +def mock_webview(mocker): + """Fixture to mock webview functions.""" + mock_create_window = mocker.patch("webview.create_window") + mock_start = mocker.patch("webview.start") + yield mock_create_window, mock_start + + +def test_desktop_client(mock_app_run, mock_webview): + """Test the desktop_client function.""" + # Since get_unused_port is mocked in test_get_unused_port, we can assume it works. + # Here we focus on verifying app.run and webview interactions. + desktop_client() + + mock_app_run.assert_called_once_with( + port=12345 + ) # Assuming 12345 is a typical port used in tests + create_window, start = mock_webview + create_window.assert_called_once_with("DFS代码生成", "http://127.0.0.1:12345") + start.assert_called_once() diff --git a/tests/test_conversion.py b/tests/test_conversion.py new file mode 100644 index 0000000..8507d26 --- /dev/null +++ b/tests/test_conversion.py @@ -0,0 +1,92 @@ +import pytest +from dfs_generate.conversion import ( + Conversion, + SQLModelConversion, + TortoiseConversion, + _pydantic_field, + _sqlmodel_field_repr, + _tortoise_field_repr, +) + +# 假设的列数据,用于模拟从数据库获取的信息 +MOCK_COLUMNS = [ + { + "COLUMN_NAME": "id", + "DATA_TYPE": "int", + "IS_NULLABLE": "NO", + "COLUMN_KEY": "PRI", + "COLUMN_COMMENT": "主键ID", + }, + { + "COLUMN_NAME": "name", + "DATA_TYPE": "varchar(100)", + "IS_NULLABLE": "YES", + "COLUMN_COMMENT": "姓名", + }, +] + +# 假定的table_name和uri +MOCK_TABLE_NAME = "users" +MOCK_URI = "mysql+pymysql://user:pass@localhost/dbname" + + +@pytest.fixture +def conversion_fixture(): + return Conversion(MOCK_TABLE_NAME, MOCK_COLUMNS, MOCK_URI) + + +@pytest.fixture +def sqlmodel_conversion_fixture(): + return SQLModelConversion(MOCK_TABLE_NAME, MOCK_COLUMNS, MOCK_URI) + + +@pytest.fixture +def tortoise_conversion_fixture(): + return TortoiseConversion(MOCK_TABLE_NAME, MOCK_COLUMNS, MOCK_URI) + + +def test_conversion_initialization(conversion_fixture): + """测试Conversion类的初始化""" + assert conversion_fixture.table_name == MOCK_TABLE_NAME + assert conversion_fixture.router_name == "users" + + +def test_sqlmodel_conversion_model(sqlmodel_conversion_fixture): + """测试SQLModelConversion的model方法输出格式""" + # 这里简化测试,只检查输出是否包含一些预期的关键字 + model_code = sqlmodel_conversion_fixture.model() + assert "class Users(SQLModel, table=True):" in model_code + assert "id: Optional[int] =" in model_code + assert "name: Optional[str] =" in model_code + + +def test_tortoise_conversion_model(tortoise_conversion_fixture): + """测试TortoiseConversion的model方法输出格式""" + model_code = tortoise_conversion_fixture.model() + assert "class Users(Model):" in model_code + assert "id = fields.Int(pk=True)" in model_code + assert "name = fields.CharField(max_length=100)" in model_code + + +def test_pydantic_field(): + """测试_pydantic_field函数的输出""" + column = MOCK_COLUMNS[1] # 使用name字段作为测试 + field_code = _pydantic_field(column, set()) + assert "name: Optional[str] = Field(None, description='姓名')" in field_code + + +def test_sqlmodel_field_repr(): + """测试_sqlmodel_field_repr函数的输出""" + column = MOCK_COLUMNS[0] # 使用id字段作为测试 + imports, field_code = set(), _sqlmodel_field_repr(column, set()) + assert "id: Optional[int] = Field(nullable=False)" in field_code + assert ( + "from datetime import datetime" not in imports + ) # id字段不应触发默认时间戳逻辑 + + +def test_tortoise_field_repr(): + """测试_tortoise_field_repr函数的输出""" + column = MOCK_COLUMNS[1] + field_code = _tortoise_field_repr(column) + assert "name = fields.CharField(max_length=100, description='姓名')" in field_code diff --git a/tests/test_tools.py b/tests/test_tools.py new file mode 100644 index 0000000..fb9b472 --- /dev/null +++ b/tests/test_tools.py @@ -0,0 +1,125 @@ +import pymysql +import pytest +from dfs_generate.tools import tran, to_pascal, to_snake +from dfs_generate.tools import MySQLConf, MySQLHelper +from unittest.mock import MagicMock +from pymysql.err import OperationalError + + +# 测试 tran 函数 +@pytest.mark.parametrize( + "t, mode, expected", + [ + ("int", "sqlalchemy", {"type": "Integer"}), + ("varchar", "tortoise-orm", {"type": "CharField"}), + ("bool", "pydantic", {"type": "bool"}), + ], +) +def test_tran(t, mode, expected): + assert tran(t, mode) == expected + + +# 测试 to_pascal 函数 +@pytest.mark.parametrize( + "snake, expected", + [ + ("hello_world", "HelloWorld"), + ("user_id", "UserId"), + ("some_value", "SomeValue"), + ], +) +def test_to_pascal(snake, expected): + assert to_pascal(snake) == expected + + +# 测试 to_snake 函数 +@pytest.mark.parametrize( + "camel, expected", + [ + ("helloWorld", "hello_world"), + ("userId", "user_id"), + ("someValue", "some_value"), + ("HTTPRequest", "http_request"), + ("123abc", "123abc"), # No change for non-camelCase inputs + ], +) +def test_to_snake(camel, expected): + assert to_snake(camel) == expected + + +def test_mysqlconf_get_db_uri(): + conf = MySQLConf( + host="localhost", user="test_user", password="secure_pwd", db="test_db" + ) + assert ( + conf.get_db_uri() + == "mysql+pymysql://test_user:secure_pwd@localhost:3306/test_db?charset=utf8" + ) + + +def test_mysqlconf_json(): + conf = MySQLConf( + host="localhost", + user="test_user", + password="pwd", + db="test_db", + port=3307, + charset="utf8mb4", + ) + expected_json = { + "host": "localhost", + "user": "test_user", + "password": "pwd", + "db": "test_db", + "port": 3307, + "charset": "utf8mb4", + } + assert conf.json() == expected_json + + +@pytest.fixture +def mysql_helper_mock(monkeypatch): + """Fixture to create a mocked MySQLHelper instance.""" + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_conn.cursor.return_value = mock_cursor + monkeypatch.setattr("pymysql.connect", lambda *args, **kwargs: mock_conn) + helper = MySQLHelper( + MySQLConf(host="localhost", user="test", password="pwd", db="test_db") + ) + return helper, mock_conn, mock_cursor + + +def test_mysqlhelper_init(mysql_helper_mock): + helper, mock_conn, _ = mysql_helper_mock + mock_conn.assert_called_once() + assert helper.conn == mock_conn + assert helper.cursor == mock_conn.cursor.return_value + + +def test_mysqlhelper_set_conn(mysql_helper_mock): + helper, mock_conn, _ = mysql_helper_mock + new_conf = MySQLConf( + host="new_host", user="new_user", password="new_pwd", db="new_db" + ) + helper.set_conn(new_conf) + mock_conn.assert_called_with( + **new_conf.json(), cursorclass=pymysql.cursors.DictCursor + ) + + +def test_mysqlhelper_close(mysql_helper_mock): + _, mock_conn, mock_cursor = mysql_helper_mock + helper = MySQLHelper( + MySQLConf(host="localhost", user="test", password="pwd", db="test_db") + ) + helper.close() + mock_cursor.close.assert_called_once() + mock_conn.close.assert_called_once() + + +def test_mysqlhelper_get_tables_error(mysql_helper_mock): + helper, _, mock_cursor = mysql_helper_mock + mock_cursor.execute.side_effect = OperationalError + with pytest.raises(OperationalError): + helper.get_tables()