From bc6a7fa6c6cf85064c5c4e8e2129d635efe8a442 Mon Sep 17 00:00:00 2001 From: LittleStone <1526631427@qq.com> Date: Wed, 30 Oct 2024 17:11:23 +0800 Subject: [PATCH 1/9] =?UTF-8?q?fix:=E5=A2=9E=E5=8A=A0ut=E5=B9=B6=E9=80=82?= =?UTF-8?q?=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- irc_dlient.py | 42 ++++++------- test_irc_client.py | 154 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+), 22 deletions(-) create mode 100644 test_irc_client.py diff --git a/irc_dlient.py b/irc_dlient.py index 28c9f3e..f54ed58 100755 --- a/irc_dlient.py +++ b/irc_dlient.py @@ -39,7 +39,6 @@ def __init__(self): class MyIRCClient: - def __init__(self, server, port, nickname, password): self.irc_react = irc.client.Reactor() self.server = self.irc_react.server() @@ -705,7 +704,7 @@ def get_mp_settings(self, connection, event): class Beatmap: - def __init__(self): + def __init__(self, client_id, client_secret): self.osu_client_id = client_id self.osu_client_secret = client_secret self.osu_token = "" @@ -783,7 +782,7 @@ def get_beatmap_info(self): )['beatmapset']['artist_unicode'] self.beatmap_star = response.json()['difficulty_rating'] self.beatmap_status = response.json()['status'] - self.beatemap_bpm = response.json()['bpm'] + self.beatmap_bpm = response.json()['bpm'] self.beatmap_cs = response.json()['cs'] self.beatmap_ar = response.json()['ar'] self.beatmap_od = response.json()['accuracy'] @@ -805,7 +804,7 @@ def get_beatmap_info(self): self.beatmap_artist = "" self.beatmap_star = 0 self.beatmap_status = "" - self.beatemap_bpm = "" + self.beatmap_bpm = "" self.beatmap_cs = "" self.beatmap_ar = "" self.beatmap_od = "" @@ -1215,25 +1214,24 @@ def calculate_pp_obj(self, mods, acc, misses, combo): return f'now:{self.currpp}pp| if FC({self.maxbeatmapcombo}x):{self.fcpp}pp| 95%:{self.fc95pp}pp| 96%:{self.fc96pp}pp| 97%:{self.fc97pp}pp| 98%:{self.fc98pp}pp| 99%:{self.fc99pp}pp| SS:{self.maxpp}pp| aim:{self.curraimpp}/{self.maxaimpp}pp| speed:{self.currspeedpp}/{self.maxspeedpp}pp| acc:{self.curraccpp}/{self.maxaccpp}pp' -# 没有maps文件夹时自动创建maps文件夹 -maps_dir = os.path.join(os.getcwd(), './maps') -if not os.path.exists(maps_dir): - os.makedirs(maps_dir) - print(f"'{maps_dir}'文件夹不存在,已经自动创建") - -config = Config() - -client_id = config.osuclientid -client_secret = config.osuclientsecret +if __name__ == '__main__': + config = Config() + # 没有maps文件夹时自动创建maps文件夹 + maps_dir = os.path.join(os.getcwd(), './maps') + if not os.path.exists(maps_dir): + os.makedirs(maps_dir) + print(f"'{maps_dir}'文件夹不存在,已经自动创建") -osu_nickname = config.osunickname -osu_password = config.osupassword + client_id = config.osuclientid + client_secret = config.osuclientsecret + osu_nickname = config.osunickname + osu_password = config.osupassword -p = Player() -r = Room() -b = Beatmap() -pp = PP() + p = Player() + r = Room() + b = Beatmap(client_id, client_secret) + pp = PP() -client = MyIRCClient(osu_server, osu_port, osu_nickname, osu_password) -client.start() + client = MyIRCClient(osu_server, osu_port, osu_nickname, osu_password) + client.start() diff --git a/test_irc_client.py b/test_irc_client.py new file mode 100644 index 0000000..750f8e1 --- /dev/null +++ b/test_irc_client.py @@ -0,0 +1,154 @@ +import unittest +from unittest.mock import patch, MagicMock +import irc_dlient + +class TestConfig(unittest.TestCase): + @patch('irc_dlient.configparser.ConfigParser') + @patch('irc_dlient.chardet.detect') + @patch('builtins.open') + def test_config_initialization(self, mock_open, mock_detect, mock_configparser): + # 设置模拟返回值 + mock_detect.return_value = {'encoding': 'utf-8'} + mock_config = MagicMock() + mock_config.__getitem__.return_value = { + 'client_id': 'test_id', + 'client_secret': 'test_secret', + 'nickname': 'test_nick', + 'password': 'test_pass', + 'mpname': 'test_mp', + 'starlimit': '5.0', + 'timelimit': '300', + 'mppassword': 'mp_pass' + } + mock_configparser.return_value = mock_config + + config = irc_dlient.Config() + + self.assertEqual(config.osuclientid, 'test_id') + self.assertEqual(config.osuclientsecret, 'test_secret') + self.assertEqual(config.osunickname, 'test_nick') + self.assertEqual(config.osupassword, 'test_pass') + self.assertEqual(config.mpname, 'test_mp') + self.assertEqual(config.starlimit, '5.0') + self.assertEqual(config.timelimit, '300') + self.assertEqual(config.mppassword, 'mp_pass') + +class TestPlayer(unittest.TestCase): + def setUp(self): + self.player = irc_dlient.Player() + + def test_add_player(self): + self.player.add_player("Player1") + self.assertIn("Player1", self.player.player_list) + + def test_remove_player(self): + self.player.add_player("Player1") + self.player.remove_player("Player1") + self.assertNotIn("Player1", self.player.player_list) + + def test_add_host(self): + self.player.add_host("Host1") + self.assertIn("Host1", self.player.room_host_list) + + def test_remove_host(self): + self.player.add_host("Host1") + self.player.remove_host("Host1") + self.assertNotIn("Host1", self.player.room_host_list) + +class TestBeatmap(unittest.TestCase): + def setUp(self): + # 从 config.ini 中读取 client_id 和 client_secret + config = irc_dlient.Config() + self.client_id = config.osuclientid + self.client_secret = config.osuclientsecret + + # 实例化 Beatmap 对象并设置 client_id 和 client_secret + self.beatmap = irc_dlient.Beatmap(self.client_id, self.client_secret) + + @patch('irc_dlient.requests.post') + def test_get_token_success(self, mock_post): + mock_response = MagicMock() + mock_response.raise_for_status = MagicMock() + mock_response.json.return_value = {'access_token': 'test_token'} + mock_post.return_value = mock_response + + self.beatmap.get_token() + + self.assertEqual(self.beatmap.osu_token, 'test_token') + + def test_get_beatmap_info_success(self): + """ + 这个测试用例将发送真实的 HTTP 请求到 osu! API + 并验证返回的真实响应是否符合预期。 + """ + # 获取 Token + self.beatmap.get_token() + self.assertIsNotNone(self.beatmap.osu_token, "Token 获取失败") + + # 设置 beatmap_id + self.beatmap.beatmap_id = '75' # osu第一个ranked图 + + # 调用获取 beatmap 信息的方法 + self.beatmap.get_beatmap_info() + + # 验证响应内容 + self.assertEqual(self.beatmap.beatmap_name, 'DISCO PRINCE') + self.assertEqual(self.beatmap.beatmap_songs_id, '1') + self.assertEqual(self.beatmap.beatmap_artist, 'Kenji Ninuma') + self.assertEqual(self.beatmap.beatmap_star, 2.55) + self.assertEqual(self.beatmap.beatmap_status, 'ranked') + self.assertEqual(self.beatmap.beatmap_bpm, 120) + self.assertEqual(self.beatmap.beatmap_cs, 4) + self.assertEqual(self.beatmap.beatmap_ar, 6) + self.assertEqual(self.beatmap.beatmap_od, 6) + self.assertEqual(self.beatmap.beatmap_hp, 6) + self.assertEqual(self.beatmap.beatmap_length, 142) + self.assertEqual(self.beatmap.beatmap_ranked_date, '2007-10-06') + self.assertEqual(self.beatmap.beatmap_osudirect_url, 'https://osu.ppy.sh/beatmaps/75') + self.assertEqual(self.beatmap.beatmap_mirror_sayo_url, 'https://osu.sayobot.cn/home?search=1') + self.assertEqual(self.beatmap.beatmap_mirror_inso_url, 'http://inso.link/yukiho/?b=75') + +class TestPP(unittest.TestCase): + @patch('irc_dlient.os.path.exists') + @patch('irc_dlient.requests.get') + def test_get_beatmap_file_exists(self, mock_get, mock_exists): + mock_exists.return_value = True + pp = irc_dlient.PP() + pp.get_beatmap_file('12345') + mock_get.assert_not_called() + + @patch('irc_dlient.os.path.exists') + @patch('irc_dlient.requests.get') + def test_get_beatmap_file_download(self, mock_get, mock_exists): + mock_exists.return_value = False + mock_response = MagicMock() + mock_response.raise_for_status = MagicMock() + mock_response.content = b'osu file content' + mock_get.return_value = mock_response + + pp = irc_dlient.PP() + pp.get_beatmap_file('12345') + + mock_get.assert_called_with('https://osu.ppy.sh/osu/12345') + # 可以进一步检查文件写入,但需要更多的patching + +class TestRoom(unittest.TestCase): + @patch('builtins.open') + def test_save_last_room_id(self, mock_open): + mock_file = MagicMock() + mock_open.return_value.__enter__.return_value = mock_file + + room = irc_dlient.Room() + room.room_id = '#room123' + room.save_last_room_id() + + mock_file.write.assert_called_with('#room123') + + @patch('builtins.open', side_effect=FileNotFoundError) + def test_get_last_room_id_file_not_found(self, mock_open): + room = irc_dlient.Room() + last_id = room.get_last_room_id() + self.assertEqual(last_id, '') + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From 6b899e0a9b35ebdfffe561741f5b90d861429e9e Mon Sep 17 00:00:00 2001 From: LittleStone <1526631427@qq.com> Date: Wed, 30 Oct 2024 17:24:07 +0800 Subject: [PATCH 2/9] =?UTF-8?q?fix:=E5=A2=9E=E5=8A=A0README=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a893a41..2207822 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,10 @@ ```python pip install -r requirements.txt ``` - +执行单元测试 +```bash +python3 -m unittest -v test_irc_client.py +``` 运行程序 ```bash python3 irc_dlient.py From 64c194a3f027d04fa223b5ae428f85637977b561 Mon Sep 17 00:00:00 2001 From: LittleStone <1526631427@qq.com> Date: Tue, 5 Nov 2024 17:24:10 +0800 Subject: [PATCH 3/9] =?UTF-8?q?fix:=E5=A2=9E=E5=8A=A0beatmap=20ut?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .coveragerc | 13 ++ README.md | 9 +- irc_dlient.py | 12 +- tests/__init__.py | 0 tests/test_irc_client.py | 377 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 399 insertions(+), 12 deletions(-) create mode 100644 .coveragerc create mode 100644 tests/__init__.py create mode 100644 tests/test_irc_client.py diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..1c48378 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,13 @@ +# .coveragerc +[run] +source = irc_dlient +omit = + tests/* + +[report] +exclude_lines = + pragma: no cover + if __name__ == "__main__": + +[html] +directory = htmlcov diff --git a/README.md b/README.md index 2207822..7daaf1b 100644 --- a/README.md +++ b/README.md @@ -9,14 +9,15 @@ ```python pip install -r requirements.txt ``` -执行单元测试 -```bash -python3 -m unittest -v test_irc_client.py -``` 运行程序 ```bash python3 irc_dlient.py ``` +执行单元测试 +```bash +pip install coverage +coverage run -m unittest discover -s tests +``` # 配置 diff --git a/irc_dlient.py b/irc_dlient.py index f54ed58..c9f1d9f 100755 --- a/irc_dlient.py +++ b/irc_dlient.py @@ -19,8 +19,6 @@ osu_server = "irc.ppy.sh" osu_port = 6667 -# 定义IRC客户端类 - class Config: def __init__(self): @@ -37,7 +35,7 @@ def __init__(self): self.timelimit = self.config['OSU']['timelimit'] self.mppassword = self.config['OSU']['mppassword'] - +# 定义IRC客户端类 class MyIRCClient: def __init__(self, server, port, nickname, password): self.irc_react = irc.client.Reactor() @@ -701,8 +699,6 @@ def get_mp_settings(self, connection, event): # 定义谱面类 - - class Beatmap: def __init__(self, client_id, client_secret): self.osu_client_id = client_id @@ -778,8 +774,7 @@ def get_beatmap_info(self): self.beatmap_songs_id = str(response.json()['beatmapset_id']) self.beatmap_name = response.json()['beatmapset']['title_unicode'] - self.beatmap_artist = response.json( - )['beatmapset']['artist_unicode'] + self.beatmap_artist = response.json()['beatmapset']['artist_unicode'] self.beatmap_star = response.json()['difficulty_rating'] self.beatmap_status = response.json()['status'] self.beatmap_bpm = response.json()['bpm'] @@ -1214,8 +1209,9 @@ def calculate_pp_obj(self, mods, acc, misses, combo): return f'now:{self.currpp}pp| if FC({self.maxbeatmapcombo}x):{self.fcpp}pp| 95%:{self.fc95pp}pp| 96%:{self.fc96pp}pp| 97%:{self.fc97pp}pp| 98%:{self.fc98pp}pp| 99%:{self.fc99pp}pp| SS:{self.maxpp}pp| aim:{self.curraimpp}/{self.maxaimpp}pp| speed:{self.currspeedpp}/{self.maxspeedpp}pp| acc:{self.curraccpp}/{self.maxaccpp}pp' +config = Config() + if __name__ == '__main__': - config = Config() # 没有maps文件夹时自动创建maps文件夹 maps_dir = os.path.join(os.getcwd(), './maps') if not os.path.exists(maps_dir): diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_irc_client.py b/tests/test_irc_client.py new file mode 100644 index 0000000..d52733b --- /dev/null +++ b/tests/test_irc_client.py @@ -0,0 +1,377 @@ +import unittest +from unittest.mock import patch, MagicMock +from io import StringIO +import irc_dlient + +class TestConfig(unittest.TestCase): + @patch('irc_dlient.configparser.ConfigParser') + @patch('irc_dlient.chardet.detect') + @patch('builtins.open') + def test_config_initialization(self, mock_open, mock_detect, mock_configparser): + # 设置模拟返回值 + mock_detect.return_value = {'encoding': 'utf-8'} + mock_config = MagicMock() + mock_config.__getitem__.return_value = { + 'client_id': 'test_id', + 'client_secret': 'test_secret', + 'nickname': 'test_nick', + 'password': 'test_pass', + 'mpname': 'test_mp', + 'starlimit': '5.0', + 'timelimit': '300', + 'mppassword': 'mp_pass' + } + mock_configparser.return_value = mock_config + + config = irc_dlient.Config() + + self.assertEqual(config.osuclientid, 'test_id') + self.assertEqual(config.osuclientsecret, 'test_secret') + self.assertEqual(config.osunickname, 'test_nick') + self.assertEqual(config.osupassword, 'test_pass') + self.assertEqual(config.mpname, 'test_mp') + self.assertEqual(config.starlimit, '5.0') + self.assertEqual(config.timelimit, '300') + self.assertEqual(config.mppassword, 'mp_pass') + +class TestPlayer(unittest.TestCase): + def setUp(self): + self.player = irc_dlient.Player() + + def test_add_player(self): + self.player.add_player("Player1") + self.assertIn("Player1", self.player.player_list) + + def test_remove_player(self): + self.player.add_player("Player1") + self.player.remove_player("Player1") + self.assertNotIn("Player1", self.player.player_list) + + def test_add_host(self): + self.player.add_host("Host1") + self.assertIn("Host1", self.player.room_host_list) + + def test_remove_host(self): + self.player.add_host("Host1") + self.player.remove_host("Host1") + self.assertNotIn("Host1", self.player.room_host_list) + +class TestBeatmap(unittest.TestCase): + def setUp(self): + # 从 config.ini 中读取 client_id 和 client_secret + config = irc_dlient.Config() + self.client_id = config.osuclientid + self.client_secret = config.osuclientsecret + + # 实例化 Beatmap 对象并设置 client_id 和 client_secret + self.beatmap = irc_dlient.Beatmap(self.client_id, self.client_secret) + + @patch('irc_dlient.requests.post') + def test_get_token_success(self, mock_post): + """ + 获取 Token 成功 + """ + mock_response = MagicMock() + mock_response.raise_for_status = MagicMock() + mock_response.json.return_value = {'access_token': 'test_token'} + mock_post.return_value = mock_response + + self.beatmap.get_token() + + self.assertEqual(self.beatmap.osu_token, 'test_token') + + @patch('irc_dlient.config') + def test_check_beatmap_if_out_of_time(self, mock_config): + """ + 检查谱面时间限制 + """ + mock_config.timelimit = 0 + self.beatmap.beatmap_length = 100 + self.beatmap.check_beatmap_if_out_of_time() + self.assertEqual(self.beatmap.check_beatmap_if_out_of_time(), False) + + self.beatmap.beatmap_length = 95950 + self.beatmap.check_beatmap_if_out_of_time() + self.assertEqual(self.beatmap.check_beatmap_if_out_of_time(), False) + + mock_config.timelimit = 100 + self.beatmap.beatmap_length = 100 + self.beatmap.check_beatmap_if_out_of_time() + self.assertEqual(self.beatmap.check_beatmap_if_out_of_time(), False) + + self.beatmap.beatmap_length = 101 + self.beatmap.check_beatmap_if_out_of_time() + self.assertEqual(self.beatmap.check_beatmap_if_out_of_time(), True) + + def test_get_beatmap_info_success(self): + """ + 发送正确的beatmap id到 osu! API + """ + # 获取 Token + self.beatmap.get_token() + self.assertIsNotNone(self.beatmap.osu_token, "Token 获取失败") + + # 设置 beatmap_id + self.beatmap.change_beatmap_id('75') # osu第一个ranked图 + + # 调用获取 beatmap 信息的方法 + self.beatmap.get_beatmap_info() + + # 验证响应内容 + self.assertEqual(self.beatmap.beatmap_name, 'DISCO PRINCE') + self.assertEqual(self.beatmap.beatmap_songs_id, '1') + self.assertEqual(self.beatmap.beatmap_artist, 'Kenji Ninuma') + self.assertEqual(self.beatmap.beatmap_star, 2.55) + self.assertEqual(self.beatmap.beatmap_status, 'ranked') + self.assertEqual(self.beatmap.beatmap_bpm, 120) + self.assertEqual(self.beatmap.beatmap_cs, 4) + self.assertEqual(self.beatmap.beatmap_ar, 6) + self.assertEqual(self.beatmap.beatmap_od, 6) + self.assertEqual(self.beatmap.beatmap_hp, 6) + self.assertEqual(self.beatmap.beatmap_length, 142) + self.assertEqual(self.beatmap.beatmap_ranked_date, '2007-10-06') + self.assertEqual(self.beatmap.beatmap_osudirect_url, 'https://osu.ppy.sh/beatmaps/75') + self.assertEqual(self.beatmap.beatmap_mirror_sayo_url, 'https://osu.sayobot.cn/home?search=1') + self.assertEqual(self.beatmap.beatmap_mirror_inso_url, 'http://inso.link/yukiho/?b=75') + + def test_get_beatmap_info_with_wrong_beatmap_id(self): + """ + 发送错误的beatmap id到 osu! API + """ + # 获取 Token + self.beatmap.get_token() + self.assertIsNotNone(self.beatmap.osu_token, "Token 获取失败") + + # 设置 beatmap_id + self.beatmap.change_beatmap_id('1') # 错误的beatmap id + + # 调用获取 beatmap 信息的方法 + self.beatmap.get_beatmap_info() + + # 验证响应内容 + self.assertEqual(self.beatmap.beatmap_name, '获取谱面信息失败') + self.assertEqual(self.beatmap.beatmap_songs_id, '') + self.assertEqual(self.beatmap.beatmap_artist, '') + self.assertEqual(self.beatmap.beatmap_star, 0) + self.assertEqual(self.beatmap.beatmap_status, '') + self.assertEqual(self.beatmap.beatmap_bpm, '') + self.assertEqual(self.beatmap.beatmap_cs, '') + self.assertEqual(self.beatmap.beatmap_ar, '') + self.assertEqual(self.beatmap.beatmap_od, '') + self.assertEqual(self.beatmap.beatmap_hp, '') + self.assertEqual(self.beatmap.beatmap_length, 0) + self.assertEqual(self.beatmap.beatmap_ranked_date, '') + self.assertEqual(self.beatmap.beatmap_osudirect_url, '') + self.assertEqual(self.beatmap.beatmap_mirror_sayo_url, '') + self.assertEqual(self.beatmap.beatmap_mirror_inso_url, '') + + def test_get_beatmap_score_success(self): + """ + 发送正确的username查询分数 + """ + # 获取 Token + self.beatmap.get_token() + self.assertIsNotNone(self.beatmap.osu_token, "Token 获取失败") + + # 设置 username + self.beatmap.get_user_id("LittleStone") + self.assertIsNotNone(self.beatmap.id2name, "用户ID获取失败") + + # 设置 beatmap_id + self.beatmap.beatmap_id = '75' # osu第一个ranked图 + + # 调用获取 beatmap 信息的方法 + self.beatmap.get_beatmap_score("LittleStone") + + self.assertEqual(self.beatmap.pr_title, '') + self.assertEqual(self.beatmap.pr_artist, '') + self.assertEqual(self.beatmap.pr_star, 0) + self.assertEqual(self.beatmap.beatmap_score_created_at, '2020-12-04') + self.assertEqual(self.beatmap.pr_acc, 83.85) + self.assertEqual(self.beatmap.pr_maxcombo, 122) + self.assertEqual(self.beatmap.pr_300, 150) + self.assertEqual(self.beatmap.pr_100, 35) + self.assertEqual(self.beatmap.pr_50, 6) + self.assertEqual(self.beatmap.pr_miss, 3) + self.assertEqual(self.beatmap.pr_pp, 28.0404) + self.assertEqual(self.beatmap.pr_rank, 'C') + self.assertEqual(self.beatmap.pr_mods, 'HDHRDT') + self.assertEqual(self.beatmap.pr_beatmap_url, 'https://osu.ppy.sh/beatmaps/75') + self.assertEqual(self.beatmap.pr_username, 'LittleStone') + self.assertEqual(self.beatmap.pr_acc, 83.85) + + def test_get_beatmap_score_with_wrong_username_1(self): + """ + 发送错误的username查询分数-场景1 + """ + # 获取 Token + self.beatmap.get_token() + self.assertIsNotNone(self.beatmap.osu_token, "Token 获取失败") + + # 设置错误的 username,并捕获输出 + with patch('sys.stdout', new=StringIO()) as fake_out: + self.beatmap.get_user_id("PPYNOTPPYGUESSIMPPYORNOT") + self.assertIn("获取用户ID失败", fake_out.getvalue(), "输出不包含预期的字符串") + + def test_get_beatmap_score_with_wrong_username_2(self): + """ + 发送错误的username查询分数-场景2 + """ + # 获取 Token + self.beatmap.get_token() + self.assertIsNotNone(self.beatmap.osu_token, "Token 获取失败") + + # 设置 username + self.beatmap.get_user_id("LittleStone") + self.assertIsNotNone(self.beatmap.id2name, "用户ID获取失败") + + # 设置 beatmap_id + self.beatmap.beatmap_id = '75' # osu第一个ranked图 + + # 设置错误的 username,并捕获输出 + with patch('sys.stdout', new=StringIO()) as fake_out: + self.beatmap.get_beatmap_score("ATRI1024") + self.assertIn("获取谱面成绩失败,错误信息:'ATRI1024'\n| [ 获取谱面成绩失败 - ]| *| [ ] 0pp acc:0% combo:0x| 0/0/0/0| date:|", + fake_out.getvalue(), "输出不包含预期的字符串") + + def test_get_beatmap_score_with_wrong_beatmap_id(self): + """ + 发送错误的beatmap id查询分数 + """ + # 获取 Token + self.beatmap.get_token() + self.assertIsNotNone(self.beatmap.osu_token, "Token 获取失败") + + # 设置 username + self.beatmap.get_user_id("LittleStone") + self.assertIsNotNone(self.beatmap.id2name, "用户ID获取失败") + + # 设置 beatmap_id + self.beatmap.beatmap_id = '1145141919810' # 不存在的图 + + # 设置错误的 username,并捕获输出 + with patch('sys.stdout', new=StringIO()) as fake_out: + self.beatmap.get_beatmap_score("LittleStone") + self.assertIn("未查询到LittleStone在该谱面上留下的成绩\n", fake_out.getvalue(), "输出不包含预期的字符串") + + @patch('irc_dlient.requests.get') + def test_get_recent_info_success(self, mock_get): + """ + 发送正确的username查询最近成绩,并模拟返回数据 + """ + # 获取 Token + self.beatmap.get_token() + self.assertIsNotNone(self.beatmap.osu_token, "Token 获取失败") + + # 设置 username + self.beatmap.get_user_id("LittleStone") + self.beatmap.id2name = {"LittleStone": "123456"} + + mock_response = MagicMock() + mock_response.raise_for_status = MagicMock() + mock_response.json.return_value = [ + { + "beatmap": { + "id": 123456, + "difficulty_rating": 5.25, + "url": "https://osu.ppy.sh/beatmapsets/123456#osu/654321" + }, + "beatmapset": { + "title_unicode": "示例歌曲", + "artist_unicode": "示例艺术家" + }, + "accuracy": 0.98, + "max_combo": 1000, + "statistics": { + "count_300": 950, + "count_100": 30, + "count_50": 10, + "count_miss": 5 + }, + "pp": 250.75, + "rank": "S", + "mods": ["HD", "HR"] + } + ] + mock_get.return_value = mock_response + + self.beatmap.get_recent_info("LittleStone") + + self.assertEqual(self.beatmap.pr_beatmap_id, 123456) + self.assertEqual(self.beatmap.pr_title, '示例歌曲') + self.assertEqual(self.beatmap.pr_artist, '示例艺术家') + self.assertEqual(self.beatmap.pr_star, 5.25) + self.assertEqual(self.beatmap.pr_acc, 98) + self.assertEqual(self.beatmap.pr_maxcombo, 1000) + self.assertEqual(self.beatmap.pr_300, 950) + self.assertEqual(self.beatmap.pr_100, 30) + self.assertEqual(self.beatmap.pr_50, 10) + self.assertEqual(self.beatmap.pr_miss, 5) + self.assertEqual(self.beatmap.pr_pp, 250.75) + self.assertEqual(self.beatmap.pr_rank, 'S') + self.assertEqual(self.beatmap.pr_mods, 'HDHR') + self.assertEqual(self.beatmap.pr_beatmap_url, 'https://osu.ppy.sh/beatmapsets/123456#osu/654321') + self.assertEqual(self.beatmap.pr_username, 'LittleStone') + + def test_get_recent_info_with_wrong_username(self): + """ + 发送错误的username查询最近成绩 + """ + # 获取 Token + self.beatmap.get_token() + self.assertIsNotNone(self.beatmap.osu_token, "Token 获取失败") + + # 设置 username + self.beatmap.get_user_id("LittleStone") + self.beatmap.id2name = {"LittleStone": "123456"} + + # 调用获取最近成绩的方法 + with patch('sys.stdout', new=StringIO()) as fake_out: + self.beatmap.get_recent_info("ATRI1024") + self.assertIn("获取最近成绩失败,错误信息:'ATRI1024'\n| [ 获取最近成绩失败 - ]| *| [ ] 0pp acc:0% combo:0x| 0/0/0/0|\n", + fake_out.getvalue(), "输出不包含预期的字符串") + +class TestPP(unittest.TestCase): + @patch('irc_dlient.os.path.exists') + @patch('irc_dlient.requests.get') + def test_get_beatmap_file_exists(self, mock_get, mock_exists): + mock_exists.return_value = True + pp = irc_dlient.PP() + pp.get_beatmap_file('12345') + mock_get.assert_not_called() + + @patch('irc_dlient.os.path.exists') + @patch('irc_dlient.requests.get') + def test_get_beatmap_file_download(self, mock_get, mock_exists): + mock_exists.return_value = False + mock_response = MagicMock() + mock_response.raise_for_status = MagicMock() + mock_response.content = b'osu file content' + mock_get.return_value = mock_response + + pp = irc_dlient.PP() + pp.get_beatmap_file('12345') + + mock_get.assert_called_with('https://osu.ppy.sh/osu/12345') + # 可以进一步检查文件写入,但需要更多的patching + +class TestRoom(unittest.TestCase): + @patch('builtins.open') + def test_save_last_room_id(self, mock_open): + mock_file = MagicMock() + mock_open.return_value.__enter__.return_value = mock_file + + room = irc_dlient.Room() + room.room_id = '#room123' + room.save_last_room_id() + + mock_file.write.assert_called_with('#room123') + + @patch('builtins.open', side_effect=FileNotFoundError) + def test_get_last_room_id_file_not_found(self, mock_open): + room = irc_dlient.Room() + last_id = room.get_last_room_id() + self.assertEqual(last_id, '') + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From 40f6719f1a2cd797f792301e35ae5a54a6c3f8e6 Mon Sep 17 00:00:00 2001 From: LittleStone <1526631427@qq.com> Date: Tue, 5 Nov 2024 17:28:42 +0800 Subject: [PATCH 4/9] =?UTF-8?q?fix:=E4=B8=8A=E4=BC=A0=E8=A6=86=E7=9B=96?= =?UTF-8?q?=E7=8E=87HTML=E6=96=87=E4=BB=B6=E5=A4=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- htmlcov/.gitignore | 2 + htmlcov/class_index.html | 163 ++++ htmlcov/coverage_html_cb_497bf287.js | 733 ++++++++++++++ htmlcov/favicon_32_cb_58284776.png | Bin 0 -> 1732 bytes htmlcov/function_index.html | 643 +++++++++++++ htmlcov/index.html | 111 +++ htmlcov/irc_dlient_py.html | 1330 ++++++++++++++++++++++++++ htmlcov/keybd_closed_cb_ce680311.png | Bin 0 -> 9004 bytes htmlcov/status.json | 1 + htmlcov/style_cb_718ce007.css | 337 +++++++ test_irc_client.py | 154 --- 11 files changed, 3320 insertions(+), 154 deletions(-) create mode 100644 htmlcov/.gitignore create mode 100644 htmlcov/class_index.html create mode 100644 htmlcov/coverage_html_cb_497bf287.js create mode 100644 htmlcov/favicon_32_cb_58284776.png create mode 100644 htmlcov/function_index.html create mode 100644 htmlcov/index.html create mode 100644 htmlcov/irc_dlient_py.html create mode 100644 htmlcov/keybd_closed_cb_ce680311.png create mode 100644 htmlcov/status.json create mode 100644 htmlcov/style_cb_718ce007.css delete mode 100644 test_irc_client.py diff --git a/htmlcov/.gitignore b/htmlcov/.gitignore new file mode 100644 index 0000000..ccccf14 --- /dev/null +++ b/htmlcov/.gitignore @@ -0,0 +1,2 @@ +# Created by coverage.py +* diff --git a/htmlcov/class_index.html b/htmlcov/class_index.html new file mode 100644 index 0000000..bcce43d --- /dev/null +++ b/htmlcov/class_index.html @@ -0,0 +1,163 @@ + + + + + Coverage report + + + + + +
+
+

