Skip to content

Commit b4c7d41

Browse files
committed
fix: windows compatibility
- fix file write error - fix datetime to timestamp error - fix read file line by line then json.parse error - test, add windows test actions - build, remove useless test packages - test, fix windows platform not work with unicode characters in codecov config
1 parent d8ed878 commit b4c7d41

File tree

8 files changed

+287
-146
lines changed

8 files changed

+287
-146
lines changed

.github/workflows/ci-test.yml

+96-4
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,106 @@ jobs:
6060
QINIU_UPLOAD_CALLBACK_URL: ${{secrets.QINIU_UPLOAD_CALLBACK_URL}}
6161
QINIU_TEST_ENV: "travis"
6262
MOCK_SERVER_ADDRESS: "http://127.0.0.1:9000"
63-
PYTHONPATH: "$PYTHONPATH:."
6463
run: |
6564
flake8 --show-source --max-line-length=160 ./qiniu
66-
coverage run -m pytest ./test_qiniu.py ./tests/cases
67-
ocular --data-file .coverage
68-
codecov
65+
python -m pytest ./test_qiniu.py tests --cov qiniu --cov-report=xml
66+
- name: Post Setup mock server
67+
if: ${{ always() }}
68+
shell: bash
69+
run: |
70+
set +e
6971
cat mock-server.pid | xargs kill
72+
rm mock-server.pid
7073
- name: Print mock server log
7174
if: ${{ failure() }}
7275
run: |
7376
cat py-mock-server.log
77+
- name: Upload results to Codecov
78+
uses: codecov/codecov-action@v4
79+
with:
80+
token: ${{ secrets.CODECOV_TOKEN }}
81+
test-win:
82+
strategy:
83+
fail-fast: false
84+
max-parallel: 1
85+
matrix:
86+
python_version: ['2.7', '3.5', '3.9']
87+
runs-on: windows-2019
88+
# make sure only one test running,
89+
# remove this when cases could run in parallel.
90+
needs: test
91+
steps:
92+
- name: Checkout repo
93+
uses: actions/checkout@v2
94+
with:
95+
ref: ${{ github.ref }}
96+
- name: Setup miniconda
97+
uses: conda-incubator/setup-miniconda@v2
98+
with:
99+
auto-update-conda: true
100+
channels: conda-forge
101+
python-version: ${{ matrix.python_version }}
102+
activate-environment: qiniu-sdk
103+
auto-activate-base: false
104+
- name: Setup pip
105+
env:
106+
PYTHON_VERSION: ${{ matrix.python_version }}
107+
PIP_BOOTSTRAP_SCRIPT_PREFIX: https://bootstrap.pypa.io/pip
108+
run: |
109+
# reinstall pip by some python(<3.7) not compatible
110+
$pyversion = [Version]"$ENV:PYTHON_VERSION"
111+
if ($pyversion -lt [Version]"3.7") {
112+
Invoke-WebRequest "$ENV:PIP_BOOTSTRAP_SCRIPT_PREFIX/$($pyversion.Major).$($pyversion.Minor)/get-pip.py" -OutFile "$ENV:TEMP\get-pip.py"
113+
python $ENV:TEMP\get-pip.py --user
114+
Remove-Item -Path "$ENV:TEMP\get-pip.py"
115+
}
116+
- name: Install dependencies
117+
run: |
118+
python -m pip install --upgrade pip
119+
python -m pip install -I -e ".[dev]"
120+
- name: Run cases
121+
env:
122+
QINIU_ACCESS_KEY: ${{ secrets.QINIU_ACCESS_KEY }}
123+
QINIU_SECRET_KEY: ${{ secrets.QINIU_SECRET_KEY }}
124+
QINIU_TEST_BUCKET: ${{ secrets.QINIU_TEST_BUCKET }}
125+
QINIU_TEST_NO_ACC_BUCKET: ${{ secrets.QINIU_TEST_NO_ACC_BUCKET }}
126+
QINIU_TEST_DOMAIN: ${{ secrets.QINIU_TEST_DOMAIN }}
127+
QINIU_UPLOAD_CALLBACK_URL: ${{secrets.QINIU_UPLOAD_CALLBACK_URL}}
128+
QINIU_TEST_ENV: "github"
129+
MOCK_SERVER_ADDRESS: "http://127.0.0.1:9000"
130+
PYTHONPATH: "$PYTHONPATH:."
131+
run: |
132+
Write-Host "======== Setup Mock Server ========="
133+
conda create -y -n mock-server python=3.10
134+
conda activate mock-server
135+
python --version
136+
$processOptions = @{
137+
FilePath="python"
138+
ArgumentList="tests/mock_server/main.py", "--port", "9000"
139+
PassThru=$true
140+
RedirectStandardOutput="py-mock-server.log"
141+
}
142+
$mocksrvp = Start-Process @processOptions
143+
$mocksrvp.Id | Out-File -FilePath "mock-server.pid"
144+
conda deactivate
145+
Write-Host "======== Running Test ========="
146+
python --version
147+
python -m pytest ./test_qiniu.py tests --cov qiniu --cov-report=xml
148+
- name: Post Setup mock server
149+
if: ${{ always() }}
150+
run: |
151+
Try {
152+
$mocksrvpid = Get-Content -Path "mock-server.pid"
153+
Stop-Process -Id $mocksrvpid
154+
Remove-Item -Path "mock-server.pid"
155+
} Catch {
156+
Write-Host -Object $_
157+
}
158+
- name: Print mock server log
159+
if: ${{ failure() }}
160+
run: |
161+
Get-Content -Path "py-mock-server.log"
162+
- name: Upload results to Codecov
163+
uses: codecov/codecov-action@v4
164+
with:
165+
token: ${{ secrets.CODECOV_TOKEN }}

