Skip to content

Commit 5777b5d

Browse files
committed
V5.0.1
1 parent 3009d0f commit 5777b5d

File tree

4 files changed

+192
-11
lines changed

4 files changed

+192
-11
lines changed

.github/workflows/workflow.yml

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
name: CI/CD Pipeline
22

3+
34
"on":
45
push:
56
branches: ["**"]
@@ -9,15 +10,36 @@ name: CI/CD Pipeline
910
jobs:
1011
test:
1112
name: Test Python ${{ matrix.python-version }} on ${{ matrix.os }}
12-
runs-on: ${{ matrix.os }}
13+
runs-on: ${{ matrix.runs-on || matrix.os }}
1314
strategy:
1415
fail-fast: false
1516
matrix:
1617
os: ["ubuntu-latest", "macos-latest", "windows-latest"]
1718
python-version: ["3.12", "3.13"]
19+
include:
20+
- os: "macos-14-arm64"
21+
runs-on: "macos-14"
22+
python-version: "3.12"
23+
- os: "macos-14-arm64"
24+
runs-on: "macos-14"
25+
python-version: "3.13"
26+
- os: "ubuntu-latest-arm64"
27+
runs-on: "ubuntu-latest"
28+
python-version: "3.12"
29+
arch: "arm64"
30+
- os: "ubuntu-latest-arm64"
31+
runs-on: "ubuntu-latest"
32+
python-version: "3.13"
33+
arch: "arm64"
1834
steps:
1935
- uses: actions/checkout@v4
2036

37+
- name: Set up QEMU for ARM64 emulation
38+
if: matrix.arch == 'arm64'
39+
uses: docker/setup-qemu-action@v3
40+
with:
41+
platforms: arm64
42+
2143
- name: Set up Python ${{ matrix.python-version }}
2244
uses: actions/setup-python@v5
2345
with:
@@ -53,12 +75,19 @@ jobs:
5375

5476
build:
5577
name: Build for Python ${{ matrix.python-version }} on ${{ matrix.os }}
56-
runs-on: ${{ matrix.os }}
78+
runs-on: ${{ matrix.runs-on || matrix.os }}
5779
if: startsWith(github.ref, 'refs/tags/v')
5880
strategy:
5981
matrix:
6082
os: ["ubuntu-latest", "macos-latest", "windows-latest"]
6183
python-version: ["3.12", "3.13"]
84+
include:
85+
- os: "macos-14-arm64"
86+
runs-on: "macos-14"
87+
python-version: "3.12"
88+
- os: "macos-14-arm64"
89+
runs-on: "macos-14"
90+
python-version: "3.13"
6291
steps:
6392
- uses: actions/checkout@v4
6493

@@ -102,10 +131,79 @@ jobs:
102131
path: dist-py${{ matrix.python-version }}-${{ matrix.os }}/
103132
retention-days: 7
104133