Coverage report: + 38% +

+ +
+ +
+ + +
+
+

+ Files + Functions + Classes +

+

+ coverage.py v7.6.4, + created at 2024-11-05 17:25 +0800 +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Fileclassstatementsmissingexcludedcoverage
irc_dlient.pyConfig1200100%
irc_dlient.pyMyIRCClient28528500%
irc_dlient.pyPlayer9074018%
irc_dlient.pyRoom4432027%
irc_dlient.pyBeatmap20723089%
irc_dlient.pyPP162127022%
irc_dlient.py(no class)10114086%
Total 901555038%
+

+ No items found using the specified filter. +

+
+ + + diff --git a/htmlcov/coverage_html_cb_497bf287.js b/htmlcov/coverage_html_cb_497bf287.js new file mode 100644 index 0000000..1face13 --- /dev/null +++ b/htmlcov/coverage_html_cb_497bf287.js @@ -0,0 +1,733 @@ +// Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +// For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +// Coverage.py HTML report browser code. +/*jslint browser: true, sloppy: true, vars: true, plusplus: true, maxerr: 50, indent: 4 */ +/*global coverage: true, document, window, $ */ + +coverage = {}; + +// General helpers +function debounce(callback, wait) { + let timeoutId = null; + return function(...args) { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + callback.apply(this, args); + }, wait); + }; +}; + +function checkVisible(element) { + const rect = element.getBoundingClientRect(); + const viewBottom = Math.max(document.documentElement.clientHeight, window.innerHeight); + const viewTop = 30; + return !(rect.bottom < viewTop || rect.top >= viewBottom); +} + +function on_click(sel, fn) { + const elt = document.querySelector(sel); + if (elt) { + elt.addEventListener("click", fn); + } +} + +// Helpers for table sorting +function getCellValue(row, column = 0) { + const cell = row.cells[column] // nosemgrep: eslint.detect-object-injection + if (cell.childElementCount == 1) { + var child = cell.firstElementChild; + if (child.tagName === "A") { + child = child.firstElementChild; + } + if (child instanceof HTMLDataElement && child.value) { + return child.value; + } + } + return cell.innerText || cell.textContent; +} + +function rowComparator(rowA, rowB, column = 0) { + let valueA = getCellValue(rowA, column); + let valueB = getCellValue(rowB, column); + if (!isNaN(valueA) && !isNaN(valueB)) { + return valueA - valueB; + } + return valueA.localeCompare(valueB, undefined, {numeric: true}); +} + +function sortColumn(th) { + // Get the current sorting direction of the selected header, + // clear state on other headers and then set the new sorting direction. + const currentSortOrder = th.getAttribute("aria-sort"); + [...th.parentElement.cells].forEach(header => header.setAttribute("aria-sort", "none")); + var direction; + if (currentSortOrder === "none") { + direction = th.dataset.defaultSortOrder || "ascending"; + } + else if (currentSortOrder === "ascending") { + direction = "descending"; + } + else { + direction = "ascending"; + } + th.setAttribute("aria-sort", direction); + + const column = [...th.parentElement.cells].indexOf(th) + + // Sort all rows and afterwards append them in order to move them in the DOM. + Array.from(th.closest("table").querySelectorAll("tbody tr")) + .sort((rowA, rowB) => rowComparator(rowA, rowB, column) * (direction === "ascending" ? 1 : -1)) + .forEach(tr => tr.parentElement.appendChild(tr)); + + // Save the sort order for next time. + if (th.id !== "region") { + let th_id = "file"; // Sort by file if we don't have a column id + let current_direction = direction; + const stored_list = localStorage.getItem(coverage.INDEX_SORT_STORAGE); + if (stored_list) { + ({th_id, direction} = JSON.parse(stored_list)) + } + localStorage.setItem(coverage.INDEX_SORT_STORAGE, JSON.stringify({ + "th_id": th.id, + "direction": current_direction + })); + if (th.id !== th_id || document.getElementById("region")) { + // Sort column has changed, unset sorting by function or class. + localStorage.setItem(coverage.SORTED_BY_REGION, JSON.stringify({ + "by_region": false, + "region_direction": current_direction + })); + } + } + else { + // Sort column has changed to by function or class, remember that. + localStorage.setItem(coverage.SORTED_BY_REGION, JSON.stringify({ + "by_region": true, + "region_direction": direction + })); + } +} + +// Find all the elements with data-shortcut attribute, and use them to assign a shortcut key. +coverage.assign_shortkeys = function () { + document.querySelectorAll("[data-shortcut]").forEach(element => { + document.addEventListener("keypress", event => { + if (event.target.tagName.toLowerCase() === "input") { + return; // ignore keypress from search filter + } + if (event.key === element.dataset.shortcut) { + element.click(); + } + }); + }); +}; + +// Create the events for the filter box. +coverage.wire_up_filter = function () { + // Populate the filter and hide100 inputs if there are saved values for them. + const saved_filter_value = localStorage.getItem(coverage.FILTER_STORAGE); + if (saved_filter_value) { + document.getElementById("filter").value = saved_filter_value; + } + const saved_hide100_value = localStorage.getItem(coverage.HIDE100_STORAGE); + if (saved_hide100_value) { + document.getElementById("hide100").checked = JSON.parse(saved_hide100_value); + } + + // Cache elements. + const table = document.querySelector("table.index"); + const table_body_rows = table.querySelectorAll("tbody tr"); + const no_rows = document.getElementById("no_rows"); + + // Observe filter keyevents. + const filter_handler = (event => { + // Keep running total of each metric, first index contains number of shown rows + const totals = new Array(table.rows[0].cells.length).fill(0); + // Accumulate the percentage as fraction + totals[totals.length - 1] = { "numer": 0, "denom": 0 }; // nosemgrep: eslint.detect-object-injection + + var text = document.getElementById("filter").value; + // Store filter value + localStorage.setItem(coverage.FILTER_STORAGE, text); + const casefold = (text === text.toLowerCase()); + const hide100 = document.getElementById("hide100").checked; + // Store hide value. + localStorage.setItem(coverage.HIDE100_STORAGE, JSON.stringify(hide100)); + + // Hide / show elements. + table_body_rows.forEach(row => { + var show = false; + // Check the text filter. + for (let column = 0; column < totals.length; column++) { + cell = row.cells[column]; + if (cell.classList.contains("name")) { + var celltext = cell.textContent; + if (casefold) { + celltext = celltext.toLowerCase(); + } + if (celltext.includes(text)) { + show = true; + } + } + } + + // Check the "hide covered" filter. + if (show && hide100) { + const [numer, denom] = row.cells[row.cells.length - 1].dataset.ratio.split(" "); + show = (numer !== denom); + } + + if (!show) { + // hide + row.classList.add("hidden"); + return; + } + + // show + row.classList.remove("hidden"); + totals[0]++; + + for (let column = 0; column < totals.length; column++) { + // Accumulate dynamic totals + cell = row.cells[column] // nosemgrep: eslint.detect-object-injection + if (cell.classList.contains("name")) { + continue; + } + if (column === totals.length - 1) { + // Last column contains percentage + const [numer, denom] = cell.dataset.ratio.split(" "); + totals[column]["numer"] += parseInt(numer, 10); // nosemgrep: eslint.detect-object-injection + totals[column]["denom"] += parseInt(denom, 10); // nosemgrep: eslint.detect-object-injection + } + else { + totals[column] += parseInt(cell.textContent, 10); // nosemgrep: eslint.detect-object-injection + } + } + }); + + // Show placeholder if no rows will be displayed. + if (!totals[0]) { + // Show placeholder, hide table. + no_rows.style.display = "block"; + table.style.display = "none"; + return; + } + + // Hide placeholder, show table. + no_rows.style.display = null; + table.style.display = null; + + const footer = table.tFoot.rows[0]; + // Calculate new dynamic sum values based on visible rows. + for (let column = 0; column < totals.length; column++) { + // Get footer cell element. + const cell = footer.cells[column]; // nosemgrep: eslint.detect-object-injection + if (cell.classList.contains("name")) { + continue; + } + + // Set value into dynamic footer cell element. + if (column === totals.length - 1) { + // Percentage column uses the numerator and denominator, + // and adapts to the number of decimal places. + const match = /\.([0-9]+)/.exec(cell.textContent); + const places = match ? match[1].length : 0; + const { numer, denom } = totals[column]; // nosemgrep: eslint.detect-object-injection + cell.dataset.ratio = `${numer} ${denom}`; + // Check denom to prevent NaN if filtered files contain no statements + cell.textContent = denom + ? `${(numer * 100 / denom).toFixed(places)}%` + : `${(100).toFixed(places)}%`; + } + else { + cell.textContent = totals[column]; // nosemgrep: eslint.detect-object-injection + } + } + }); + + document.getElementById("filter").addEventListener("input", debounce(filter_handler)); + document.getElementById("hide100").addEventListener("input", debounce(filter_handler)); + + // Trigger change event on setup, to force filter on page refresh + // (filter value may still be present). + document.getElementById("filter").dispatchEvent(new Event("input")); + document.getElementById("hide100").dispatchEvent(new Event("input")); +}; +coverage.FILTER_STORAGE = "COVERAGE_FILTER_VALUE"; +coverage.HIDE100_STORAGE = "COVERAGE_HIDE100_VALUE"; + +// Set up the click-to-sort columns. +coverage.wire_up_sorting = function () { + document.querySelectorAll("[data-sortable] th[aria-sort]").forEach( + th => th.addEventListener("click", e => sortColumn(e.target)) + ); + + // Look for a localStorage item containing previous sort settings: + let th_id = "file", direction = "ascending"; + const stored_list = localStorage.getItem(coverage.INDEX_SORT_STORAGE); + if (stored_list) { + ({th_id, direction} = JSON.parse(stored_list)); + } + let by_region = false, region_direction = "ascending"; + const sorted_by_region = localStorage.getItem(coverage.SORTED_BY_REGION); + if (sorted_by_region) { + ({ + by_region, + region_direction + } = JSON.parse(sorted_by_region)); + } + + const region_id = "region"; + if (by_region && document.getElementById(region_id)) { + direction = region_direction; + } + // If we are in a page that has a column with id of "region", sort on + // it if the last sort was by function or class. + let th; + if (document.getElementById(region_id)) { + th = document.getElementById(by_region ? region_id : th_id); + } + else { + th = document.getElementById(th_id); + } + th.setAttribute("aria-sort", direction === "ascending" ? "descending" : "ascending"); + th.click() +}; + +coverage.INDEX_SORT_STORAGE = "COVERAGE_INDEX_SORT_2"; +coverage.SORTED_BY_REGION = "COVERAGE_SORT_REGION"; + +// Loaded on index.html +coverage.index_ready = function () { + coverage.assign_shortkeys(); + coverage.wire_up_filter(); + coverage.wire_up_sorting(); + + on_click(".button_prev_file", coverage.to_prev_file); + on_click(".button_next_file", coverage.to_next_file); + + on_click(".button_show_hide_help", coverage.show_hide_help); +}; + +// -- pyfile stuff -- + +coverage.LINE_FILTERS_STORAGE = "COVERAGE_LINE_FILTERS"; + +coverage.pyfile_ready = function () { + // If we're directed to a particular line number, highlight the line. + var frag = location.hash; + if (frag.length > 2 && frag[1] === "t") { + document.querySelector(frag).closest(".n").classList.add("highlight"); + coverage.set_sel(parseInt(frag.substr(2), 10)); + } + else { + coverage.set_sel(0); + } + + on_click(".button_toggle_run", coverage.toggle_lines); + on_click(".button_toggle_mis", coverage.toggle_lines); + on_click(".button_toggle_exc", coverage.toggle_lines); + on_click(".button_toggle_par", coverage.toggle_lines); + + on_click(".button_next_chunk", coverage.to_next_chunk_nicely); + on_click(".button_prev_chunk", coverage.to_prev_chunk_nicely); + on_click(".button_top_of_page", coverage.to_top); + on_click(".button_first_chunk", coverage.to_first_chunk); + + on_click(".button_prev_file", coverage.to_prev_file); + on_click(".button_next_file", coverage.to_next_file); + on_click(".button_to_index", coverage.to_index); + + on_click(".button_show_hide_help", coverage.show_hide_help); + + coverage.filters = undefined; + try { + coverage.filters = localStorage.getItem(coverage.LINE_FILTERS_STORAGE); + } catch(err) {} + + if (coverage.filters) { + coverage.filters = JSON.parse(coverage.filters); + } + else { + coverage.filters = {run: false, exc: true, mis: true, par: true}; + } + + for (cls in coverage.filters) { + coverage.set_line_visibilty(cls, coverage.filters[cls]); // nosemgrep: eslint.detect-object-injection + } + + coverage.assign_shortkeys(); + coverage.init_scroll_markers(); + coverage.wire_up_sticky_header(); + + document.querySelectorAll("[id^=ctxs]").forEach( + cbox => cbox.addEventListener("click", coverage.expand_contexts) + ); + + // Rebuild scroll markers when the window height changes. + window.addEventListener("resize", coverage.build_scroll_markers); +}; + +coverage.toggle_lines = function (event) { + const btn = event.target.closest("button"); + const category = btn.value + const show = !btn.classList.contains("show_" + category); + coverage.set_line_visibilty(category, show); + coverage.build_scroll_markers(); + coverage.filters[category] = show; + try { + localStorage.setItem(coverage.LINE_FILTERS_STORAGE, JSON.stringify(coverage.filters)); + } catch(err) {} +}; + +coverage.set_line_visibilty = function (category, should_show) { + const cls = "show_" + category; + const btn = document.querySelector(".button_toggle_" + category); + if (btn) { + if (should_show) { + document.querySelectorAll("#source ." + category).forEach(e => e.classList.add(cls)); + btn.classList.add(cls); + } + else { + document.querySelectorAll("#source ." + category).forEach(e => e.classList.remove(cls)); + btn.classList.remove(cls); + } + } +}; + +// Return the nth line div. +coverage.line_elt = function (n) { + return document.getElementById("t" + n)?.closest("p"); +}; + +// Set the selection. b and e are line numbers. +coverage.set_sel = function (b, e) { + // The first line selected. + coverage.sel_begin = b; + // The next line not selected. + coverage.sel_end = (e === undefined) ? b+1 : e; +}; + +coverage.to_top = function () { + coverage.set_sel(0, 1); + coverage.scroll_window(0); +}; + +coverage.to_first_chunk = function () { + coverage.set_sel(0, 1); + coverage.to_next_chunk(); +}; + +coverage.to_prev_file = function () { + window.location = document.getElementById("prevFileLink").href; +} + +coverage.to_next_file = function () { + window.location = document.getElementById("nextFileLink").href; +} + +coverage.to_index = function () { + location.href = document.getElementById("indexLink").href; +} + +coverage.show_hide_help = function () { + const helpCheck = document.getElementById("help_panel_state") + helpCheck.checked = !helpCheck.checked; +} + +// Return a string indicating what kind of chunk this line belongs to, +// or null if not a chunk. +coverage.chunk_indicator = function (line_elt) { + const classes = line_elt?.className; + if (!classes) { + return null; + } + const match = classes.match(/\bshow_\w+\b/); + if (!match) { + return null; + } + return match[0]; +}; + +coverage.to_next_chunk = function () { + const c = coverage; + + // Find the start of the next colored chunk. + var probe = c.sel_end; + var chunk_indicator, probe_line; + while (true) { + probe_line = c.line_elt(probe); + if (!probe_line) { + return; + } + chunk_indicator = c.chunk_indicator(probe_line); + if (chunk_indicator) { + break; + } + probe++; + } + + // There's a next chunk, `probe` points to it. + var begin = probe; + + // Find the end of this chunk. + var next_indicator = chunk_indicator; + while (next_indicator === chunk_indicator) { + probe++; + probe_line = c.line_elt(probe); + next_indicator = c.chunk_indicator(probe_line); + } + c.set_sel(begin, probe); + c.show_selection(); +}; + +coverage.to_prev_chunk = function () { + const c = coverage; + + // Find the end of the prev colored chunk. + var probe = c.sel_begin-1; + var probe_line = c.line_elt(probe); + if (!probe_line) { + return; + } + var chunk_indicator = c.chunk_indicator(probe_line); + while (probe > 1 && !chunk_indicator) { + probe--; + probe_line = c.line_elt(probe); + if (!probe_line) { + return; + } + chunk_indicator = c.chunk_indicator(probe_line); + } + + // There's a prev chunk, `probe` points to its last line. + var end = probe+1; + + // Find the beginning of this chunk. + var prev_indicator = chunk_indicator; + while (prev_indicator === chunk_indicator) { + probe--; + if (probe <= 0) { + return; + } + probe_line = c.line_elt(probe); + prev_indicator = c.chunk_indicator(probe_line); + } + c.set_sel(probe+1, end); + c.show_selection(); +}; + +// Returns 0, 1, or 2: how many of the two ends of the selection are on +// the screen right now? +coverage.selection_ends_on_screen = function () { + if (coverage.sel_begin === 0) { + return 0; + } + + const begin = coverage.line_elt(coverage.sel_begin); + const end = coverage.line_elt(coverage.sel_end-1); + + return ( + (checkVisible(begin) ? 1 : 0) + + (checkVisible(end) ? 1 : 0) + ); +}; + +coverage.to_next_chunk_nicely = function () { + if (coverage.selection_ends_on_screen() === 0) { + // The selection is entirely off the screen: + // Set the top line on the screen as selection. + + // This will select the top-left of the viewport + // As this is most likely the span with the line number we take the parent + const line = document.elementFromPoint(0, 0).parentElement; + if (line.parentElement !== document.getElementById("source")) { + // The element is not a source line but the header or similar + coverage.select_line_or_chunk(1); + } + else { + // We extract the line number from the id + coverage.select_line_or_chunk(parseInt(line.id.substring(1), 10)); + } + } + coverage.to_next_chunk(); +}; + +coverage.to_prev_chunk_nicely = function () { + if (coverage.selection_ends_on_screen() === 0) { + // The selection is entirely off the screen: + // Set the lowest line on the screen as selection. + + // This will select the bottom-left of the viewport + // As this is most likely the span with the line number we take the parent + const line = document.elementFromPoint(document.documentElement.clientHeight-1, 0).parentElement; + if (line.parentElement !== document.getElementById("source")) { + // The element is not a source line but the header or similar + coverage.select_line_or_chunk(coverage.lines_len); + } + else { + // We extract the line number from the id + coverage.select_line_or_chunk(parseInt(line.id.substring(1), 10)); + } + } + coverage.to_prev_chunk(); +}; + +// Select line number lineno, or if it is in a colored chunk, select the +// entire chunk +coverage.select_line_or_chunk = function (lineno) { + var c = coverage; + var probe_line = c.line_elt(lineno); + if (!probe_line) { + return; + } + var the_indicator = c.chunk_indicator(probe_line); + if (the_indicator) { + // The line is in a highlighted chunk. + // Search backward for the first line. + var probe = lineno; + var indicator = the_indicator; + while (probe > 0 && indicator === the_indicator) { + probe--; + probe_line = c.line_elt(probe); + if (!probe_line) { + break; + } + indicator = c.chunk_indicator(probe_line); + } + var begin = probe + 1; + + // Search forward for the last line. + probe = lineno; + indicator = the_indicator; + while (indicator === the_indicator) { + probe++; + probe_line = c.line_elt(probe); + indicator = c.chunk_indicator(probe_line); + } + + coverage.set_sel(begin, probe); + } + else { + coverage.set_sel(lineno); + } +}; + +coverage.show_selection = function () { + // Highlight the lines in the chunk + document.querySelectorAll("#source .highlight").forEach(e => e.classList.remove("highlight")); + for (let probe = coverage.sel_begin; probe < coverage.sel_end; probe++) { + coverage.line_elt(probe).querySelector(".n").classList.add("highlight"); + } + + coverage.scroll_to_selection(); +}; + +coverage.scroll_to_selection = function () { + // Scroll the page if the chunk isn't fully visible. + if (coverage.selection_ends_on_screen() < 2) { + const element = coverage.line_elt(coverage.sel_begin); + coverage.scroll_window(element.offsetTop - 60); + } +}; + +coverage.scroll_window = function (to_pos) { + window.scroll({top: to_pos, behavior: "smooth"}); +}; + +coverage.init_scroll_markers = function () { + // Init some variables + coverage.lines_len = document.querySelectorAll("#source > p").length; + + // Build html + coverage.build_scroll_markers(); +}; + +coverage.build_scroll_markers = function () { + const temp_scroll_marker = document.getElementById("scroll_marker") + if (temp_scroll_marker) temp_scroll_marker.remove(); + // Don't build markers if the window has no scroll bar. + if (document.body.scrollHeight <= window.innerHeight) { + return; + } + + const marker_scale = window.innerHeight / document.body.scrollHeight; + const line_height = Math.min(Math.max(3, window.innerHeight / coverage.lines_len), 10); + + let previous_line = -99, last_mark, last_top; + + const scroll_marker = document.createElement("div"); + scroll_marker.id = "scroll_marker"; + document.getElementById("source").querySelectorAll( + "p.show_run, p.show_mis, p.show_exc, p.show_exc, p.show_par" + ).forEach(element => { + const line_top = Math.floor(element.offsetTop * marker_scale); + const line_number = parseInt(element.querySelector(".n a").id.substr(1)); + + if (line_number === previous_line + 1) { + // If this solid missed block just make previous mark higher. + last_mark.style.height = `${line_top + line_height - last_top}px`; + } + else { + // Add colored line in scroll_marker block. + last_mark = document.createElement("div"); + last_mark.id = `m${line_number}`; + last_mark.classList.add("marker"); + last_mark.style.height = `${line_height}px`; + last_mark.style.top = `${line_top}px`; + scroll_marker.append(last_mark); + last_top = line_top; + } + + previous_line = line_number; + }); + + // Append last to prevent layout calculation + document.body.append(scroll_marker); +}; + +coverage.wire_up_sticky_header = function () { + const header = document.querySelector("header"); + const header_bottom = ( + header.querySelector(".content h2").getBoundingClientRect().top - + header.getBoundingClientRect().top + ); + + function updateHeader() { + if (window.scrollY > header_bottom) { + header.classList.add("sticky"); + } + else { + header.classList.remove("sticky"); + } + } + + window.addEventListener("scroll", updateHeader); + updateHeader(); +}; + +coverage.expand_contexts = function (e) { + var ctxs = e.target.parentNode.querySelector(".ctxs"); + + if (!ctxs.classList.contains("expanded")) { + var ctxs_text = ctxs.textContent; + var width = Number(ctxs_text[0]); + ctxs.textContent = ""; + for (var i = 1; i < ctxs_text.length; i += width) { + key = ctxs_text.substring(i, i + width).trim(); + ctxs.appendChild(document.createTextNode(contexts[key])); + ctxs.appendChild(document.createElement("br")); + } + ctxs.classList.add("expanded"); + } +}; + +document.addEventListener("DOMContentLoaded", () => { + if (document.body.classList.contains("indexfile")) { + coverage.index_ready(); + } + else { + coverage.pyfile_ready(); + } +}); diff --git a/htmlcov/favicon_32_cb_58284776.png b/htmlcov/favicon_32_cb_58284776.png new file mode 100644 index 0000000000000000000000000000000000000000..8649f0475d8d20793b2ec431fe25a186a414cf10 GIT binary patch literal 1732 zcmV;#20QtQP)K2KOkBOVxIZChq#W-v7@TU%U6P(wycKT1hUJUToW3ke1U1ONa4 z000000000000000bb)GRa9mqwR9|UWHy;^RUrt?IT__Y0JUcxmBP0(51q1>E00030 z|NrOz)aw7%8sJzM<5^g%z7^qE`}_Ot|JUUG(NUkWzR|7K?Zo%@_v-8G-1N%N=D$;; zw;keH4dGY$`1t4M=HK_s*zm^0#KgqfwWhe3qO_HtvXYvtjgX>;-~C$L`&k>^R)9)7 zdPh2TL^pCnHC#0+_4D)M`p?qp!pq{jO_{8;$fbaflbx`Tn52n|n}8VFRTA1&ugOP< zPd{uvFjz7t*Vot1&d$l-xWCk}s;sQL&#O(Bskh6gqNJv>#iB=ypG1e3K!K4yc7!~M zfj4S*g^zZ7eP$+_Sl07Z646l;%urinP#D8a6TwRtnLIRcI!r4f@bK~9-`~;E(N?Lv zSEst7s;rcxsi~}{Nsytfz@MtUoR*iFc8!#vvx}Umhm4blk(_~MdVD-@dW&>!Nn~ro z_E~-ESVQAj6Wmn;(olz(O&_{U2*pZBc1aYjMh>Dq3z|6`jW`RDHV=t3I6yRKJ~LOX zz_z!!vbVXPqob#=pj3^VMT?x6t(irRmSKsMo1~LLkB&=#j!=M%NP35mfqim$drWb9 zYIb>no_LUwc!r^NkDzs4YHu@=ZHRzrafWDZd1EhEVq=tGX?tK$pIa)DTh#bkvh!J- z?^%@YS!U*0E8$q$_*aOTQ&)Ra64g>ep;BdcQgvlg8qQHrP*E$;P{-m=A*@axn@$bO zO-Y4JzS&EAi%YG}N?cn?YFS7ivPY=EMV6~YH;+Xxu|tefLS|Aza)Cg6us#)=JW!uH zQa?H>d^j+YHCtyjL^LulF*05|F$RG!AX_OHVI&MtA~_@=5_lU|0000rbW%=J06GH4 z^5LD8b8apw8vNh1ua1mF{{Hy)_U`NA;Nacc+sCpuHXa-V{r&yz?c(9#+}oX+NmiRW z+W-IqK1oDDR5;6GfCDCOP5}iL5fK(cB~ET81`MFgF2kGa9AjhSIk~-E-4&*tPPKdiilQJ11k_J082ZS z>@TvivP!5ZFG?t@{t+GpR3XR&@*hA_VE1|Lo8@L@)l*h(Z@=?c-NS$Fk&&61IzUU9 z*nPqBM=OBZ-6ka1SJgGAS-Us5EN)r#dUX%>wQZLa2ytPCtMKp)Ob z*xcu38Z&d5<-NBS)@jRD+*!W*cf-m_wmxDEqBf?czI%3U0J$Xik;lA`jg}VH?(S(V zE!M3;X2B8w0TnnW&6(8;_Uc)WD;Ms6PKP+s(sFgO!}B!^ES~GDt4qLPxwYB)^7)XA zZwo9zDy-B0B+jT6V=!=bo(zs_8{eBA78gT9GH$(DVhz;4VAYwz+bOIdZ-PNb|I&rl z^XG=vFLF)1{&nT2*0vMz#}7^9hXzzf&ZdKlEj{LihP;|;Ywqn35ajP?H?7t|i-Un% z&&kxee@9B{nwgv1+S-~0)E1{ob1^Wn`F2isurqThKK=3%&;`@{0{!D- z&CSj80t;uPu&FaJFtSXKH#ajgGj}=sEad7US6jP0|Db@0j)?(5@sf<7`~a9>s;wCa zm^)spe{uxGFmrJYI9cOh7s$>8Npkt-5EWB1UKc`{W{y5Ce$1+nM9Cr;);=Ju#N^62OSlJMn7omiUgP&ErsYzT~iGxcW aE(`!K@+CXylaC4j0000 + + + + Coverage report + + + + + +
+
+

