Zb6QE#~kTz(zhTIN%QudSvQ`0G>Ygxxl>6*(_A
z;)HXfBeIe51-a`f!pwmJTKr*7
zXsnGkGOimvJ_Jr@t+U}?NqRjo!?I-UEWU=!`~{*8P0gys#j&D>AuHu}C$W&>+G!re
z4caOIs6>E!a+%FBxEB<@hn@!dmw6GtM`kA=4Q9#E8yIL%*0+;l8mBNL-C3|^_T+(X
z#~J>cuJ@2dHjK^V;N%%p5
z$zGEP4i3zM=EB}BEf<5DcR|_6oVEu7u>NO9m8HLMM
z-HTgr%dWcZ)ERmg3=WVM@d}0fzsG#e{M+ZXbo-;Fr3SQclOrt~M`Y+-4Qv@O;wt!j
z62XVz#Blxs>2d!8v6AH})NMm%#9@CQX&7#6oryCs{o5sNX5&hAaB`@iQMOs&4({~<
z7t4y-$~74wGRVU4{LK5?e(k^S&s^r?1*0y&=Rlu4Vl&q-%EmPuI27ySH4;pW@VOaRu!gUj&SIxbxu
z_0I0kyu+TsX!ZCNOUQ;4P!rdcG>>O>6J|BPjT}q(ukgs8+vY+HiJ`7P$cKkze}S%2
zw-0GyYr^)Mm*a^O%*tT&;h1WCta;Z-UTUX3xX!|V=N>%U-tmRx3Jq>yv85ZXl{vXJ
z(PKXlK@?45y7XD2gH6bBqar{hc{w@;%{6%T8=QrbG>E$fZVOrf{n`zI9sf3+{}D?5*U<9+_x{;qIVw$Bz^D9Y$g=;=-+nu<-x2B6zu=?(
zWv0SkpZ(9Z!k=lY?nih3u3oz4un6ep$*}FzJ3P)}$S#vQ&I^1DUQsxs$pyMJL|sH7
zi<=8gF2J+hXM_9A#}7lBoU#^Pej70~D3@>2i|!IF5c;&$|D|#)_4CSIKCM~}IY(p0
zk>Z@;0|b8tg}ndTs^5c$?p|`%LFwGbt5KQlPpV}EH1@v>nYFFDVCplX92ykj)9Js;Ue@p
z>37>%QiOg`&a4TImr^sysqJVxUcNOu4L+)K7W=`YD>H1MRcFdp^uF`q>M7fFVsV!h
z{{mXbPzJR2mcP`UEX9*2d4qt04a5Jy#Qb>ywLt^q8iG3!;$k)OgoZlyhhB1Cl7YrmnyQ}@4@3*U+^!Gah-b?`G>-wnkt=^+)YF7
z&fM?cLpe+Afgk^4$ib`sH^MJxkrlex%@KsJr^HnLi@g26zn8SPv%b_$fv_gqtjwy^}tYHor%ekCJ
z=8)5vLpq>Un`~2J*8RDE_}+iO{rLWH-}mEt{c`?en!`P{%CWLRS2GaFW1ku9zpG>QzFI@Y~b
z!mNz4lgkvjMc)S5fUUeXNRGSa?*x+lB-(;jX$%(S^p(L07fl(ly)Ja#2H&gDb{z88N5Slw$kCc=U8HTXNl_Klmb;qUpg5uOuq0t
zdOaiZYWf5|ZEF{dSkPWMkLB&%hg!GMKvXB6VoP4uByEhgEGytVK`iayrKvL#Dev#9
zY7RH;3w
zl2TI0L0LWE4WvJW@1^eKufoab86)d{`TNVTO^PAD=rhW9I6BXQBy@!{>Ff*7(0~zI
z`J@CmDKEE@O;}FDMiMB9KGXwwVE9_^K;BXYnp8`vr@7dgYBcr;@`;2N7SxcD8Rlre
zr!T|*t48nTs(2)PQh&1gbhuV1d9-AVb|~>1u~ag6{8tBPe0oU4dr^5Ags7o-r2NUj
z9Pc^}*Gb4tnFWayhiV7BS^gLatfU^#EqJ#tzka}xZVw(Q^mE{(W5cmGrg?-WrzA3W
zBEskum!7A{Roe`tiBfz{1KMhRvS{{-kiTrc#l;1>3cA#q;ZQd?)73va1?g$3o5E06
zE?H1Y-`c8uPFY^)XM{X|IFjhl>$})ICRlJHmf3d#f$|3$Y8sd1Mj4@u5%+
zyk=951R5_0GGPzrWil#a;T#&J63v;ce9F?w^C5ORMp=uzs2&1efn
z-l8kFv&}u=l5s}|>%fPwFJJ=-v6KofHxR^Ovf32{ej|EbFX!cq~f!&>`5zR}N
z%6kuz?imIWpXYl%YQf~;pfpgz+t1qkod~_|buL*D9ls*3XzU1oppuTtojh58E
z$7GUk!B-ml*r>M+c`VJ1-{>7r+hdLjV*|*#3B&j|4fvA^{149tRZAR6BTdWZB>nj6Ad0%kym}~6ukzGK6AAF;Z~qE(
zny|iU)RG`QW7@hrI9j>2p`=k3BIiNxF_S8zOr@Il1VSu{uQ$y0nGBO9v2b{C)w02C
zrhggNvD9NvXnOI5w%WPz*p4^CqyE~0qtY0IqwNhf{@Jk+hbC5VpGo+!rH~6iY|ExB
z-{Vmf+UL7nSo!nAxz6=|m^4_H{9NNKX^EPI!=#{D?+}vIr@{u6e6I8|H9kj8f}H1e
zIq7J{LzO=hk3>dV-0=sXDv<%J!ag`ZY-2ZiM15DdrUpS*3Zc%XyuAdRA(-n
z->C{mWMqVC>FbYNPw8>xMagD_hPi(Df^JRz4QQu=3W0Pee#1|h5Wud{ZIBV|)AQ&p
z_9ntgb`K=Q@-{##UCVDogW{Ok2Dp&CI4#mz(JBY7_YM>U&xv
zYl#grw5R@IW5SwANUvnCtw$4~`TRXxBi%{hTA_3hZvWy*YG?ud%D_9z)9X#mc8*57
z4r2jp^ImPT>kz*jaV6V&L`6T(ivmi#R>7Ta
zKu=lhH_L-RWsGfs-B9~Dp#?0hrcCqtT0Ub8liLet3#e{EX~k$y<|98^+%=K$hFEcB
z{x?uGvt}-wj~R3(#-9{h5EQXHOFVU9sJ7WI4MWRr7S9$_>M`v59Nt
zSG7GCevAzyZ(B3T?sv?zg~o79fKcJ56+==>0~aO(xvcu$&slYc@gRQPAY=0aO2H|4
z%}p>J)E7{^J*}YfgK-Bw@)Le!7fYwa=zG)~cY+%r{abJRlo5#(J_IPJd$eDRRz_V+
ze@fg$>(R;HQQ=i=n^qLY!U8$CXcCjMs1UgIaA7
z(>mHGygg>$_8lw=pO8D;hr~u2vAO?uQU%Da70`Ab`}4wOcRD`(79$g8rEH74ri<0g
zc=h_}3n!LLG+14as&5WqUehbCf}I2H_wf(9oO2=t=`m`|Fjgjs5>eQCek?J9k_4R@
zAMXcmH#p~X(Va8mp2jp{Xc>|Wg3=~PWtzi&Pl_7_aC((X)zrYoXYB@0o`YYaWMW~k
zv%c>H5A&Q~-~AUsbVxa$T%++!8*Wze+TMgbsG|OjpMmjka9%61J})X2!yKHik2WTp
nGm1&nqy3c};6Gspf3NxLILzO*{O_;jhMyC|x7z=Z0meT8;mw#`
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()