codecov.yml

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,28 @@
11
codecov:
22
ci:
3-
- prow.qiniu.io # prow 里面运行需添加,其他 CI 不要
4-
require_ci_to_pass: no # 改为 no,否则 codecov 会等待其他 GitHub 上所有 CI 通过才会留言。
3+
- prow.qiniu.io # prow need this. seems useless
4+
require_ci_to_pass: no # `no` means the bot will comment on the PR even before all ci passed
55

6-
github_checks: #关闭github checks
6+
github_checks: # close github checks
77
annotations: false
88

99
comment:
1010
layout: "reach, diff, flags, files"
11-
behavior: new # 默认是更新旧留言,改为 new,删除旧的,增加新的。
11+
behavior: new # `new` means the bot will comment a new message instead of edit the old one
1212
require_changes: false # if true: only post the comment if coverage changes
1313
require_base: no # [yes :: must have a base report to post]
1414
require_head: yes # [yes :: must have a head report to post]
1515
branches: # branch names that can post comment
1616
- "master"
1717

1818
coverage:
19-
status: # 评判 pr 通过的标准
19+
status: # check coverage status to pass or fail
2020
patch: off
21-
project: # project 统计所有代码x
21+
project: # project analyze all code in the project
2222
default:
2323
# basic
24-
target: 73.5% # 总体通过标准
25-
threshold: 3% # 允许单次下降的幅度
24+
target: 73.5% # the minimum coverage ratio that the commit must meet
25+
threshold: 3% # allow the coverage to drop
2626
base: auto
2727
if_not_found: success
2828
if_ci_failed: error

qiniu/http/regions_provider.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
import logging
66
import tempfile
77
import os
8+
import shutil
89

910
from qiniu.compat import json, b as to_bytes
10-
from qiniu.utils import io_md5
11+
from qiniu.utils import io_md5, dt2ts
1112

1213
from .endpoint import Endpoint
1314
from .region import Region, ServiceName
@@ -264,7 +265,7 @@ def _persist_region(region):
264265
},
265266
ttl=region.ttl,
266267
# use datetime.datetime.timestamp() when min version of python >= 3
267-
createTime=int(float(region.create_time.strftime('%s.%f')) * 1000)
268+
createTime=dt2ts(region.create_time)
268269
)._asdict()
269270

270271

@@ -338,8 +339,10 @@ def _walk_persist_cache_file(persist_path, ignore_parse_error=False):
338339