Coverage report: + 38% +

+ +
+ +
+ + +
+
+

+ Files + Functions + Classes +

+

+ coverage.py v7.6.4, + created at 2024-11-05 17:25 +0800 +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Filefunctionstatementsmissingexcludedcoverage
irc_dlient.pyConfig.__init__1200100%
irc_dlient.pyMyIRCClient.__init__8800%
irc_dlient.pyMyIRCClient.start1100%
irc_dlient.pyMyIRCClient.reset_all4400%
irc_dlient.pyMyIRCClient.restart6600%
irc_dlient.pyMyIRCClient.start_periodic_task3300%
irc_dlient.pyMyIRCClient.stop_periodic_task3300%
irc_dlient.pyMyIRCClient.check_last_room_status111100%
irc_dlient.pyMyIRCClient.check_room_status202000%
irc_dlient.pyMyIRCClient.export_json111100%
irc_dlient.pyMyIRCClient.on_connect121200%
irc_dlient.pyMyIRCClient.on_connect.send_loop3300%
irc_dlient.pyMyIRCClient.on_privmsg121200%
irc_dlient.pyMyIRCClient.on_pubmsg19119100%
irc_dlient.pyPlayer.__init__800100%
irc_dlient.pyPlayer.add_player200100%
irc_dlient.pyPlayer.add_host200100%
irc_dlient.pyPlayer.remove_host200100%
irc_dlient.pyPlayer.remain_hosts_to_player7700%
irc_dlient.pyPlayer.extract_player_name5500%
irc_dlient.pyPlayer.convert_host101000%
irc_dlient.pyPlayer.remove_player200100%
irc_dlient.pyPlayer.reset_player_list1100%
irc_dlient.pyPlayer.reset_host_list1100%
irc_dlient.pyPlayer.clear_approved_list4400%
irc_dlient.pyPlayer.host_rotate_pending3300%
irc_dlient.pyPlayer.reverse_host_pending2200%
irc_dlient.pyPlayer.host_rotate6600%
irc_dlient.pyPlayer.vote_for_abort7700%
irc_dlient.pyPlayer.vote_for_start7700%
irc_dlient.pyPlayer.vote_for_host_rotate141400%
irc_dlient.pyPlayer.vote_for_close_room7700%
irc_dlient.pyRoom.__init__300100%
irc_dlient.pyRoom.set_game_start_time2200%
irc_dlient.pyRoom.reset_game_start_time1100%
irc_dlient.pyRoom.get_last_room_id72071%
irc_dlient.pyRoom.save_last_room_id62067%
irc_dlient.pyRoom.help1100%
irc_dlient.pyRoom.change_room_id2200%
irc_dlient.pyRoom.send_msg2200%
irc_dlient.pyRoom.create_room2200%
irc_dlient.pyRoom.join_room2200%
irc_dlient.pyRoom.close_room2200%
irc_dlient.pyRoom.change_host2200%
irc_dlient.pyRoom.start_room2200%
irc_dlient.pyRoom.abort_room2200%
irc_dlient.pyRoom.change_password2200%
irc_dlient.pyRoom.change_beatmap_to2200%
irc_dlient.pyRoom.change_mods_to_FM2200%
irc_dlient.pyRoom.get_mp_settings2200%
irc_dlient.pyBeatmap.__init__3500100%
irc_dlient.pyBeatmap.clear_cache2200%
irc_dlient.pyBeatmap.get_token93067%
irc_dlient.pyBeatmap.get_beatmap_info391097%
irc_dlient.pyBeatmap.change_beatmap_id200100%
irc_dlient.pyBeatmap.check_beatmap_if_out_of_star5500%
irc_dlient.pyBeatmap.check_beatmap_if_out_of_time500100%
irc_dlient.pyBeatmap.return_beatmap_info3300%
irc_dlient.pyBeatmap.get_match_info9900%
irc_dlient.pyBeatmap.get_user_id1100100%
irc_dlient.pyBeatmap.get_beatmap_score4500100%
irc_dlient.pyBeatmap.get_recent_info4200100%
irc_dlient.pyPP.__init__2500100%
irc_dlient.pyPP.get_beatmap_file111091%
irc_dlient.pyPP.calculate_pp_fully565600%
irc_dlient.pyPP.calculate_pp_obj707000%
irc_dlient.py(no function)10114086%
Total 901555038%
+

+ No items found using the specified filter. +

+
+ + + diff --git a/htmlcov/index.html b/htmlcov/index.html new file mode 100644 index 0000000..2d6df08 --- /dev/null +++ b/htmlcov/index.html @@ -0,0 +1,111 @@ + + + + + Coverage report + + + + + +
+
+

Coverage report: + 38% +

+ +
+ +
+ + +
+
+

+ Files + Functions + Classes +

+

+ coverage.py v7.6.4, + created at 2024-11-05 17:25 +0800 +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Filestatementsmissingexcludedcoverage
irc_dlient.py901555038%
Total901555038%
+

+ No items found using the specified filter. +

+
+ + + diff --git a/htmlcov/irc_dlient_py.html b/htmlcov/irc_dlient_py.html new file mode 100644 index 0000000..5777a73 --- /dev/null +++ b/htmlcov/irc_dlient_py.html @@ -0,0 +1,1330 @@ + + + + + Coverage for irc_dlient.py: 38% + + + + + +
+
+

+ Coverage for irc_dlient.py: + 38% +

+ +

+ 901 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.6.4, + created at 2024-11-05 17:25 +0800 +

+ +
+
+
+

1import irc.client 

+

2import re 

+

3import requests 

+

4 

+

5import threading 

+

6import chardet 

+

7import configparser 

+

8 

+

9from requests.exceptions import HTTPError 

+

10 

+

11from datetime import datetime 

+

12 

+

13import rosu_pp_py as rosu 

+

14 

+

15import os 

+

16import json 

+

17import time 

+

18 

+

19osu_server = "irc.ppy.sh" 

+

20osu_port = 6667 

+

21 

+

22 

+

23class Config: 

+

24 def __init__(self): 

+

25 self.config = configparser.ConfigParser() 

+

26 with open('config.ini', 'rb') as f: 

+

27 encoding = chardet.detect(f.read())['encoding'] 

+

28 self.config.read('config.ini', encoding=encoding) 

+

29 self.osuclientid = self.config['OSUAPI']['client_id'] 

+

30 self.osuclientsecret = self.config['OSUAPI']['client_secret'] 

+

31 self.osunickname = self.config['OSUAPI']['nickname'] 

+

32 self.osupassword = self.config['OSUAPI']['password'] 

+

33 self.mpname = self.config['OSU']['mpname'] 

+

34 self.starlimit = self.config['OSU']['starlimit'] 

+

35 self.timelimit = self.config['OSU']['timelimit'] 

+

36 self.mppassword = self.config['OSU']['mppassword'] 

+

37 

+

38# 定义IRC客户端类 

+

39class MyIRCClient: 

+

40 def __init__(self, server, port, nickname, password): 

+

41 self.irc_react = irc.client.Reactor() 

+

42 self.server = self.irc_react.server() 

+

43 self.server.connect(server, port, nickname, password) 

+

44 self.irc_react.add_global_handler("welcome", self.on_connect) 

+

45 self.irc_react.add_global_handler("pubmsg", self.on_pubmsg) 

+

46 self.irc_react.add_global_handler("privmsg", self.on_privmsg) 

+

47 self.timer = None # 定义定时器 

+

48 self.restarting_task = threading.Thread(target=(self.restart)) 

+

49 

+

50 def start(self): 

+

51 self.irc_react.process_forever() 

+

52 

+

53 def reset_all(self): 

+

54 # 重置 

+

55 p.reset_player_list() 

+

56 p.reset_host_list() 

+