134+
build-linux-arm64:
135+
name: Build Linux ARM64 wheels
136+
runs-on: ubuntu-latest
137+
if: startsWith(github.ref, 'refs/tags/v')
138+
strategy:
139+
matrix:
140+
python-version: ["3.12", "3.13"]
141+
steps:
142+
- uses: actions/checkout@v4
143+
144+
- name: Set up Docker Buildx
145+
uses: docker/setup-buildx-action@v3
146+
with:
147+
platforms: linux/arm64
148+
149+
- name: Build ARM64 wheel using Docker
150+
run: |
151+
# Create a multi-stage Dockerfile for ARM64 builds
152+
cat << 'EOF' > Dockerfile.arm64
153+
FROM python:${{ matrix.python-version }}-slim
154+
155+
WORKDIR /build
156+
157+
# Install build dependencies
158+
RUN apt-get update && apt-get install -y \
159+
build-essential \
160+
git \
161+
&& rm -rf /var/lib/apt/lists/*
162+
163+
# Install Poetry
164+
RUN pip install poetry
165+
166+
# Copy project files
167+
COPY . .
168+
169+
# Configure Poetry and build
170+
RUN poetry config virtualenvs.create false \
171+
&& poetry install --only main --no-interaction --no-ansi \
172+
&& poetry run python build.py \
173+
&& poetry build
174+
175+
# Copy artifacts to output volume
176+
CMD ["cp", "-r", "dist/", "/output/"]
177+
EOF
178+
179+
# Build using buildx for ARM64
180+
docker buildx build \
181+
--platform linux/arm64 \
182+
--file Dockerfile.arm64 \
183+
--tag pythonlogs-arm64-builder:${{ matrix.python-version }} \
184+
--load \
185+
.
186+
187+
# Create output directory
188+
mkdir -p dist-arm64-py${{ matrix.python-version }}
189+
190+
# Run container to extract artifacts
191+
docker run --rm \
192+
--platform linux/arm64 \
193+
-v $(pwd)/dist-arm64-py${{ matrix.python-version }}:/output \
194+
pythonlogs-arm64-builder:${{ matrix.python-version }}
195+
196+
- name: Upload Linux ARM64 Python ${{ matrix.python-version }} artifacts
197+
uses: actions/upload-artifact@v4
198+
with:
199+
name: python-packages-${{ matrix.python-version }}-linux-arm64
200+
path: dist-arm64-py${{ matrix.python-version }}/
201+
retention-days: 7
202+
105203
release:
106204
name: Create Release
107205
runs-on: ubuntu-latest
108-
needs: build
206+
needs: [build, build-linux-arm64]
109207
if: startsWith(github.ref, 'refs/tags/v')
110208
permissions:
111209
contents: write

pythonLogs/log_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def is_older_than_x_days(path: str, days: int) -> bool:
139139
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), path)
140140

141141
try:
142-
if int(days) in (0, 1):
142+
if int(days) == 0:
143143
cutoff_time = datetime.now()
144144
else:
145145
cutoff_time = datetime.now() - timedelta(days=int(days))

tests/core/test_log_utils.py

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -420,10 +420,9 @@ def test_is_older_than_x_days(self):
420420
try:
421421
assert os.path.isfile(file_path) == True
422422

423-
# When days=1, it compares against current time, so the file should be "older"
424-
# due to the small time difference since creation
423+
# When days=1, it compares against 1 day ago, so newly created file should NOT be older
425424
result = log_utils.is_older_than_x_days(file_path, 1)
426-
assert result == True
425+
assert result == False
427426

428427
# When days=5, it compares against 5 days ago, so newly created file should NOT be older
429428
result = log_utils.is_older_than_x_days(file_path, 5)
@@ -1555,3 +1554,88 @@ def mock_copyfileobj(*args, **kwargs):
15551554
# Cleanup: remove the test file if it still exists
15561555
if os.path.exists(test_file):
15571556
os.unlink(test_file)
1557+
1558+
def test_gzip_file_windows_retry_mechanism_coverage(self):
1559+
"""Test gzip_file_with_sufix Windows retry mechanism for coverage."""
1560+
with tempfile.TemporaryDirectory() as temp_dir:
1561+
# Create a test file
1562+
test_file = os.path.join(temp_dir, "test_retry.log")
1563+
with open(test_file, "w") as f:
1564+
f.write("test content for retry mechanism")
1565+
1566+
import unittest.mock
1567+
1568+
# Mock to simulate Windows platform and PermissionError on first attempt
1569+
call_count = 0
1570+
original_open = open
1571+
1572+
def mock_open_side_effect(*args, **kwargs):
1573+
nonlocal call_count
1574+
call_count += 1
1575+
if args[0] == test_file and call_count == 1:
1576+
# First call - simulate Windows file locking
1577+
raise PermissionError("The process cannot access the file")
1578+
else:
1579+
# Subsequent calls - use real open
1580+
return original_open(*args, **kwargs)
1581+
1582+
# Mock sys.platform to be Windows and time.sleep to verify retry
1583+
with unittest.mock.patch('pythonLogs.log_utils.sys.platform', 'win32'):
1584+
with unittest.mock.patch('pythonLogs.log_utils.time.sleep') as mock_sleep:
1585+
with unittest.mock.patch('pythonLogs.log_utils.open', side_effect=mock_open_side_effect):
1586+
# This should succeed after retry, covering lines 259-261
1587+
result = log_utils.gzip_file_with_sufix(test_file, "retry_coverage")
1588+
1589+
# Verify retry was attempted (sleep was called) - line 260
1590+
mock_sleep.assert_called_once_with(0.1)
1591+
1592+
# Verify the operation eventually succeeded
1593+
assert result is not None
1594+
assert result.endswith("_retry_coverage.log.gz")
1595+
assert not os.path.exists(test_file) # Original should be deleted
1596+
1597+
# Clean up the gzipped file
1598+
if result and os.path.exists(result):
1599+
os.unlink(result)
1600+
1601+
def test_gzip_file_windows_retry_exhausted_coverage(self):
1602+
"""Test gzip_file_with_sufix when Windows retries are exhausted for coverage."""
1603+
with tempfile.TemporaryDirectory() as temp_dir:
1604+
# Create a test file
1605+
test_file = os.path.join(temp_dir, "test_retry_fail.log")
1606+
with open(test_file, "w") as f:
1607+
f.write("test content for retry exhaustion")
1608+
1609+
import unittest.mock
1610+
1611+
# Mock to always raise PermissionError to exhaust retries
1612+
def mock_open_side_effect(*args, **kwargs):
1613+
if args[0] == test_file:
1614+
# Always fail - simulate persistent Windows file locking
1615+
raise PermissionError("The process cannot access the file - persistent lock")
1616+
else:
1617+
# Other opens work normally
1618+
return open(*args, **kwargs)
1619+
1620+
# Mock sys.platform to be Windows and capture stderr
1621+
with unittest.mock.patch('pythonLogs.log_utils.sys.platform', 'win32'):
1622+
with unittest.mock.patch('pythonLogs.log_utils.time.sleep') as mock_sleep:
1623+
with unittest.mock.patch('pythonLogs.log_utils.open', side_effect=mock_open_side_effect):
1624+
# Capture stderr to verify error logging
1625+
stderr_capture = io.StringIO()
1626+
with contextlib.redirect_stderr(stderr_capture):
1627+
with pytest.raises(PermissionError) as exc_info:
1628+
# This should exhaust retries and fail, covering lines 262-264
1629+
log_utils.gzip_file_with_sufix(test_file, "retry_fail")
1630+
1631+
# Verify retries were attempted (should be called twice for 3 attempts)
1632+
assert mock_sleep.call_count == 2
1633+
1634+
# Verify error was logged to stderr (line 263)
1635+
output = stderr_capture.getvalue()
1636+
assert "Unable to gzip log file" in output
1637+
assert test_file in output
1638+
assert "persistent lock" in output
1639+
1640+
# Verify the exception was re-raised (line 264)
1641+
assert "persistent lock" in str(exc_info.value)

tests/core/test_log_utils_windows.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,9 @@ def test_is_older_than_x_days_windows_safe(self):
112112

113113
assert os.path.isfile(file_path) == True
114114

115-
# When days=1, it compares against current time, so file should be "older"
116-
# due to the small time difference since creation
115+
# When days=1, it compares against 1 day ago, so newly created file should NOT be older
117116
result = log_utils.is_older_than_x_days(file_path, 1)
118-
assert result == True
117+
assert result == False
119118

120119
# When days=5, it compares against 5 days ago, so newly created file should NOT be older
121120
result = log_utils.is_older_than_x_days(file_path, 5)
@@ -408,7 +407,7 @@ def windows_file_worker(worker_id):
408407

409408
# Verify all workers completed successfully
410409
for result in results:
411-
assert result['is_old'] == True # Files should be considered "old"
410+
assert result['is_old'] == False # Files should NOT be considered "old" (created recently)
412411
assert result['gzip_result'] is not None
413412
assert f"worker_{result['worker_id']}" in result['gzip_result']
414413

0 commit comments

Comments
 (0)