339340
with open(persist_path, 'r') as f:
340341
for line in f:
342+
if not line.strip():
343+
continue
341344
try:
342-
cache_key, regions = _parse_persisted_regions(line)
345+
cache_key, regions = _parse_persisted_regions(line.strip())
343346
yield cache_key, regions
344347
except Exception as err:
345348
if not ignore_parse_error:
@@ -655,7 +658,7 @@ def __shrink_cache(self):
655658
)
656659

657660
# rename file
658-
os.rename(shrink_file_path, self._cache_scope.persist_path)
661+
shutil.move(shrink_file_path, self._cache_scope.persist_path)
659662
except FileAlreadyLocked:
660663
pass
661664
finally:

qiniu/region.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77

88
from .compat import json, s as str_from_bytes
9-
from .utils import urlsafe_base64_decode
9+
from .utils import urlsafe_base64_decode, dt2ts
1010
from .config import UC_HOST, is_customized_default, get_default
1111
from .http.endpoint import Endpoint as _HTTPEndpoint
1212
from .http.regions_provider import Region as _HTTPRegion, ServiceName, get_default_regions_provider
@@ -190,7 +190,7 @@ def get_bucket_hosts(self, ak, bucket, home_dir=None, force=False):
190190

191191
ttl = region.ttl if region.ttl > 0 else 24 * 3600 # 1 day
192192
# use datetime.datetime.timestamp() when min version of python >= 3
193-
create_time = int(float(region.create_time.strftime('%s.%f')) * 1000)
193+
create_time = dt2ts(region.create_time)
194194
bucket_hosts['deadline'] = create_time + ttl
195195

196196
return bucket_hosts

qiniu/utils.py

+29-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# -*- coding: utf-8 -*-
22
from hashlib import sha1, new as hashlib_new
33
from base64 import urlsafe_b64encode, urlsafe_b64decode
4-
from datetime import datetime
4+
from datetime import datetime, tzinfo, timedelta
5+
56
from .compat import b, s
67

78
try:
@@ -236,3 +237,30 @@ def canonical_mime_header_key(field_name):
236237
result += ch
237238
upper = ch == "-"
238239
return result
240+
241+
242+
class _UTC_TZINFO(tzinfo):
243+
def utcoffset(self, dt):
244+
return timedelta(hours=0)
245+
246+
def tzname(self, dt):
247+
return "UTC"
248+
249+
def dst(self, dt):
250+
return timedelta(0)
251+
252+
253+
def dt2ts(dt):
254+
"""
255+
converte datetime to timestamp
256+
257+
Parameters
258+
----------
259+
dt: datetime.datetime
260+
"""
261+
if not dt.tzinfo:
262+
st = (dt - datetime(1970, 1, 1)).total_seconds()
263+
else:
264+
st = (dt - datetime(1970, 1, 1, tzinfo=_UTC_TZINFO())).total_seconds()
265+
266+
return int(st)

setup.py

-4
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,8 @@ def find_version(*file_paths):
4242
'Operating System :: OS Independent',
4343
'Programming Language :: Python',
4444
'Programming Language :: Python :: 2',
45-
'Programming Language :: Python :: 2.6',
4645
'Programming Language :: Python :: 2.7',
4746
'Programming Language :: Python :: 3',
48-
'Programming Language :: Python :: 3.3',
4947
'Programming Language :: Python :: 3.4',
5048
'Programming Language :: Python :: 3.5',
5149
'Programming Language :: Python :: 3.6',
@@ -66,8 +64,6 @@ def find_version(*file_paths):
6664
'pytest',
6765
'pytest-cov',
6866
'freezegun',
69-
'scrutinizer-ocular',
70-
'codecov'
7167
]
7268
},
7369

test_qiniu.py

-123
Original file line numberDiff line numberDiff line change
@@ -65,129 +65,6 @@ def remove_temp_file(file):
6565
except OSError:
6666
pass
6767