57 p.clear_approved_list() 

+

58 b.clear_cache() 

+

59 

+

60 def restart(self): 

+

61 print(f'尝试重启...{datetime.now()+datetime.timedelta(hours=8)}') 

+

62 time.sleep(120) 

+

63 self.reset_all() 

+

64 r.create_room(self.server, "") 

+

65 self.restarting_task = threading.Thread(target=(self.restart)) 

+

66 print(f'重启完成{datetime.now()+datetime.timedelta(hours=8)}') 

+

67 

+

68 # 定义定时任务,每60s执行一次,检查房间状态 

+

69 def start_periodic_task(self): 

+

70 # Save the Timer object in an instance variable 

+

71 self.timer = threading.Timer(60, self.start_periodic_task) 

+

72 self.timer.start() 

+

73 self.check_room_status(r.room_id) 

+

74 

+

75 # 停止定时任务 

+

76 def stop_periodic_task(self): 

+

77 if self.timer is not None: 

+

78 self.timer.cancel() 

+

79 self.timer = None 

+

80 

+

81 def check_last_room_status(self,last_room_id): 

+

82 if last_room_id == "": 

+

83 return False 

+

84 b.get_token() 

+

85 try: 

+

86 text = b.get_match_info(re.findall(r'\d+', last_room_id)[0]) 

+

87 except: 

+

88 text = "" 

+

89 # match-disbanded #比赛关闭 

+

90 if "match-disbanded" in str(text['events']): 

+

91 return False 

+

92 else: 

+

93 r.change_room_id(last_room_id) 

+

94 return True 

+

95 

+

96 # 这是检查房间状态的流程 用于定时任务 

+

97 def check_room_status(self, room_id): 

+

98 b.get_token() 

+

99 try: 

+

100 text = b.get_match_info(re.findall(r'\d+', room_id)[0]) 

+

101 except: 

+

102 text = "" 

+

103 # match-disbanded #比赛关闭 

+

104 try: 

+

105 if ("match-disbanded" in str(text['events'])) == True: 

+

106 self.stop_periodic_task() 

+

107 # 重置 

+

108 p.reset_player_list() 

+

109 p.reset_host_list() 

+

110 p.clear_approved_list() 

+

111 p.approved_host_rotate_list.clear() 

+

112 b.clear_cache() 

+

113 # 尝试重新创建房间 

+

114 try: 

+

115 r.create_room(self.server, "") 

+

116 except: 

+

117 print("创建房间失败") 

+

118 self.timer.start() 

+

119 except: 

+

120 print("无法判断比赛信息") 

+

121 

+

122 def export_json(self): 

+

123 result = {} 

+

124 result['player_list'] = p.player_list 

+

125 result['beatmap_name'] = b.beatmap_name 

+

126 result['beatmap_artist'] = b.beatmap_artist 

+

127 result['beatmap_star'] = b.beatmap_star 

+

128 

+

129 try: 

+

130 with open('data.json', 'w', encoding='utf-8') as f: 

+

131 json.dump(result, f) 

+

132 print("导出json") 

+

133 except: 

+

134 print("导出json失败") 

+

135 

+

136 def on_connect(self, connection, event): 

+

137 last_room_id = r.get_last_room_id() 

+

138 

+

139 # 如果房间存在 

+

140 if self.check_last_room_status(last_room_id): 

+

141 r.join_room(connection, event) 

+

142 r.change_password(connection, event) 

+

143 r.get_mp_settings(connection, event) 

+

144 try: 

+

145 self.start_periodic_task() 

+

146 except: 

+

147 print("定时任务启动失败") 

+

148 # 如果房间不存在 

+

149 else: 

+

150 r.create_room(connection, event) 

+

151 

+

152 def send_loop(): 

+

153 while True: 

+

154 message = input(">") 

+

155 r.send_msg(connection, event, message) 

+

156 

+

157 threading.Thread(target=(send_loop)).start() 

+

158 

+

159 def on_privmsg(self, connection, event): 

+

160 # 打印接收到的私人消息 

+

161 print(f"收到私人消息 {event.source.split('!')[0]}:{event.arguments[0]}") 

+

162 text = event.arguments[0] 

+

163 # 匹配第一次收到的房间号 

+

164 if text.find("Created the tournament match") != -1 and event.source.find("BanchoBot") != -1: 

+

165 try: 

+

166 romm_id = "#mp_"+re.findall(r'\d+', text)[0] 

+

167 except: 

+

168 romm_id = "" 

+

169 # 更新room变量 

+

170 r.change_room_id(romm_id) 

+

171 # 加入并监听房间 

+

172 r.join_room(connection, event) 

+

173 # 修改房间密码 

+

174 r.change_password(connection, event) 

+

175 # 保存房间IDs 

+

176 r.save_last_room_id() 

+

177 # 启动定时任务 

+

178 self.start_periodic_task() 

+

179 

+

180 def on_pubmsg(self, connection, event): 

+

181 

+

182 try: 

+

183 # 打印接收到的消息 

+

184 print(f"收到频道消息 {event.source.split('!')[0]}:{event.arguments[0]}") 

+

185 text = event.arguments[0] 

+

186 # 判断是否是banchobot发送的消息 

+

187 if event.source.find("BanchoBot") != -1 or event.source.find("ATRI1024") != -1: 

+

188 # 加入房间 

+

189 if text.find("joined in slot") != -1: 

+

190 # 尝试 

+

191 try: 

+

192 playerid = re.findall( 

+

193 r'.*(?= joined in slot)', text)[0] 

+

194 except: 

+

195 playerid = "" 

+

196 print(f'玩家{playerid}加入房间') 

+

197 # 发送欢迎消息 

+

198 if "ATRI1024" not in playerid: 

+

199 if b.beatmap_length != "" and r.game_start_time != "": 

+

200 timeleft = int(b.beatmap_length)+10 - \ 

+

201 int((datetime.now()-r.game_start_time).seconds) 

+

202 text_timeleft = f'| 剩余游玩时间:{timeleft}s 请主人耐心等待哦~' 

+

203 else: 

+

204 timeleft = 0 

+

205 text_Welcome = f'欢迎{playerid}酱~\(≧▽≦)/ 输入help获取指令详情' 

+

206 if timeleft > 0: 

+

207 r.send_msg(connection, event, 

+

208 text_Welcome+text_timeleft) 

+

209 else: 

+

210 r.send_msg(connection, event, text_Welcome) 

+

211 # 如果第一次加入房间,更换房主,清空房主队列,设置FM 

+

212 if len(p.player_list) == 0: 

+

213 p.reset_host_list() 

+

214 r.change_host(connection, event, playerid) 

+

215 r.change_mods_to_FM(connection, event) 

+

216 # 加入房间队列,玩家队列 

+

217 p.add_host(playerid) 

+

218 p.add_player(playerid) 

+

219 print(f'玩家队列{p.player_list}') 

+

220 print(f'房主队列{p.room_host_list}') 

+

221 # 输出 

+

222 self.export_json() 

+

223 # 离开房间 

+

224 if text.find("left the game") != -1: 

+

225 # 尝试 

+

226 try: 

+

227 playerid = re.findall(r'.*(?= left the game)', text)[0] 

+

228 except: 

+

229 playerid = "" 

+

230 print(f'玩家{playerid}离开房间') 

+

231 # 不移除房主队列 

+

232 p.remove_player(playerid) 

+

233 # 房主离开立刻更换房主 

+

234 if playerid == p.room_host and len(p.player_list) != 0: 

+

235 p.host_rotate(connection, event) 

+

236 print(f'玩家队列{p.player_list}') 

+

237 # 输出 

+

238 self.export_json() 

+

239 # 修改 on_pubmsg 方法中处理玩家列表的部分 

+

240 if text.find("Slot") != -1: 

+

241 players = re.findall(r'Slot \d+\s+(?:Not Ready|Ready)\s+(https://osu\.ppy\.sh/u/\d+\s+.+)', text) 

+

242 if players: 

+

243 for player_info in players: 

+

244 player_id = p.extract_player_name(player_info) 

+

245 if player_id: 

+

246 p.add_host(player_id) 

+

247 p.add_player(player_id) 

+

248 print(f'玩家队列{p.player_list}') 

+

249 print(f'房主队列{p.room_host_list}') 

+

250 # 输出 

+

251 self.export_json() 

+

252 # 这个是加入房间后要把当前beatmap_id给change一下 

+

253 if text.find("Beatmap:") != -1: 

+

254 match = re.search(r'https://osu\.ppy\.sh/b/(\d+)', text) 

+

255 if match: 

+

256 beatmap_id = match.group(1) 

+

257 b.change_beatmap_id(beatmap_id) 

+

258 

+

259 # 谱面变化 

+

260 if text.find("Beatmap changed to") != -1: 

+

261 # 尝试 

+

262 try: 

+

263 beatmap_url = re.findall(r'(?<=\()\S+(?=\))', text)[0] 

+

264 beatmap_id = re.findall(r'\d+', beatmap_url)[0] 

+

265 except: 

+

266 beatmap_url = "" 

+

267 beatmap_id = "" 

+

268 

+

269 last_beatmap_id = b.beatmap_id 

+

270 if last_beatmap_id == "": 

+

271 last_beatmap_id = "3459231" 

+

272 b.change_beatmap_id(beatmap_id) 

+

273 # 获取谱面信息 

+

274 b.get_token() 

+

275 b.get_beatmap_info() 

+

276 

+

277 if b.check_beatmap_if_out_of_star(): 

+

278 r.send_msg(connection, event, 

+

279 f'{b.beatmap_star}*>{config.starlimit}* 请重新选择') 

+

280 r.change_beatmap_to(connection, event, last_beatmap_id) 

+

281 b.change_beatmap_id(last_beatmap_id) 

+

282 return 

+

283 if b.check_beatmap_if_out_of_time(): 

+

284 r.send_msg(connection, event, 

+

285 f'{b.beatmap_length}s>{config.timelimit}s 请重新选择') 

+

286 r.change_beatmap_to(connection, event, last_beatmap_id) 

+

287 b.change_beatmap_id(last_beatmap_id) 

+

288 return 

+

289 

+

290 r.send_msg(connection, event, b.return_beatmap_info()) 

+

291 # 输出 

+

292 self.export_json() 

+

293 

+

294 # 房主变化 

+

295 if text.find("became the host") != -1: 

+

296 # 尝试 

+

297 try: 

+

298 p.room_host = re.findall( 

+

299 r'.*(?= became the host)', text)[0] 

+

300 except: 

+

301 p.room_host = "" 

+

302 print(f'房主变为{p.room_host}') 

+

303 

+

304 # 准备就绪,开始游戏 

+

305 if text.find("All players are ready") != -1: 

+

306 r.start_room(connection, event) 

+

307 

+

308 # 开始游戏 

+

309 if text.find("The match has started") != -1: 

+

310 # 将房主队列第一个人移动到最后 

+

311 p.host_rotate_pending(connection, event) 

+

312 print(f'游戏开始,房主队列{p.room_host_list}') 

+

313 p.clear_approved_list() 

+

314 # 获取游戏开始时间 

+

315 r.set_game_start_time() 

+

316 

+

317 # 游戏结束,更换房主 

+

318 if text.find("The match has finished") != -1: 

+

319 # 对比房主队列,去除离开的玩家,更新房主队列 

+

320 p.host_rotate(connection, event) 

+

321 print(f'游戏结束,房主队列{p.room_host_list}') 

+

322 # 换了房主以后立即清空投票列表 

+

323 p.approved_host_rotate_list.clear() 

+

324 p.clear_approved_list() 

+

325 # 发送队列 

+

326 p.convert_host() 

+

327 r.send_msg(connection, event, str( 

+

328 f'当前队列:{p.room_host_list_apprence_text}')) 

+

329 # 重置游戏开始时间 

+

330 r.reset_game_start_time() 

+

331 

+

332 # 游戏被丢弃 

+

333 if text.find("Aborted the match") != -1: 

+

334 # 判断游戏是否结束 

+

335 timeleft = int(b.beatmap_length)+10 - \ 

+

336 int((datetime.now()-r.game_start_time).seconds) 

+

337 if timeleft > 0: # 大于0代表没打,先不更换房主,退回队列 

+

338 p.reverse_host_pending(connection, event) 

+

339 print("比赛被丢弃,房主队列退回") 

+

340 else: # 小于0代表已经打完,更换房主 

+

341 # 对比房主队列,去除离开的玩家,更新房主队列 

+

342 p.host_rotate(connection, event) 

+

343 print(f'游戏结束,房主队列{p.room_host_list}') 

+

344 # 换了房主以后立即清空投票列表 

+

345 p.approved_host_rotate_list.clear() 

+

346 p.clear_approved_list() 

+

347 # 发送队列 

+

348 p.convert_host() 

+

349 r.send_msg(connection, event, str( 

+

350 f'当前队列:{p.room_host_list_apprence_text}')) 

+

351 # 重置游戏开始时间 

+

352 r.reset_game_start_time() 

+

353 # bancho重启 

+

354 if text.find("Bancho will be right back!") != -1: 

+

355 r.send_msg(connection, event, 

+

356 "Bancho重启中,房间将在2min后自动重启") 

+

357 self.restarting_task.start() 

+

358 

+

359 # 玩家发送的消息响应部分 

+

360 

+

361 # 投票丢弃游戏 

+

362 if text in ["!abort", "!abort", "!ABORT", "!ABORT"]: 

+

363 p.vote_for_abort(connection, event) 

+

364 

+

365 # 投票开始游戏 

+

366 if text in ["!start", "!start", "!START", "!START"]: 

+

367 p.vote_for_start(connection, event) 

+

368 

+

369 # 投票跳过房主 

+

370 if text in ["!skip", "!skip", "!SKIP", "!SKIP"]: 

+

371 p.vote_for_host_rotate(connection, event) 

+

372 

+

373 # 投票关闭房间s 

+

374 if text in ["!close", "!close", "!CLOSE", "!CLOSE"]: 

+

375 p.vote_for_close_room(connection, event) 

+

376 

+

377 # 手动查看队列,就只返回前面剩余多少人 

+

378 if text in ["!queue", "!queue", "!QUEUE", "!QUEUE", "!q", "!q", "!Q", "!Q"]: 

+

379 p.convert_host() 

+

380 index = p.remain_hosts_to_player(event.source.split('!')[0]) 

+

381 r.send_msg(connection, event, str( 

+

382 f'你前面剩余人数:{index}')) 

+

383 

+

384 # 帮助 

+

385 if text in ["help", "HELP", "!help", "!help", "!HELP", "!HELP", "!h", "!h", "!H", "!H"]: 

+

386 r.send_msg(connection, event, r.help()) 

+

387 

+

388 # ping 

+

389 if text in ["ping", "PING", "!ping", "!ping", "!PING", "!PING"]: 

+

390 r.send_msg(connection, event, r'pong') 

+

391 

+

392 # 快速查询成绩 

+

393 if text in ["!pr", "!pr", "!PR", "!PR", "!p", "!p", "!P", "!P"]: 

+

394 b.get_user_id(event.source.split('!')[0]) 

+

395 detail_1 = b.get_recent_info(event.source.split('!')[0]) 

+

396 pp.get_beatmap_file(beatmap_id=b.pr_beatmap_id) 

+

397 print(b.pr_mods) 

+

398 detail_2 = pp.calculate_pp_obj( 

+

399 mods=b.pr_mods, combo=b.pr_maxcombo, acc=b.pr_acc, misses=b.pr_miss) 

+

400 r.send_msg(connection, event, detail_1) 

+

401 r.send_msg(connection, event, detail_2) 

+

402 

+

403 # 快速当前谱面成绩 

+

404 if text in ["!s", "!s", "!S", "!S"]: 

+

405 b.get_user_id(event.source.split('!')[0]) 

+

406 s = b.get_beatmap_score(event.source.split('!')[0]) 

+

407 r.send_msg(connection, event, s) 

+

408 if s.find("未查询到") == -1: 

+

409 pp.get_beatmap_file(beatmap_id=b.beatmap_id) 

+

410 r.send_msg(connection, event, pp.calculate_pp_obj( 

+

411 mods=b.pr_mods, combo=b.pr_maxcombo, acc=b.pr_acc, misses=b.pr_miss)) 

+

412 

+

413 # 快速查询谱面得分情况 

+

414 if text.find("!m+") != -1: 

+

415 try: 

+

416 modslist_str = re.findall(r'\+(.*)', event.arguments[0])[0] 

+

417 except: 

+

418 modslist_str = "" 

+

419 pp.get_beatmap_file(beatmap_id=b.beatmap_id) 

+

420 r.send_msg(connection, event, pp.calculate_pp_fully(modslist_str)) 

+

421 if text.find("!M+") != -1: 

+

422 try: 

+

423 modslist_str = re.findall(r'\+(.*)', event.arguments[0])[0] 

+

424 except: 

+

425 modslist_str = "" 

+

426 pp.get_beatmap_file(beatmap_id=b.beatmap_id) 

+

427 r.send_msg(connection, event, pp.calculate_pp_fully(modslist_str)) 

+

428 

+

429 if text.find("!M+") != -1: 

+

430 try: 

+

431 modslist_str = re.findall(r'\+(.*)', event.arguments[0])[0] 

+

432 except: 

+

433 modslist_str = "" 

+

434 pp.get_beatmap_file(beatmap_id=b.beatmap_id) 

+

435 r.send_msg(connection, event, pp.calculate_pp_fully(modslist_str)) 

+

436 

+

437 if text.find("!M+") != -1: 

+

438 try: 

+

439 modslist_str = re.findall(r'\+(.*)', event.arguments[0])[0] 

+

440 except: 

+

441 modslist_str = "" 

+

442 pp.get_beatmap_file(beatmap_id=b.beatmap_id) 

+

443 r.send_msg(connection, event, pp.calculate_pp_fully(modslist_str)) 

+

444 

+

445 if text in ["!m", "!m", "!M", "!M"]: 

+

446 pp.get_beatmap_file(beatmap_id=b.beatmap_id) 

+

447 r.send_msg(connection, event,pp.calculate_pp_fully('')) 

+

448 

+

449 # 快速获取剩余时间 大约10s游戏延迟 

+

450 if text in ["!ttl", "!ttl", "!TTL", "!TTL"]: 

+

451 if b.beatmap_length != "" and r.game_start_time != "": 

+

452 timeleft = int(b.beatmap_length)+10 - \ 

+

453 int((datetime.now()-r.game_start_time).seconds) 

+

454 r.send_msg(connection, event, f'剩余游玩时间:{timeleft}s') 

+

455 else: 

+

456 r.send_msg(connection, event, f'剩余游玩时间:未知') 

+

457 

+

458 if text in ["!i", "!i"]: 

+

459 b.get_token() 

+

460 b.get_beatmap_info() 

+

461 r.send_msg(connection, event, b.return_beatmap_info()) 

+

462 

+

463 if text in ["!about", "!about", "!ABOUT", "!ABORT"]: 

+

464 r.send_msg(connection, event, 

+

465 "[https://github.com/Ohdmire/osu-ircbot-py ATRI高性能bot]") 

+

466 

+

467 except Exception as e: 

+

468 print(f'-----------------未知错误---------------------\n{e}') 

+

469 

+

470 

+

471# 定义玩家类 

+

472class Player: 

+

473 def __init__(self): 

+

474 self.player_list = [] 

+

475 self.room_host_list = [] 

+

476 self.room_host_list_apprence_text = "" 

+

477 self.approved_abort_list = [] 

+

478 self.approved_start_list = [] 

+

479 self.approved_host_rotate_list = [] 

+

480 self.approved_close_list = [] 

+

481 self.room_host = "" 

+

482 

+

483 def add_player(self, name): 

+

484 if name not in self.player_list: 

+

485 self.player_list.append(name) 

+

486 

+

487 def add_host(self, name): 

+

488 if name not in self.room_host_list: 

+

489 self.room_host_list.append(name) 

+

490 

+

491 def remove_host(self, name): 

+

492 if name in self.room_host_list: 

+

493 self.room_host_list.remove(name) 

+

494 

+

495 def remain_hosts_to_player(self, name): 

+

496 name_normalized = name.replace(" ", "_") 

+

497 for index, host in enumerate(self.room_host_list): 

+

498 host_normalized = host.replace(" ", "_") 

+

499 if name_normalized == host_normalized: 

+

500 return index 

+

501 print(f"{name} is not in room_host_list", self.room_host_list) 

+

502 return -1 # 如果没有找到匹配的名字,返回-1 

+

503 

+

504 def extract_player_name(self, text): 

+

505 match = re.search(r'https://osu\.ppy\.sh/u/\d+\s*(.*?)(?:\s*\[.*\])?$', text) 

+

506 if match: 

+

507 playername = match.group(1).strip() 

+

508 return playername 

+

509 return "" 

+

510 

+

511 def convert_host(self): 

+

512 try: 

+

513 self.room_host_list_apprence_text = "" 

+

514 for index, host in enumerate(self.room_host_list): 

+

515 if index == 0: 

+

516 self.room_host_list_apprence_text += host 

+

517 else: 

+

518 # 在每个字符之间插入零宽空格 

+

519 self.room_host_list_apprence_text += "\u200B".join(host) 

+

520 

+

521 if index < len(self.room_host_list) - 1: 

+

522 self.room_host_list_apprence_text += "-->" 

+

523 except: 

+

524 print("房主队列转换失败") 

+

525 

+

526 def remove_player(self, name): 

+

527 if name in self.player_list: 

+

528 self.player_list.remove(name) 

+

529 

+

530 def reset_player_list(self): 

+

531 self.player_list.clear() 

+

532 

+

533 def reset_host_list(self): 

+

534 self.room_host_list.clear() 

+

535 

+

536 def clear_approved_list(self): 

+

537 self.approved_abort_list.clear() 

+

538 self.approved_start_list.clear() 

+

539 self.approved_close_list.clear() 

+

540 self.approved_host_rotate_list.clear() 

+

541 

+

542 def host_rotate_pending(self, connection, event): 

+

543 now_host = self.room_host_list[0] 

+

544 self.remove_host(now_host) 

+

545 self.add_host(now_host) 

+

546 

+

547 def reverse_host_pending(self, connection, event): 

+

548 self.remove_host(self.room_host) 

+

549 self.room_host_list.insert(0, self.room_host) 

+

550 

+

551 def host_rotate(self, connection, event): 

+

552 result_list = [] 

+

553 for i in self.room_host_list: 

+

554 if i in self.player_list: 

+

555 result_list.append(i) 

+

556 self.room_host_list = result_list 

+

557 r.change_host(connection, event, self.room_host_list[0]) 

+

558 

+

559 def vote_for_abort(self, connection, event): 

+

560 # 获取发送者名字 

+

561 name = event.source.split('!')[0] 

+

562 if name not in self.approved_abort_list: 

+

563 self.approved_abort_list.append(name) 

+

564 if len(self.approved_abort_list) >= round(len(self.player_list)/2): 

+

565 r.abort_room(connection, event) 

+

566 self.approved_abort_list.clear() 

+

567 else: 

+

568 r.send_msg(connection, event, r'输入!abort强制放弃比赛 {} / {} '.format( 

+

569 str(len(self.approved_abort_list)), str(round(len(self.player_list)/2)))) 

+

570 

+

571 def vote_for_start(self, connection, event): 

+

572 # 获取发送者名字 

+

573 name = event.source.split('!')[0] 

+

574 if name not in self.approved_start_list: 

+

575 self.approved_start_list.append(name) 

+

576 if len(self.approved_start_list) >= round(len(self.player_list)/2): 

+

577 r.start_room(connection, event) 

+

578 self.approved_start_list.clear() 

+

579 else: 

+

580 r.send_msg(connection, event, r'输入!start强制开始比赛 {} / {} '.format( 

+

581 str(len(self.approved_start_list)), str(round(len(self.player_list)/2)))) 

+

582 

+

583 def vote_for_host_rotate(self, connection, event): 

+

584 # 获取发送者名字 

+

585 name = event.source.split('!')[0] 

+

586 # 如果发送者是房主,直接换房主 

+

587 if name == self.room_host: 

+

588 self.host_rotate_pending(connection, event) 

+

589 self.host_rotate(connection, event) 

+

590 self.approved_host_rotate_list.clear() 

+

591 print("房主自行更换") 

+

592 return 

+

593 if name not in self.approved_host_rotate_list: 

+

594 self.approved_host_rotate_list.append(name) 

+

595 if len(self.approved_host_rotate_list) >= round(len(self.player_list)/2): 

+

596 self.host_rotate_pending(connection, event) 

+

597 self.host_rotate(connection, event) 

+

598 self.approved_host_rotate_list.clear() 

+

599 else: 

+

600 r.send_msg(connection, event, r'输入!skip强制跳过房主 {} / {} '.format( 

+

601 str(len(self.approved_host_rotate_list)), str(round(len(self.player_list)/2)))) 

+

602 

+

603 def vote_for_close_room(self, connection, event): 

+

604 # 获取发送者名字 

+

605 name = event.source.split('!')[0] 

+

606 if name not in self.approved_close_list: 

+

607 self.approved_close_list.append(name) 

+

608 if len(self.approved_close_list) == len(self.player_list): 

+

609 r.close_room(connection, event) 

+

610 self.approved_close_list.clear() 

+

611 else: 

+

612 r.send_msg(connection, event, r'输入!close强制关闭房间(1min后自动重启) {} / {} '.format( 

+

613 str(len(self.approved_close_list)), str(len(self.player_list)))) 

+

614 

+

615 

+

616# 定义房间操作类 

+

617class Room: 

+

618 def __init__(self): 

+

619 self.room_id = "" 

+

620 self.last_romm_id = "" 

+

621 self.game_start_time = "" 

+

622 

+

623 def set_game_start_time(self): 

+

624 self.game_start_time = datetime.now() 

+

625 return self.game_start_time 

+

626 

+

627 def reset_game_start_time(self): 

+

628 self.game_start_time = "" 

+

629 

+

630 def get_last_room_id(self): 

+

631 try: 

+

632 with open('last_room_id.txt', 'r') as f: 

+

633 self.last_romm_id = f.read() 

+

634 print(f'获取上一个房间ID{self.last_romm_id}') 

+

635 except: 

+

636 print("未获取上一个房间ID") 

+

637 return self.last_romm_id 

+

638 

+

639 # 保存当前id到文件 

+

640 def save_last_room_id(self): 

+

641 try: 

+

642 with open('last_room_id.txt', 'w') as f: 

+

643 f.write(self.room_id) 

+

644 print(f'保存当前房间ID{self.room_id}') 

+

645 except: 

+

646 print("未保存当前房间ID") 

+

647 

+

648 def help(self): 

+

649 return r'!queue(!q) 查看队列 | !abort 投票丢弃游戏 | !start 投票开始游戏 | !skip 投票跳过房主 | !pr(!p) 查询最近成绩 | !s 查询当前谱面bp | !m+{MODS} 查询谱面模组PP| !i 返回当前谱面信息| !ttl 查询剩余时间 | !close 投票关闭(1min后自动重启)房间 | help(!h) 查看帮助 | !about 关于机器人' 

+

650 

+

651 def change_room_id(self, id): 

+

652 self.room_id = id 

+

653 print(f'更换当前房间ID为{self.room_id}') 

+

654 

+

655 def send_msg(self, connection, evetn, msg_text): 

+

656 connection.privmsg(self.room_id, msg_text) 

+

657 print("发送消息:"+msg_text) 

+

658 

+

659 def create_room(self, connection, event): 

+

660 connection.privmsg( 

+

661 "BanchoBot", "!mp make "+config.mpname) 

+

662 print("创建房间") 

+

663 

+

664 def join_room(self, connection, event): 

+

665 connection.join(self.room_id) # 加入 #osu 频道 

+

666 print(f'加入房间{self.room_id}') 

+

667 

+

668 def close_room(self, connection, event): 

+

669 connection.privmsg(self.room_id, "!mp close") 

+

670 print(f'关闭房间{self.room_id}') 

+

671 

+

672 def change_host(self, connection, event, playerid): 

+

673 connection.privmsg(self.room_id, "!mp host "+playerid) 

+

674 print("更换房主为 "+playerid) 

+

675 

+

676 def start_room(self, connection, event): 

+

677 connection.privmsg(self.room_id, "!mp start") 

+

678 print("开始游戏") 

+

679 

+

680 def abort_room(self, connection, event): 

+

681 connection.privmsg(self.room_id, "!mp abort") 

+

682 print("丢弃游戏") 

+

683 

+

684 def change_password(self, connection, event): 

+

685 connection.privmsg(self.room_id, "!mp password "+config.mppassword) 

+

686 print("修改密码") 

+

687 

+

688 def change_beatmap_to(self, connection, event, beatmapid): 

+

689 connection.privmsg(self.room_id, "!mp map "+beatmapid) 

+

690 print("更换谱面为"+beatmapid) 

+

691 

+

692 def change_mods_to_FM(self, connection, event): 

+

693 connection.privmsg(self.room_id, "!mp mods FreeMod") 

+

694 print("开启Freemod") 

+

695 

+

696 def get_mp_settings(self, connection, event): 

+

697 connection.privmsg(self.room_id, "!mp settings") 

+

698 print("获取房间详情成功") 

+

699 

+

700 

+

701# 定义谱面类 

+

702class Beatmap: 

+

703 def __init__(self, client_id, client_secret): 

+

704 self.osu_client_id = client_id 

+

705 self.osu_client_secret = client_secret 

+

706 self.osu_token = "" 

+

707 self.beatmap_id = "" 

+

708 self.beatmap_songs_id = "" 

+

709 self.beatmap_name = "" 

+

710 self.beatmap_artist = "" 

+

711 self.beatmap_star = 0 

+

712 self.beatmap_status = "" 

+

713 self.beatemap_bpm = "" 

+

714 self.beatmap_cs = "" 

+

715 self.beatmap_ar = "" 

+

716 self.beatmap_od = "" 

+

717 self.beatmap_hp = "" 

+

718 self.beatmap_length = 0 

+

719 self.beatmap_ranked_date = "" 

+

720 self.beatmatp_submit_date = "" 

+

721 self.beatmap_mirror_sayo_url = "" 

+

722 self.beatmap_osudirect_url = "" 

+

723 

+

724 self.id2name = {} 

+

725 

+

726 self.pr_beatmap_id = "" 

+

727 self.pr_beatmap_url = "" 

+

728 

+

729 self.pr_title = "" 

+

730 self.pr_artist = "" 

+

731 self.pr_star = "" 

+

732 

+

733 self.pr_acc = 0 

+

734 self.pr_maxcombo = 0 

+

735 self.pr_300 = 0 

+

736 self.pr_100 = 0 

+

737 self.pr_50 = 0 

+

738 self.pr_miss = 0 

+

739 self.pr_pp = 0 

+

740 self.pr_rank = "" 

+

741 self.pr_mods = "" 

+

742 

+

743 self.pr_username = "" 

+

744 

+

745 def clear_cache(self): 

+

746 self.osu_token = "" 

+

747 self.id2name.clear() 

+

748 

+

749 def get_token(self): 

+

750 try: 

+

751 url = 'https://osu.ppy.sh/oauth/token' 

+

752 data = { 

+

753 'client_id': self.osu_client_id, 

+

754 'client_secret': self.osu_client_secret, 

+

755 'grant_type': 'client_credentials', 

+

756 'scope': 'public' 

+

757 } 

+

758 response = requests.post(url, data=data) 

+

759 response.raise_for_status() # 如果请求失败,这会抛出一个异常 

+

760 self.osu_token = response.json()['access_token'] 

+

761 except: 

+

762 self.osu_token = "" 

+

763 print("获取访问令牌失败") 

+

764 

+

765 # 使用访问令牌查询 

+

766 def get_beatmap_info(self): 

+

767 

+

768 try: 

+

769 url = f'https://osu.ppy.sh/api/v2/beatmaps/'+self.beatmap_id 

+

770 headers = {'Authorization': f'Bearer {self.osu_token}'} 

+

771 response = requests.get(url, headers=headers) 

+

772 response.raise_for_status() # 如果请求失败,这会抛出一个异常 

+

773 

+

774 self.beatmap_songs_id = str(response.json()['beatmapset_id']) 

+

775 

+

776 self.beatmap_name = response.json()['beatmapset']['title_unicode'] 

+

777 self.beatmap_artist = response.json()['beatmapset']['artist_unicode'] 

+

778 self.beatmap_star = response.json()['difficulty_rating'] 

+

779 self.beatmap_status = response.json()['status'] 

+

780 self.beatmap_bpm = response.json()['bpm'] 

+

781 self.beatmap_cs = response.json()['cs'] 

+

782 self.beatmap_ar = response.json()['ar'] 

+

783 self.beatmap_od = response.json()['accuracy'] 

+

784 self.beatmap_hp = response.json()['drain'] 

+

785 self.beatmap_length = response.json()['total_length'] 

+

786 if self.beatmap_status == "ranked": 

+

787 self.beatmap_ranked_date = response.json( 

+

788 )['beatmapset']['ranked_date'][:10] 

+

789 else: 

+

790 self.beatmap_ranked_date = response.json( 

+

791 )['beatmapset']['submitted_date'][:10] 

+

792 self.beatmap_mirror_sayo_url = "https://osu.sayobot.cn/home?search="+self.beatmap_songs_id 

+

793 self.beatmap_mirror_inso_url = "http://inso.link/yukiho/?b="+self.beatmap_id 

+

794 self.beatmap_osudirect_url = response.json()['url'] 

+

795 except Exception as e: 

+

796 print(f'获取谱面信息失败,原因:{e}') 

+

797 self.beatmap_name = "获取谱面信息失败" 

+

798 self.beatmap_songs_id = "" 

+

799 self.beatmap_artist = "" 

+

800 self.beatmap_star = 0 

+

801 self.beatmap_status = "" 

+

802 self.beatmap_bpm = "" 

+

803 self.beatmap_cs = "" 

+

804 self.beatmap_ar = "" 

+

805 self.beatmap_od = "" 

+

806 self.beatmap_hp = "" 

+

807 self.beatmap_length = 0 

+

808 self.beatmap_ranked_date = "" 

+

809 self.beatmap_mirror_sayo_url = "" 

+

810 self.beatmap_mirror_inso_url = "" 

+

811 self.beatmap_osudirect_url = "" 

+

812 

+

813 def change_beatmap_id(self, id): 

+

814 self.beatmap_id = id 

+

815 print(f'更换谱面ID为 {self.beatmap_id}') 

+

816 

+

817 def check_beatmap_if_out_of_star(self): 

+

818 if float(config.starlimit) == 0: 

+

819 return False 

+

820 if self.beatmap_star > float(config.starlimit): 

+

821 return True 

+

822 else: 

+

823 return False 

+

824 

+

825 def check_beatmap_if_out_of_time(self): 

+

826 if float(config.timelimit) == 0: 

+

827 return False 

+

828 if self.beatmap_length > float(config.timelimit): 

+

829 return True 

+

830 else: 

+

831 return False 

+

832 

+

833 def return_beatmap_info(self): 

+

834 result = r'{} {}| {}*| [{} {} - {}]| bpm:{} length:{}s| ar:{} cs:{} od:{} hp:{}| [{} Sayobot] OR [{} inso]'.format(self.beatmap_ranked_date, self.beatmap_status, self.beatmap_star, self.beatmap_osudirect_url, 

+

835 self.beatmap_name, self.beatmap_artist, self.beatemap_bpm, self.beatmap_length, self.beatmap_ar, self.beatmap_cs, self.beatmap_od, self.beatmap_hp, self.beatmap_mirror_sayo_url, self.beatmap_mirror_inso_url) 

+

836 print(result) 

+

837 return result 

+

838 

+

839 def get_match_info(self, match_id): 

+

840 try: 

+

841 url = f'https://osu.ppy.sh/api/v2/matches/{match_id}' 

+

842 headers = {'Authorization': f'Bearer {self.osu_token}'} 

+

843 response = requests.get(url, headers=headers) 

+

844 response.raise_for_status() # 如果请求失败,这将抛出一个异常 

+

845 return response.json() 

+

846 except: 

+

847 print("获取比赛信息失败") 

+

848 return "" 

+

849 

+

850 def get_user_id(self, username): 

+

851 try: 

+

852 if username not in self.id2name: 

+

853 print("获取用户ID") 

+

854 url = f'https://osu.ppy.sh/api/v2/users/{username}?key=username' 

+

855 headers = {'Authorization': f'Bearer {self.osu_token}'} 

+

856 response = requests.get(url, headers=headers) 

+

857 response.raise_for_status() # 如果请求失败,这将抛出一个异常 

+

858 self.id2name[username] = response.json()['id'] 

+

859 print(self.id2name) 

+

860 except: 

+

861 print("获取用户ID失败") 

+

862 

+

863 def get_beatmap_score(self, username): 

+

864 try: 

+

865 user_id = self.id2name[username] 

+

866 url = f"https://osu.ppy.sh/api/v2/beatmaps/{self.beatmap_id}/scores/users/{user_id}" 

+

867 headers = {'Authorization': f'Bearer {self.osu_token}'} 

+

868 response = requests.get(url, headers=headers) 

+

869 response.raise_for_status() # 如果请求失败,这会抛出一个异常 

+

870 

+

871 self.pr_title = self.beatmap_name 

+

872 self.pr_artist = self.beatmap_artist 

+

873 self.pr_star = self.beatmap_star 

+

874 

+

875 self.beatmap_score_created_at = response.json()[ 

+

876 'score']['created_at'][:10] 

+

877 

+

878 self.pr_acc = response.json()['score']['accuracy'] 

+

879 self.pr_maxcombo = response.json()['score']['max_combo'] 

+

880 self.pr_300 = response.json()['score']['statistics']['count_300'] 

+

881 self.pr_100 = response.json()['score']['statistics']['count_100'] 

+

882 self.pr_50 = response.json()['score']['statistics']['count_50'] 

+

883 self.pr_miss = response.json()['score']['statistics']['count_miss'] 

+

884 self.pr_pp = response.json()['score']['pp'] 

+

885 self.pr_rank = response.json()['score']['rank'] 

+

886 self.pr_mods = response.json()['score']['mods'] 

+

887 self.pr_mods = "".join([str(mod) for mod in self.pr_mods]) 

+

888 

+

889 self.pr_beatmap_url = response.json()['score']['beatmap']['url'] 

+

890 

+

891 self.pr_username = username 

+

892 

+

893 self.pr_acc = round(self.pr_acc*100, 2) 

+

894 

+

895 except HTTPError: 

+

896 print(f"未查询到{username}在该谱面上留下的成绩") 

+

897 return f"未查询到{username}在该谱面上留下的成绩" 

+

898 

+

899 except Exception as e: 

+

900 print(f'获取谱面成绩失败,错误信息:{e}') 

+

901 self.pr_title = "获取谱面成绩失败" 

+

902 self.pr_artist = "" 

+

903 self.pr_star = "" 

+

904 self.pr_acc = 0 

+

905 self.pr_maxcombo = 0 

+

906 self.pr_300 = 0 

+

907 self.pr_100 = 0 

+

908 self.pr_50 = 0 

+

909 self.pr_miss = 0 

+

910 self.pr_pp = 0 

+

911 self.pr_rank = "" 

+

912 self.pr_mods = "" 

+

913 self.pr_username = "" 

+

914 

+

915 self.beatmap_score_created_at = "" 

+

916 

+

917 result = r'{}| [{} {} - {}]| {}*| {} [ {} ] {}pp acc:{}% combo:{}x| {}/{}/{}/{}| date:{}|'.format( 

+

918 self.pr_username, self.pr_beatmap_url, self.pr_title, self.pr_artist, self.pr_star, self.pr_mods, self.pr_rank, self.pr_pp, self.pr_acc, self.pr_maxcombo, self.pr_300, self.pr_100, self.pr_50, self.pr_miss, self.beatmap_score_created_at) 

+

919 print(result) 

+

920 return result 

+

921 

+

922 def get_recent_info(self, username): 

+

923 try: 

+

924 user_id = self.id2name[username] 

+

925 url = f'https://osu.ppy.sh/api/v2/users/{user_id}/scores/recent?&include_fails=1' 

+

926 headers = {'Authorization': f'Bearer {self.osu_token}'} 

+

927 response = requests.get(url, headers=headers) 

+

928 response.raise_for_status() # 如果请求失败,这将抛出一个异常 

+

929 

+

930 self.pr_beatmap_id = response.json()[0]['beatmap']['id'] 

+

931 self.pr_title = response.json()[0]['beatmapset']['title_unicode'] 

+

932 self.pr_artist = response.json()[0]['beatmapset']['artist_unicode'] 

+

933 self.pr_star = response.json()[0]['beatmap']['difficulty_rating'] 

+

934 

+

935 self.pr_acc = response.json()[0]['accuracy'] 

+

936 self.pr_maxcombo = response.json()[0]['max_combo'] 

+

937 self.pr_300 = response.json()[0]['statistics']['count_300'] 

+

938 self.pr_100 = response.json()[0]['statistics']['count_100'] 

+

939 self.pr_50 = response.json()[0]['statistics']['count_50'] 

+

940 self.pr_miss = response.json()[0]['statistics']['count_miss'] 

+

941 self.pr_pp = response.json()[0]['pp'] 

+

942 self.pr_rank = response.json()[0]['rank'] 

+

943 self.pr_mods = response.json()[0]['mods'] 

+

944 self.pr_mods = "".join([str(mod) for mod in self.pr_mods]) 

+

945 

+

946 self.pr_beatmap_url = response.json()[0]['beatmap']['url'] 

+

947 

+

948 self.pr_username = username 

+

949 

+

950 self.pr_acc = round(self.pr_acc*100, 2) 

+

951 

+

952 except Exception as e: 

+

953 print(f'获取最近成绩失败,错误信息:{e}') 

+

954 self.pr_title = "获取最近成绩失败" 

+

955 self.pr_artist = "" 

+

956 self.pr_star = "" 

+

957 self.pr_acc = 0 

+

958 self.pr_maxcombo = 0 

+

959 self.pr_300 = 0 

+

960 self.pr_100 = 0 

+

961 self.pr_50 = 0 

+

962 self.pr_miss = 0 

+

963 self.pr_pp = 0 

+

964 self.pr_rank = "" 

+

965 self.pr_mods = "" 

+

966 self.pr_username = "" 

+

967 self.pr_beatmap_url = "" 

+

968 

+

969 result = r'{}| [{} {} - {}]| {}*| {} [ {} ] {}pp acc:{}% combo:{}x| {}/{}/{}/{}|'.format( 

+

970 self.pr_username, self.pr_beatmap_url, self.pr_title, self.pr_artist, self.pr_star, self.pr_mods, self.pr_rank, self.pr_pp, self.pr_acc, self.pr_maxcombo, self.pr_300, self.pr_100, self.pr_50, self.pr_miss,) 

+

971 print(result) 

+

972 return result 

+

973 

+

974 

+

975class PP: 

+

976 def __init__(self): 

+

977 self.beatmap_id = "" 

+

978 self.mods = 0 

+

979 self.acc = 0 

+

980 self.combo = 0 

+

981 self.misses = 0 

+

982 

+

983 self.maxbeatmapcombo = 0 

+

984 

+

985 self.stars = 0 

+

986 

+

987 self.maxpp = 0 

+

988 self.maxaimpp = 0 

+

989 self.maxspeedpp = 0 

+

990 self.maxaccpp = 0 

+

991 

+

992 self.afterar = 0 

+

993 self.aftercs = 0 

+

994 self.afterod = 0 

+

995 self.afterhp = 0 

+

996 

+

997 self.currpp = 0 

+

998 self.curraimpp = 0 

+

999 self.currspeedpp = 0 

+

1000 self.curraccpp = 0 

+

1001 

+

1002 self.fcpp = 0 

+

1003 self.fc95pp = 0 

+

1004 self.fc96pp = 0 

+

1005 self.fc97pp = 0 

+

1006 self.fc98pp = 0 

+

1007 self.fc99pp = 0 

+

1008 

+

1009 

+

1010 def get_beatmap_file(self, beatmap_id): 

+

1011 self.beatmap_id = beatmap_id 

+

1012 

+

1013 if os.path.exists(f'./maps/{beatmap_id}.osu'): 

+

1014 print(f'谱面文件已存在') 

+

1015 else: 

+