68-
69-
class UtilsTest(unittest.TestCase):
70-
def test_urlsafe(self):
71-
a = 'hello\x96'
72-
u = urlsafe_base64_encode(a)
73-
assert b(a) == urlsafe_base64_decode(u)
74-
75-
def test_canonical_mime_header_key(self):
76-
field_names = [
77-
":status",
78-
":x-test-1",
79-
":x-Test-2",
80-
"content-type",
81-
"CONTENT-LENGTH",
82-
"oRiGin",
83-
"ReFer",
84-
"Last-Modified",
85-
"acCePt-ChArsEt",
86-
"x-test-3",
87-
"cache-control",
88-
]
89-
expect_canonical_field_names = [
90-
":status",
91-
":x-test-1",
92-
":x-Test-2",
93-
"Content-Type",
94-
"Content-Length",
95-
"Origin",
96-
"Refer",
97-
"Last-Modified",
98-
"Accept-Charset",
99-
"X-Test-3",
100-
"Cache-Control",
101-
]
102-
assert len(field_names) == len(expect_canonical_field_names)
103-
for i in range(len(field_names)):
104-
assert canonical_mime_header_key(field_names[i]) == expect_canonical_field_names[i]
105-
106-
def test_entry(self):
107-
case_list = [
108-
{
109-
'msg': 'normal',
110-
'bucket': 'qiniuphotos',
111-
'key': 'gogopher.jpg',
112-
'expect': 'cWluaXVwaG90b3M6Z29nb3BoZXIuanBn'
113-
},
114-
{
115-
'msg': 'key empty',
116-
'bucket': 'qiniuphotos',
117-
'key': '',
118-
'expect': 'cWluaXVwaG90b3M6'
119-
},
120-
{
121-
'msg': 'key undefined',
122-
'bucket': 'qiniuphotos',
123-
'key': None,
124-
'expect': 'cWluaXVwaG90b3M='
125-
},
126-
{
127-
'msg': 'key need replace plus symbol',
128-
'bucket': 'qiniuphotos',
129-
'key': '012ts>a',
130-
'expect': 'cWluaXVwaG90b3M6MDEydHM-YQ=='
131-
},
132-
{
133-
'msg': 'key need replace slash symbol',
134-
'bucket': 'qiniuphotos',
135-
'key': '012ts?a',
136-
'expect': 'cWluaXVwaG90b3M6MDEydHM_YQ=='
137-
}
138-
]
139-
for c in case_list:
140-
assert c.get('expect') == entry(c.get('bucket'), c.get('key')), c.get('msg')
141-
142-
def test_decode_entry(self):
143-
case_list = [
144-
{
145-
'msg': 'normal',
146-
'expect': {
147-
'bucket': 'qiniuphotos',
148-
'key': 'gogopher.jpg'
149-
},
150-
'entry': 'cWluaXVwaG90b3M6Z29nb3BoZXIuanBn'
151-
},
152-
{
153-
'msg': 'key empty',
154-
'expect': {
155-
'bucket': 'qiniuphotos',
156-
'key': ''
157-
},
158-
'entry': 'cWluaXVwaG90b3M6'
159-
},
160-
{
161-
'msg': 'key undefined',
162-
'expect': {
163-
'bucket': 'qiniuphotos',
164-
'key': None
165-
},
166-
'entry': 'cWluaXVwaG90b3M='
167-
},
168-
{
169-
'msg': 'key need replace plus symbol',
170-
'expect': {
171-
'bucket': 'qiniuphotos',
172-
'key': '012ts>a'
173-
},
174-
'entry': 'cWluaXVwaG90b3M6MDEydHM-YQ=='
175-
},
176-
{
177-
'msg': 'key need replace slash symbol',
178-
'expect': {
179-
'bucket': 'qiniuphotos',
180-
'key': '012ts?a'
181-
},
182-
'entry': 'cWluaXVwaG90b3M6MDEydHM_YQ=='
183-
}
184-
]
185-
for c in case_list:
186-
bucket, key = decode_entry(c.get('entry'))
187-
assert bucket == c.get('expect').get('bucket'), c.get('msg')
188-
assert key == c.get('expect').get('key'), c.get('msg')
189-
190-
19168
class BucketTestCase(unittest.TestCase):
19269
q = Auth(access_key, secret_key)
19370
bucket = BucketManager(q)

0 commit comments

Comments
 (0)