1016 try: 

+

1017 url = f'https://osu.ppy.sh/osu/{beatmap_id}' 

+

1018 response = requests.get(url) 

+

1019 response.raise_for_status() # 如果请求失败,这会抛出一个异常 

+

1020 with open(f'./maps/{beatmap_id}.osu', 'wb') as f: 

+

1021 f.write(response.content) 

+

1022 except: 

+

1023 print("获取谱面文件失败") 

+

1024 

+

1025 def calculate_pp_fully(self, mods): 

+

1026 try: 

+

1027 self.mods = mods 

+

1028 

+

1029 beatmap = rosu.Beatmap(path=f"./maps/{self.beatmap_id}.osu") 

+

1030 

+

1031 max_perf = rosu.Performance(mods=mods) 

+

1032 

+

1033 attrs = max_perf.calculate(beatmap) 

+

1034 

+

1035 self.maxpp = attrs.pp 

+

1036 

+

1037 # 计算maxbeatmapcombo 

+

1038 self.maxbeatmapcombo = attrs.difficulty.max_combo 

+

1039 

+

1040 # 计算stars 

+

1041 self.stars = attrs.difficulty.stars 

+

1042 

+

1043 # 计算4维 

+

1044 beatmap_attr_builder = rosu.BeatmapAttributesBuilder(mods=mods) 

+

1045 beatmap_attr_builder.set_map(beatmap) 

+

1046 beatmap_attr = beatmap_attr_builder.build() 

+

1047 self.afterar = beatmap_attr.ar 

+

1048 self.aftercs = beatmap_attr.cs 

+

1049 self.afterod = beatmap_attr.od 

+

1050 self.afterhp = beatmap_attr.hp 

+

1051 

+

1052 # 计算if 95% pp 

+

1053 max_perf.set_accuracy(95) 

+

1054 fc95_perf = max_perf.calculate(beatmap) 

+

1055 self.fc95pp = fc95_perf.pp 

+

1056 

+

1057 # 计算if 96% pp 

+

1058 max_perf.set_accuracy(96) 

+

1059 fc96_perf = max_perf.calculate(beatmap) 

+

1060 self.fc96pp = fc96_perf.pp 

+

1061 

+

1062 # 计算if 97% pp 

+

1063 max_perf.set_accuracy(97) 

+

1064 fc97_perf = max_perf.calculate(beatmap) 

+

1065 self.fc97pp = fc97_perf.pp 

+

1066 

+

1067 # 计算if 98% pp 

+

1068 max_perf.set_accuracy(98) 

+

1069 fc98_perf = max_perf.calculate(beatmap) 

+

1070 self.fc98pp = fc98_perf.pp 

+

1071 

+

1072 # 计算if 99% pp 

+

1073 max_perf.set_accuracy(99) 

+

1074 fc99_perf = max_perf.calculate(beatmap) 

+

1075 self.fc99pp = fc99_perf.pp 

+

1076 

+

1077 self.maxpp = round(self.maxpp) 

+

1078 self.fc95pp = round(self.fc95pp) 

+

1079 self.fc96pp = round(self.fc96pp) 

+

1080 self.fc97pp = round(self.fc97pp) 

+

1081 self.fc98pp = round(self.fc98pp) 

+

1082 self.fc99pp = round(self.fc99pp) 

+

1083 self.stars = round(self.stars, 2) 

+

1084 

+

1085 self.afterar = round(self.afterar, 1) 

+

1086 self.aftercs = round(self.aftercs, 1) 

+

1087 self.afterod = round(self.afterod, 1) 

+

1088 self.afterhp = round(self.afterhp, 1) 

+

1089 

+

1090 except: 

+

1091 print("计算pp失败") 

+

1092 self.maxpp = 0 

+

1093 self.maxbeatmapcombo = 0 

+

1094 self.fc95pp = 0 

+

1095 self.fc96pp = 0 

+

1096 self.fc97pp = 0 

+

1097 self.fc98pp = 0 

+

1098 self.fc99pp = 0 

+

1099 self.stars = 0 

+

1100 

+

1101 self.afterar = 0 

+

1102 self.aftercs = 0 

+

1103 self.afterod = 0 

+

1104 self.afterhp = 0 

+

1105 

+

1106 return f'{self.mods}| {self.stars}*| {self.maxbeatmapcombo}x| ar:{self.afterar} cs:{self.aftercs} od:{self.afterod} hp:{self.afterhp} | SS:{self.maxpp}pp| 99%:{self.fc99pp}pp| 98%:{self.fc98pp}pp| 97%:{self.fc97pp}pp| 96%:{self.fc96pp}pp| 95%:{self.fc95pp}pp' 

+

1107 

+

1108 def calculate_pp_obj(self, mods, acc, misses, combo): 

+

1109 

+

1110 try: 

+

1111 

+

1112 self.mods = mods 

+

1113 

+

1114 map = rosu.Beatmap(path=f"./maps/{self.beatmap_id}.osu") 

+

1115 

+

1116 max_perf = rosu.Performance(mods=mods) 

+

1117 

+

1118 attrs = max_perf.calculate(map) 

+

1119 

+

1120 self.maxpp = attrs.pp 

+

1121 

+

1122 self.maxbeatmapcombo = attrs.difficulty.max_combo 

+

1123 

+

1124 self.maxaimpp = attrs.pp_aim 

+

1125 self.maxspeedpp = attrs.pp_speed 

+

1126 self.maxaccpp = attrs.pp_accuracy 

+

1127 

+

1128 # 计算玩家的current performance 

+

1129 max_perf.set_misses(misses) 

+

1130 max_perf.set_accuracy(acc) 

+

1131 max_perf.set_combo(combo) 

+

1132 

+

1133 curr_perf = max_perf.calculate(map) 

+

1134 self.currpp = curr_perf.pp 

+

1135 self.curraccpp = curr_perf.pp 

+

1136 self.curraimpp = curr_perf.pp_aim 

+

1137 self.currspeedpp = curr_perf.pp_speed 

+

1138 self.curraccpp = curr_perf.pp_accuracy 

+

1139 

+

1140 # 计算if fc pp 

+

1141 max_perf.set_misses(0) 

+

1142 max_perf.set_combo(None) 

+

1143 

+

1144 fc_perf = max_perf.calculate(map) 

+

1145 self.fcpp = fc_perf.pp 

+

1146 

+

1147 # 计算if 95% pp 

+

1148 max_perf.set_accuracy(95) 

+

1149 fc95_perf = max_perf.calculate(map) 

+

1150 self.fc95pp = fc95_perf.pp 

+

1151 

+

1152 # 计算if 96% pp 

+

1153 max_perf.set_accuracy(96) 

+

1154 fc96_perf = max_perf.calculate(map) 

+

1155 self.fc96pp = fc96_perf.pp 

+

1156 

+

1157 # 计算if 97% pp 

+

1158 max_perf.set_accuracy(97) 

+

1159 fc97_perf = max_perf.calculate(map) 

+

1160 self.fc97pp = fc97_perf.pp 

+

1161 

+

1162 # 计算if 98% pp 

+

1163 max_perf.set_accuracy(98) 

+

1164 fc98_perf = max_perf.calculate(map) 

+

1165 self.fc98pp = fc98_perf.pp 

+

1166 

+

1167 # 计算if 99% pp 

+

1168 max_perf.set_accuracy(99) 

+

1169 fc99_perf = max_perf.calculate(map) 

+

1170 self.fc99pp = fc99_perf.pp 

+

1171 

+

1172 self.maxpp = round(self.maxpp) 

+

1173 self.maxaimpp = round(self.maxaimpp) 

+

1174 self.maxspeedpp = round(self.maxspeedpp) 

+

1175 self.maxaccpp = round(self.maxaccpp) 

+

1176 

+

1177 self.currpp = round(self.currpp) 

+

1178 self.curraimpp = round(self.curraimpp) 

+

1179 self.currspeedpp = round(self.currspeedpp) 

+

1180 self.curraccpp = round(self.curraccpp) 

+

1181 

+

1182 self.fcpp = round(self.fcpp) 

+

1183 self.fc95pp = round(self.fc95pp) 

+

1184 self.fc96pp = round(self.fc96pp) 

+

1185 self.fc97pp = round(self.fc97pp) 

+

1186 self.fc98pp = round(self.fc98pp) 

+

1187 self.fc99pp = round(self.fc99pp) 

+

1188 

+

1189 except Exception as e: 

+

1190 print(f'计算pp失败: {e}') 

+

1191 self.maxpp = 0 

+

1192 self.maxaimpp = 0 

+

1193 self.maxspeedpp = 0 

+

1194 self.maxaccpp = 0 

+

1195 

+

1196 self.maxbeatmapcombo = 0 

+

1197 

+

1198 self.currpp = 0 

+

1199 self.curraimpp = 0 

+

1200 self.currspeedpp = 0 

+

1201 self.curraccpp = 0 

+

1202 

+

1203 self.fcpp = 0 

+

1204 self.fc95pp = 0 

+

1205 self.fc96pp = 0 

+

1206 self.fc97pp = 0 

+

1207 self.fc98pp = 0 

+

1208 self.fc99pp = 0 

+

1209 

+

1210 return f'now:{self.currpp}pp| if FC({self.maxbeatmapcombo}x):{self.fcpp}pp| 95%:{self.fc95pp}pp| 96%:{self.fc96pp}pp| 97%:{self.fc97pp}pp| 98%:{self.fc98pp}pp| 99%:{self.fc99pp}pp| SS:{self.maxpp}pp| aim:{self.curraimpp}/{self.maxaimpp}pp| speed:{self.currspeedpp}/{self.maxspeedpp}pp| acc:{self.curraccpp}/{self.maxaccpp}pp' 

+

1211 

+

1212config = Config() 

+

1213 

+

1214if __name__ == '__main__': 

+

1215 # 没有maps文件夹时自动创建maps文件夹 

+

1216 maps_dir = os.path.join(os.getcwd(), './maps') 

+

1217 if not os.path.exists(maps_dir): 

+

1218 os.makedirs(maps_dir) 

+

1219 print(f"'{maps_dir}'文件夹不存在,已经自动创建") 

+

1220 

+

1221 client_id = config.osuclientid 

+

1222 client_secret = config.osuclientsecret 

+

1223 

+

1224 osu_nickname = config.osunickname 

+

1225 osu_password = config.osupassword 

+

1226 

+

1227 p = Player() 

+

1228 r = Room() 

+

1229 b = Beatmap(client_id, client_secret) 

+

1230 pp = PP() 

+

1231 

+

1232 client = MyIRCClient(osu_server, osu_port, osu_nickname, osu_password) 

+

1233 client.start() 

+
+ + + diff --git a/htmlcov/keybd_closed_cb_ce680311.png b/htmlcov/keybd_closed_cb_ce680311.png new file mode 100644 index 0000000000000000000000000000000000000000..ba119c47df81ed2bbd27a06988abf700139c4f99 GIT binary patch literal 9004 zcmeHLc{tSF+aIY=A^R4_poB4tZAN2XC;O7M(inrW3}(h&Q4}dl*&-65$i9^&vW6_# zcM4g`Qix=GhkBl;=lwnJ@Ap2}^}hc-b6vBXb3XUyzR%~}_c`-Dw+!?&>5p(90RRB> zXe~7($~PP3eT?=X<@3~Q1w84vX~IoSx~1#~02+TopXK(db;4v6!{+W`RHLkkHO zo;+s?)puc`+$yOwHv>I$5^8v^F3<|$44HA8AFnFB0cAP|C`p}aSMJK*-CUB{eQ!;K z-9Ju3OQ+xVPr3P#o4>_lNBT;M+1vgV&B~6!naOGHb-LFA9TkfHv1IFA1Y!Iz!Zl3) z%c#-^zNWPq7U_}6I7aHSmFWi125RZrNBKyvnV^?64)zviS;E!UD%LaGRl6@zn!3E{ zJ`B$5``cH_3a)t1#6I7d==JeB_IcSU%=I#DrRCBGm8GvCmA=+XHEvC2SIfsNa0(h9 z7P^C4U`W@@`9p>2f^zyb5B=lpc*RZMn-%%IqrxSWQF8{ec3i?-AB(_IVe z)XgT>Y^u41MwOMFvU=I4?!^#jaS-%bjnx@ zmL44yVEslR_ynm18F!u}Ru#moEn3EE?1=9@$B1Z5aLi5b8{&?V(IAYBzIar!SiY3< z`l0V)djHtrImy}(!7x-Pmq+njM)JFQ9mx*(C+9a3M)(_SW|lrN=gfxFhStu^zvynS zm@gl;>d8i8wpUkX42vS3BEzE3-yctH%t0#N%s+6-&_<*Fe7+h=`=FM?DOg1)eGL~~ zQvIFm$D*lqEh07XrXY=jb%hdyP4)`wyMCb$=-z9(lOme9=tirVkb)_GOl2MJn;=Ky z^0pV1owR7KP-BSxhI@@@+gG0roD-kXE1;!#R7KY1QiUbyDdTElm|ul7{mMdF1%UDJ z_vp=Vo!TCF?D*?u% zk~}4!xK2MSQd-QKC0${G=ZRv2x8%8ZqdfR!?Dv=5Mj^8WU)?iH;C?o6rSQy*^YwQb zf@5V)q=xah#a3UEIBC~N7on(p4jQd4K$|i7k`d8mw|M{Mxapl46Z^X^9U}JgqH#;T z`CTzafpMD+J-LjzF+3Xau>xM_sXisRj6m-287~i9g|%gHc}v77>n_+p7ZgmJszx!b zSmL4wV;&*5Z|zaCk`rOYFdOjZLLQr!WSV6AlaqYh_OE)>rYdtx`gk$yAMO=-E1b~J zIZY6gM*}1UWsJ)TW(pf1=h?lJy_0TFOr|nALGW>$IE1E7z+$`^2WJY+>$$nJo8Rs` z)xS>AH{N~X3+b=2+8Q_|n(1JoGv55r>TuwBV~MXE&9?3Zw>cIxnOPNs#gh~C4Zo=k z&!s;5)^6UG>!`?hh0Q|r|Qbm>}pgtOt23Vh!NSibozH$`#LSiYL)HR4bkfEJMa zBHwC3TaHx|BzD|MXAr>mm&FbZXeEX-=W}Ji&!pji4sO$#0Wk^Q7j%{8#bJPn$C=E% zPlB}0)@Ti^r_HMJrTMN?9~4LQbIiUiOKBVNm_QjABKY4;zC88yVjvB>ZETNzr%^(~ zI3U&Ont?P`r&4 z#Bp)jcVV_N_{c1_qW}_`dQm)D`NG?h{+S!YOaUgWna4i8SuoLcXAZ|#Jh&GNn7B}3 z?vZ8I{LpmCYT=@6)dLPd@|(;d<08ufov%+V?$mgUYQHYTrc%eA=CDUzK}v|G&9}yJ z)|g*=+RH1IQ>rvkY9UIam=fkxWDyGIKQ2RU{GqOQjD8nG#sl+$V=?wpzJdT=wlNWr z1%lw&+;kVs(z?e=YRWRA&jc75rQ~({*TS<( z8X!j>B}?Bxrrp%wEE7yBefQ?*nM20~+ZoQK(NO_wA`RNhsqVkXHy|sod@mqen=B#@ zmLi=x2*o9rWqTMWoB&qdZph$~qkJJTVNc*8^hU?gH_fY{GYPEBE8Q{j0Y$tvjMv%3 z)j#EyBf^7n)2d8IXDYX2O0S%ZTnGhg4Ss#sEIATKpE_E4TU=GimrD5F6K(%*+T-!o z?Se7^Vm`$ZKDwq+=~jf?w0qC$Kr&R-;IF#{iLF*8zKu8(=#chRO;>x zdM;h{i{RLpJgS!B-ueTFs8&4U4+D8|7nP~UZ@P`J;*0sj^#f_WqT#xpA?@qHonGB& zQ<^;OLtOG1w#)N~&@b0caUL7syAsAxV#R`n>-+eVL9aZwnlklzE>-6!1#!tVA`uNo z>Gv^P)sohc~g_1YMC;^f(N<{2y5C^;QCEXo;LQ^#$0 zr>jCrdoeXuff!dJ^`#=Wy2Gumo^Qt7BZrI~G+Pyl_kL>is3P0^JlE;Sjm-YfF~I>t z_KeNpK|5U&F4;v?WS&#l(jxUWDarfcIcl=-6!8>^S`57!M6;hZea5IFA@)2+*Rt85 zi-MBs_b^DU8LygXXQGkG+86N7<%M|baM(orG*ASffC`p!?@m{qd}IcYmZyi^d}#Q& zNjk-0@CajpUI-gPm20ERVDO!L8@p`tMJ69FD(ASIkdoLdiRV6h9TPKRz>2WK4upHd z6OZK33EP?`GoJkXh)S035}uLUO$;TlXwNdMg-WOhLB)7a`-%*a9lFmjf6n+4ZmIHN z-V@$ z8PXsoR4*`5RwXz=A8|5;aXKtSHFccj%dG7cO~UBJnt)61K>-uPX)`vu{7fcX6_>zZ zw_2V&Li+7mxbf!f7{Rk&VVyY!UtZywac%g!cH+xh#j$a`uf?XWl<``t`36W;p7=_* zO6uf~2{sAdkZn=Ts@p0>8N8rzw2ZLS@$ibV-c-QmG@%|3gUUrRxu=e*ekhTa+f?8q z3$JVGPr9w$VQG~QCq~Y=2ThLIH!T@(>{NihJ6nj*HA_C#Popv)CBa)+UI-bx8u8zfCT^*1|k z&N9oFYsZEijPn31Yx_yO5pFs>0tOAV=oRx~Wpy5ie&S_449m4R^{LWQMA~}vocV1O zIf#1ZV85E>tvZE4mz~zn{hs!pkIQM;EvZMimqiPAJu-9P@mId&nb$lsrICS=)zU3~ zn>a#9>}5*3N)9;PTMZ)$`5k} z?iG}Rwj$>Y*|(D3S3e&fxhaPHma8@vwu(cwdlaCjX+NIK6=$H4U`rfzcWQVOhp{fnzuZhgCCGpw|p zTi`>cv~xVzdx|^`C0vXdlMwPae3S?>3|7v$e*Bs6-5gS>>FMHk_r2M(ADOV{KV7+6 zA@5Q(mdx%7J}MY}K461iuQ}5GwDGI=Yc&g0MZHu)7gC3{5@QZj6SJl*o0MS2Cl_ia zyK?9QmC9tJ6yn{EA-erJ4wk$+!E#X(s~9h^HOmQ_|6V_s1)k;%9Q6Niw}SyT?jxl4 z;HYz2$Nj$8Q_*Xo`TWEUx^Q9b+ik@$o39`mlY&P}G8wnjdE+Dlj?uL;$aB$n;x zWoh-M_u>9}_Ok@d_uidMqz10zJc}RQijPW3Fs&~1am=j*+A$QWTvxf9)6n;n8zTQW z!Q_J1%apTsJzLF`#^P_#mRv2Ya_keUE7iMSP!ha-WQoo0vZZG?gyR;+4q8F6tL#u< zRj8Hu5f-p1$J;)4?WpGL{4@HmJ6&tF9A5Tc8Trp>;Y>{^s?Q1&bam}?OjsnKd?|Z82aix26wUOLxbEW~E)|CgJ#)MLf_me# zv4?F$o@A~Um)6>HlM0=3Bd-vc91EM}D+t6-@!}O%i*&Wl%@#C8X+?5+nv`oPu!!=5 znbL+Fk_#J_%8vOq^FIv~5N(nk03kyo1p@l|1c+rO^zCG3bk2?|%AF;*|4si1XM<`a z1NY0-8$wv?&129!(g_A1lXR!+pD*1*cF?T~e1d6*G1Fz)jcSaZoKpxtA%FNnKP2jo zLXn@OR#1z@6zuH%mMB98}-t zHJqClsZ!G5xMSgIs_=<8sBePXxfoXsuvy`|buON9BX%s-o>OVLA)k3W=wKnw1?so$ zEjm0aS=zu@Xu#;{A)QTjJ$a9_={++ACkRY*sk3jLk&Fu}RxR<-DXR<`5`$VNG*wJE zidM6VzaQ!M0gbQM98@x@;#0qUS8O)p6mrYwTk*;8J~!ovbY6jon^Ki}uggd3#J5G8 z>awvtF85Y<9yE{Iag}J7O7)1O=ylk^255@XmV5J06-{xaaSNASZoTKKp~$tSxdUI~ zU1RZ&UuW37Ro&_ryj^cSt$Jd&pt|+h!A&dwcr&`S=R5E`=6Tm`+(qGm@$YZ8(8@a$ zXfo@Rwtvm7N3RMmVCb7radAs-@QtCXx^CQ-<)V>QPLZy@jH{#dc4#(y zV)6Hp{ZMz!|NG8!>i01gZMy)G<8Hf2X7e&LH_gOaajW<<^Xi55@OnlY*|S|*TS8;u_nHbv7lgmmZ+Q<5 zi!*lLCJmdpyzl(L${$C?(pVo|oR%r~x_B_ocPePa_);27^=n4L=`toZ;xdBut9rSv z?wDQ7j2I3WQBdhz%X7`2YaG_y|wA!7|s?k;A&WNMLMTZEzCaE^d??E&u?f=ejQBR~|< z)=thyP2(p8r6mt?Ad}tXAP_GvF9|P630I;$1cpQ+Ay7C34hK^ZV3H4kjPV8&NP>G5 zKRDEIBrFl{M#j4mfP0)68&?mqJP1S?2mU0djAGTjDV;wZ?6vplNn~3Hn$nP>%!dMi zz@bnC7zzi&k&s{QDWkf&zgrVXKUJjY3Gv3bL0}S4h>OdgEJ$Q^&p-VAr3J}^a*+rz z!jW7(h*+GuCyqcC{MD(Ovj^!{pB^OKUe|uy&bD?CN>KZrf3?v>>l*xSvnQiH-o^ViN$%FRdm9url;%(*jf5H$*S)8;i0xWHdl>$p);nH9v0)YfW?Vz$! zNCeUbi9`NEg(i^57y=fzM@1o*z*Bf6?QCV>2p9}(BLlYsOCfMjFv1pw1mlo)Py{8v zppw{MDfEeWN+n>Ne~oI7%9cU}mz0r3!es2gNF0t5jkGipjIo2lz;-e)7}Ul_#!eDv zw;#>kI>;#-pyfeu3Fsd^2F@6=oh#8r9;A!G0`-mm7%{=S;Ec(bJ=I_`FodKGQVNEY zmXwr4{9*jpDl%4{ggQZ5Ac z%wYTdl*!1c5^)%^E78Q&)ma|27c6j(a=)g4sGrp$r{jv>>M2 z6y)E5|Aooe!PSfKzvKA>`a6pfK3=E8vL14ksP&f=>gOP?}rG6ye@9ZR3 zJF*vsh*P$w390i!FV~~_Hv6t2Zl<4VUi|rNja#boFt{%q~xGb z(2petq9A*_>~B*>?d?Olx^lmYg4)}sH2>G42RE; literal 0 HcmV?d00001 diff --git a/htmlcov/status.json b/htmlcov/status.json new file mode 100644 index 0000000..53ff2d4 --- /dev/null +++ b/htmlcov/status.json @@ -0,0 +1 @@ +{"note":"This file is an internal implementation detail to speed up HTML report generation. Its format can change at any time. You might be looking for the JSON report: https://coverage.rtfd.io/cmd.html#cmd-json","format":5,"version":"7.6.4","globals":"7722bd33af8ef24175ac77e782f1542f","files":{"irc_dlient_py":{"hash":"1fa59ce03a1d9ab0412319ffb3832540","index":{"url":"irc_dlient_py.html","file":"irc_dlient.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":901,"n_excluded":0,"n_missing":555,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}}}} \ No newline at end of file diff --git a/htmlcov/style_cb_718ce007.css b/htmlcov/style_cb_718ce007.css new file mode 100644 index 0000000..3cdaf05 --- /dev/null +++ b/htmlcov/style_cb_718ce007.css @@ -0,0 +1,337 @@ +@charset "UTF-8"; +/* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ +/* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */ +/* Don't edit this .css file. Edit the .scss file instead! */ +html, body, h1, h2, h3, p, table, td, th { margin: 0; padding: 0; border: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } + +body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-size: 1em; background: #fff; color: #000; } + +@media (prefers-color-scheme: dark) { body { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { body { color: #eee; } } + +html > body { font-size: 16px; } + +a:active, a:focus { outline: 2px dashed #007acc; } + +p { font-size: .875em; line-height: 1.4em; } + +table { border-collapse: collapse; } + +td { vertical-align: top; } + +table tr.hidden { display: none !important; } + +p#no_rows { display: none; font-size: 1.15em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; } + +a.nav { text-decoration: none; color: inherit; } + +a.nav:hover { text-decoration: underline; color: inherit; } + +.hidden { display: none; } + +header { background: #f8f8f8; width: 100%; z-index: 2; border-bottom: 1px solid #ccc; } + +@media (prefers-color-scheme: dark) { header { background: black; } } + +@media (prefers-color-scheme: dark) { header { border-color: #333; } } + +header .content { padding: 1rem 3.5rem; } + +header h2 { margin-top: .5em; font-size: 1em; } + +header h2 a.button { font-family: inherit; font-size: inherit; border: 1px solid; border-radius: .2em; background: #eee; color: inherit; text-decoration: none; padding: .1em .5em; margin: 1px calc(.1em + 1px); cursor: pointer; border-color: #ccc; } + +@media (prefers-color-scheme: dark) { header h2 a.button { background: #333; } } + +@media (prefers-color-scheme: dark) { header h2 a.button { border-color: #444; } } + +header h2 a.button.current { border: 2px solid; background: #fff; border-color: #999; cursor: default; } + +@media (prefers-color-scheme: dark) { header h2 a.button.current { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { header h2 a.button.current { border-color: #777; } } + +header p.text { margin: .5em 0 -.5em; color: #666; font-style: italic; } + +@media (prefers-color-scheme: dark) { header p.text { color: #aaa; } } + +header.sticky { position: fixed; left: 0; right: 0; height: 2.5em; } + +header.sticky .text { display: none; } + +header.sticky h1, header.sticky h2 { font-size: 1em; margin-top: 0; display: inline-block; } + +header.sticky .content { padding: 0.5rem 3.5rem; } + +header.sticky .content p { font-size: 1em; } + +header.sticky ~ #source { padding-top: 6.5em; } + +main { position: relative; z-index: 1; } + +footer { margin: 1rem 3.5rem; } + +footer .content { padding: 0; color: #666; font-style: italic; } + +@media (prefers-color-scheme: dark) { footer .content { color: #aaa; } } + +#index { margin: 1rem 0 0 3.5rem; } + +h1 { font-size: 1.25em; display: inline-block; } + +#filter_container { float: right; margin: 0 2em 0 0; line-height: 1.66em; } + +#filter_container #filter { width: 10em; padding: 0.2em 0.5em; border: 2px solid #ccc; background: #fff; color: #000; } + +@media (prefers-color-scheme: dark) { #filter_container #filter { border-color: #444; } } + +@media (prefers-color-scheme: dark) { #filter_container #filter { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { #filter_container #filter { color: #eee; } } + +#filter_container #filter:focus { border-color: #007acc; } + +#filter_container :disabled ~ label { color: #ccc; } + +@media (prefers-color-scheme: dark) { #filter_container :disabled ~ label { color: #444; } } + +#filter_container label { font-size: .875em; color: #666; } + +@media (prefers-color-scheme: dark) { #filter_container label { color: #aaa; } } + +header button { font-family: inherit; font-size: inherit; border: 1px solid; border-radius: .2em; background: #eee; color: inherit; text-decoration: none; padding: .1em .5em; margin: 1px calc(.1em + 1px); cursor: pointer; border-color: #ccc; } + +@media (prefers-color-scheme: dark) { header button { background: #333; } } + +@media (prefers-color-scheme: dark) { header button { border-color: #444; } } + +header button:active, header button:focus { outline: 2px dashed #007acc; } + +header button.run { background: #eeffee; } + +@media (prefers-color-scheme: dark) { header button.run { background: #373d29; } } + +header button.run.show_run { background: #dfd; border: 2px solid #00dd00; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { header button.run.show_run { background: #373d29; } } + +header button.mis { background: #ffeeee; } + +@media (prefers-color-scheme: dark) { header button.mis { background: #4b1818; } } + +header button.mis.show_mis { background: #fdd; border: 2px solid #ff0000; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { header button.mis.show_mis { background: #4b1818; } } + +header button.exc { background: #f7f7f7; } + +@media (prefers-color-scheme: dark) { header button.exc { background: #333; } } + +header button.exc.show_exc { background: #eee; border: 2px solid #808080; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { header button.exc.show_exc { background: #333; } } + +header button.par { background: #ffffd5; } + +@media (prefers-color-scheme: dark) { header button.par { background: #650; } } + +header button.par.show_par { background: #ffa; border: 2px solid #bbbb00; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { header button.par.show_par { background: #650; } } + +#help_panel, #source p .annotate.long { display: none; position: absolute; z-index: 999; background: #ffffcc; border: 1px solid #888; border-radius: .2em; color: #333; padding: .25em .5em; } + +#source p .annotate.long { white-space: normal; float: right; top: 1.75em; right: 1em; height: auto; } + +#help_panel_wrapper { float: right; position: relative; } + +#keyboard_icon { margin: 5px; } + +#help_panel_state { display: none; } + +#help_panel { top: 25px; right: 0; padding: .75em; border: 1px solid #883; color: #333; } + +#help_panel .keyhelp p { margin-top: .75em; } + +#help_panel .legend { font-style: italic; margin-bottom: 1em; } + +.indexfile #help_panel { width: 25em; } + +.pyfile #help_panel { width: 18em; } + +#help_panel_state:checked ~ #help_panel { display: block; } + +kbd { border: 1px solid black; border-color: #888 #333 #333 #888; padding: .1em .35em; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-weight: bold; background: #eee; border-radius: 3px; } + +#source { padding: 1em 0 1em 3.5rem; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; } + +#source p { position: relative; white-space: pre; } + +#source p * { box-sizing: border-box; } + +#source p .n { float: left; text-align: right; width: 3.5rem; box-sizing: border-box; margin-left: -3.5rem; padding-right: 1em; color: #999; user-select: none; } + +@media (prefers-color-scheme: dark) { #source p .n { color: #777; } } + +#source p .n.highlight { background: #ffdd00; } + +#source p .n a { scroll-margin-top: 6em; text-decoration: none; color: #999; } + +@media (prefers-color-scheme: dark) { #source p .n a { color: #777; } } + +#source p .n a:hover { text-decoration: underline; color: #999; } + +@media (prefers-color-scheme: dark) { #source p .n a:hover { color: #777; } } + +#source p .t { display: inline-block; width: 100%; box-sizing: border-box; margin-left: -.5em; padding-left: 0.3em; border-left: 0.2em solid #fff; } + +@media (prefers-color-scheme: dark) { #source p .t { border-color: #1e1e1e; } } + +#source p .t:hover { background: #f2f2f2; } + +@media (prefers-color-scheme: dark) { #source p .t:hover { background: #282828; } } + +#source p .t:hover ~ .r .annotate.long { display: block; } + +#source p .t .com { color: #008000; font-style: italic; line-height: 1px; } + +@media (prefers-color-scheme: dark) { #source p .t .com { color: #6a9955; } } + +#source p .t .key { font-weight: bold; line-height: 1px; } + +#source p .t .str { color: #0451a5; } + +@media (prefers-color-scheme: dark) { #source p .t .str { color: #9cdcfe; } } + +#source p.mis .t { border-left: 0.2em solid #ff0000; } + +#source p.mis.show_mis .t { background: #fdd; } + +@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t { background: #4b1818; } } + +#source p.mis.show_mis .t:hover { background: #f2d2d2; } + +@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t:hover { background: #532323; } } + +#source p.run .t { border-left: 0.2em solid #00dd00; } + +#source p.run.show_run .t { background: #dfd; } + +@media (prefers-color-scheme: dark) { #source p.run.show_run .t { background: #373d29; } } + +#source p.run.show_run .t:hover { background: #d2f2d2; } + +@media (prefers-color-scheme: dark) { #source p.run.show_run .t:hover { background: #404633; } } + +#source p.exc .t { border-left: 0.2em solid #808080; } + +#source p.exc.show_exc .t { background: #eee; } + +@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t { background: #333; } } + +#source p.exc.show_exc .t:hover { background: #e2e2e2; } + +@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t:hover { background: #3c3c3c; } } + +#source p.par .t { border-left: 0.2em solid #bbbb00; } + +#source p.par.show_par .t { background: #ffa; } + +@media (prefers-color-scheme: dark) { #source p.par.show_par .t { background: #650; } } + +#source p.par.show_par .t:hover { background: #f2f2a2; } + +@media (prefers-color-scheme: dark) { #source p.par.show_par .t:hover { background: #6d5d0c; } } + +#source p .r { position: absolute; top: 0; right: 2.5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; } + +#source p .annotate { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; color: #666; padding-right: .5em; } + +@media (prefers-color-scheme: dark) { #source p .annotate { color: #ddd; } } + +#source p .annotate.short:hover ~ .long { display: block; } + +#source p .annotate.long { width: 30em; right: 2.5em; } + +#source p input { display: none; } + +#source p input ~ .r label.ctx { cursor: pointer; border-radius: .25em; } + +#source p input ~ .r label.ctx::before { content: "▶ "; } + +#source p input ~ .r label.ctx:hover { background: #e8f4ff; color: #666; } + +@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { background: #0f3a42; } } + +@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { color: #aaa; } } + +#source p input:checked ~ .r label.ctx { background: #d0e8ff; color: #666; border-radius: .75em .75em 0 0; padding: 0 .5em; margin: -.25em 0; } + +@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { background: #056; } } + +@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { color: #aaa; } } + +#source p input:checked ~ .r label.ctx::before { content: "▼ "; } + +#source p input:checked ~ .ctxs { padding: .25em .5em; overflow-y: scroll; max-height: 10.5em; } + +#source p label.ctx { color: #999; display: inline-block; padding: 0 .5em; font-size: .8333em; } + +@media (prefers-color-scheme: dark) { #source p label.ctx { color: #777; } } + +#source p .ctxs { display: block; max-height: 0; overflow-y: hidden; transition: all .2s; padding: 0 .5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; white-space: nowrap; background: #d0e8ff; border-radius: .25em; margin-right: 1.75em; text-align: right; } + +@media (prefers-color-scheme: dark) { #source p .ctxs { background: #056; } } + +#index { font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.875em; } + +#index table.index { margin-left: -.5em; } + +#index td, #index th { text-align: right; padding: .25em .5em; border-bottom: 1px solid #eee; } + +@media (prefers-color-scheme: dark) { #index td, #index th { border-color: #333; } } + +#index td.name, #index th.name { text-align: left; width: auto; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; min-width: 15em; } + +#index th { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-style: italic; color: #333; cursor: pointer; } + +@media (prefers-color-scheme: dark) { #index th { color: #ddd; } } + +#index th:hover { background: #eee; } + +@media (prefers-color-scheme: dark) { #index th:hover { background: #333; } } + +#index th .arrows { color: #666; font-size: 85%; font-family: sans-serif; font-style: normal; pointer-events: none; } + +#index th[aria-sort="ascending"], #index th[aria-sort="descending"] { white-space: nowrap; background: #eee; padding-left: .5em; } + +@media (prefers-color-scheme: dark) { #index th[aria-sort="ascending"], #index th[aria-sort="descending"] { background: #333; } } + +#index th[aria-sort="ascending"] .arrows::after { content: " ▲"; } + +#index th[aria-sort="descending"] .arrows::after { content: " ▼"; } + +#index td.name { font-size: 1.15em; } + +#index td.name a { text-decoration: none; color: inherit; } + +#index td.name .no-noun { font-style: italic; } + +#index tr.total td, #index tr.total_dynamic td { font-weight: bold; border-top: 1px solid #ccc; border-bottom: none; } + +#index tr.region:hover { background: #eee; } + +@media (prefers-color-scheme: dark) { #index tr.region:hover { background: #333; } } + +#index tr.region:hover td.name { text-decoration: underline; color: inherit; } + +#scroll_marker { position: fixed; z-index: 3; right: 0; top: 0; width: 16px; height: 100%; background: #fff; border-left: 1px solid #eee; will-change: transform; } + +@media (prefers-color-scheme: dark) { #scroll_marker { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { #scroll_marker { border-color: #333; } } + +#scroll_marker .marker { background: #ccc; position: absolute; min-height: 3px; width: 100%; } + +@media (prefers-color-scheme: dark) { #scroll_marker .marker { background: #444; } } diff --git a/test_irc_client.py b/test_irc_client.py deleted file mode 100644 index 750f8e1..0000000 --- a/test_irc_client.py +++ /dev/null @@ -1,154 +0,0 @@ -import unittest -from unittest.mock import patch, MagicMock -import irc_dlient - -class TestConfig(unittest.TestCase): - @patch('irc_dlient.configparser.ConfigParser') - @patch('irc_dlient.chardet.detect') - @patch('builtins.open') - def test_config_initialization(self, mock_open, mock_detect, mock_configparser): - # 设置模拟返回值 - mock_detect.return_value = {'encoding': 'utf-8'} - mock_config = MagicMock() - mock_config.__getitem__.return_value = { - 'client_id': 'test_id', - 'client_secret': 'test_secret', - 'nickname': 'test_nick', - 'password': 'test_pass', - 'mpname': 'test_mp', - 'starlimit': '5.0', - 'timelimit': '300', - 'mppassword': 'mp_pass' - } - mock_configparser.return_value = mock_config - - config = irc_dlient.Config() - - self.assertEqual(config.osuclientid, 'test_id') - self.assertEqual(config.osuclientsecret, 'test_secret') - self.assertEqual(config.osunickname, 'test_nick') - self.assertEqual(config.osupassword, 'test_pass') - self.assertEqual(config.mpname, 'test_mp') - self.assertEqual(config.starlimit, '5.0') - self.assertEqual(config.timelimit, '300') - self.assertEqual(config.mppassword, 'mp_pass') - -class TestPlayer(unittest.TestCase): - def setUp(self): - self.player = irc_dlient.Player() - - def test_add_player(self): - self.player.add_player("Player1") - self.assertIn("Player1", self.player.player_list) - - def test_remove_player(self): - self.player.add_player("Player1") - self.player.remove_player("Player1") - self.assertNotIn("Player1", self.player.player_list) - - def test_add_host(self): - self.player.add_host("Host1") - self.assertIn("Host1", self.player.room_host_list) - - def test_remove_host(self): - self.player.add_host("Host1") - self.player.remove_host("Host1") - self.assertNotIn("Host1", self.player.room_host_list) - -class TestBeatmap(unittest.TestCase): - def setUp(self): - # 从 config.ini 中读取 client_id 和 client_secret - config = irc_dlient.Config() - self.client_id = config.osuclientid - self.client_secret = config.osuclientsecret - - # 实例化 Beatmap 对象并设置 client_id 和 client_secret - self.beatmap = irc_dlient.Beatmap(self.client_id, self.client_secret) - - @patch('irc_dlient.requests.post') - def test_get_token_success(self, mock_post): - mock_response = MagicMock() - mock_response.raise_for_status = MagicMock() - mock_response.json.return_value = {'access_token': 'test_token'} - mock_post.return_value = mock_response - - self.beatmap.get_token() - - self.assertEqual(self.beatmap.osu_token, 'test_token') - - def test_get_beatmap_info_success(self): - """ - 这个测试用例将发送真实的 HTTP 请求到 osu! API - 并验证返回的真实响应是否符合预期。 - """ - # 获取 Token - self.beatmap.get_token() - self.assertIsNotNone(self.beatmap.osu_token, "Token 获取失败") - - # 设置 beatmap_id - self.beatmap.beatmap_id = '75' # osu第一个ranked图 - - # 调用获取 beatmap 信息的方法 - self.beatmap.get_beatmap_info() - - # 验证响应内容 - self.assertEqual(self.beatmap.beatmap_name, 'DISCO PRINCE') - self.assertEqual(self.beatmap.beatmap_songs_id, '1') - self.assertEqual(self.beatmap.beatmap_artist, 'Kenji Ninuma') - self.assertEqual(self.beatmap.beatmap_star, 2.55) - self.assertEqual(self.beatmap.beatmap_status, 'ranked') - self.assertEqual(self.beatmap.beatmap_bpm, 120) - self.assertEqual(self.beatmap.beatmap_cs, 4) - self.assertEqual(self.beatmap.beatmap_ar, 6) - self.assertEqual(self.beatmap.beatmap_od, 6) - self.assertEqual(self.beatmap.beatmap_hp, 6) - self.assertEqual(self.beatmap.beatmap_length, 142) - self.assertEqual(self.beatmap.beatmap_ranked_date, '2007-10-06') - self.assertEqual(self.beatmap.beatmap_osudirect_url, 'https://osu.ppy.sh/beatmaps/75') - self.assertEqual(self.beatmap.beatmap_mirror_sayo_url, 'https://osu.sayobot.cn/home?search=1') - self.assertEqual(self.beatmap.beatmap_mirror_inso_url, 'http://inso.link/yukiho/?b=75') - -class TestPP(unittest.TestCase): - @patch('irc_dlient.os.path.exists') - @patch('irc_dlient.requests.get') - def test_get_beatmap_file_exists(self, mock_get, mock_exists): - mock_exists.return_value = True - pp = irc_dlient.PP() - pp.get_beatmap_file('12345') - mock_get.assert_not_called() - - @patch('irc_dlient.os.path.exists') - @patch('irc_dlient.requests.get') - def test_get_beatmap_file_download(self, mock_get, mock_exists): - mock_exists.return_value = False - mock_response = MagicMock() - mock_response.raise_for_status = MagicMock() - mock_response.content = b'osu file content' - mock_get.return_value = mock_response - - pp = irc_dlient.PP() - pp.get_beatmap_file('12345') - - mock_get.assert_called_with('https://osu.ppy.sh/osu/12345') - # 可以进一步检查文件写入,但需要更多的patching - -class TestRoom(unittest.TestCase): - @patch('builtins.open') - def test_save_last_room_id(self, mock_open): - mock_file = MagicMock() - mock_open.return_value.__enter__.return_value = mock_file - - room = irc_dlient.Room() - room.room_id = '#room123' - room.save_last_room_id() - - mock_file.write.assert_called_with('#room123') - - @patch('builtins.open', side_effect=FileNotFoundError) - def test_get_last_room_id_file_not_found(self, mock_open): - room = irc_dlient.Room() - last_id = room.get_last_room_id() - self.assertEqual(last_id, '') - -if __name__ == '__main__': - unittest.main() \ No newline at end of file From edd5029a6f45790821adde6c49c5ce0d3bf4505e Mon Sep 17 00:00:00 2001 From: LittleStone <1526631427@qq.com> Date: Tue, 5 Nov 2024 17:38:50 +0800 Subject: [PATCH 5/9] =?UTF-8?q?fix:=E9=85=8D=E7=BD=AECI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5e3ab53 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,40 @@ +# .github/workflows/ci.yml +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: 检出代码 + uses: actions/checkout@v3 + + - name: 设置 Python 版本 + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: 安装依赖项 + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install coverage codecov + + - name: 运行测试并收集覆盖率 + run: | + coverage run -m unittest discover -s tests + coverage xml + + - name: 上传覆盖率到 Codecov + uses: codecov/codecov-action@v3 + with: + files: coverage.xml + flags: unittests + name: codecov-umbrella + fail_ci_if_error: true \ No newline at end of file From b169e3a7cd2f2f578fe220d0e10833c6e780b6f6 Mon Sep 17 00:00:00 2001 From: LittleStone <1526631427@qq.com> Date: Tue, 5 Nov 2024 21:06:20 +0800 Subject: [PATCH 6/9] =?UTF-8?q?fix:=E4=BF=AE=E6=94=B9UT=20=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E9=9C=80=E8=A6=81=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- irc_dlient.py | 46 ++++---- tests/test_irc_client.py | 233 +++++++++++++++++++++++++++------------ 2 files changed, 186 insertions(+), 93 deletions(-) diff --git a/irc_dlient.py b/irc_dlient.py index c9f1d9f..6eb4564 100755 --- a/irc_dlient.py +++ b/irc_dlient.py @@ -37,10 +37,11 @@ def __init__(self): # 定义IRC客户端类 class MyIRCClient: - def __init__(self, server, port, nickname, password): + def __init__(self, server, port, config): self.irc_react = irc.client.Reactor() + self.config = config self.server = self.irc_react.server() - self.server.connect(server, port, nickname, password) + self.server.connect(server, port, config.osunickname, config.osupassword) self.irc_react.add_global_handler("welcome", self.on_connect) self.irc_react.add_global_handler("pubmsg", self.on_pubmsg) self.irc_react.add_global_handler("privmsg", self.on_privmsg) @@ -276,13 +277,13 @@ def on_pubmsg(self, connection, event): if b.check_beatmap_if_out_of_star(): r.send_msg(connection, event, - f'{b.beatmap_star}*>{config.starlimit}* 请重新选择') + f'{b.beatmap_star}*>{self.config.starlimit}* 请重新选择') r.change_beatmap_to(connection, event, last_beatmap_id) b.change_beatmap_id(last_beatmap_id) return if b.check_beatmap_if_out_of_time(): r.send_msg(connection, event, - f'{b.beatmap_length}s>{config.timelimit}s 请重新选择') + f'{b.beatmap_length}s>{self.config.timelimit}s 请重新选择') r.change_beatmap_to(connection, event, last_beatmap_id) b.change_beatmap_id(last_beatmap_id) return @@ -615,10 +616,11 @@ def vote_for_close_room(self, connection, event): # 定义房间操作类 class Room: - def __init__(self): + def __init__(self, config): self.room_id = "" self.last_romm_id = "" self.game_start_time = "" + self.config = config def set_game_start_time(self): self.game_start_time = datetime.now() @@ -629,7 +631,7 @@ def reset_game_start_time(self): def get_last_room_id(self): try: - with open('last_room_id.txt', 'r') as f: + with open(self.config.last_room_id_path, 'r') as f: self.last_romm_id = f.read() print(f'获取上一个房间ID{self.last_romm_id}') except: @@ -658,7 +660,7 @@ def send_msg(self, connection, evetn, msg_text): def create_room(self, connection, event): connection.privmsg( - "BanchoBot", "!mp make "+config.mpname) + "BanchoBot", "!mp make "+self.config.mpname) print("创建房间") def join_room(self, connection, event): @@ -682,7 +684,7 @@ def abort_room(self, connection, event): print("丢弃游戏") def change_password(self, connection, event): - connection.privmsg(self.room_id, "!mp password "+config.mppassword) + connection.privmsg(self.room_id, "!mp password "+self.config.mppassword) print("修改密码") def change_beatmap_to(self, connection, event, beatmapid): @@ -700,9 +702,10 @@ def get_mp_settings(self, connection, event): # 定义谱面类 class Beatmap: - def __init__(self, client_id, client_secret): - self.osu_client_id = client_id - self.osu_client_secret = client_secret + def __init__(self, config): + self.osu_client_id = config.osuclientid + self.osu_client_secret = config.osuclientsecret + self.config = config self.osu_token = "" self.beatmap_id = "" self.beatmap_songs_id = "" @@ -815,17 +818,17 @@ def change_beatmap_id(self, id): print(f'更换谱面ID为 {self.beatmap_id}') def check_beatmap_if_out_of_star(self): - if float(config.starlimit) == 0: + if float(self.config.starlimit) == 0: return False - if self.beatmap_star > float(config.starlimit): + if self.beatmap_star > float(self.config.starlimit): return True else: return False def check_beatmap_if_out_of_time(self): - if float(config.timelimit) == 0: + if float(self.config.timelimit) == 0: return False - if self.beatmap_length > float(config.timelimit): + if self.beatmap_length > float(self.config.timelimit): return True else: return False @@ -1209,7 +1212,6 @@ def calculate_pp_obj(self, mods, acc, misses, combo): return f'now:{self.currpp}pp| if FC({self.maxbeatmapcombo}x):{self.fcpp}pp| 95%:{self.fc95pp}pp| 96%:{self.fc96pp}pp| 97%:{self.fc97pp}pp| 98%:{self.fc98pp}pp| 99%:{self.fc99pp}pp| SS:{self.maxpp}pp| aim:{self.curraimpp}/{self.maxaimpp}pp| speed:{self.currspeedpp}/{self.maxspeedpp}pp| acc:{self.curraccpp}/{self.maxaccpp}pp' -config = Config() if __name__ == '__main__': # 没有maps文件夹时自动创建maps文件夹 @@ -1218,16 +1220,12 @@ def calculate_pp_obj(self, mods, acc, misses, combo): os.makedirs(maps_dir) print(f"'{maps_dir}'文件夹不存在,已经自动创建") - client_id = config.osuclientid - client_secret = config.osuclientsecret - - osu_nickname = config.osunickname - osu_password = config.osupassword + config = Config() p = Player() - r = Room() - b = Beatmap(client_id, client_secret) + r = Room(config) + b = Beatmap(config) pp = PP() - client = MyIRCClient(osu_server, osu_port, osu_nickname, osu_password) + client = MyIRCClient(osu_server, osu_port, config) client.start() diff --git a/tests/test_irc_client.py b/tests/test_irc_client.py index d52733b..899a06c 100644 --- a/tests/test_irc_client.py +++ b/tests/test_irc_client.py @@ -1,4 +1,5 @@ import unittest +import requests from unittest.mock import patch, MagicMock from io import StringIO import irc_dlient @@ -58,13 +59,17 @@ def test_remove_host(self): class TestBeatmap(unittest.TestCase): def setUp(self): - # 从 config.ini 中读取 client_id 和 client_secret - config = irc_dlient.Config() - self.client_id = config.osuclientid - self.client_secret = config.osuclientsecret - - # 实例化 Beatmap 对象并设置 client_id 和 client_secret - self.beatmap = irc_dlient.Beatmap(self.client_id, self.client_secret) + # 构造模拟config + self.mock_config = MagicMock() + self.mock_config.osuclientid = 'test_client_id' + self.mock_config.osuclientsecret = 'test_client_secret' + self.mock_config.timelimit = '100' # Example value + self.mock_config.starlimit = '5.0' + self.mock_config.mpname = 'TestMP' + self.mock_config.mppassword = 'testpassword' + + # 实例化Beatmap + self.beatmap = irc_dlient.Beatmap(self.mock_config) @patch('irc_dlient.requests.post') def test_get_token_success(self, mock_post): @@ -80,12 +85,21 @@ def test_get_token_success(self, mock_post): self.assertEqual(self.beatmap.osu_token, 'test_token') - @patch('irc_dlient.config') - def test_check_beatmap_if_out_of_time(self, mock_config): + def test_clear_cache(self): + """ + 清除缓存 + """ + self.beatmap.osu_token = "test_token" + self.beatmap.id2name = {"test_user": "test_id"} + self.beatmap.clear_cache() + self.assertEqual(self.beatmap.osu_token, "") + self.assertEqual(self.beatmap.id2name, {}) + + def test_check_beatmap_if_out_of_time(self): """ 检查谱面时间限制 """ - mock_config.timelimit = 0 + self.mock_config.timelimit = 0 self.beatmap.beatmap_length = 100 self.beatmap.check_beatmap_if_out_of_time() self.assertEqual(self.beatmap.check_beatmap_if_out_of_time(), False) @@ -94,7 +108,7 @@ def test_check_beatmap_if_out_of_time(self, mock_config): self.beatmap.check_beatmap_if_out_of_time() self.assertEqual(self.beatmap.check_beatmap_if_out_of_time(), False) - mock_config.timelimit = 100 + self.mock_config.timelimit = 100 self.beatmap.beatmap_length = 100 self.beatmap.check_beatmap_if_out_of_time() self.assertEqual(self.beatmap.check_beatmap_if_out_of_time(), False) @@ -103,13 +117,32 @@ def test_check_beatmap_if_out_of_time(self, mock_config): self.beatmap.check_beatmap_if_out_of_time() self.assertEqual(self.beatmap.check_beatmap_if_out_of_time(), True) - def test_get_beatmap_info_success(self): + @patch('irc_dlient.requests.get') + def test_get_beatmap_info_success(self, mock_get): """ 发送正确的beatmap id到 osu! API """ - # 获取 Token - self.beatmap.get_token() - self.assertIsNotNone(self.beatmap.osu_token, "Token 获取失败") + # 构造mock数据 + mock_response = MagicMock() + mock_response.raise_for_status = MagicMock() + mock_response.json.return_value = { + 'beatmapset_id': '1', + 'beatmapset': { + 'title_unicode': 'DISCO PRINCE', + 'artist_unicode': 'Kenji Ninuma', + 'ranked_date': '2007-10-06' + }, + 'difficulty_rating': 2.55, + 'status': 'ranked', + 'bpm': 120, + 'cs': 4, + 'ar': 6, + 'accuracy': 6, + 'drain': 6, + 'total_length': 142, + 'url': 'https://osu.ppy.sh/beatmaps/75' + } + mock_get.return_value = mock_response # 设置 beatmap_id self.beatmap.change_beatmap_id('75') # osu第一个ranked图 @@ -134,10 +167,16 @@ def test_get_beatmap_info_success(self): self.assertEqual(self.beatmap.beatmap_mirror_sayo_url, 'https://osu.sayobot.cn/home?search=1') self.assertEqual(self.beatmap.beatmap_mirror_inso_url, 'http://inso.link/yukiho/?b=75') - def test_get_beatmap_info_with_wrong_beatmap_id(self): + @patch('irc_dlient.requests.get') + def test_get_beatmap_info_with_wrong_beatmap_id(self, mock_get): """ 发送错误的beatmap id到 osu! API """ + # 构造mock数据 + mock_response = MagicMock() + mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError() + mock_get.return_value = mock_response + # 获取 Token self.beatmap.get_token() self.assertIsNotNone(self.beatmap.osu_token, "Token 获取失败") @@ -165,18 +204,39 @@ def test_get_beatmap_info_with_wrong_beatmap_id(self): self.assertEqual(self.beatmap.beatmap_mirror_sayo_url, '') self.assertEqual(self.beatmap.beatmap_mirror_inso_url, '') - def test_get_beatmap_score_success(self): + @patch('irc_dlient.requests.get') + def test_get_beatmap_score_success(self, mock_get): """ 发送正确的username查询分数 """ - # 获取 Token - self.beatmap.get_token() - self.assertIsNotNone(self.beatmap.osu_token, "Token 获取失败") - - # 设置 username + # 设置username + self.beatmap.id2name = {"LittleStone": "123456"} self.beatmap.get_user_id("LittleStone") self.assertIsNotNone(self.beatmap.id2name, "用户ID获取失败") + mock_response = MagicMock() + mock_response.raise_for_status = MagicMock() + mock_response.json.return_value = { + 'score': { + 'created_at': '2020-12-04T00:00:00+00:00', + 'accuracy': 0.8385, + 'max_combo': 122, + 'statistics': { + 'count_300': 150, + 'count_100': 35, + 'count_50': 6, + 'count_miss': 3 + }, + 'pp': 28.0404, + 'rank': 'C', + 'mods': ['HD', 'HR', 'DT'], + 'beatmap': { + 'url': 'https://osu.ppy.sh/beatmaps/75' + } + } + } + mock_get.return_value = mock_response + # 设置 beatmap_id self.beatmap.beatmap_id = '75' # osu第一个ranked图 @@ -200,73 +260,88 @@ def test_get_beatmap_score_success(self): self.assertEqual(self.beatmap.pr_username, 'LittleStone') self.assertEqual(self.beatmap.pr_acc, 83.85) - def test_get_beatmap_score_with_wrong_username_1(self): + @patch('sys.stdout', new_callable=StringIO) + def test_get_beatmap_score_with_wrong_username_1(self, fake_out): """ 发送错误的username查询分数-场景1 """ - # 获取 Token - self.beatmap.get_token() - self.assertIsNotNone(self.beatmap.osu_token, "Token 获取失败") + self.beatmap.id2name = {} + self.beatmap.get_beatmap_score("PPYNOTPPYGUESSIMPPYORNOT") + self.assertIn("获取谱面成绩失败", fake_out.getvalue(), "Output does not contain expected failure message.") - # 设置错误的 username,并捕获输出 - with patch('sys.stdout', new=StringIO()) as fake_out: - self.beatmap.get_user_id("PPYNOTPPYGUESSIMPPYORNOT") - self.assertIn("获取用户ID失败", fake_out.getvalue(), "输出不包含预期的字符串") - - def test_get_beatmap_score_with_wrong_username_2(self): + @patch('sys.stdout', new_callable=StringIO) + def test_get_beatmap_score_with_wrong_username_2(self, fake_out): """ 发送错误的username查询分数-场景2 """ - # 获取 Token - self.beatmap.get_token() - self.assertIsNotNone(self.beatmap.osu_token, "Token 获取失败") + # 构造mock数据 + mock_post = MagicMock() + mock_post.raise_for_status = MagicMock() + mock_post.json.return_value = {'access_token': 'test_token'} + with patch('irc_dlient.requests.post', return_value=mock_post): + self.beatmap.get_token() + self.assertEqual(self.beatmap.osu_token, 'test_token') # 设置 username - self.beatmap.get_user_id("LittleStone") - self.assertIsNotNone(self.beatmap.id2name, "用户ID获取失败") + self.beatmap.id2name = {"LittleStone": "123456"} # 设置 beatmap_id self.beatmap.beatmap_id = '75' # osu第一个ranked图 # 设置错误的 username,并捕获输出 - with patch('sys.stdout', new=StringIO()) as fake_out: - self.beatmap.get_beatmap_score("ATRI1024") - self.assertIn("获取谱面成绩失败,错误信息:'ATRI1024'\n| [ 获取谱面成绩失败 - ]| *| [ ] 0pp acc:0% combo:0x| 0/0/0/0| date:|", - fake_out.getvalue(), "输出不包含预期的字符串") + self.beatmap.get_beatmap_score("ATRI1024") + self.assertIn( + "获取谱面成绩失败,错误信息:'ATRI1024'\n| [ 获取谱面成绩失败 - ]| *| [ ] 0pp acc:0% combo:0x| 0/0/0/0| date:|\n", + fake_out.getvalue(), + "Output does not contain expected failure message." + ) - def test_get_beatmap_score_with_wrong_beatmap_id(self): + @patch('irc_dlient.requests.get') + @patch('irc_dlient.requests.post') + def test_get_beatmap_score_with_wrong_beatmap_id(self, mock_post, mock_get): """ 发送错误的beatmap id查询分数 """ + # 构造mock数据 + mock_post_response = MagicMock() + mock_post_response.raise_for_status = MagicMock() + mock_post_response.json.return_value = {'access_token': 'test_token'} + mock_post.return_value = mock_post_response + # 获取 Token self.beatmap.get_token() - self.assertIsNotNone(self.beatmap.osu_token, "Token 获取失败") + self.assertEqual(self.beatmap.osu_token, 'test_token') # 设置 username - self.beatmap.get_user_id("LittleStone") - self.assertIsNotNone(self.beatmap.id2name, "用户ID获取失败") + self.beatmap.id2name = {"LittleStone": "123456"} # 设置 beatmap_id self.beatmap.beatmap_id = '1145141919810' # 不存在的图 - # 设置错误的 username,并捕获输出 - with patch('sys.stdout', new=StringIO()) as fake_out: + # 调用获取谱面成绩的方法 + mock_get_response = MagicMock() + mock_get_response.raise_for_status.side_effect = requests.exceptions.HTTPError() + mock_get.return_value = mock_get_response + + with patch('sys.stdout', new_callable=StringIO) as fake_out: self.beatmap.get_beatmap_score("LittleStone") - self.assertIn("未查询到LittleStone在该谱面上留下的成绩\n", fake_out.getvalue(), "输出不包含预期的字符串") + self.assertIn( + "未查询到LittleStone在该谱面上留下的成绩\n", + fake_out.getvalue(), + "Output does not contain expected failure message." + ) @patch('irc_dlient.requests.get') def test_get_recent_info_success(self, mock_get): """ 发送正确的username查询最近成绩,并模拟返回数据 """ - # 获取 Token - self.beatmap.get_token() - self.assertIsNotNone(self.beatmap.osu_token, "Token 获取失败") - - # 设置 username - self.beatmap.get_user_id("LittleStone") + # 设置username self.beatmap.id2name = {"LittleStone": "123456"} + self.beatmap.get_user_id("LittleStone") + self.assertIsNotNone(self.beatmap.id2name, "用户ID获取失败") + # 构造返回数据 mock_response = MagicMock() mock_response.raise_for_status = MagicMock() mock_response.json.return_value = [ @@ -313,23 +388,32 @@ def test_get_recent_info_success(self, mock_get): self.assertEqual(self.beatmap.pr_beatmap_url, 'https://osu.ppy.sh/beatmapsets/123456#osu/654321') self.assertEqual(self.beatmap.pr_username, 'LittleStone') - def test_get_recent_info_with_wrong_username(self): + @patch('sys.stdout', new_callable=StringIO) + def test_get_recent_info_with_wrong_username(self, fake_out): """ 发送错误的username查询最近成绩 """ - # 获取 Token - self.beatmap.get_token() - self.assertIsNotNone(self.beatmap.osu_token, "Token 获取失败") + # 构造mock数据 + mock_post = MagicMock() + mock_post.raise_for_status = MagicMock() + mock_post.json.return_value = {'access_token': 'test_token'} + with patch('irc_dlient.requests.post', return_value=mock_post): + self.beatmap.get_token() + self.assertEqual(self.beatmap.osu_token, 'test_token') - # 设置 username - self.beatmap.get_user_id("LittleStone") + # 设置 username self.beatmap.id2name = {"LittleStone": "123456"} + # 设置beatmap id + self.beatmap.beatmap_id = '75' + # 调用获取最近成绩的方法 - with patch('sys.stdout', new=StringIO()) as fake_out: - self.beatmap.get_recent_info("ATRI1024") - self.assertIn("获取最近成绩失败,错误信息:'ATRI1024'\n| [ 获取最近成绩失败 - ]| *| [ ] 0pp acc:0% combo:0x| 0/0/0/0|\n", - fake_out.getvalue(), "输出不包含预期的字符串") + self.beatmap.get_recent_info("ATRI1024") + self.assertIn( + "获取最近成绩失败,错误信息:'ATRI1024'\n| [ 获取最近成绩失败 - ]| *| [ ] 0pp acc:0% combo:0x| 0/0/0/0|\n", + fake_out.getvalue(), + "Output does not contain expected failure message." + ) class TestPP(unittest.TestCase): @patch('irc_dlient.os.path.exists') @@ -356,21 +440,32 @@ def test_get_beatmap_file_download(self, mock_get, mock_exists): # 可以进一步检查文件写入,但需要更多的patching class TestRoom(unittest.TestCase): + def setUp(self): + # 构造模拟config + self.mock_config = MagicMock() + self.mock_config.osuclientid = 'test_client_id' + self.mock_config.osuclientsecret = 'test_client_secret' + self.mock_config.timelimit = '100' # Example value + self.mock_config.starlimit = '5.0' + self.mock_config.mpname = 'TestMP' + self.mock_config.mppassword = 'testpassword' + + # 实例化Room + self.room = irc_dlient.Room(self.mock_config) + @patch('builtins.open') def test_save_last_room_id(self, mock_open): mock_file = MagicMock() mock_open.return_value.__enter__.return_value = mock_file - room = irc_dlient.Room() - room.room_id = '#room123' - room.save_last_room_id() + self.room.room_id = '#room123' + self.room.save_last_room_id() mock_file.write.assert_called_with('#room123') @patch('builtins.open', side_effect=FileNotFoundError) def test_get_last_room_id_file_not_found(self, mock_open): - room = irc_dlient.Room() - last_id = room.get_last_room_id() + last_id = self.room.get_last_room_id() self.assertEqual(last_id, '') if __name__ == '__main__': From e19cca03d0f3708608169fe540a5216096976c04 Mon Sep 17 00:00:00 2001 From: LittleStone <47311214+SuperEgoKoishi@users.noreply.github.com> Date: Tue, 5 Nov 2024 21:16:48 +0800 Subject: [PATCH 7/9] Update ci.yml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加codecov token --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e3ab53..5a3dfd3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,7 @@ jobs: uses: codecov/codecov-action@v3 with: files: coverage.xml + token: 4a07675d-ac1b-481f-b392-b2096882f061 flags: unittests name: codecov-umbrella - fail_ci_if_error: true \ No newline at end of file + fail_ci_if_error: true From 16b8157715e22a386da8ccc1db24610cdc7a17c1 Mon Sep 17 00:00:00 2001 From: LittleStone <47311214+SuperEgoKoishi@users.noreply.github.com> Date: Tue, 5 Nov 2024 21:29:37 +0800 Subject: [PATCH 8/9] Update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加覆盖率徽章 --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 7daaf1b..f5f7074 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # osu-ircbot-py 基于python使用irc自动轮换房主的osu机器人 +[![codecov](https://codecov.io/gh/SuperEgoKoishi/osu-ircbot-py/graph/badge.svg?token=Z4HLQQIACJ)](https://codecov.io/gh/SuperEgoKoishi/osu-ircbot-py) + # 文档 输入help获取吧 咕咕咕 From 2b5f739ebff5e1d1700971818598c34ff581094c Mon Sep 17 00:00:00 2001 From: LittleStone <1526631427@qq.com> Date: Tue, 5 Nov 2024 21:57:22 +0800 Subject: [PATCH 9/9] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E4=B8=80=E4=B8=AA?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=BC=95=E5=85=A5bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- irc_dlient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/irc_dlient.py b/irc_dlient.py index 6eb4564..2c17fc7 100755 --- a/irc_dlient.py +++ b/irc_dlient.py @@ -631,7 +631,7 @@ def reset_game_start_time(self): def get_last_room_id(self): try: - with open(self.config.last_room_id_path, 'r') as f: + with open('last_room_id.txt', 'r') as f: self.last_romm_id = f.read() print(f'获取上一个房间ID{self.last_romm_id}') except: